build: migrate the build tool from vue-cli to vite4 (#208)

This commit is contained in:
Fine0830
2022-12-17 14:07:03 +08:00
committed by GitHub
parent 1e0c253488
commit 44dcb1e7f6
214 changed files with 27014 additions and 54234 deletions

View File

@@ -14,44 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<Line
:data="data"
:intervalTime="intervalTime"
:config="config"
@click="clickEvent"
/>
<Line :data="data" :intervalTime="intervalTime" :config="config" @click="clickEvent" />
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import Line from "./Line.vue";
import {
AreaConfig,
EventParams,
RelatedTrace,
Filters,
} from "@/types/dashboard";
import type { PropType } from "vue";
import Line from "./Line.vue";
import type { AreaConfig, EventParams, RelatedTrace, Filters } from "@/types/dashboard";
/*global defineProps, defineEmits */
const emits = defineEmits(["click"]);
defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
config: {
type: Object as PropType<
AreaConfig & {
filters: Filters;
relatedTrace: RelatedTrace;
id: string;
associate: { widgetId: string }[];
}
>,
default: () => ({}),
},
});
function clickEvent(params: EventParams) {
emits("click", params);
}
/*global defineProps, defineEmits */
const emits = defineEmits(["click"]);
defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
config: {
type: Object as PropType<
AreaConfig & {
filters: Filters;
relatedTrace: RelatedTrace;
id: string;
associate: { widgetId: string }[];
}
>,
default: () => ({}),
},
});
function clickEvent(params: EventParams) {
emits("click", params);
}
</script>

View File

@@ -14,141 +14,124 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="graph" :class="isRight ? 'flex-h' : 'flex-v'">
<Graph
:option="option"
@select="clickEvent"
:filters="config.filters"
:associate="config.associate || []"
/>
<Graph :option="option" @select="clickEvent" :filters="config.filters" :associate="config.associate || []" />
<Legend :config="config.legend" :data="data" :intervalTime="intervalTime" />
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import type { PropType } from "vue";
import {
BarConfig,
EventParams,
RelatedTrace,
Filters,
} from "@/types/dashboard";
import useLegendProcess from "@/hooks/useLegendProcessor";
import { computed } from "vue";
import type { PropType } from "vue";
import type { BarConfig, EventParams, RelatedTrace, Filters } from "@/types/dashboard";
import useLegendProcess from "@/hooks/useLegendProcessor";
/*global defineProps, defineEmits */
const emits = defineEmits(["click"]);
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
theme: { type: String, default: "light" },
config: {
type: Object as PropType<
BarConfig & {
filters: Filters;
relatedTrace: RelatedTrace;
id: string;
associate: { widgetId: string }[];
}
>,
default: () => ({}),
},
});
const { showEchartsLegend, isRight, chartColors } = useLegendProcess(
props.config.legend
);
const option = computed(() => getOption());
function getOption() {
const keys = Object.keys(props.data || {}).filter(
(i: any) => Array.isArray(props.data[i]) && props.data[i].length
);
const temp = keys.map((i: string) => {
if (!props.intervalTime) {
return;
}
return {
data: props.data[i].map((item: number, itemIndex: number) => [
props.intervalTime[itemIndex],
item,
]),
name: i,
type: "bar",
symbol: "none",
stack: "sum",
lineStyle: {
width: 1.5,
type: "dotted",
},
showBackground: props.config.showBackground,
backgroundStyle: {
color: "rgba(180, 180, 180, 0.1)",
},
};
/*global defineProps, defineEmits */
const emits = defineEmits(["click"]);
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
theme: { type: String, default: "light" },
config: {
type: Object as PropType<
BarConfig & {
filters: Filters;
relatedTrace: RelatedTrace;
id: string;
associate: { widgetId: string }[];
}
>,
default: () => ({}),
},
});
const color: string[] = chartColors(keys);
return {
color,
tooltip: {
trigger: "none",
const { showEchartsLegend, isRight, chartColors } = useLegendProcess(props.config.legend);
const option = computed(() => getOption());
function getOption() {
const keys = Object.keys(props.data || {}).filter((i: any) => Array.isArray(props.data[i]) && props.data[i].length);
const temp = keys.map((i: string) => {
if (!props.intervalTime) {
return;
}
return {
data: props.data[i].map((item: number, itemIndex: number) => [props.intervalTime[itemIndex], item]),
name: i,
type: "bar",
symbol: "none",
stack: "sum",
lineStyle: {
width: 1.5,
type: "dotted",
},
showBackground: props.config.showBackground,
backgroundStyle: {
color: "rgba(180, 180, 180, 0.1)",
},
};
});
const color: string[] = chartColors(keys);
return {
color,
tooltip: {
trigger: "none",
axisPointer: {
type: "cross",
color: "#333",
backgroundColor: "rgba(255, 255, 255, 0.8)",
},
},
legend: {
type: "scroll",
show: showEchartsLegend(keys),
icon: "circle",
top: 0,
left: 0,
itemWidth: 12,
textStyle: {
color: "#333",
},
},
grid: {
top: keys.length === 1 ? 15 : 40,
left: 0,
right: 10,
bottom: 5,
containLabel: true,
},
axisPointer: {
type: "cross",
color: "#333",
backgroundColor: "rgba(255, 255, 255, 0.8)",
label: {
color: "#333",
backgroundColor: "rgba(255, 255, 255, 0.8)",
},
},
},
legend: {
type: "scroll",
show: showEchartsLegend(keys),
icon: "circle",
top: 0,
left: 0,
itemWidth: 12,
textStyle: {
color: "#333",
xAxis: {
type: "category",
axisTick: {
lineStyle: { color: "#c1c5ca41" },
alignWithLabel: true,
},
splitLine: { show: false },
axisLine: { lineStyle: { color: "rgba(0,0,0,0)" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
},
},
grid: {
top: keys.length === 1 ? 15 : 40,
left: 0,
right: 10,
bottom: 5,
containLabel: true,
},
axisPointer: {
label: {
color: "#333",
backgroundColor: "rgba(255, 255, 255, 0.8)",
yAxis: {
type: "value",
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: "#c1c5ca41", type: "dashed" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
},
},
xAxis: {
type: "category",
axisTick: {
lineStyle: { color: "#c1c5ca41" },
alignWithLabel: true,
},
splitLine: { show: false },
axisLine: { lineStyle: { color: "rgba(0,0,0,0)" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
},
yAxis: {
type: "value",
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: "#c1c5ca41", type: "dashed" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
},
series: temp,
};
}
function clickEvent(params: EventParams) {
emits("click", params);
}
series: temp,
};
}
function clickEvent(params: EventParams) {
emits("click", params);
}
</script>
<style lang="scss" scoped>
.graph {
width: 100%;
height: 100%;
}
.graph {
width: 100%;
height: 100%;
}
</style>

View File

@@ -30,52 +30,49 @@ limitations under the License. -->
<div class="center no-data" v-else>{{ t("noData") }}</div>
</template>
<script lang="ts" setup>
import { computed, PropType } from "vue";
import { useI18n } from "vue-i18n";
import { CardConfig, MetricConfigOpt } from "@/types/dashboard";
import { computed } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import type { CardConfig, MetricConfigOpt } from "@/types/dashboard";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number }>,
default: () => ({}),
},
config: {
type: Object as PropType<CardConfig & { metricConfig?: MetricConfigOpt[] }>,
default: () => ({
fontSize: 12,
showUnit: true,
textAlign: "center",
metricConfig: [],
}),
},
});
const { t } = useI18n();
const metricConfig = computed(() => props.config.metricConfig || []);
const key = computed(() => Object.keys(props.data)[0]);
const singleVal = computed(() => Number(props.data[key.value]));
const unit = computed(
() =>
metricConfig.value[0] &&
encodeURIComponent(metricConfig.value[0].unit || "")
);
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number }>,
default: () => ({}),
},
config: {
type: Object as PropType<CardConfig & { metricConfig?: MetricConfigOpt[] }>,
default: () => ({
fontSize: 12,
showUnit: true,
textAlign: "center",
metricConfig: [],
}),
},
});
const { t } = useI18n();
const metricConfig = computed(() => props.config.metricConfig || []);
const key = computed(() => Object.keys(props.data)[0]);
const singleVal = computed(() => Number(props.data[key.value]));
const unit = computed(() => metricConfig.value[0] && encodeURIComponent(metricConfig.value[0].unit || ""));
</script>
<style lang="scss" scoped>
.chart-card {
color: #333;
width: 100%;
height: 100%;
display: flex;
align-items: center;
}
.chart-card {
color: #333;
width: 100%;
height: 100%;
display: flex;
align-items: center;
}
.no-data {
height: 100%;
color: #666;
}
.no-data {
height: 100%;
color: #666;
}
.unit {
display: inline-block;
margin-left: 2px;
}
.unit {
display: inline-block;
margin-left: 2px;
}
</style>

View File

@@ -15,12 +15,7 @@ limitations under the License. -->
<template>
<div class="table">
<div class="search">
<el-input
v-model="searchText"
placeholder="Search for more endpoints"
@change="searchList"
class="inputs"
>
<el-input v-model="searchText" placeholder="Search for more endpoints" @change="searchList" class="inputs">
<template #append>
<el-button @click="searchList" class="btn">
<Icon size="middle" iconName="search" />
@@ -33,11 +28,7 @@ limitations under the License. -->
<el-table v-loading="chartLoading" :data="endpoints" style="width: 100%">
<el-table-column label="Endpoints" fixed min-width="220">
<template #default="scope">
<span
class="link"
@click="clickEndpoint(scope)"
:style="{ fontSize: `${config.fontSize}px` }"
>
<span class="link" @click="clickEndpoint(scope)" :style="{ fontSize: `${config.fontSize}px` }">
{{ scope.row.label }}
</span>
</template>
@@ -58,152 +49,137 @@ limitations under the License. -->
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
import type { PropType } from "vue";
import { EndpointListConfig } from "@/types/dashboard";
import { Endpoint } from "@/types/selector";
import { useDashboardStore } from "@/store/modules/dashboard";
import {
useQueryPodsMetrics,
usePodsSource,
} from "@/hooks/useMetricsProcessor";
import { EntityType } from "../data";
import router from "@/router";
import getDashboard from "@/hooks/useDashboardsSession";
import { MetricConfigOpt } from "@/types/dashboard";
import ColumnGraph from "./components/ColumnGraph.vue";
import { ref, watch } from "vue";
import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
import type { PropType } from "vue";
import type { EndpointListConfig } from "@/types/dashboard";
import type { Endpoint } from "@/types/selector";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useQueryPodsMetrics, usePodsSource } from "@/hooks/useMetricsProcessor";
import { EntityType } from "../data";
import router from "@/router";
import getDashboard from "@/hooks/useDashboardsSession";
import type { MetricConfigOpt } from "@/types/dashboard";
import ColumnGraph from "./components/ColumnGraph.vue";
/*global defineProps */
const props = defineProps({
data: {
type: Object,
},
config: {
type: Object as PropType<
EndpointListConfig & {
i: string;
metrics: string[];
metricTypes: string[];
} & { metricConfig: MetricConfigOpt[] }
>,
default: () => ({
metrics: [],
metricTypes: [],
dashboardName: "",
fontSize: 12,
i: "",
}),
},
needQuery: { type: Boolean, default: false },
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
const { t } = useI18n();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const chartLoading = ref<boolean>(false);
const endpoints = ref<Endpoint[]>([]);
const searchText = ref<string>("");
const colMetrics = ref<string[]>([]);
const metricConfig = ref<MetricConfigOpt[]>(props.config.metricConfig || []);
const metricTypes = ref<string[]>(props.config.metricTypes || []);
if (props.needQuery) {
queryEndpoints();
}
async function queryEndpoints() {
chartLoading.value = true;
const resp = await selectorStore.getEndpoints({
keyword: searchText.value,
/*global defineProps */
const props = defineProps({
data: {
type: Object,
},
config: {
type: Object as PropType<
EndpointListConfig & {
i: string;
metrics: string[];
metricTypes: string[];
} & { metricConfig: MetricConfigOpt[] }
>,
default: () => ({
metrics: [],
metricTypes: [],
dashboardName: "",
fontSize: 12,
i: "",
}),
},
needQuery: { type: Boolean, default: false },
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
chartLoading.value = false;
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
endpoints.value = selectorStore.pods;
queryEndpointMetrics(endpoints.value);
}
async function queryEndpointMetrics(currentPods: Endpoint[]) {
if (!currentPods.length) {
return;
}
const metrics = props.config.metrics || [];
const types = props.config.metricTypes || [];
if (metrics.length && metrics[0] && types.length && types[0]) {
const params = await useQueryPodsMetrics(
currentPods,
props.config,
EntityType[2].value
);
const json = await dashboardStore.fetchMetricValue(params);
const { t } = useI18n();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const chartLoading = ref<boolean>(false);
const endpoints = ref<Endpoint[]>([]);
const searchText = ref<string>("");
const colMetrics = ref<string[]>([]);
const metricConfig = ref<MetricConfigOpt[]>(props.config.metricConfig || []);
const metricTypes = ref<string[]>(props.config.metricTypes || []);
if (json.errors) {
ElMessage.error(json.errors);
return;
}
const { data, names, metricConfigArr, metricTypesArr } = usePodsSource(
currentPods,
json,
{
...props.config,
metricConfig: metricConfig.value,
}
);
endpoints.value = data;
colMetrics.value = names;
metricTypes.value = metricTypesArr;
metricConfig.value = metricConfigArr;
return;
}
endpoints.value = currentPods;
}
function clickEndpoint(scope: any) {
const { dashboard } = getDashboard({
name: props.config.dashboardName,
layer: dashboardStore.layerId,
entity: EntityType[2].value,
});
if (!dashboard) {
ElMessage.error("No this dashboard");
return;
}
router.push(
`/dashboard/${dashboard.layer}/${dashboard.entity}/${selectorStore.currentService.id}/${scope.row.id}/${dashboard.name}`
);
}
async function searchList() {
await queryEndpoints();
}
watch(
() => [
...(props.config.metricTypes || []),
...(props.config.metrics || []),
...(props.config.metricConfig || []),
],
(data, old) => {
if (JSON.stringify(data) === JSON.stringify(old)) {
return;
}
metricConfig.value = props.config.metricConfig;
queryEndpointMetrics(endpoints.value);
}
);
watch(
() => selectorStore.currentService,
() => {
if (props.needQuery) {
queryEndpoints();
}
);
async function queryEndpoints() {
chartLoading.value = true;
const resp = await selectorStore.getEndpoints({
keyword: searchText.value,
});
chartLoading.value = false;
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
endpoints.value = selectorStore.pods;
queryEndpointMetrics(endpoints.value);
}
async function queryEndpointMetrics(currentPods: Endpoint[]) {
if (!currentPods.length) {
return;
}
const metrics = props.config.metrics || [];
const types = props.config.metricTypes || [];
if (metrics.length && metrics[0] && types.length && types[0]) {
const params = await useQueryPodsMetrics(currentPods, props.config, EntityType[2].value);
const json = await dashboardStore.fetchMetricValue(params);
if (json.errors) {
ElMessage.error(json.errors);
return;
}
const { data, names, metricConfigArr, metricTypesArr } = usePodsSource(currentPods, json, {
...props.config,
metricConfig: metricConfig.value,
});
endpoints.value = data;
colMetrics.value = names;
metricTypes.value = metricTypesArr;
metricConfig.value = metricConfigArr;
return;
}
endpoints.value = currentPods;
}
function clickEndpoint(scope: any) {
const { dashboard } = getDashboard({
name: props.config.dashboardName,
layer: dashboardStore.layerId,
entity: EntityType[2].value,
});
if (!dashboard) {
ElMessage.error("No this dashboard");
return;
}
router.push(
`/dashboard/${dashboard.layer}/${dashboard.entity}/${selectorStore.currentService.id}/${scope.row.id}/${dashboard.name}`,
);
}
async function searchList() {
await queryEndpoints();
}
watch(
() => [...(props.config.metricTypes || []), ...(props.config.metrics || []), ...(props.config.metricConfig || [])],
(data, old) => {
if (JSON.stringify(data) === JSON.stringify(old)) {
return;
}
metricConfig.value = props.config.metricConfig;
queryEndpointMetrics(endpoints.value);
},
);
watch(
() => selectorStore.currentService,
() => {
queryEndpoints();
},
);
</script>
<style lang="scss" scoped>
@import "./style.scss";
@import "./style.scss";
.tips {
color: rgba(255, 0, 0, 0.5);
}
.tips {
color: rgba(255, 0, 0, 0.5);
}
</style>

View File

@@ -16,147 +16,147 @@ limitations under the License. -->
<Graph :option="option" />
</template>
<script lang="ts" setup>
import { computed } from "vue";
import type { PropType } from "vue";
import { computed } from "vue";
import type { PropType } from "vue";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{
[key: string]: { nodes: number[][]; buckets: number[][] };
}>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
config: {
type: Object as PropType<any>,
default: () => ({ metrics: [] }),
},
});
const option = computed(() => getOption());
function getOption() {
const metric = props.config.metrics && props.config.metrics[0];
let nodes: any = [];
if (metric) {
nodes = (props.data[metric] && props.data[metric].nodes) || [];
}
const source = (nodes || []).map((d: number[]) => d[2]);
const maxItem = Math.max(...source);
const minItem = Math.min(...source);
const colorBox = [
"#fff",
"#FDF0F0",
"#FAE2E2",
"#F8D3D3",
"#F6C4C4",
"#F4B5B5",
"#F1A7A7",
"#EF9898",
"#E86C6C",
"#E44E4E",
"#E23F3F",
"#DF3131",
"#DD2222",
"#CE2020",
"#C01D1D",
"#B11B1B",
"#A21919",
"#851414",
"#761212",
"#671010",
];
return {
tooltip: {
position: "top",
// formatter: (a: any) =>
// `${a.data[1] * 100}${props.standard.unit} [ ${a.data[2]} ]`,
// textStyle: {
// fontSize: 13,
// color: "#ccc",
// },
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{
[key: string]: { nodes: number[][]; buckets: number[][] };
}>,
default: () => ({}),
},
grid: {
top: 15,
left: 0,
right: 10,
bottom: 5,
containLabel: true,
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
config: {
type: Object as PropType<any>,
default: () => ({ metrics: [] }),
},
xAxis: {
type: "category",
data: props.intervalTime,
axisTick: {
lineStyle: { color: "#c1c5ca" },
alignWithLabel: true,
});
const option = computed(() => getOption());
function getOption() {
const metric = props.config.metrics && props.config.metrics[0];
let nodes: any = [];
if (metric) {
nodes = (props.data[metric] && props.data[metric].nodes) || [];
}
const source = (nodes || []).map((d: number[]) => d[2]);
const maxItem = Math.max(...source);
const minItem = Math.min(...source);
const colorBox = [
"#fff",
"#FDF0F0",
"#FAE2E2",
"#F8D3D3",
"#F6C4C4",
"#F4B5B5",
"#F1A7A7",
"#EF9898",
"#E86C6C",
"#E44E4E",
"#E23F3F",
"#DF3131",
"#DD2222",
"#CE2020",
"#C01D1D",
"#B11B1B",
"#A21919",
"#851414",
"#761212",
"#671010",
];
return {
tooltip: {
position: "top",
// formatter: (a: any) =>
// `${a.data[1] * 100}${props.standard.unit} [ ${a.data[2]} ]`,
// textStyle: {
// fontSize: 13,
// color: "#ccc",
// },
},
splitLine: { show: false },
axisLine: { lineStyle: { color: "rgba(0,0,0,0)" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
},
visualMap: [
{
min: minItem,
max: maxItem,
show: false,
type: "piecewise",
calculable: true,
pieces: generatePieces(maxItem, colorBox, minItem),
grid: {
top: 15,
left: 0,
right: 10,
bottom: 5,
containLabel: true,
},
],
yAxis: {
type: "category",
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: "#c1c5ca", type: "dashed" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
data: props.data.buckets,
},
series: [
{
type: "heatmap",
data: nodes || [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: "rgba(0, 0, 0, 0.5)",
xAxis: {
type: "category",
data: props.intervalTime,
axisTick: {
lineStyle: { color: "#c1c5ca" },
alignWithLabel: true,
},
splitLine: { show: false },
axisLine: { lineStyle: { color: "rgba(0,0,0,0)" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
},
visualMap: [
{
min: minItem,
max: maxItem,
show: false,
type: "piecewise",
calculable: true,
pieces: generatePieces(maxItem, colorBox, minItem),
},
],
yAxis: {
type: "category",
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: "#c1c5ca", type: "dashed" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
data: props.data.buckets,
},
series: [
{
type: "heatmap",
data: nodes || [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: "rgba(0, 0, 0, 0.5)",
},
},
},
},
],
};
}
function generatePieces(maxValue: number, colorBox: string[], minItem: number) {
if (maxValue < minItem) {
return [];
],
};
}
const pieces = [];
let quotient = 1;
let temp = {} as { min: number; max: number; color: string };
temp.max = minItem;
temp.min = minItem;
temp.color = colorBox[0];
pieces.push(temp);
if (maxValue && maxValue >= 19) {
quotient = Math.floor(maxValue / 19);
for (let i = 1; i < 20; i++) {
temp = {} as any;
if (i === 1) {
temp.min = minItem;
} else {
temp.min = quotient * (i - 1);
}
temp.max = quotient * i;
temp.color = colorBox[i];
pieces.push(temp);
function generatePieces(maxValue: number, colorBox: string[], minItem: number) {
if (maxValue < minItem) {
return [];
}
const pieces = [];
let quotient = 1;
let temp = {} as { min: number; max: number; color: string };
temp.max = minItem;
temp.min = minItem;
temp.color = colorBox[0];
pieces.push(temp);
if (maxValue && maxValue >= 19) {
quotient = Math.floor(maxValue / 19);
for (let i = 1; i < 20; i++) {
temp = {} as any;
if (i === 1) {
temp.min = minItem;
} else {
temp.min = quotient * (i - 1);
}
temp.max = quotient * i;
temp.color = colorBox[i];
pieces.push(temp);
}
}
const length = pieces.length;
if (length) {
const item = pieces[length - 1];
item.max = maxValue;
}
return pieces;
}
const length = pieces.length;
if (length) {
const item = pieces[length - 1];
item.max = maxValue;
}
return pieces;
}
</script>

View File

@@ -15,12 +15,7 @@ limitations under the License. -->
<template>
<div class="table">
<div class="search">
<el-input
v-model="searchText"
placeholder="Please input instance name"
@change="searchList"
class="inputs"
>
<el-input v-model="searchText" placeholder="Please input instance name" @change="searchList" class="inputs">
<template #append>
<el-button class="btn" @click="searchList">
<Icon size="sm" iconName="search" />
@@ -32,11 +27,7 @@ limitations under the License. -->
<el-table v-loading="chartLoading" :data="instances" style="width: 100%">
<el-table-column label="Service Instances" fixed min-width="320">
<template #default="scope">
<span
class="link"
@click="clickInstance(scope)"
:style="{ fontSize: `${config.fontSize}px` }"
>
<span class="link" @click="clickInstance(scope)" :style="{ fontSize: `${config.fontSize}px` }">
{{ scope.row.label }}
</span>
</template>
@@ -87,174 +78,153 @@ limitations under the License. -->
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { ElMessage } from "element-plus";
import type { PropType } from "vue";
import { useSelectorStore } from "@/store/modules/selectors";
import { useDashboardStore } from "@/store/modules/dashboard";
import { InstanceListConfig } from "@/types/dashboard";
import { Instance } from "@/types/selector";
import {
useQueryPodsMetrics,
usePodsSource,
} from "@/hooks/useMetricsProcessor";
import { EntityType } from "../data";
import router from "@/router";
import getDashboard from "@/hooks/useDashboardsSession";
import { MetricConfigOpt } from "@/types/dashboard";
import ColumnGraph from "./components/ColumnGraph.vue";
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { ElMessage } from "element-plus";
import type { PropType } from "vue";
import { useSelectorStore } from "@/store/modules/selectors";
import { useDashboardStore } from "@/store/modules/dashboard";
import type { InstanceListConfig } from "@/types/dashboard";
import type { Instance } from "@/types/selector";
import { useQueryPodsMetrics, usePodsSource } from "@/hooks/useMetricsProcessor";
import { EntityType } from "../data";
import router from "@/router";
import getDashboard from "@/hooks/useDashboardsSession";
import type { MetricConfigOpt } from "@/types/dashboard";
import ColumnGraph from "./components/ColumnGraph.vue";
/*global defineProps */
const props = defineProps({
config: {
type: Object as PropType<
InstanceListConfig & {
i: string;
metrics: string[];
metricTypes: string[];
isEdit: boolean;
} & { metricConfig: MetricConfigOpt[] }
>,
default: () => ({
dashboardName: "",
fontSize: 12,
i: "",
metrics: [],
metricTypes: [],
}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
needQuery: { type: Boolean, default: false },
});
const { t } = useI18n();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const chartLoading = ref<boolean>(false);
const instances = ref<Instance[]>([]); // current instances
const pageSize = 10;
const searchText = ref<string>("");
const colMetrics = ref<string[]>([]);
const metricConfig = ref<MetricConfigOpt[]>(props.config.metricConfig || []);
const metricTypes = ref<string[]>(props.config.metricTypes || []);
if (props.needQuery) {
queryInstance();
}
async function queryInstance() {
chartLoading.value = true;
const resp = await selectorStore.getServiceInstances();
chartLoading.value = false;
if (resp && resp.errors) {
ElMessage.error(resp.errors);
instances.value = [];
return;
}
instances.value = selectorStore.pods.filter(
(d: unknown, index: number) => index < pageSize
);
queryInstanceMetrics(instances.value);
}
async function queryInstanceMetrics(currentInstances: Instance[]) {
if (!currentInstances.length) {
return;
}
const metrics = props.config.metrics || [];
const types = props.config.metricTypes || [];
if (metrics.length && metrics[0] && types.length && types[0]) {
const params = await useQueryPodsMetrics(
currentInstances,
props.config,
EntityType[3].value
);
const json = await dashboardStore.fetchMetricValue(params);
if (json.errors) {
ElMessage.error(json.errors);
return;
}
const { data, names, metricConfigArr, metricTypesArr } = usePodsSource(
currentInstances,
json,
{
...props.config,
metricConfig: metricConfig.value,
}
);
instances.value = data;
colMetrics.value = names;
metricTypes.value = metricTypesArr;
metricConfig.value = metricConfigArr;
return;
}
instances.value = currentInstances;
}
function clickInstance(scope: any) {
const { dashboard } = getDashboard({
name: props.config.dashboardName,
layer: dashboardStore.layerId,
entity: EntityType[3].value,
/*global defineProps */
const props = defineProps({
config: {
type: Object as PropType<
InstanceListConfig & {
i: string;
metrics: string[];
metricTypes: string[];
isEdit: boolean;
} & { metricConfig: MetricConfigOpt[] }
>,
default: () => ({
dashboardName: "",
fontSize: 12,
i: "",
metrics: [],
metricTypes: [],
}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
needQuery: { type: Boolean, default: false },
});
if (!dashboard) {
ElMessage.error("No this dashboard");
return;
}
router.push(
`/dashboard/${dashboard.layer}/${dashboard.entity}/${
selectorStore.currentService.id
}/${scope.row.id}/${dashboard.name.split(" ").join("-")}`
);
}
function changePage(pageIndex: number) {
instances.value = selectorStore.pods.filter((d: unknown, index: number) => {
if (index >= (pageIndex - 1) * pageSize && index < pageIndex * pageSize) {
return d;
}
});
queryInstanceMetrics(instances.value);
}
function searchList() {
const searchInstances = selectorStore.pods.filter((d: { label: string }) =>
d.label.includes(searchText.value)
);
instances.value = searchInstances.filter(
(d: unknown, index: number) => index < pageSize
);
queryInstanceMetrics(instances.value);
}
watch(
() => [
...(props.config.metricTypes || []),
...(props.config.metrics || []),
...(props.config.metricConfig || []),
],
(data, old) => {
if (JSON.stringify(data) === JSON.stringify(old)) {
return;
}
metricConfig.value = props.config.metricConfig;
queryInstanceMetrics(instances.value);
}
);
watch(
() => selectorStore.currentService,
() => {
const { t } = useI18n();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const chartLoading = ref<boolean>(false);
const instances = ref<Instance[]>([]); // current instances
const pageSize = 10;
const searchText = ref<string>("");
const colMetrics = ref<string[]>([]);
const metricConfig = ref<MetricConfigOpt[]>(props.config.metricConfig || []);
const metricTypes = ref<string[]>(props.config.metricTypes || []);
if (props.needQuery) {
queryInstance();
}
);
async function queryInstance() {
chartLoading.value = true;
const resp = await selectorStore.getServiceInstances();
chartLoading.value = false;
if (resp && resp.errors) {
ElMessage.error(resp.errors);
instances.value = [];
return;
}
instances.value = selectorStore.pods.filter((d: unknown, index: number) => index < pageSize);
queryInstanceMetrics(instances.value);
}
async function queryInstanceMetrics(currentInstances: Instance[]) {
if (!currentInstances.length) {
return;
}
const metrics = props.config.metrics || [];
const types = props.config.metricTypes || [];
if (metrics.length && metrics[0] && types.length && types[0]) {
const params = await useQueryPodsMetrics(currentInstances, props.config, EntityType[3].value);
const json = await dashboardStore.fetchMetricValue(params);
if (json.errors) {
ElMessage.error(json.errors);
return;
}
const { data, names, metricConfigArr, metricTypesArr } = usePodsSource(currentInstances, json, {
...props.config,
metricConfig: metricConfig.value,
});
instances.value = data;
colMetrics.value = names;
metricTypes.value = metricTypesArr;
metricConfig.value = metricConfigArr;
return;
}
instances.value = currentInstances;
}
function clickInstance(scope: any) {
const { dashboard } = getDashboard({
name: props.config.dashboardName,
layer: dashboardStore.layerId,
entity: EntityType[3].value,
});
if (!dashboard) {
ElMessage.error("No this dashboard");
return;
}
router.push(
`/dashboard/${dashboard.layer}/${dashboard.entity}/${selectorStore.currentService.id}/${
scope.row.id
}/${dashboard.name.split(" ").join("-")}`,
);
}
function changePage(pageIndex: number) {
instances.value = selectorStore.pods.filter((d: unknown, index: number) => {
if (index >= (pageIndex - 1) * pageSize && index < pageIndex * pageSize) {
return d;
}
});
queryInstanceMetrics(instances.value);
}
function searchList() {
const searchInstances = selectorStore.pods.filter((d: { label: string }) => d.label.includes(searchText.value));
instances.value = searchInstances.filter((d: unknown, index: number) => index < pageSize);
queryInstanceMetrics(instances.value);
}
watch(
() => [...(props.config.metricTypes || []), ...(props.config.metrics || []), ...(props.config.metricConfig || [])],
(data, old) => {
if (JSON.stringify(data) === JSON.stringify(old)) {
return;
}
metricConfig.value = props.config.metricConfig;
queryInstanceMetrics(instances.value);
},
);
watch(
() => selectorStore.currentService,
() => {
queryInstance();
},
);
</script>
<style lang="scss" scoped>
@import "./style.scss";
@import "./style.scss";
.attributes {
max-height: 400px;
overflow: auto;
}
.attributes {
max-height: 400px;
overflow: auto;
}
</style>

View File

@@ -25,174 +25,161 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import type { PropType } from "vue";
import {
LineConfig,
EventParams,
RelatedTrace,
Filters,
} from "@/types/dashboard";
import Legend from "./components/Legend.vue";
import useLegendProcess from "@/hooks/useLegendProcessor";
import { computed, ref } from "vue";
import type { PropType } from "vue";
import type { LineConfig, EventParams, RelatedTrace, Filters } from "@/types/dashboard";
import Legend from "./components/Legend.vue";
import useLegendProcess from "@/hooks/useLegendProcessor";
/*global defineProps, defineEmits */
const emits = defineEmits(["click"]);
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
theme: { type: String, default: "light" },
config: {
type: Object as PropType<
LineConfig & {
filters?: Filters;
relatedTrace?: RelatedTrace;
id?: string;
associate: { widgetId: string }[];
}
>,
default: () => ({
step: false,
smooth: false,
showSymbol: false,
opacity: 0.4,
showXAxis: true,
showYAxis: true,
smallTips: false,
showlabels: true,
}),
},
});
const setRight = ref<boolean>(false);
const option = computed(() => getOption());
function getOption() {
const { showEchartsLegend, isRight, chartColors } = useLegendProcess(
props.config.legend
);
setRight.value = isRight;
const keys = Object.keys(props.data || {}).filter(
(i: any) => Array.isArray(props.data[i]) && props.data[i].length
);
const temp = keys.map((i: any) => {
const serie: any = {
data: props.data[i].map((item: any, itemIndex: number) => [
props.intervalTime[itemIndex],
item,
]),
name: i,
type: "line",
symbol: "circle",
symbolSize: 8,
showSymbol: props.config.showSymbol,
step: props.config.step,
smooth: props.config.smooth,
lineStyle: {
width: 1.5,
type: "solid",
},
};
if (props.config.type === "Area") {
serie.areaStyle = {
opacity: props.config.opacity || 0.4,
};
}
return serie;
/*global defineProps, defineEmits */
const emits = defineEmits(["click"]);
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
theme: { type: String, default: "light" },
config: {
type: Object as PropType<
LineConfig & {
filters?: Filters;
relatedTrace?: RelatedTrace;
id?: string;
associate?: { widgetId: string }[];
}
>,
default: () => ({
step: false,
smooth: false,
showSymbol: false,
opacity: 0.4,
showXAxis: true,
showYAxis: true,
smallTips: false,
showlabels: true,
}),
},
});
const color: string[] = chartColors(keys);
const tooltip = {
trigger: "none",
axisPointer: {
type: "cross",
color: "#333",
backgroundColor: "rgba(255, 255, 255, 0.8)",
},
// trigger: "axis",
// textStyle: {
// fontSize: 12,
// color: "#333",
// },
// enterable: true,
// confine: true,
extraCssText: "max-height: 300px; overflow: auto; border: none;",
};
const tips = {
formatter(params: any) {
return `${params[0].value[1]}`;
},
confine: true,
extraCssText: `height: 20px; padding:0 2px;`,
trigger: "axis",
textStyle: {
fontSize: 12,
color: "#333",
},
};
return {
color,
tooltip: props.config.smallTips ? tips : tooltip,
legend: {
type: "scroll",
show: showEchartsLegend(keys),
icon: "circle",
top: 0,
left: 0,
itemWidth: 12,
textStyle: {
color: props.theme === "dark" ? "#fff" : "#333",
},
},
axisPointer: {
label: {
const setRight = ref<boolean>(false);
const option = computed(() => getOption());
function getOption() {
const { showEchartsLegend, isRight, chartColors } = useLegendProcess(props.config.legend);
setRight.value = isRight;
const keys = Object.keys(props.data || {}).filter((i: any) => Array.isArray(props.data[i]) && props.data[i].length);
const temp = keys.map((i: any) => {
const serie: any = {
data: props.data[i].map((item: any, itemIndex: number) => [props.intervalTime[itemIndex], item]),
name: i,
type: "line",
symbol: "circle",
symbolSize: 8,
showSymbol: props.config.showSymbol,
step: props.config.step,
smooth: props.config.smooth,
lineStyle: {
width: 1.5,
type: "solid",
},
};
if (props.config.type === "Area") {
serie.areaStyle = {
opacity: props.config.opacity || 0.4,
};
}
return serie;
});
const color: string[] = chartColors(keys);
const tooltip = {
trigger: "none",
axisPointer: {
type: "cross",
color: "#333",
backgroundColor: "rgba(255, 255, 255, 0.8)",
},
},
grid: {
top: showEchartsLegend(keys) ? 35 : 10,
left: 0,
right: 10,
bottom: 5,
containLabel:
props.config.showlabels === undefined ? true : props.config.showlabels,
},
xAxis: {
type: "category",
show: props.config.showXAxis,
axisTick: {
lineStyle: { color: "#c1c5ca41" },
alignWithLabel: true,
// trigger: "axis",
// textStyle: {
// fontSize: 12,
// color: "#333",
// },
// enterable: true,
// confine: true,
extraCssText: "max-height: 300px; overflow: auto; border: none;",
};
const tips = {
formatter(params: any) {
return `${params[0].value[1]}`;
},
splitLine: { show: false },
axisLine: { lineStyle: { color: "rgba(0,0,0,0)" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
},
yAxis: {
show: props.config.showYAxis,
type: "value",
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: "#c1c5ca41", type: "dashed" } },
axisLabel: {
color: "#9da5b2",
fontSize: "11",
show: props.config.showYAxis,
confine: true,
extraCssText: `height: 20px; padding:0 2px;`,
trigger: "axis",
textStyle: {
fontSize: 12,
color: "#333",
},
},
series: temp,
};
}
};
function clickEvent(params: EventParams) {
emits("click", params);
}
return {
color,
tooltip: props.config.smallTips ? tips : tooltip,
legend: {
type: "scroll",
show: showEchartsLegend(keys),
icon: "circle",
top: 0,
left: 0,
itemWidth: 12,
textStyle: {
color: props.theme === "dark" ? "#fff" : "#333",
},
},
axisPointer: {
label: {
color: "#333",
backgroundColor: "rgba(255, 255, 255, 0.8)",
},
},
grid: {
top: showEchartsLegend(keys) ? 35 : 10,
left: 0,
right: 10,
bottom: 5,
containLabel: props.config.showlabels === undefined ? true : props.config.showlabels,
},
xAxis: {
type: "category",
show: props.config.showXAxis,
axisTick: {
lineStyle: { color: "#c1c5ca41" },
alignWithLabel: true,
},
splitLine: { show: false },
axisLine: { lineStyle: { color: "rgba(0,0,0,0)" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
},
yAxis: {
show: props.config.showYAxis,
type: "value",
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: "#c1c5ca41", type: "dashed" } },
axisLabel: {
color: "#9da5b2",
fontSize: "11",
show: props.config.showYAxis,
},
},
series: temp,
};
}
function clickEvent(params: EventParams) {
emits("click", params);
}
</script>
<style lang="scss" scoped>
.graph {
width: 100%;
height: 100%;
}
.graph {
width: 100%;
height: 100%;
}
</style>

View File

@@ -15,12 +15,7 @@ limitations under the License. -->
<template>
<div class="table">
<div class="search">
<el-input
v-model="searchText"
placeholder="Please input service name"
@change="searchList"
class="inputs mt-5"
>
<el-input v-model="searchText" placeholder="Please input service name" @change="searchList" class="inputs mt-5">
<template #append>
<el-button class="btn" @click="searchList">
<Icon size="sm" iconName="search" />
@@ -37,23 +32,14 @@ limitations under the License. -->
:border="true"
:style="{ fontSize: '14px' }"
>
<el-table-column
fixed
label="Service Groups"
v-if="config.showGroup"
min-width="150"
>
<el-table-column fixed label="Service Groups" v-if="config.showGroup" min-width="150">
<template #default="scope">
{{ scope.row.group }}
</template>
</el-table-column>
<el-table-column fixed label="Service Names" min-width="220">
<template #default="scope">
<span
class="link"
:style="{ fontSize: `${config.fontSize}px` }"
@click="clickService(scope)"
>
<span class="link" :style="{ fontSize: `${config.fontSize}px` }" @click="clickService(scope)">
{{ scope.row.label }}
</span>
</template>
@@ -85,89 +71,83 @@ limitations under the License. -->
</div>
</template>
<script setup lang="ts">
import { watch, ref } from "vue";
import { ElMessage } from "element-plus";
import type { PropType } from "vue";
import { ServiceListConfig } from "@/types/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { Service } from "@/types/selector";
import {
useQueryPodsMetrics,
usePodsSource,
} from "@/hooks/useMetricsProcessor";
import { EntityType } from "../data";
import router from "@/router";
import getDashboard from "@/hooks/useDashboardsSession";
import { MetricConfigOpt } from "@/types/dashboard";
import ColumnGraph from "./components/ColumnGraph.vue";
import { watch, ref } from "vue";
import { ElMessage } from "element-plus";
import type { PropType } from "vue";
import type { ServiceListConfig } from "@/types/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import type { Service } from "@/types/selector";
import { useQueryPodsMetrics, usePodsSource } from "@/hooks/useMetricsProcessor";
import { EntityType } from "../data";
import router from "@/router";
import getDashboard from "@/hooks/useDashboardsSession";
import type { MetricConfigOpt } from "@/types/dashboard";
import ColumnGraph from "./components/ColumnGraph.vue";
/*global defineProps */
const props = defineProps({
data: {
type: Object,
},
config: {
type: Object as PropType<
ServiceListConfig & {
i: string;
metrics: string[];
metricTypes: string[];
isEdit: boolean;
names: string[];
metricConfig: MetricConfigOpt[];
}
>,
default: () => ({ dashboardName: "", fontSize: 12 }),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
isEdit: { type: Boolean, default: false },
});
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const appStore = useAppStoreWithOut();
const chartLoading = ref<boolean>(false);
const pageSize = 10;
const services = ref<Service[]>([]);
const colMetrics = ref<string[]>([]);
const searchText = ref<string>("");
const groups = ref<any>({});
const sortServices = ref<(Service & { merge: boolean })[]>([]);
const metricConfig = ref<MetricConfigOpt[]>(props.config.metricConfig || []);
const metricTypes = ref<string[]>(props.config.metricTypes || []);
queryServices();
async function queryServices() {
chartLoading.value = true;
const resp = await selectorStore.fetchServices(dashboardStore.layerId);
chartLoading.value = false;
if (resp.errors) {
ElMessage.error(resp.errors);
}
sortServices.value = selectorStore.services.sort((a: any, b: any) => {
const groupA = a.group.toUpperCase();
const groupB = b.group.toUpperCase();
if (groupA < groupB) {
return -1;
}
if (groupA > groupB) {
return 1;
}
return 0;
/*global defineProps */
const props = defineProps({
data: {
type: Object,
},
config: {
type: Object as PropType<
ServiceListConfig & {
i: string;
metrics: string[];
metricTypes: string[];
isEdit: boolean;
names: string[];
metricConfig: MetricConfigOpt[];
}
>,
default: () => ({ dashboardName: "", fontSize: 12 }),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
isEdit: { type: Boolean, default: false },
});
const s = sortServices.value.filter(
(d: Service, index: number) => index < pageSize
);
setServices(s);
}
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const appStore = useAppStoreWithOut();
const chartLoading = ref<boolean>(false);
const pageSize = 10;
const services = ref<Service[]>([]);
const colMetrics = ref<string[]>([]);
const searchText = ref<string>("");
const groups = ref<any>({});
const sortServices = ref<(Service & { merge: boolean })[]>([]);
const metricConfig = ref<MetricConfigOpt[]>(props.config.metricConfig || []);
const metricTypes = ref<string[]>(props.config.metricTypes || []);
function setServices(arr: (Service & { merge: boolean })[]) {
groups.value = {};
const map: { [key: string]: any[] } = arr.reduce(
(result: { [key: string]: any[] }, item: any) => {
queryServices();
async function queryServices() {
chartLoading.value = true;
const resp = await selectorStore.fetchServices(dashboardStore.layerId);
chartLoading.value = false;
if (resp.errors) {
ElMessage.error(resp.errors);
}
sortServices.value = selectorStore.services.sort((a: any, b: any) => {
const groupA = a.group.toUpperCase();
const groupB = b.group.toUpperCase();
if (groupA < groupB) {
return -1;
}
if (groupA > groupB) {
return 1;
}
return 0;
});
const s = sortServices.value.filter((d: Service, index: number) => index < pageSize);
setServices(s);
}
function setServices(arr: (Service & { merge: boolean })[]) {
groups.value = {};
const map: { [key: string]: any[] } = arr.reduce((result: { [key: string]: any[] }, item: any) => {
item.group = item.group || "";
if (result[item.group]) {
item.merge = true;
@@ -177,132 +157,118 @@ function setServices(arr: (Service & { merge: boolean })[]) {
}
result[item.group].push(item);
return result;
},
{}
);
const list = Object.values(map).flat(1);
const obj = {} as any;
for (const s of list) {
s.group = s.group || "";
if (!obj[s.group]) {
obj[s.group] = 1;
} else {
obj[s.group]++;
}
groups.value[s.group] = obj[s.group];
}
services.value = list;
queryServiceMetrics(services.value);
}
function clickService(scope: any) {
const { dashboard } = getDashboard({
name: props.config.dashboardName,
layer: dashboardStore.layerId,
entity: EntityType[0].value,
});
if (!dashboard) {
ElMessage.error("No this dashboard");
return;
}
const path = `/dashboard/${dashboard.layer}/${dashboard.entity}/${scope.row.id}/${dashboard.name}`;
router.push(path);
}
async function queryServiceMetrics(currentServices: Service[]) {
if (!currentServices.length) {
return;
}
const metrics = props.config.metrics || [];
const types = props.config.metricTypes || [];
if (metrics.length && metrics[0] && types.length && types[0]) {
const params = await useQueryPodsMetrics(
currentServices,
{ ...props.config, metricConfig: metricConfig.value || [] },
EntityType[0].value
);
const json = await dashboardStore.fetchMetricValue(params);
if (json.errors) {
ElMessage.error(json.errors);
return;
}
const { data, names, metricConfigArr, metricTypesArr } = usePodsSource(
currentServices,
json,
{
...props.config,
metricConfig: metricConfig.value || [],
}, {});
const list = Object.values(map).flat(1);
const obj = {} as any;
for (const s of list) {
s.group = s.group || "";
if (!obj[s.group]) {
obj[s.group] = 1;
} else {
obj[s.group]++;
}
);
services.value = data;
colMetrics.value = names;
metricTypes.value = metricTypesArr;
metricConfig.value = metricConfigArr;
return;
}
services.value = currentServices;
}
function objectSpanMethod(param: any): any {
if (!props.config.showGroup) {
return;
}
if (param.columnIndex !== 0) {
return;
}
if (param.row.merge) {
return {
rowspan: 0,
colspan: 0,
};
}
return { rowspan: groups.value[param.row.group], colspan: 1 };
}
function changePage(pageIndex: number) {
const arr = sortServices.value.filter((d: Service, index: number) => {
if (index >= (pageIndex - 1) * pageSize && index < pageSize * pageIndex) {
return d;
groups.value[s.group] = obj[s.group];
}
});
setServices(arr);
}
function searchList() {
const searchServices = sortServices.value.filter((d: { label: string }) =>
d.label.includes(searchText.value)
);
const services = searchServices.filter(
(d: unknown, index: number) => index < pageSize
);
setServices(services);
}
watch(
() => [
...(props.config.metricTypes || []),
...(props.config.metrics || []),
...(props.config.metricConfig || []),
],
(data, old) => {
if (JSON.stringify(data) === JSON.stringify(old)) {
return;
}
metricConfig.value = props.config.metricConfig;
services.value = list;
queryServiceMetrics(services.value);
}
);
watch(
() => appStore.durationTime,
() => {
if (dashboardStore.entity === EntityType[1].value) {
queryServices();
function clickService(scope: any) {
const { dashboard } = getDashboard({
name: props.config.dashboardName,
layer: dashboardStore.layerId,
entity: EntityType[0].value,
});
if (!dashboard) {
ElMessage.error("No this dashboard");
return;
}
const path = `/dashboard/${dashboard.layer}/${dashboard.entity}/${scope.row.id}/${dashboard.name}`;
router.push(path);
}
);
async function queryServiceMetrics(currentServices: Service[]) {
if (!currentServices.length) {
return;
}
const metrics = props.config.metrics || [];
const types = props.config.metricTypes || [];
if (metrics.length && metrics[0] && types.length && types[0]) {
const params = await useQueryPodsMetrics(
currentServices,
{ ...props.config, metricConfig: metricConfig.value || [] },
EntityType[0].value,
);
const json = await dashboardStore.fetchMetricValue(params);
if (json.errors) {
ElMessage.error(json.errors);
return;
}
const { data, names, metricConfigArr, metricTypesArr } = usePodsSource(currentServices, json, {
...props.config,
metricConfig: metricConfig.value || [],
});
services.value = data;
colMetrics.value = names;
metricTypes.value = metricTypesArr;
metricConfig.value = metricConfigArr;
return;
}
services.value = currentServices;
}
function objectSpanMethod(param: any): any {
if (!props.config.showGroup) {
return;
}
if (param.columnIndex !== 0) {
return;
}
if (param.row.merge) {
return {
rowspan: 0,
colspan: 0,
};
}
return { rowspan: groups.value[param.row.group], colspan: 1 };
}
function changePage(pageIndex: number) {
const arr = sortServices.value.filter((d: Service, index: number) => {
if (index >= (pageIndex - 1) * pageSize && index < pageSize * pageIndex) {
return d;
}
});
setServices(arr);
}
function searchList() {
const searchServices = sortServices.value.filter((d: { label: string }) => d.label.includes(searchText.value));
const services = searchServices.filter((d: unknown, index: number) => index < pageSize);
setServices(services);
}
watch(
() => [...(props.config.metricTypes || []), ...(props.config.metrics || []), ...(props.config.metricConfig || [])],
(data, old) => {
if (JSON.stringify(data) === JSON.stringify(old)) {
return;
}
metricConfig.value = props.config.metricConfig;
queryServiceMetrics(services.value);
},
);
watch(
() => appStore.durationTime,
() => {
if (dashboardStore.entity === EntityType[1].value) {
queryServices();
}
},
);
</script>
<style lang="scss" scoped>
@import "./style.scss";
@import "./style.scss";
</style>

View File

@@ -26,103 +26,97 @@ limitations under the License. -->
<div class="row flex-h" v-for="key in dataKeys" :key="key">
<div class="name" :style="`width: ${nameWidth}`">{{ key }}</div>
<div class="value-col" v-if="config.showTableValues">
{{
config.metricTypes[0] === "readMetricsValue"
? data[key]
: data[key][data[key].length - 1 || 0]
}}
{{ config.metricTypes[0] === "readMetricsValue" ? data[key] : data[key][data[key].length - 1 || 0] }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}),
},
config: {
type: Object as PropType<{
showTableValues: boolean;
tableHeaderCol2: string;
tableHeaderCol1: string;
metricTypes: string[];
}>,
default: () => ({ showTableValues: true }),
},
});
import { computed } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}),
},
config: {
type: Object as PropType<{
showTableValues: boolean;
tableHeaderCol2: string;
tableHeaderCol1: string;
metricTypes: string[];
}>,
default: () => ({ showTableValues: true }),
},
});
const { t } = useI18n();
const nameWidth = computed(() =>
props.config.showTableValues ? "80%" : "100%"
);
const dataKeys = computed(() => {
if (props.config.metricTypes[0] === "readMetricsValue") {
const keys = Object.keys(props.data || {});
const { t } = useI18n();
const nameWidth = computed(() => (props.config.showTableValues ? "80%" : "100%"));
const dataKeys = computed(() => {
if (props.config.metricTypes[0] === "readMetricsValue") {
const keys = Object.keys(props.data || {});
return keys;
}
const keys = Object.keys(props.data || {}).filter(
(i: string) => Array.isArray(props.data[i]) && props.data[i].length,
);
return keys;
}
const keys = Object.keys(props.data || {}).filter(
(i: string) => Array.isArray(props.data[i]) && props.data[i].length
);
return keys;
});
});
</script>
<style lang="scss" scoped>
.chart-table {
height: 100%;
width: 100%;
overflow: auto;
.name {
padding-left: 15px;
}
.row {
border-left: 1px solid #ccc;
height: 20px;
.chart-table {
height: 100%;
width: 100%;
overflow: auto;
div {
overflow: hidden;
text-overflow: ellipsis;
border-right: 1px solid #ccc;
text-align: center;
.name {
padding-left: 15px;
}
.row {
border-left: 1px solid #ccc;
height: 20px;
line-height: 20px;
display: inline-block;
width: 100%;
div {
overflow: hidden;
text-overflow: ellipsis;
border-right: 1px solid #ccc;
text-align: center;
height: 20px;
line-height: 20px;
display: inline-block;
}
div:last-child {
border-bottom: 1px solid #ccc;
}
div:nth-last-child(2) {
border-bottom: 1px solid #ccc;
}
}
div:last-child {
border-bottom: 1px solid #ccc;
.dark {
color: #eee;
}
div:nth-last-child(2) {
border-bottom: 1px solid #ccc;
.row:first-child {
div {
border-top: 1px solid #ccc;
background: #eee;
}
}
.header {
color: #000;
font-weight: bold;
}
.value-col {
width: 50%;
}
}
.dark {
color: #eee;
}
.row:first-child {
div {
border-top: 1px solid #ccc;
background: #eee;
}
}
.header {
color: #000;
font-weight: bold;
}
.value-col {
width: 50%;
}
}
</style>

View File

@@ -32,22 +32,14 @@ limitations under the License. -->
<div class="operation" @click="handleClick(i.name)">
<span>{{ t("copy") }}</span>
</div>
<div
class="operation"
@click="viewTrace(i)"
v-show="refIdType === RefIdTypes[0].value"
>
<div class="operation" @click="viewTrace(i)" v-show="refIdType === RefIdTypes[0].value">
<span>{{ t("viewTrace") }}</span>
</div>
</el-popover>
</div>
<el-progress
:stroke-width="6"
:percentage="
isNaN(Number(i.value) / maxValue)
? 0
: (Number(i.value) / maxValue) * 100
"
:percentage="isNaN(Number(i.value) / maxValue) ? 0 : (Number(i.value) / maxValue) * 100"
:color="TextColors[config.color || 'purple']"
:show-text="false"
/>
@@ -66,149 +58,142 @@ limitations under the License. -->
<div class="center no-data" v-else>No Data</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { computed, ref } from "vue";
import copy from "@/utils/copy";
import { TextColors } from "@/views/dashboard/data";
import Trace from "@/views/dashboard/related/trace/Index.vue";
import { QueryOrders, Status, RefIdTypes } from "../data";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{
[key: string]: { name: string; value: number; id: string }[];
}>,
default: () => ({}),
},
config: {
type: Object as PropType<{
color: string;
metrics: string[];
relatedTrace: any;
}>,
default: () => ({ color: "purple" }),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
const { t } = useI18n();
const showTrace = ref<boolean>(false);
const traceOptions = ref<{ type: string; filters?: unknown }>({
type: "Trace",
});
const refIdType = computed(
() =>
(props.config.relatedTrace && props.config.relatedTrace.refIdType) ||
RefIdTypes[0].value
);
const key = computed(() => Object.keys(props.data)[0] || "");
const available = computed(
() =>
Array.isArray(props.data[key.value]) &&
props.data[key.value][0] &&
props.data[key.value][0].value
);
const maxValue = computed(() => {
if (!(props.data[key.value] && props.data[key.value].length)) {
return 0;
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { computed, ref } from "vue";
import copy from "@/utils/copy";
import { TextColors } from "@/views/dashboard/data";
import Trace from "@/views/dashboard/related/trace/Index.vue";
import { QueryOrders, Status, RefIdTypes } from "../data";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{
[key: string]: { name: string; value: number; id: string }[];
}>,
default: () => ({}),
},
config: {
type: Object as PropType<{
color: string;
metrics: string[];
relatedTrace: any;
}>,
default: () => ({ color: "purple" }),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
const { t } = useI18n();
const showTrace = ref<boolean>(false);
const traceOptions = ref<{ type: string; filters?: unknown }>({
type: "Trace",
});
const refIdType = computed(
() => (props.config.relatedTrace && props.config.relatedTrace.refIdType) || RefIdTypes[0].value,
);
const key = computed(() => Object.keys(props.data)[0] || "");
const available = computed(
() => Array.isArray(props.data[key.value]) && props.data[key.value][0] && props.data[key.value][0].value,
);
const maxValue = computed(() => {
if (!(props.data[key.value] && props.data[key.value].length)) {
return 0;
}
const temp: number[] = props.data[key.value].map((i: any) => i.value);
return Math.max.apply(null, temp);
});
function handleClick(i: string) {
copy(i);
}
function viewTrace(item: { name: string; id: string; value: unknown }) {
const filters = {
...item,
queryOrder: QueryOrders[1].value,
status: Status[2].value,
id: item.id || item.name,
metricValue: [{ label: props.config.metrics[0], data: item.value, value: item.name }],
};
traceOptions.value = {
...traceOptions.value,
filters,
};
showTrace.value = true;
}
const temp: number[] = props.data[key.value].map((i: any) => i.value);
return Math.max.apply(null, temp);
});
function handleClick(i: string) {
copy(i);
}
function viewTrace(item: { name: string; id: string; value: unknown }) {
const filters = {
...item,
queryOrder: QueryOrders[1].value,
status: Status[2].value,
id: item.id || item.name,
metricValue: [
{ label: props.config.metrics[0], data: item.value, value: item.name },
],
};
traceOptions.value = {
...traceOptions.value,
filters,
};
showTrace.value = true;
}
</script>
<style lang="scss" scoped>
.top-list {
height: 100%;
overflow: auto;
padding: 10px;
}
.tools {
justify-content: space-between;
}
.progress-bar {
font-size: 12px;
color: #333;
}
.chart-slow-i {
padding: 6px 0;
}
.chart-slow {
height: 100%;
}
.desc {
flex-grow: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.calls {
font-size: 12px;
padding: 0 5px;
display: inline-block;
background-color: #40454e;
color: #eee;
border-radius: 4px;
}
.chart-slow-link {
padding: 4px 10px 7px 10px;
border-radius: 4px;
border: 1px solid #ddd;
color: #333;
background-color: #fff;
will-change: opacity, background-color;
transition: opacity 0.3s, background-color 0.3s;
}
.no-data {
height: 100%;
color: #666;
box-sizing: border-box;
display: -webkit-box;
-webkit-box-orient: horizontal;
-webkit-box-pack: center;
-webkit-box-align: center;
}
.operation-icon {
color: #333;
}
.operation {
padding: 5px 0;
color: #333;
cursor: pointer;
position: relative;
text-align: center;
font-size: 12px;
&:hover {
color: #409eff;
background-color: #eee;
.top-list {
height: 100%;
overflow: auto;
padding: 10px;
}
.tools {
justify-content: space-between;
}
.progress-bar {
font-size: 12px;
color: #333;
}
.chart-slow-i {
padding: 6px 0;
}
.chart-slow {
height: 100%;
}
.desc {
flex-grow: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.calls {
font-size: 12px;
padding: 0 5px;
display: inline-block;
background-color: #40454e;
color: #eee;
border-radius: 4px;
}
.chart-slow-link {
padding: 4px 10px 7px 10px;
border-radius: 4px;
border: 1px solid #ddd;
color: #333;
background-color: #fff;
will-change: opacity, background-color;
transition: opacity 0.3s, background-color 0.3s;
}
.no-data {
height: 100%;
color: #666;
box-sizing: border-box;
display: -webkit-box;
-webkit-box-orient: horizontal;
-webkit-box-pack: center;
-webkit-box-align: center;
}
.operation-icon {
color: #333;
}
.operation {
padding: 5px 0;
color: #333;
cursor: pointer;
position: relative;
text-align: center;
font-size: 12px;
&:hover {
color: #409eff;
background-color: #eee;
}
}
}
</style>

View File

@@ -16,9 +16,7 @@ limitations under the License. -->
<template>
<el-table-column
v-for="(metric, index) in colMetrics"
:label="`${decodeURIComponent(
getLabel(metric, index)
)} ${decodeURIComponent(getUnit(index))}`"
:label="`${decodeURIComponent(getLabel(metric, index))} ${decodeURIComponent(getUnit(index))}`"
:key="metric + index"
min-width="150"
>
@@ -37,18 +35,11 @@ limitations under the License. -->
showlabels: false,
}"
/>
<span
class="item flex-h"
v-else-if="useListConfig(config, index).isAvg"
>
<span class="item flex-h" v-else-if="useListConfig(config, index).isAvg">
<el-popover placement="left" :width="400" trigger="click">
<template #reference>
<span class="trend">
<Icon
iconName="timeline"
size="middle"
style="color: #409eff"
/>
<Icon iconName="timeline" size="middle" style="color: #409eff" />
</span>
</template>
<div class="view-line">
@@ -75,99 +66,85 @@ limitations under the License. -->
/>
</span>
</span>
<Card
v-else
:data="{ [metric]: scope.row[metric] }"
:config="{ textAlign: 'left' }"
/>
<Card v-else :data="{ [metric]: scope.row[metric] }" :config="{ textAlign: 'left' }" />
</div>
</template>
</el-table-column>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { MetricConfigOpt } from "@/types/dashboard";
import { useListConfig } from "@/hooks/useListConfig";
import Line from "../Line.vue";
import Card from "../Card.vue";
import { MetricQueryTypes } from "@/hooks/data";
import type { PropType } from "vue";
import type { MetricConfigOpt } from "@/types/dashboard";
import { useListConfig } from "@/hooks/useListConfig";
import Line from "../Line.vue";
import Card from "../Card.vue";
import { MetricQueryTypes } from "@/hooks/data";
/*global defineProps */
const props = defineProps({
colMetrics: { type: Object },
config: {
type: Object as PropType<{
i: string;
metrics: string[];
metricTypes: string[];
metricConfig: MetricConfigOpt[];
}>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
/*global defineProps */
const props = defineProps({
colMetrics: { type: Object },
config: {
type: Object as PropType<{
i: string;
metrics: string[];
metricTypes: string[];
metricConfig: MetricConfigOpt[];
}>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
function getUnit(index: string) {
const i = Number(index);
const u =
props.config.metricConfig &&
props.config.metricConfig[i] &&
props.config.metricConfig[i].unit;
if (u) {
return `(${encodeURIComponent(u)})`;
}
return encodeURIComponent("");
}
function getLabel(metric: string, index: string) {
const i = Number(index);
const label =
props.config.metricConfig &&
props.config.metricConfig[i] &&
props.config.metricConfig[i].label;
if (label) {
if (
props.config.metricTypes[i] === MetricQueryTypes.ReadLabeledMetricsValues
) {
const name = (label || "")
.split(",")
.map((item: string) => item.replace(/^\s*|\s*$/g, ""))[
props.config.metricConfig[i].index || 0
];
return encodeURIComponent(name || "");
function getUnit(index: string) {
const i = Number(index);
const u = props.config.metricConfig && props.config.metricConfig[i] && props.config.metricConfig[i].unit;
if (u) {
return `(${encodeURIComponent(u)})`;
}
return encodeURIComponent(label);
return encodeURIComponent("");
}
function getLabel(metric: string, index: string) {
const i = Number(index);
const label = props.config.metricConfig && props.config.metricConfig[i] && props.config.metricConfig[i].label;
if (label) {
if (props.config.metricTypes[i] === MetricQueryTypes.ReadLabeledMetricsValues) {
const name = (label || "").split(",").map((item: string) => item.replace(/^\s*|\s*$/g, ""))[
props.config.metricConfig[i].index || 0
];
return encodeURIComponent(name || "");
}
return encodeURIComponent(label);
}
return encodeURIComponent(metric);
}
return encodeURIComponent(metric);
}
</script>
<style lang="scss" scoped>
.chart {
height: 40px;
}
.chart {
height: 40px;
}
.view-line {
width: 380px;
height: 200px;
}
.view-line {
width: 380px;
height: 200px;
}
.item {
display: inline-block;
width: 100%;
height: 100%;
}
.item {
display: inline-block;
width: 100%;
height: 100%;
}
.trend {
width: 30px;
display: inline-block;
height: 100%;
cursor: pointer;
}
.trend {
width: 30px;
display: inline-block;
height: 100%;
cursor: pointer;
}
.value {
display: inline-block;
flex-grow: 2;
height: 100%;
width: calc(100% - 30px);
}
.value {
display: inline-block;
flex-grow: 2;
height: 100%;
width: calc(100% - 30px);
}
</style>

View File

@@ -81,115 +81,107 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { LegendOptions } from "@/types/dashboard";
import useLegendProcess from "@/hooks/useLegendProcessor";
import { computed } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import type { LegendOptions } from "@/types/dashboard";
import useLegendProcess from "@/hooks/useLegendProcessor";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}),
},
config: {
type: Object as PropType<LegendOptions>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
const { t } = useI18n();
const tableData: any = computed(() => {
const { aggregations } = useLegendProcess(props.config);
return aggregations(props.data, props.intervalTime).source;
});
const headerRow = computed(() => {
const { aggregations } = useLegendProcess(props.config);
return aggregations(props.data, props.intervalTime).headers;
});
const isRight = computed(() => useLegendProcess(props.config).isRight);
const width = computed(() =>
props.config.width
? props.config.width + "px"
: isRight.value
? "150px"
: "100%"
);
const colors = computed(() => {
const keys = Object.keys(props.data || {}).filter(
(i: any) => Array.isArray(props.data[i]) && props.data[i].length
);
const { chartColors } = useLegendProcess(props.config);
return chartColors(keys);
});
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}),
},
config: {
type: Object as PropType<LegendOptions>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
const { t } = useI18n();
const tableData: any = computed(() => {
const { aggregations } = useLegendProcess(props.config);
return aggregations(props.data, props.intervalTime).source;
});
const headerRow = computed(() => {
const { aggregations } = useLegendProcess(props.config);
return aggregations(props.data, props.intervalTime).headers;
});
const isRight = computed(() => useLegendProcess(props.config).isRight);
const width = computed(() => (props.config.width ? props.config.width + "px" : isRight.value ? "150px" : "100%"));
const colors = computed(() => {
const keys = Object.keys(props.data || {}).filter((i: any) => Array.isArray(props.data[i]) && props.data[i].length);
const { chartColors } = useLegendProcess(props.config);
return chartColors(keys);
});
</script>
<style lang="scss" scoped>
table {
font-size: 12px;
white-space: nowrap;
margin: 0;
border: none;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
}
table th {
padding: 5px;
}
table thead th {
position: sticky;
top: 0;
z-index: 1;
width: 25vw;
background: #fff;
text-align: left;
}
.name {
cursor: pointer;
}
table td {
padding: 5px;
}
table thead th:first-child {
position: sticky;
left: 0;
z-index: 2;
}
table tbody th {
font-weight: bold;
font-style: normal;
text-align: left;
background: #fff;
position: sticky;
left: 0;
z-index: 1;
}
[role="region"][aria-labelledby][tabindex] {
overflow: auto;
}
i {
font-style: normal;
}
.value {
span {
display: inline-block;
padding: 5px;
width: 80px;
table {
font-size: 12px;
white-space: nowrap;
margin: 0;
border: none;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
}
}
.list {
height: 360px;
overflow: auto;
}
table th {
padding: 5px;
}
table thead th {
position: sticky;
top: 0;
z-index: 1;
width: 25vw;
background: #fff;
text-align: left;
}
.name {
cursor: pointer;
}
table td {
padding: 5px;
}
table thead th:first-child {
position: sticky;
left: 0;
z-index: 2;
}
table tbody th {
font-weight: bold;
font-style: normal;
text-align: left;
background: #fff;
position: sticky;
left: 0;
z-index: 1;
}
[role="region"][aria-labelledby][tabindex] {
overflow: auto;
}
i {
font-style: normal;
}
.value {
span {
display: inline-block;
padding: 5px;
width: 80px;
}
}
.list {
height: 360px;
overflow: auto;
}
</style>