feat: Implement templates for dashboards (#28)

This commit is contained in:
Fine0830 2022-03-19 12:11:35 +08:00 committed by GitHub
parent 1cf3887675
commit 597e98e291
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1583 additions and 1193 deletions

View File

@ -15,7 +15,6 @@ limitations under the License. -->
<template> <template>
<router-view :key="$route.fullPath" /> <router-view :key="$route.fullPath" />
</template> </template>
<style> <style>
#app { #app {
color: #2c3e50; color: #2c3e50;

View File

@ -20,7 +20,6 @@ import { watch, ref, Ref, onMounted, onBeforeUnmount, unref } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import { useECharts } from "@/hooks/useEcharts"; import { useECharts } from "@/hooks/useEcharts";
import { addResizeListener, removeResizeListener } from "@/utils/event"; import { addResizeListener, removeResizeListener } from "@/utils/event";
import { useTimeoutFn } from "@/hooks/useTimeout";
/*global Nullable, defineProps, defineEmits*/ /*global Nullable, defineProps, defineEmits*/
const emits = defineEmits(["select"]); const emits = defineEmits(["select"]);
@ -40,9 +39,12 @@ const props = defineProps({
onMounted(async () => { onMounted(async () => {
await setOptions(props.option); await setOptions(props.option);
addResizeListener(unref(chartRef), resize); addResizeListener(unref(chartRef), resize);
useTimeoutFn(() => { setTimeout(() => {
const instance = getInstance(); const instance = getInstance();
if (!instance) {
return;
}
instance.on("click", (params: any) => { instance.on("click", (params: any) => {
emits("select", params); emits("select", params);
}); });

View File

@ -18,6 +18,7 @@ export const TypeOfMetrics = {
variable: "$name: String!", variable: "$name: String!",
query: `typeOfMetrics(name: $name)`, query: `typeOfMetrics(name: $name)`,
}; };
export const listMetrics = { export const listMetrics = {
variable: "$regex: String", variable: "$regex: String",
query: ` query: `
@ -30,17 +31,40 @@ export const listMetrics = {
`, `,
}; };
export const queryHeatMap = { export const getAllTemplates = {
variable: ["$condition: MetricsCondition!, $duration: Duration!"],
query: ` query: `
readHeatMap: readHeatMap(condition: $condition, duration: $duration) { getAllTemplates {
values { id,
configuration,
}
`,
};
export const addTemplate = {
variable: "$setting: NewDashboardSetting!",
query: `
addTemplate(setting: $setting) {
id id
values status
} message
buckets { }`,
min };
max export const changeTemplate = {
} variable: "$setting: DashboardSetting!",
query: `
changeTemplate(setting: $setting) {
id
status
message
}`,
};
export const deleteTemplate = {
variable: "$id: String!",
query: `
disableTemplate(id: $id) {
id
status
message
}`, }`,
}; };

View File

@ -16,12 +16,21 @@
*/ */
import { import {
TypeOfMetrics, TypeOfMetrics,
queryHeatMap,
listMetrics, listMetrics,
getAllTemplates,
addTemplate,
changeTemplate,
deleteTemplate,
} from "../fragments/dashboard"; } from "../fragments/dashboard";
export const queryTypeOfMetrics = `query typeOfMetrics(${TypeOfMetrics.variable}) {${TypeOfMetrics.query}}`; export const queryTypeOfMetrics = `query typeOfMetrics(${TypeOfMetrics.variable}) {${TypeOfMetrics.query}}`;
export const readHeatMap = `query queryData(${queryHeatMap.variable}) {${queryHeatMap.query}}`;
export const queryMetrics = `query queryData(${listMetrics.variable}) {${listMetrics.query}}`; export const queryMetrics = `query queryData(${listMetrics.variable}) {${listMetrics.query}}`;
export const addNewTemplate = `mutation template(${addTemplate.variable}) {${addTemplate.query}}`;
export const updateTemplate = `mutation template(${changeTemplate.variable}) {${changeTemplate.query}}`;
export const removeTemplate = `mutation template(${deleteTemplate.variable}) {${deleteTemplate.query}}`;
export const getTemplates = `query templates {${getAllTemplates.query}}`;

View File

@ -85,11 +85,3 @@ export const RespFields: any = {
refId refId
}`, }`,
}; };
export enum CalculationType {
Plus = "+",
Minus = "-",
Multiplication = "*",
Division = "/",
"Convert Unix Timestamp(milliseconds)" = "milliseconds",
"Convert Unix Timestamp(seconds)" = "seconds",
}

View File

@ -14,12 +14,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import dayjs from "dayjs";
import { RespFields, MetricQueryTypes } from "./data"; import { RespFields, MetricQueryTypes } from "./data";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { Instance, Endpoint, Service } from "@/types/selector"; import { Instance, Endpoint, Service } from "@/types/selector";
import { StandardConfig } from "@/types/dashboard";
export function useQueryProcessor(config: any) { export function useQueryProcessor(config: any) {
if (!(config.metrics && config.metrics[0])) { if (!(config.metrics && config.metrics[0])) {
@ -46,7 +48,6 @@ export function useQueryProcessor(config: any) {
} }
const fragment = config.metrics.map((name: string, index: number) => { const fragment = config.metrics.map((name: string, index: number) => {
const metricType = config.metricTypes[index] || ""; const metricType = config.metricTypes[index] || "";
const labels = ["0", "1", "2", "3", "4"];
if ( if (
[ [
MetricQueryTypes.ReadSampledRecords, MetricQueryTypes.ReadSampledRecords,
@ -62,10 +63,13 @@ export function useQueryProcessor(config: any) {
normal: selectorStore.currentService.normal, normal: selectorStore.currentService.normal,
scope: dashboardStore.entity, scope: dashboardStore.entity,
topN: 10, topN: 10,
order: "DES", order: config.standard.sortOrder || "DES",
}; };
} else { } else {
if (metricType === MetricQueryTypes.ReadLabeledMetricsValues) { if (metricType === MetricQueryTypes.ReadLabeledMetricsValues) {
const labels = (config.labelsIndex || "")
.split(",")
.map((item: string) => item.replace(/^\s*|\s*$/g, ""));
variables.push(`$labels${index}: [String!]!`); variables.push(`$labels${index}: [String!]!`);
conditions[`labels${index}`] = labels; conditions[`labels${index}`] = labels;
} }
@ -121,7 +125,11 @@ export function useQueryProcessor(config: any) {
} }
export function useSourceProcessor( export function useSourceProcessor(
resp: { errors: string; data: { [key: string]: any } }, resp: { errors: string; data: { [key: string]: any } },
config: { metrics: string[]; metricTypes: string[] } config: {
metrics: string[];
metricTypes: string[];
standard: StandardConfig;
}
) { ) {
if (resp.errors) { if (resp.errors) {
ElMessage.error(resp.errors); ElMessage.error(resp.errors);
@ -135,16 +143,20 @@ export function useSourceProcessor(
if (type === MetricQueryTypes.ReadMetricsValues) { if (type === MetricQueryTypes.ReadMetricsValues) {
source[m] = resp.data[keys[index]].values.values.map( source[m] = resp.data[keys[index]].values.values.map(
(d: { value: number }) => d.value (d: { value: number }) => aggregation(d.value, config.standard)
); );
} }
if (type === MetricQueryTypes.ReadLabeledMetricsValues) { if (type === MetricQueryTypes.ReadLabeledMetricsValues) {
const resVal = Object.values(resp.data)[0] || []; const resVal = Object.values(resp.data)[0] || [];
const labelsIdx = ["0", "1", "2", "3", "4"]; const labels = (config.standard.metricLabels || "")
const labels = ["P50", "P75", "P90", "P95", "P99"]; .split(",")
.map((item: string) => item.replace(/^\s*|\s*$/g, ""));
const labelsIdx = (config.standard.labelsIndex || "")
.split(",")
.map((item: string) => item.replace(/^\s*|\s*$/g, ""));
for (const item of resVal) { for (const item of resVal) {
const values = item.values.values.map( const values = item.values.values.map((d: { value: number }) =>
(d: { value: number }) => d.value aggregation(Number(d.value), config.standard)
); );
const indexNum = labelsIdx.findIndex((d: string) => d === item.label); const indexNum = labelsIdx.findIndex((d: string) => d === item.label);
@ -156,13 +168,22 @@ export function useSourceProcessor(
} }
} }
if (type === MetricQueryTypes.ReadMetricsValue) { if (type === MetricQueryTypes.ReadMetricsValue) {
source[m] = Object.values(resp.data)[0]; source[m] = aggregation(
Number(Object.values(resp.data)[0]),
config.standard
);
} }
if ( if (
type === MetricQueryTypes.SortMetrics || type === MetricQueryTypes.SortMetrics ||
type === MetricQueryTypes.ReadSampledRecords type === MetricQueryTypes.ReadSampledRecords
) { ) {
source[m] = Object.values(resp.data)[0] || []; source[m] = (Object.values(resp.data)[0] || []).map(
(d: { value: unknown; name: string }) => {
d.value = aggregation(Number(d.value), config.standard);
return d;
}
);
} }
if (type === MetricQueryTypes.READHEATMAP) { if (type === MetricQueryTypes.READHEATMAP) {
const resVal = Object.values(resp.data)[0] || {}; const resVal = Object.values(resp.data)[0] || {};
@ -205,7 +226,6 @@ export function useQueryPodsMetrics(
}; };
const variables: string[] = [`$duration: Duration!`]; const variables: string[] = [`$duration: Duration!`];
const { currentService } = selectorStore; const { currentService } = selectorStore;
const fragmentList = pods.map( const fragmentList = pods.map(
( (
d: (Instance | Endpoint | Service) & { normal: boolean }, d: (Instance | Endpoint | Service) & { normal: boolean },
@ -286,3 +306,34 @@ export function useQueryTopologyMetrics(metrics: string[], ids: string[]) {
return { queryStr, conditions }; return { queryStr, conditions };
} }
function aggregation(val: number, standard: any): number | string {
let data: number | string = val;
if (!isNaN(standard.plus)) {
data = val + Number(standard.plus);
return data;
}
if (!isNaN(standard.minus)) {
data = val - Number(standard.plus);
return data;
}
if (!isNaN(standard.multiply)) {
data = val * Number(standard.multiply);
return data;
}
if (!isNaN(standard.divide)) {
data = val / Number(standard.divide);
return data;
}
if (standard.milliseconds) {
data = dayjs(val).format("YYYY-MM-DD HH:mm:ss");
return data;
}
if (standard.milliseconds) {
data = dayjs.unix(val).format("YYYY-MM-DD HH:mm:ss");
return data;
}
return data;
}

View File

@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<section class="app-main"> <section class="app-main flex-v">
<router-view v-slot="{ Component }" :key="$route.fullPath"> <router-view v-slot="{ Component }" :key="$route.fullPath">
<keep-alive> <keep-alive>
<component :is="Component" /> <component :is="Component" />
@ -21,7 +21,19 @@ limitations under the License. -->
</router-view> </router-view>
</section> </section>
</template> </template>
<script lang="ts" setup></script> <script lang="ts" setup>
import { ElMessage } from "element-plus";
import { useAppStoreWithOut } from "@/store/modules/app";
const appStore = useAppStoreWithOut();
if (!appStore.utc) {
const res = appStore.queryOAPTimeInfo();
if (res.errors) {
ElMessage.error(res.errors);
}
}
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
.app-main { .app-main {
height: calc(100% - 40px); height: calc(100% - 40px);

View File

@ -24,7 +24,7 @@ limitations under the License. -->
@input="changeTimeRange" @input="changeTimeRange"
/> />
<span> <span>
UTC{{ utcHour >= 0 ? "+" : "" UTC{{ appStore.utcHour >= 0 ? "+" : ""
}}{{ `${appStore.utcHour}:${appStore.utcMin}` }} }}{{ `${appStore.utcHour}:${appStore.utcMin}` }}
</span> </span>
<span title="refresh" class="ghost ml-5 cp" @click="handleReload"> <span title="refresh" class="ghost ml-5 cp" @click="handleReload">
@ -46,17 +46,6 @@ const route = useRoute();
const pageName = ref<string>(""); const pageName = ref<string>("");
const timeRange = ref<number>(0); const timeRange = ref<number>(0);
const theme = ref<string>("light"); const theme = ref<string>("light");
let utc = localStorage.getItem("utc") || "";
if (!utc.includes(":")) {
utc =
(localStorage.getItem("utc") || -(new Date().getTimezoneOffset() / 60)) +
":0";
}
const utcArr = (utc || "").split(":");
const utcHour = isNaN(Number(utcArr[0])) ? 0 : Number(utcArr[0]);
const utcMin = isNaN(Number(utcArr[1])) ? 0 : Number(utcArr[1]);
appStore.setUTC(utcHour, utcMin);
const setConfig = (value: string) => { const setConfig = (value: string) => {
pageName.value = value || ""; pageName.value = value || "";
@ -72,7 +61,7 @@ const handleReload = () => {
const time: Date[] = [new Date(new Date().getTime() - gap), new Date()]; const time: Date[] = [new Date(new Date().getTime() - gap), new Date()];
appStore.setDuration(timeFormat(time)); appStore.setDuration(timeFormat(time));
}; };
function changeTimeRange(val: Date[]) { function changeTimeRange(val: Date[] | any) {
timeRange.value = timeRange.value =
val[1].getTime() - val[0].getTime() > 60 * 24 * 60 * 60 * 1000 ? 1 : 0; val[1].getTime() - val[0].getTime() > 60 * 24 * 60 * 60 * 1000 ? 1 : 0;
if (timeRange.value) { if (timeRange.value) {

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
const msg = { const msg = {
generalService: "General Service", general: "General Service",
services: "Services", services: "Services",
service: "Service", service: "Service",
traces: "Traces", traces: "Traces",
@ -96,6 +96,22 @@ const msg = {
taskList: "Task List", taskList: "Task List",
sampledTraces: "Sampled Traces", sampledTraces: "Sampled Traces",
editTab: "Enable editing tab names", editTab: "Enable editing tab names",
label: "Service Name",
id: "Service ID",
setRoot: "Set this to root",
setNormal: "Set this to normal",
export: "Export Dashboard Templates",
import: "Import Dashboard Templates",
yes: "Yes",
no: "No",
tableHeaderCol1: "Name of the first column of the table",
tableHeaderCol2: "Name of the second column of the table",
showXAxis: "Show X Axis",
showYAxis: "Show Y Axis",
nameError: "The dashboard name cannot be duplicate",
showGroup: "Show Group",
noRoot: "Please set a root dashboard for",
noWidget: "Please add widgets.",
hourTip: "Select Hour", hourTip: "Select Hour",
minuteTip: "Select Minute", minuteTip: "Select Minute",
secondTip: "Select Second", secondTip: "Select Second",
@ -234,7 +250,7 @@ const msg = {
parentService: "Parent Service", parentService: "Parent Service",
isParentService: "Set Parent Service", isParentService: "Set Parent Service",
noneParentService: "No Parent Service", noneParentService: "No Parent Service",
serviceGroup: "Service Group", group: "Service Group",
endpointFilter: "Endpoint Filter", endpointFilter: "Endpoint Filter",
databaseView: "Database", databaseView: "Database",
browserView: "Browser", browserView: "Browser",
@ -331,8 +347,6 @@ const msg = {
conditionNotice: conditionNotice:
"Notice: Please press Enter after inputting a tag, key of content, exclude key of content(key=value).", "Notice: Please press Enter after inputting a tag, key of content, exclude key of content(key=value).",
cacheModalTitle: "Clear cache reminder", cacheModalTitle: "Clear cache reminder",
yes: "Yes",
no: "No",
cacheReminderContent: cacheReminderContent:
"SkyWalking detected dashboard template updates, do you want to update?", "SkyWalking detected dashboard template updates, do you want to update?",
language: "Language", language: "Language",

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
const msg = { const msg = {
generalService: "普通服务", general: "普通服务",
services: "服务", services: "服务",
traces: "跟踪", traces: "跟踪",
metrics: "指标", metrics: "指标",
@ -96,6 +96,22 @@ const msg = {
taskList: "任务列表", taskList: "任务列表",
sampledTraces: "采样的追踪", sampledTraces: "采样的追踪",
editTab: "开启编辑Tab的名称", editTab: "开启编辑Tab的名称",
label: "服务名称",
id: "服务编号",
setRoot: "设置成为根",
setNormal: "设置成为普通",
export: "导出仪表板模板",
import: "导入仪表板模板",
yes: "是",
no: "否",
tableHeaderCol1: "表格的第一列的名称",
tableHeaderCol2: "表格的第二列的名称",
showXAxis: "显示X轴",
showYAxis: "显示Y轴",
nameError: "仪表板名称不能重复",
noRoot: "请设置根仪表板,为",
showGroup: "显示分组",
noWidget: "请添加组件",
hourTip: "选择小时", hourTip: "选择小时",
minuteTip: "选择分钟", minuteTip: "选择分钟",
secondTip: "选择秒数", secondTip: "选择秒数",
@ -236,7 +252,7 @@ const msg = {
parentService: "父级服务", parentService: "父级服务",
isParentService: "设置父服务", isParentService: "设置父服务",
noneParentService: "不设置父服务", noneParentService: "不设置父服务",
serviceGroup: "服务组", group: "服务组",
endpointFilter: "端点过滤器", endpointFilter: "端点过滤器",
databaseView: "数据库视图", databaseView: "数据库视图",
browserView: "浏览器视图", browserView: "浏览器视图",
@ -332,8 +348,6 @@ const msg = {
conditionNotice: conditionNotice:
"请输入一个标签、内容关键词或者内容不包含的关键词(key=value)之后回车", "请输入一个标签、内容关键词或者内容不包含的关键词(key=value)之后回车",
cacheModalTitle: "清除缓存提醒", cacheModalTitle: "清除缓存提醒",
yes: "是的",
no: "不",
cacheReminderContent: "SkyWalking检测到仪表板模板更新是否需要更新", cacheReminderContent: "SkyWalking检测到仪表板模板更新是否需要更新",
language: "语言", language: "语言",
}; };

View File

@ -36,17 +36,7 @@ export const routesDatabase: Array<RouteRecordRaw> = [
headPath: "/database", headPath: "/database",
exact: true, exact: true,
}, },
component: () => import("@/views/service/Service.vue"), component: () => import("@/views/Layer.vue"),
},
{
path: "/database/:id/:type",
name: "DatabasePanel",
meta: {
title: "databasePanel",
headPath: "/database",
exact: true,
},
component: () => import("@/views/service/Panel.vue"),
}, },
], ],
}, },

View File

@ -20,9 +20,9 @@ import Layout from "@/layout/Index.vue";
export const routesGen: Array<RouteRecordRaw> = [ export const routesGen: Array<RouteRecordRaw> = [
{ {
path: "", path: "",
name: "GeneralService", name: "General",
meta: { meta: {
title: "generalService", title: "general",
icon: "chart", icon: "chart",
hasGroup: false, hasGroup: false,
exact: true, exact: true,
@ -30,24 +30,14 @@ export const routesGen: Array<RouteRecordRaw> = [
component: Layout, component: Layout,
children: [ children: [
{ {
path: "/generalService", path: "/general",
name: "Services", name: "GeneralServices",
meta: { meta: {
title: "services", title: "services",
headPath: "/generalService/service", headPath: "/general/service",
exact: true, exact: true,
}, },
component: () => import("@/views/service/Service.vue"), component: () => import("@/views/Layer.vue"),
},
{
path: "/generalService/service/:id/:type",
name: "GeneralServicePanel",
meta: {
title: "generalServicePanel",
headPath: "/generalService/service",
exact: true,
},
component: () => import("@/views/service/Panel.vue"),
}, },
], ],
}, },

View File

@ -15,8 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
// import Layout from "@/layout/Index.vue"; import { routesGen } from "./general";
import { routesGen } from "./generalService";
import { routesMesh } from "./serviceMesh"; import { routesMesh } from "./serviceMesh";
import { routesDatabase } from "./database"; import { routesDatabase } from "./database";
import { routesInfra } from "./infrastructure"; import { routesInfra } from "./infrastructure";
@ -52,9 +51,10 @@ router.beforeEach((to, from, next) => {
(window as any).axiosCancel = []; (window as any).axiosCancel = [];
} }
if (to.path === "/") { if (to.path === "/") {
next({ path: "/generalService" }); next({ path: "/general" });
} else { } else {
next(); next();
} }
}); });
export default router; export default router;

View File

@ -36,7 +36,7 @@ export const routesMesh: Array<RouteRecordRaw> = [
title: "services", title: "services",
headPath: "/mesh/services", headPath: "/mesh/services",
}, },
component: () => import("@/views/service/Service.vue"), component: () => import("@/views/Layer.vue"),
}, },
{ {
path: "/mesh/controlPanel", path: "/mesh/controlPanel",
@ -45,7 +45,7 @@ export const routesMesh: Array<RouteRecordRaw> = [
title: "controlPanel", title: "controlPanel",
headPath: "/mesh/controlPanel", headPath: "/mesh/controlPanel",
}, },
component: () => import("@/views/service/Service.vue"), component: () => import("@/views/Layer.vue"),
}, },
{ {
path: "/mesh/dataPanel", path: "/mesh/dataPanel",
@ -54,40 +54,7 @@ export const routesMesh: Array<RouteRecordRaw> = [
title: "dataPanel", title: "dataPanel",
headPath: "/mesh/dataPanel", headPath: "/mesh/dataPanel",
}, },
component: () => import("@/views/service/Service.vue"), component: () => import("@/views/Layer.vue"),
},
{
path: "/mesh/services/:id/:type",
name: "MeshServicePanel",
meta: {
title: "meshServicePanel",
headPath: "/mesh/services",
exact: true,
notShow: true,
},
component: () => import("@/views/service/Panel.vue"),
},
{
path: "/mesh/controlPanel/:id/:type",
name: "MeshControlPanel",
meta: {
title: "controlPanel",
headPath: "/mesh/controlPanel",
exact: true,
notShow: true,
},
component: () => import("@/views/service/Panel.vue"),
},
{
path: "/mesh/dataPanel/:id/:type",
name: "MeshDataPanel",
meta: {
title: "dataPanel",
headPath: "/mesh/dataPanel",
exact: true,
notShow: true,
},
component: () => import("@/views/service/Panel.vue"),
}, },
], ],
}, },

View File

@ -29,161 +29,3 @@ export const NewControl = {
metrics: [""], metrics: [""],
metricTypes: [""], metricTypes: [""],
}; };
export const ConfigData: any = {
x: 0,
y: 0,
w: 8,
h: 12,
i: "0",
metrics: ["service_resp_time"],
metricTypes: ["readMetricsValues"],
type: "Widget",
widget: {
title: "service_resp_time",
tips: "Tooltip",
},
graph: {
type: "Line",
showXAxis: true,
showYAxis: true,
},
standard: {
sortOrder: "DEC",
unit: "min",
},
children: [],
};
export const ConfigData1: any = {
x: 0,
y: 0,
w: 8,
h: 12,
i: "0",
metrics: ["service_instance_resp_time"],
metricTypes: ["readMetricsValues"],
type: "Widget",
widget: {
title: "service_instance_resp_time",
tips: "Tooltip",
},
graph: {
type: "Line",
showXAxis: true,
showYAxis: true,
},
standard: {
sortOrder: "DEC",
unit: "min",
},
children: [],
};
export const ConfigData2: any = {
x: 0,
y: 0,
w: 8,
h: 12,
i: "0",
metrics: ["endpoint_avg"],
metricTypes: ["readMetricsValues"],
type: "Widget",
widget: {
title: "endpoint_avg",
tips: "Tooltip",
},
graph: {
type: "Line",
showXAxis: true,
showYAxis: true,
},
standard: {
sortOrder: "DEC",
unit: "min",
},
children: [],
};
export const ConfigData3: any = [
{
x: 0,
y: 0,
w: 8,
h: 12,
i: "0",
metrics: ["all_heatmap"],
metricTypes: ["readHeatMap"],
type: "Widget",
widget: {
title: "all_heatmap",
tips: "Tooltip",
},
graph: {
type: "HeatMap",
},
standard: {
unit: "min",
},
children: [],
},
];
export const ConfigData4: any = {
x: 0,
y: 0,
w: 8,
h: 12,
i: "0",
metrics: ["service_relation_server_resp_time"],
metricTypes: ["readMetricsValues"],
type: "Widget",
widget: {
title: "service_relation_server_resp_time",
tips: "Tooltip",
},
graph: {
type: "Line",
},
standard: {
unit: "min",
},
children: [],
};
export const ConfigData5: any = {
x: 0,
y: 0,
w: 8,
h: 12,
i: "0",
metrics: ["endpoint_relation_cpm"],
metricTypes: ["readMetricsValues"],
type: "Widget",
widget: {
title: "endpoint_relation_cpm",
tips: "Tooltip",
},
graph: {
type: "Line",
},
standard: {
unit: "min",
},
children: [],
};
export const ConfigData6: any = {
x: 0,
y: 0,
w: 8,
h: 12,
i: "0",
metrics: ["service_instance_relation_server_cpm"],
metricTypes: ["readMetricsValues"],
type: "Widget",
widget: {
title: "service_instance_relation_server_cpm",
tips: "Tooltip",
},
graph: {
type: "Line",
},
standard: {
unit: "min",
},
children: [],
};

View File

@ -16,10 +16,12 @@
*/ */
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { store } from "@/store"; import { store } from "@/store";
import graphql from "@/graphql";
import { Duration, DurationTime } from "@/types/app"; import { Duration, DurationTime } from "@/types/app";
import getLocalTime from "@/utils/localtime"; import getLocalTime from "@/utils/localtime";
import getDurationRow from "@/utils/dateTime"; import { AxiosResponse } from "axios";
import dateFormatStep, { dateFormatTime } from "@/utils/dateFormat"; import dateFormatStep, { dateFormatTime } from "@/utils/dateFormat";
import { TimeType } from "@/constants/data";
/*global Nullable*/ /*global Nullable*/
interface AppState { interface AppState {
durationRow: any; durationRow: any;
@ -35,7 +37,11 @@ interface AppState {
export const appStore = defineStore({ export const appStore = defineStore({
id: "app", id: "app",
state: (): AppState => ({ state: (): AppState => ({
durationRow: getDurationRow(), durationRow: {
start: new Date(new Date().getTime() - 1800000),
end: new Date(),
step: TimeType.MINUTE_TIME,
},
utc: "", utc: "",
utcHour: 0, utcHour: 0,
utcMin: 0, utcMin: 0,
@ -102,7 +108,6 @@ export const appStore = defineStore({
actions: { actions: {
setDuration(data: Duration): void { setDuration(data: Duration): void {
this.durationRow = data; this.durationRow = data;
localStorage.setItem("durationRow", JSON.stringify(data, null, 0));
if ((window as any).axiosCancel.length !== 0) { if ((window as any).axiosCancel.length !== 0) {
for (const event of (window as any).axiosCancel) { for (const event of (window as any).axiosCancel) {
setTimeout(event(), 0); setTimeout(event(), 0);
@ -116,7 +121,6 @@ export const appStore = defineStore({
this.utcMin = utcMin; this.utcMin = utcMin;
this.utcHour = utcHour; this.utcHour = utcHour;
this.utc = `${utcHour}:${utcMin}`; this.utc = `${utcHour}:${utcMin}`;
localStorage.setItem("utc", this.utc);
}, },
setEventStack(funcs: (() => void)[]): void { setEventStack(funcs: (() => void)[]): void {
this.eventStack = funcs; this.eventStack = funcs;
@ -139,6 +143,18 @@ export const appStore = defineStore({
500 500
); );
}, },
async queryOAPTimeInfo() {
const res: AxiosResponse = await graphql
.query("queryOAPTimeInfo")
.params({});
if (res.data.errors) {
this.utc = -(new Date().getTimezoneOffset() / 60) + ":0";
return res.data;
}
this.utc = res.data.data.getTimeInfo.timezone / 100 + ":0";
return res.data;
},
}, },
}); });
export function useAppStoreWithOut(): any { export function useAppStoreWithOut(): any {

View File

@ -19,20 +19,14 @@ import { store } from "@/store";
import { LayoutConfig } from "@/types/dashboard"; import { LayoutConfig } from "@/types/dashboard";
import graphql from "@/graphql"; import graphql from "@/graphql";
import query from "@/graphql/fetch"; import query from "@/graphql/fetch";
import { import { DashboardItem } from "@/types/dashboard";
ConfigData,
ConfigData1,
ConfigData2,
ConfigData3,
ConfigData4,
ConfigData5,
ConfigData6,
} from "../data";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { NewControl } from "../data"; import { NewControl } from "../data";
import { Duration } from "@/types/app"; import { Duration } from "@/types/app";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
interface DashboardState { interface DashboardState {
showConfig: boolean; showConfig: boolean;
layout: LayoutConfig[]; layout: LayoutConfig[];
@ -44,12 +38,14 @@ interface DashboardState {
selectorStore: any; selectorStore: any;
showTopology: boolean; showTopology: boolean;
currentTabItems: LayoutConfig[]; currentTabItems: LayoutConfig[];
dashboards: DashboardItem[];
currentDashboard: Nullable<DashboardItem>;
} }
export const dashboardStore = defineStore({ export const dashboardStore = defineStore({
id: "dashboard", id: "dashboard",
state: (): DashboardState => ({ state: (): DashboardState => ({
layout: [ConfigData], layout: [],
showConfig: false, showConfig: false,
selectedGrid: null, selectedGrid: null,
entity: "", entity: "",
@ -59,11 +55,20 @@ export const dashboardStore = defineStore({
selectorStore: useSelectorStore(), selectorStore: useSelectorStore(),
showTopology: false, showTopology: false,
currentTabItems: [], currentTabItems: [],
dashboards: [],
currentDashboard: null,
}), }),
actions: { actions: {
setLayout(data: LayoutConfig[]) { setLayout(data: LayoutConfig[]) {
this.layout = data; this.layout = data;
}, },
resetDashboards(list: DashboardItem[]) {
this.dashboards = list;
sessionStorage.setItem("dashboards", JSON.stringify(list));
},
setCurrentDashboard(item: DashboardItem) {
this.currentDashboard = item;
},
addControl(type: string) { addControl(type: string) {
const newItem: LayoutConfig = { const newItem: LayoutConfig = {
...NewControl, ...NewControl,
@ -129,7 +134,7 @@ export const dashboardStore = defineStore({
if (idx < 0) { if (idx < 0) {
return; return;
} }
const tabIndex = this.layout[idx].activedTabIndex; const tabIndex = this.layout[idx].activedTabIndex || 0;
const { children } = this.layout[idx].children[tabIndex]; const { children } = this.layout[idx].children[tabIndex];
const newItem: LayoutConfig = { const newItem: LayoutConfig = {
...NewControl, ...NewControl,
@ -229,28 +234,6 @@ export const dashboardStore = defineStore({
}, },
setEntity(type: string) { setEntity(type: string) {
this.entity = type; this.entity = type;
// todo
if (type === "ServiceInstance") {
this.layout = [ConfigData1];
}
if (type === "Endpoint") {
this.layout = [ConfigData2];
}
if (type == "All") {
this.layout = ConfigData3;
}
if (type == "Service") {
this.layout = [ConfigData];
}
if (type == "ServiceRelation") {
this.layout = [ConfigData4];
}
if (type == "ServiceInstanceRelation") {
this.layout = [ConfigData6];
}
if (type == "EndpointRelation") {
this.layout = [ConfigData5];
}
}, },
setTopology(show: boolean) { setTopology(show: boolean) {
this.showTopology = show; this.showTopology = show;
@ -303,6 +286,154 @@ export const dashboardStore = defineStore({
const res: AxiosResponse = await query(param); const res: AxiosResponse = await query(param);
return res.data; return res.data;
}, },
async fetchTemplates() {
const res: AxiosResponse = await graphql.query("getTemplates").params({});
if (res.data.errors) {
return res.data;
}
const data = res.data.data.getAllTemplates;
const list = [];
for (const t of data) {
const c = JSON.parse(t.configuration);
const key = [c.layer, c.entity, c.name.split(" ").join("-")].join("_");
list.push({
id: t.id,
layer: c.layer,
entity: c.entity,
name: c.name,
isRoot: c.isRoot,
});
sessionStorage.setItem(
key,
JSON.stringify({ id: t.id, configuration: c })
);
}
sessionStorage.setItem("dashboards", JSON.stringify(list));
return res.data;
},
async setDashboards() {
if (!sessionStorage.getItem("dashboards")) {
const res = await this.fetchTemplates();
if (res.errors) {
this.dashboards = [];
ElMessage.error(res.errors);
return;
}
}
this.dashboards = JSON.parse(
sessionStorage.getItem("dashboards") || "[]"
);
},
async updateDashboard(setting: { id: string; configuration: string }) {
const res: AxiosResponse = await graphql.query("updateTemplate").params({
setting,
});
if (res.data.errors) {
ElMessage.error(res.data.errors);
return res.data;
}
const json = res.data.data.changeTemplate;
if (!json.status) {
ElMessage.error(json.message);
return res.data;
}
ElMessage.success("Saved successfully");
return res.data;
},
async saveDashboard() {
if (!this.currentDashboard.name) {
ElMessage.error("The dashboard name is needed.");
return;
}
const c = {
children: this.layout,
...this.currentDashboard,
};
let res: any;
let json;
if (this.currentDashboard.id) {
res = await this.updateDashboard({
id: this.currentDashboard.id,
configuration: JSON.stringify(c),
});
json = res.data.changeTemplate;
} else {
c.isRoot = false;
const index = this.dashboards.findIndex(
(d: DashboardItem) =>
d.name === this.currentDashboard.name &&
d.entity === this.currentDashboard.entity &&
d.layer === this.currentDashboard.layerId
);
if (index > -1) {
const { t } = useI18n();
ElMessage.error(t("nameError"));
return;
}
res = await graphql
.query("addNewTemplate")
.params({ setting: { configuration: JSON.stringify(c) } });
json = res.data.data.addTemplate;
}
if (res.data.errors || res.errors) {
ElMessage.error(res.data.errors);
return res.data;
}
if (!json.status) {
ElMessage.error(json.message);
return;
}
if (!this.currentDashboard.id) {
ElMessage.success("Saved successfully");
}
const key = [
this.currentDashboard.layer,
this.currentDashboard.entity,
this.currentDashboard.name.split(" ").join("-"),
].join("_");
if (this.currentDashboard.id) {
sessionStorage.removeItem(key);
this.dashboards = this.dashboards.filter(
(d: DashboardItem) => d.id !== this.currentDashboard.id
);
}
this.dashboards.push({
...this.currentDashboard,
id: json.id,
});
const l = { id: json.id, configuration: c };
sessionStorage.setItem(key, JSON.stringify(l));
sessionStorage.setItem("dashboards", JSON.stringify(this.dashboards));
},
async deleteDashboard() {
const res: AxiosResponse = await graphql
.query("removeTemplate")
.params({ id: this.currentDashboard.id });
if (res.data.errors) {
ElMessage.error(res.data.errors);
return res.data;
}
const json = res.data.data.disableTemplate;
if (!json.status) {
ElMessage.error(json.message);
return res.data;
}
this.dashboards = this.dashboards.filter(
(d: any) => d.id !== this.currentDashboard.id
);
const key = [
this.currentDashboard.layer,
this.currentDashboard.entity,
this.currentDashboard.name.split(" ").join("-"),
].join("_");
sessionStorage.removeItem(key);
},
}, },
}); });

View File

@ -38,7 +38,7 @@ interface LogState {
} }
export const logStore = defineStore({ export const logStore = defineStore({
id: "trace", id: "log",
state: (): LogState => ({ state: (): LogState => ({
services: [{ value: "0", label: "All" }], services: [{ value: "0", label: "All" }],
instances: [{ value: "0", label: "All" }], instances: [{ value: "0", label: "All" }],
@ -101,7 +101,7 @@ export const logStore = defineStore({
] || [{ value: "0", label: "All" }]; ] || [{ value: "0", label: "All" }];
return res.data; return res.data;
}, },
async queryLogsByKeywords() { async getLogsByKeywords() {
const res: AxiosResponse = await graphql const res: AxiosResponse = await graphql
.query("queryLogsByKeywords") .query("queryLogsByKeywords")
.params({}); .params({});

View File

@ -44,7 +44,7 @@ interface ProfileState {
highlightTop: boolean; highlightTop: boolean;
} }
export const traceStore = defineStore({ export const profileStore = defineStore({
id: "profile", id: "profile",
state: (): ProfileState => ({ state: (): ProfileState => ({
services: [{ value: "0", label: "All" }], services: [{ value: "0", label: "All" }],
@ -208,5 +208,5 @@ export const traceStore = defineStore({
}); });
export function useProfileStore(): any { export function useProfileStore(): any {
return traceStore(store); return profileStore(store);
} }

View File

@ -14,6 +14,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
export type DashboardItem = {
id?: string;
entity: string;
layer: string;
isRoot: boolean;
name: string;
};
export interface LayoutConfig { export interface LayoutConfig {
x: number; x: number;
y: number; y: number;
@ -38,8 +46,8 @@ export interface WidgetConfig {
export interface StandardConfig { export interface StandardConfig {
sortOrder?: string; sortOrder?: string;
unit?: string; unit?: string;
max?: string; labelsIndex?: string;
min?: string; metricLabels?: string;
plus?: string; plus?: string;
minus?: string; minus?: string;
multiply?: string; multiply?: string;
@ -99,6 +107,7 @@ export interface ServiceListConfig {
type?: string; type?: string;
dashboardName: string; dashboardName: string;
fontSize: number; fontSize: number;
showGroup: boolean;
} }
export interface InstanceListConfig { export interface InstanceListConfig {

View File

@ -14,31 +14,31 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Duration } from "@/types/app";
import { TimeType } from "@/constants/data";
/** export const readFile = (event: any) => {
* init or generate durationRow Obj and save localStorage. return new Promise((resolve) => {
*/ const { files } = event.target;
const getDurationRow = (): Duration => { if (files.length < 1) {
const durationRowString = localStorage.getItem("durationRow"); return;
let durationRow: Duration; }
if (durationRowString && durationRowString !== "") { const file = files[0];
durationRow = JSON.parse(durationRowString); const reader: FileReader = new FileReader();
durationRow = { reader.readAsText(file);
start: new Date(durationRow.start), reader.onload = function () {
end: new Date(durationRow.end), if (typeof this.result === "string") {
step: durationRow.step, resolve(JSON.parse(this.result));
};
} else {
durationRow = {
start: new Date(new Date().getTime() - 900000),
end: new Date(),
step: TimeType.MINUTE_TIME,
};
localStorage.setItem("durationRow", JSON.stringify(durationRow, null, 0));
} }
return durationRow;
}; };
});
export default getDurationRow; };
export const saveFile = (data: any, name: string) => {
const newData = JSON.stringify(data);
const tagA = document.createElement("a");
tagA.download = name;
tagA.style.display = "none";
const blob = new Blob([newData]);
tagA.href = URL.createObjectURL(blob);
document.body.appendChild(tagA);
tagA.click();
document.body.removeChild(tagA);
};

View File

@ -14,9 +14,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import graphql from "@/graphql";
import { AxiosResponse } from "axios";
const getLocalTime = (utc: string, time: Date): Date => { const getLocalTime = (utc: string, time: Date): Date => {
const utcArr = utc.split(":"); const utcArr = utc.split(":");
const utcHour = isNaN(Number(utcArr[0])) ? 0 : Number(utcArr[0]); const utcHour = isNaN(Number(utcArr[0])) ? 0 : Number(utcArr[0]);
@ -28,31 +25,4 @@ const getLocalTime = (utc: string, time: Date): Date => {
return new Date(utcTime + 3600000 * utcHour + utcMin * 60000); return new Date(utcTime + 3600000 * utcHour + utcMin * 60000);
}; };
const setTimezoneOffset = () => {
window.localStorage.setItem(
"utc",
-(new Date().getTimezoneOffset() / 60) + ":0"
);
};
export const queryOAPTimeInfo = async (): Promise<void> => {
let utc = window.localStorage.getItem("utc");
if (!utc) {
const res: AxiosResponse = await graphql
.query("queryOAPTimeInfo")
.params({});
if (
!res.data ||
!res.data.data ||
!res.data.data.getTimeInfo ||
!res.data.data.getTimeInfo.timezone
) {
setTimezoneOffset();
return;
}
utc = res.data.data.getTimeInfo.timezone / 100 + ":0";
window.localStorage.setItem("utc", utc);
}
};
export default getLocalTime; export default getLocalTime;

101
src/views/Layer.vue Normal file
View File

@ -0,0 +1,101 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<Edit v-if="dashboardStore.currentDashboard" />
<div class="no-root" v-else>{{ t("noRoot") }} {{ layer }}</div>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { EntityType } from "./dashboard/data";
import { useDashboardStore } from "@/store/modules/dashboard";
import Edit from "./dashboard/Edit.vue";
const { t } = useI18n();
const route = useRoute();
const dashboardStore = useDashboardStore();
const routeNames = [
"GeneralServices",
"Database",
"MeshServices",
"ControlPanel",
"DataPanel",
];
const layer = ref<string>("GENERAL");
getDashboard();
async function getDashboard() {
dashboardStore.setCurrentDashboard(null);
setLayer(String(route.name));
await dashboardStore.setDashboards();
const index = dashboardStore.dashboards.findIndex(
(d: { name: string; isRoot: boolean; layer: string; entity: string }) =>
d.layer === layer.value && d.entity === EntityType[1].value && d.isRoot
);
if (index < 0) {
return;
}
const d = dashboardStore.dashboards[index];
dashboardStore.setCurrentDashboard(d);
}
function setLayer(n: string) {
switch (n) {
case routeNames[0]:
layer.value = "GENERAL";
break;
case routeNames[1]:
layer.value = "VIRTUAL_DATABASE";
break;
case routeNames[2]:
layer.value = "MESH";
break;
case routeNames[3]:
layer.value = "MESH_CP";
break;
case routeNames[4]:
layer.value = "MESH_DP";
break;
default:
layer.value = "GENERAL";
break;
}
dashboardStore.setLayer(layer.value);
dashboardStore.setEntity(EntityType[1].value);
// appStore.setPageTitle(layer.value);
}
watch(
() => route.name,
(name: unknown) => {
if (!name) {
return;
}
getDashboard();
}
);
</script>
<style lang="scss" scoped>
.no-root {
padding: 15px;
width: 100%;
text-align: center;
color: #888;
}
.layer {
height: 100%;
}
</style>

View File

@ -76,7 +76,6 @@ import { ref, reactive } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import timeFormat from "@/utils/timeFormat"; import timeFormat from "@/utils/timeFormat";
import { ElSwitch } from "element-plus";
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const state = reactive<{ timer: ReturnType<typeof setInterval> | null }>({ const state = reactive<{ timer: ReturnType<typeof setInterval> | null }>({

View File

@ -65,7 +65,6 @@ const tagsList = ref<string[]>([]);
function removeTags(index: number) { function removeTags(index: number) {
tagsList.value.splice(index, 1); tagsList.value.splice(index, 1);
updateTags(); updateTags();
localStorage.setItem("traceTags", JSON.stringify(this.tagsList));
} }
function addLabels() { function addLabels() {
if (!tags.value) { if (!tags.value) {

View File

@ -13,8 +13,12 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<Tool /> <Tool v-if="p.entity" />
<div class="ds-main" @click="handleClick"> <div
class="ds-main"
@click="handleClick"
:style="{ height: p.entity ? 'calc(100% - 45px)' : '100%' }"
>
<grid-layout /> <grid-layout />
<el-dialog <el-dialog
v-model="dashboardStore.showConfig" v-model="dashboardStore.showConfig"
@ -38,9 +42,10 @@ limitations under the License. -->
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import GridLayout from "./panel/Layout.vue"; import GridLayout from "./panel/Layout.vue";
// import { LayoutConfig } from "@/types/dashboard";
import Tool from "./panel/Tool.vue"; import Tool from "./panel/Tool.vue";
import Widget from "./configuration/Widget.vue"; import Widget from "./configuration/Widget.vue";
import TopologyConfig from "./configuration/Topology.vue"; import TopologyConfig from "./configuration/Topology.vue";
@ -51,26 +56,34 @@ import { useAppStoreWithOut } from "@/store/modules/app";
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const { t } = useI18n(); const { t } = useI18n();
// fetch layout data from serve side const p = useRoute().params;
// const layout: any[] = [ const layoutKey = ref<string>(`${p.layerId}_${p.entity}_${p.name}`);
// { x: 0, y: 0, w: 4, h: 12, i: "0" },
// { x: 4, y: 0, w: 4, h: 12, i: "1" }, setTemplate();
// { x: 8, y: 0, w: 4, h: 15, i: "2" }, async function setTemplate() {
// { x: 12, y: 0, w: 4, h: 9, i: "3" }, await dashboardStore.setDashboards();
// { x: 16, y: 0, w: 4, h: 9, i: "4" },
// { x: 20, y: 0, w: 4, h: 9, i: "5" }, if (!p.entity) {
// { x: 0, y: 12, w: 4, h: 15, i: "7" }, const { layer, entity, name } = dashboardStore.currentDashboard;
// { x: 4, y: 12, w: 4, h: 15, i: "8" }, layoutKey.value = `${layer}_${entity}_${name.split(" ").join("-")}`;
// { x: 8, y: 15, w: 4, h: 12, i: "9" }, }
// { x: 12, y: 9, w: 4, h: 12, i: "10" }, const c: { configuration: string; id: string } = JSON.parse(
// { x: 16, y: 9, w: 4, h: 12, i: "11" }, sessionStorage.getItem(layoutKey.value) || "{}"
// { x: 20, y: 9, w: 4, h: 15, i: "12" }, );
// { x: 0, y: 27, w: 4, h: 12, i: "14" }, const layout: any = c.configuration || {};
// { x: 4, y: 27, w: 4, h: 12, i: "15" }, dashboardStore.setLayout(layout.children || []);
// { x: 8, y: 27, w: 4, h: 15, i: "16" }, appStore.setPageTitle(layout.name);
// ];
// dashboardStore.setLayout(layout); if (!dashboardStore.currentDashboard) {
appStore.setPageTitle("Dashboard Name"); dashboardStore.setCurrentDashboard({
layer: p.layerId,
entity: p.entity,
name: String(p.name).split("-").join(" "),
id: c.id,
isRoot: layout.isRoot,
});
}
}
function handleClick(e: any) { function handleClick(e: any) {
e.stopPropagation(); e.stopPropagation();
if (e.target.className === "ds-main") { if (e.target.className === "ds-main") {
@ -81,18 +94,6 @@ function handleClick(e: any) {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.ds-main { .ds-main {
height: calc(100% - 45px);
overflow: auto; overflow: auto;
} }
.layout {
height: 100%;
flex-grow: 2;
overflow: hidden;
}
.grids {
height: 100%;
overflow-y: auto;
}
</style> </style>

View File

@ -17,13 +17,14 @@ limitations under the License. -->
<div class="flex-h header" style="margin: 10px 0"> <div class="flex-h header" style="margin: 10px 0">
<el-input <el-input
v-model="searchText" v-model="searchText"
placeholder="Please input" placeholder="Please input name"
class="input-with-search" class="input-with-search"
size="small" size="small"
@change="searchDashboards"
> >
<template #append> <template #append>
<el-button size="small"> <el-button size="small">
<Icon size="lg" iconName="search" /> <Icon size="sm" iconName="search" />
</el-button> </el-button>
</template> </template>
</el-input> </el-input>
@ -33,38 +34,106 @@ limitations under the License. -->
</el-button> </el-button>
</router-link> </router-link>
</div> </div>
<el-table :data="tableData" style="width: 100%" max-height="550"> <div class="table">
<el-table-column fixed prop="name" label="Name" /> <el-table
<el-table-column prop="type" label="Type" /> :data="dashboards"
<el-table-column prop="date" label="Date" /> :style="{ width: '100%', fontSize: '13px' }"
v-loading="loading"
ref="multipleTableRef"
:default-sort="{ prop: 'name' }"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="layer" label="Layer" width="200" />
<el-table-column prop="entity" label="Entity" width="200" />
<el-table-column prop="isRoot" label="Root" width="100">
<template #default="scope">
<span>
{{ scope.row.isRoot ? t("yes") : t("no") }}
</span>
</template>
</el-table-column>
<el-table-column label="Operations"> <el-table-column label="Operations">
<template #default="scope"> <template #default="scope">
<el-button size="small" @click="handleEdit(scope.$index, scope.row)"> <el-button size="small" @click="handleView(scope.row)">
{{ t("view") }} {{ t("view") }}
</el-button> </el-button>
<el-button size="small" @click="handleEdit(scope.$index, scope.row)"> <el-button size="small" @click="handleEdit(scope.row)">
{{ t("edit") }} {{ t("edit") }}
</el-button> </el-button>
<el-button <el-popconfirm
size="small" title="Are you sure to delete this?"
type="danger" @confirm="handleDelete(scope.row)"
@click="handleDelete(scope.$index, scope.row)"
> >
<template #reference>
<el-button size="small" type="danger">
{{ t("delete") }} {{ t("delete") }}
</el-button> </el-button>
</template> </template>
</el-popconfirm>
<el-popconfirm
title="Are you sure to set this?"
@confirm="setRoot(scope.row)"
v-if="scope.row.entity === EntityType[1].value"
>
<template #reference>
<el-button size="small" style="width: 120px" type="danger">
{{ scope.row.isRoot ? t("setNormal") : t("setRoot") }}
</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="toggle-selection">
<el-button size="default" class="btn" @click="exportTemplates">
<Icon class="mr-5" iconName="save_alt" />
{{ t("export") }}
</el-button>
<el-button class="ml-10 btn" size="default">
<input
ref="dashboardFile"
id="dashboard-file"
class="import-template"
type="file"
name="file"
title=""
accept=".json"
@change="importTemplates"
/>
<label for="dashboard-file" class="input-label">
<Icon class="mr-5" iconName="folder_open" />
{{ t("import") }}
</label>
</el-button>
</div>
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { ElTable, ElTableColumn, ElButton, ElInput } from "element-plus"; import { ElMessageBox, ElMessage } from "element-plus";
import type { ElTable } from "element-plus";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { useDashboardStore } from "@/store/modules/dashboard";
import router from "@/router";
import { DashboardItem } from "@/types/dashboard";
import { saveFile, readFile } from "@/utils/file";
import { EntityType } from "./data";
import { findLastKey } from "lodash";
/*global Nullable*/
const { t } = useI18n();
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
appStore.setPageTitle("Dashboard List"); const dashboardStore = useDashboardStore();
const dashboards = ref<DashboardItem[]>([]);
const searchText = ref<string>("");
const loading = ref<boolean>(false);
const multipleTableRef = ref<InstanceType<typeof ElTable>>();
const multipleSelection = ref<DashboardItem[]>([]);
const dashboardFile = ref<Nullable<HTMLDivElement>>(null);
// # - os-linux // # - os-linux
// # - k8s // # - k8s
// # - general(agent-installed) // # - general(agent-installed)
@ -76,36 +145,218 @@ appStore.setPageTitle("Dashboard List");
// # - cache // # - cache
// # - browser // # - browser
// # - skywalking // # - skywalking
const { t } = useI18n(); appStore.setPageTitle("Dashboard List");
const searchText = ref<string>(""); const handleSelectionChange = (val: DashboardItem[]) => {
const tableData = [ multipleSelection.value = val;
{
date: "2016-05-03",
name: "xxx",
type: "general",
},
{
date: "2016-05-02",
name: "xxx",
type: "k8s",
},
{
date: "2016-05-04",
name: "xxx",
type: "database",
},
{
date: "2016-05-01",
name: "xxx",
type: "mesh",
},
];
const handleEdit = (index: number, row: any) => {
console.log(index, row);
}; };
const handleDelete = (index: number, row: any) => { setList();
console.log(index, row); async function setList() {
await dashboardStore.setDashboards();
dashboards.value = dashboardStore.dashboards;
}
async function importTemplates(event: any) {
const arr: any = await readFile(event);
for (const item of arr) {
const { layer, name, entity } = item.configuration;
const index = dashboardStore.dashboards.findIndex(
(d: DashboardItem) =>
d.name === name && d.entity === entity && d.layer === layer && !item.id
);
if (index > -1) {
return ElMessage.error(t("nameError"));
}
}
loading.value = true;
for (const item of arr) {
const { layer, name, entity, isRoot, children } = item.configuration;
const index = dashboardStore.dashboards.findIndex(
(d: DashboardItem) => d.id === item.id
);
const p: DashboardItem = {
name: name,
layer: layer,
entity: entity,
isRoot: false,
}; };
if (index > -1) {
p.id = item.id;
p.isRoot = isRoot;
}
dashboardStore.setCurrentDashboard(p);
dashboardStore.setLayout(children);
await dashboardStore.saveDashboard();
}
dashboards.value = dashboardStore.dashboards;
loading.value = false;
dashboardFile.value = null;
}
function exportTemplates() {
const arr = multipleSelection.value.sort(
(a: DashboardItem, b: DashboardItem) => {
return a.name.localeCompare(b.name);
}
);
const templates = arr.map((d: DashboardItem) => {
const key = [d.layer, d.entity, d.name.split(" ").join("-")].join("_");
const layout = JSON.parse(sessionStorage.getItem(key) || "{}");
return layout;
});
const name = `dashboards.json`;
saveFile(templates, name);
setTimeout(() => {
multipleTableRef.value!.clearSelection();
}, 2000);
}
function handleView(row: DashboardItem) {
dashboardStore.setCurrentDashboard(row);
router.push(
`/dashboard/${row.layer}/${row.entity}/${row.name.split(" ").join("-")}`
);
}
async function setRoot(row: DashboardItem) {
const items: DashboardItem[] = [];
loading.value = true;
for (const d of dashboardStore.dashboards) {
if (d.id === row.id) {
d.isRoot = !row.isRoot;
const key = [d.layer, d.entity, d.name.split(" ").join("-")].join("_");
const layout = sessionStorage.getItem(key) || "{}";
const c = {
...JSON.parse(layout).configuration,
...d,
};
delete c.id;
const setting = {
id: d.id,
configuration: JSON.stringify(c),
};
const res = await dashboardStore.updateDashboard(setting);
if (res.data.changeTemplate.id) {
sessionStorage.setItem(
key,
JSON.stringify({
id: d.id,
configuration: c,
})
);
}
} else {
if (
d.layer === row.layer &&
d.entity === row.entity &&
row.isRoot === false &&
d.isRoot === true
) {
d.isRoot = false;
const key = [d.layer, d.entity, d.name.split(" ").join("-")].join("_");
const layout = sessionStorage.getItem(key) || "{}";
const c = {
...JSON.parse(layout).configuration,
...d,
};
const setting = {
id: d.id,
configuration: JSON.stringify(c),
};
const res = await dashboardStore.updateDashboard(setting);
if (res.data.changeTemplate.id) {
sessionStorage.setItem(
key,
JSON.stringify({
id: d.id,
configuration: c,
})
);
}
}
}
items.push(d);
}
dashboardStore.resetDashboards(items);
searchDashboards();
loading.value = false;
}
function handleEdit(row: DashboardItem) {
ElMessageBox.prompt("Please input dashboard name", "Edit", {
confirmButtonText: "OK",
cancelButtonText: "Cancel",
inputValue: row.name,
})
.then(({ value }) => {
updateName(row, value);
})
.catch(() => {
ElMessage({
type: "info",
message: "Input canceled",
});
});
}
async function updateName(d: DashboardItem, value: string) {
const key = [d.layer, d.entity, d.name.split(" ").join("-")].join("_");
const layout = sessionStorage.getItem(key) || "{}";
const c = {
...JSON.parse(layout).configuration,
...d,
name: value,
};
delete c.id;
const setting = {
id: d.id,
configuration: JSON.stringify(c),
};
loading.value = true;
const res = await dashboardStore.updateDashboard(setting);
loading.value = false;
if (!res.data.changeTemplate.id) {
return;
}
dashboardStore.setCurrentDashboard({
...d,
name: value,
});
dashboards.value = dashboardStore.dashboards.map((item: any) => {
if (dashboardStore.currentDashboard.id === item.id) {
item = dashboardStore.currentDashboard;
}
return item;
});
dashboardStore.resetDashboards(dashboards.value);
sessionStorage.setItem("dashboards", JSON.stringify(dashboards.value));
sessionStorage.removeItem(key);
const str = [
dashboardStore.currentDashboard.layer,
dashboardStore.currentDashboard.entity,
dashboardStore.currentDashboard.name.split(" ").join("-"),
].join("_");
sessionStorage.setItem(
str,
JSON.stringify({
id: d.id,
configuration: c,
})
);
searchText.value = "";
}
async function handleDelete(row: DashboardItem) {
dashboardStore.setCurrentDashboard(row);
loading.value = true;
await dashboardStore.deleteDashboard();
dashboards.value = dashboardStore.dashboards;
loading.value = false;
sessionStorage.setItem("dashboards", JSON.stringify(dashboards.value));
sessionStorage.removeItem(
`${row.layer}_${row.entity}_${row.name.split(" ").join("-")}`
);
}
function searchDashboards() {
const list = JSON.parse(sessionStorage.getItem("dashboards") || "[]");
dashboards.value = list.filter((d: { name: string }) =>
d.name.includes(searchText.value)
);
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.header { .header {
@ -120,4 +371,32 @@ const handleDelete = (index: number, row: any) => {
width: 300px; width: 300px;
margin-left: 20px; margin-left: 20px;
} }
.table {
padding: 20px;
background-color: #fff;
box-shadow: 0px 1px 4px 0px #00000029;
border-radius: 5px;
}
.toggle-selection {
margin-top: 20px;
background-color: #fff;
}
.btn {
width: 220px;
font-size: 13px;
}
.import-template {
display: none;
}
.input-label {
line-height: 30px;
height: 30px;
width: 220px;
cursor: pointer;
}
</style> </style>

View File

@ -51,15 +51,17 @@ limitations under the License. -->
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, onBeforeMount } from "vue"; import { reactive } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import router from "@/router"; import router from "@/router";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { EntityType } from "./data"; import { EntityType } from "./data";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { useDashboardStore } from "@/store/modules/dashboard";
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
appStore.setPageTitle("Dashboard New"); appStore.setPageTitle("Dashboard New");
const { t } = useI18n(); const { t } = useI18n();
const selectorStore = useSelectorStore(); const selectorStore = useSelectorStore();
@ -69,12 +71,30 @@ const states = reactive({
entity: EntityType[0].value, entity: EntityType[0].value,
layers: [], layers: [],
}); });
setLayers();
dashboardStore.setDashboards();
const onCreate = () => { const onCreate = () => {
const index = dashboardStore.dashboards.findIndex(
(d: { name: string; entity: string; layer: string }) =>
d.name === states.name &&
states.entity === d.entity &&
states.selectedLayer === d.layer
);
if (index > -1) {
ElMessage.error(t("nameError"));
return;
}
dashboardStore.setCurrentDashboard({
name: states.name,
entity: states.entity,
layer: states.selectedLayer,
});
const name = states.name.split(" ").join("-"); const name = states.name.split(" ").join("-");
const path = `/dashboard/${states.selectedLayer}/${states.entity}/${name}`; const path = `/dashboard/${states.selectedLayer}/${states.entity}/${name}`;
router.push(path); router.push(path);
}; };
onBeforeMount(async () => { async function setLayers() {
const resp = await selectorStore.fetchLayers(); const resp = await selectorStore.fetchLayers();
if (resp.errors) { if (resp.errors) {
ElMessage.error(resp.errors); ElMessage.error(resp.errors);
@ -83,11 +103,11 @@ onBeforeMount(async () => {
states.layers = resp.data.layers.map((d: string) => { states.layers = resp.data.layers.map((d: string) => {
return { label: d, value: d }; return { label: d, value: d };
}); });
}); }
function changeLayer(opt: { label: string; value: string }[]) { function changeLayer(opt: { label: string; value: string }[] | any) {
states.selectedLayer = opt[0].value; states.selectedLayer = opt[0].value;
} }
function changeEntity(opt: { label: string; value: string }[]) { function changeEntity(opt: { label: string; value: string }[] | any) {
states.entity = opt[0].value; states.entity = opt[0].value;
} }
</script> </script>

View File

@ -17,6 +17,9 @@ limitations under the License. -->
<div class="graph" v-loading="loading"> <div class="graph" v-loading="loading">
<div class="header"> <div class="header">
<span>{{ dashboardStore.selectedGrid.widget.title }}</span> <span>{{ dashboardStore.selectedGrid.widget.title }}</span>
<span v-show="dashboardStore.selectedGrid.standard.unit" class="unit">
({{ dashboardStore.selectedGrid.standard.unit }})
</span>
<div class="tips" v-show="dashboardStore.selectedGrid.widget.tips"> <div class="tips" v-show="dashboardStore.selectedGrid.widget.tips">
<el-tooltip :content="dashboardStore.selectedGrid.widget.tips"> <el-tooltip :content="dashboardStore.selectedGrid.widget.tips">
<Icon iconName="info_outline" size="sm" /> <Icon iconName="info_outline" size="sm" />
@ -33,6 +36,8 @@ limitations under the License. -->
i: dashboardStore.selectedGrid.i, i: dashboardStore.selectedGrid.i,
metrics: dashboardStore.selectedGrid.metrics, metrics: dashboardStore.selectedGrid.metrics,
metricTypes: dashboardStore.selectedGrid.metricTypes, metricTypes: dashboardStore.selectedGrid.metricTypes,
standard: dashboardStore.selectedGrid.standard,
isEdit: true,
}" }"
/> />
<div v-show="!dashboardStore.selectedGrid.graph.type" class="no-data"> <div v-show="!dashboardStore.selectedGrid.graph.type" class="no-data">
@ -55,7 +60,7 @@ limitations under the License. -->
<WidgetOptions /> <WidgetOptions />
</el-collapse-item> </el-collapse-item>
<el-collapse-item :title="t('standardOptions')" name="4"> <el-collapse-item :title="t('standardOptions')" name="4">
<StandardOptions /> <StandardOptions @update="getSource" @loading="setLoading" />
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
</div> </div>
@ -98,7 +103,7 @@ export default defineComponent({
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const states = reactive<{ const states = reactive<{
activeNames: string; activeNames: string;
source: any; source: unknown;
index: string; index: string;
visType: Option[]; visType: Option[];
}>({ }>({
@ -209,4 +214,9 @@ export default defineComponent({
.ds-name { .ds-name {
margin-bottom: 10px; margin-bottom: 10px;
} }
.unit {
display: inline-block;
margin-left: 5px;
}
</style> </style>

View File

@ -13,14 +13,15 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div v-show="states.isTable" class="ds-name"> <div v-if="states.isList && states.dashboardList.length" class="ds-name">
<div>{{ t("dashboards") }}</div> <div>{{ t("dashboards") }}</div>
<el-input <Selector
v-model="states.dashboardName" :value="states.dashboardName"
placeholder="Please input dashboard name" :options="states.dashboardList"
size="small"
placeholder="Please select a dashboard name"
@change="changeDashboard" @change="changeDashboard"
class="selectors" class="selectors"
size="small"
/> />
</div> </div>
<div>{{ t("metrics") }}</div> <div>{{ t("metrics") }}</div>
@ -42,13 +43,13 @@ limitations under the License. -->
:options="states.metricTypeList[index]" :options="states.metricTypeList[index]"
size="small" size="small"
:disabled=" :disabled="
dashboardStore.selectedGrid.graph.type && !states.isTable && index !== 0 dashboardStore.selectedGrid.graph.type && !states.isList && index !== 0
" "
@change="changeMetricType(index, $event)" @change="changeMetricType(index, $event)"
class="selectors" class="selectors"
/> />
<span <span
v-show="states.isTable || states.metricTypes[0] === 'readMetricsValues'" v-show="states.isList || states.metricTypes[0] === 'readMetricsValues'"
> >
<Icon <Icon
class="cp mr-5" class="cp mr-5"
@ -88,18 +89,19 @@ import { Option } from "@/types/app";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { import {
MetricTypes, MetricTypes,
TableChartTypes, ListChartTypes,
MetricCatalog, MetricCatalog,
DefaultGraphConfig, DefaultGraphConfig,
EntityType, EntityType,
ChartTypes, ChartTypes,
PodsChartTypes, PodsChartTypes,
TableEntity, ListEntity,
} from "../../data"; } from "../../data";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import Icon from "@/components/Icon.vue"; import Icon from "@/components/Icon.vue";
import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor"; import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { DashboardItem } from "@/types/dashboard";
/*global defineEmits */ /*global defineEmits */
const { t } = useI18n(); const { t } = useI18n();
@ -111,26 +113,31 @@ const states = reactive<{
metricTypes: string[]; metricTypes: string[];
metricTypeList: Option[][]; metricTypeList: Option[][];
visTypes: Option[]; visTypes: Option[];
isTable: boolean; isList: boolean;
metricList: (Option & { type: string })[]; metricList: (Option & { type: string })[];
dashboardName: string; dashboardName: string;
dashboardList: (DashboardItem & { label: string; value: string })[];
}>({ }>({
metrics: metrics && metrics.length ? metrics : [""], metrics: metrics && metrics.length ? metrics : [""],
metricTypes: metricTypes && metricTypes.length ? metricTypes : [""], metricTypes: metricTypes && metricTypes.length ? metricTypes : [""],
metricTypeList: [], metricTypeList: [],
visTypes: [], visTypes: [],
isTable: false, isList: false,
metricList: [], metricList: [],
dashboardName: graph.dashboardName, dashboardName: graph.dashboardName,
dashboardList: [],
}); });
states.isTable = TableChartTypes.includes(graph.type); states.isList = ListChartTypes.includes(graph.type);
states.visTypes = setVisTypes(); states.visTypes = setVisTypes();
setDashboards();
setMetricType(); setMetricType();
async function setMetricType(catalog?: string) { async function setMetricType(catalog?: string) {
if (states.isTable) { const { graph } = dashboardStore.selectedGrid;
catalog = catalog || TableEntity[graph.type]; if (states.isList) {
catalog = catalog || ListEntity[graph.type];
} else { } else {
catalog = catalog || dashboardStore.entity; catalog = catalog || dashboardStore.entity;
} }
@ -140,24 +147,82 @@ async function setMetricType(catalog?: string) {
return; return;
} }
states.metricList = (json.data.metrics || []).filter( states.metricList = (json.data.metrics || []).filter(
(d: { catalog: string }) => catalog === (MetricCatalog as any)[d.catalog] (d: { catalog: string; type: string }) => {
if (states.isList || graph.type === "Table") {
if (
d.type === "REGULAR_VALUE" &&
catalog === (MetricCatalog as any)[d.catalog]
) {
return d;
}
} else {
if (catalog === (MetricCatalog as any)[d.catalog]) {
return d;
}
}
}
); );
const metrics: any = states.metricList.filter( const metrics: any = states.metricList.filter(
(d: { value: string; type: string }) => { (d: { value: string; type: string }) => {
const metric = states.metrics.filter((m: string) => m === d.value)[0]; const index = states.metrics.findIndex((m: string) => m === d.value);
if (metric) { if (index > -1) {
return d; return d;
} }
} }
); );
if (metrics.length) {
states.metrics = metrics.map((d: { value: string }) => d.value);
} else {
states.metrics = [""];
states.metricTypes = [""];
}
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
metrics: states.metrics,
metricTypes: states.metricTypes,
});
states.metricTypeList = [];
for (const metric of metrics) { for (const metric of metrics) {
states.metricTypeList.push(MetricTypes[metric.type]); if (states.metrics.includes(metric.value)) {
const arr = setMetricTypeList(metric.type);
states.metricTypeList.push(arr);
}
} }
if (states.metrics && states.metrics[0]) { if (states.metrics && states.metrics[0]) {
queryMetrics(); queryMetrics();
} else {
emit("update", {});
} }
} }
function setDashboards() {
const { graph } = dashboardStore.selectedGrid;
const list = JSON.parse(sessionStorage.getItem("dashboards") || "[]");
states.dashboardList = list.reduce(
(
prev: (DashboardItem & { label: string; value: string })[],
d: DashboardItem
) => {
if (d.layer === dashboardStore.layerId) {
if (
(d.entity === EntityType[0].value && graph.type === "ServiceList") ||
(d.entity === EntityType[2].value && graph.type === "EndpointList") ||
(d.entity === EntityType[3].value && graph.type === "InstanceList")
) {
prev.push({
...d,
value: d.name,
label: d.name,
});
}
}
return prev;
},
[]
);
}
function setVisTypes() { function setVisTypes() {
let graphs = []; let graphs = [];
if (dashboardStore.entity === EntityType[0].value) { if (dashboardStore.entity === EntityType[0].value) {
@ -168,7 +233,7 @@ function setVisTypes() {
); );
} else { } else {
graphs = ChartTypes.filter( graphs = ChartTypes.filter(
(d: Option) => !TableChartTypes.includes(d.value) (d: Option) => !ListChartTypes.includes(d.value)
); );
} }
@ -177,10 +242,9 @@ function setVisTypes() {
function changeChartType(item: Option) { function changeChartType(item: Option) {
const graph = DefaultGraphConfig[item.value]; const graph = DefaultGraphConfig[item.value];
states.isTable = TableChartTypes.includes(graph.type);
dashboardStore.selectWidget({ ...dashboardStore.selectedGrid, graph }); dashboardStore.selectWidget({ ...dashboardStore.selectedGrid, graph });
states.isTable = TableChartTypes.includes(graph.type); states.isList = ListChartTypes.includes(graph.type);
if (states.isTable) { if (states.isList) {
dashboardStore.selectWidget({ dashboardStore.selectWidget({
...dashboardStore.selectedGrid, ...dashboardStore.selectedGrid,
metrics: [""], metrics: [""],
@ -194,12 +258,15 @@ function changeChartType(item: Option) {
EndpointList: EntityType[2].value, EndpointList: EntityType[2].value,
ServiceList: EntityType[0].value, ServiceList: EntityType[0].value,
}; };
if (catalog[graph.type]) {
setMetricType(catalog[graph.type]); setMetricType(catalog[graph.type]);
} setDashboards();
states.dashboardName = "";
} }
function changeMetrics(index: number, arr: (Option & { type: string })[]) { function changeMetrics(
index: number,
arr: (Option & { type: string })[] | any
) {
if (!arr.length) { if (!arr.length) {
states.metricTypeList = []; states.metricTypeList = [];
states.metricTypes = []; states.metricTypes = [];
@ -212,33 +279,34 @@ function changeMetrics(index: number, arr: (Option & { type: string })[]) {
states.metrics[index] = arr[0].value; states.metrics[index] = arr[0].value;
const typeOfMetrics = arr[0].type; const typeOfMetrics = arr[0].type;
states.metricTypeList[index] = MetricTypes[typeOfMetrics]; states.metricTypeList[index] = setMetricTypeList(typeOfMetrics);
states.metricTypes[index] = MetricTypes[typeOfMetrics][0].value; states.metricTypes[index] = MetricTypes[typeOfMetrics][0].value;
dashboardStore.selectWidget({ dashboardStore.selectWidget({
...dashboardStore.selectedGrid, ...dashboardStore.selectedGrid,
...{ metricTypes: states.metricTypes, metrics: states.metrics }, ...{ metricTypes: states.metricTypes, metrics: states.metrics },
}); });
if (states.isTable) { if (states.isList) {
return; return;
} }
queryMetrics(); queryMetrics();
} }
function changeMetricType(index: number, opt: Option[]) { function changeMetricType(index: number, opt: Option[] | any) {
const metric = const metric =
states.metricList.filter( states.metricList.filter(
(d: Option) => states.metrics[index] === d.value (d: Option) => states.metrics[index] === d.value
)[0] || {}; )[0] || {};
if (states.isTable) { const l = setMetricTypeList(metric.type);
if (states.isList) {
states.metricTypes[index] = opt[0].value; states.metricTypes[index] = opt[0].value;
states.metricTypeList[index] = (MetricTypes as any)[metric.type]; states.metricTypeList[index] = l;
} else { } else {
states.metricTypes = states.metricTypes.map((d: string) => { states.metricTypes = states.metricTypes.map((d: string) => {
d = opt[0].value; d = opt[0].value;
return d; return d;
}); });
states.metricTypeList = states.metricTypeList.map((d: Option[]) => { states.metricTypeList = states.metricTypeList.map((d: Option[]) => {
d = (MetricTypes as any)[metric.type]; d = l;
return d; return d;
}); });
@ -247,13 +315,17 @@ function changeMetricType(index: number, opt: Option[]) {
...dashboardStore.selectedGrid, ...dashboardStore.selectedGrid,
...{ metricTypes: states.metricTypes }, ...{ metricTypes: states.metricTypes },
}); });
if (states.isTable) { if (states.isList) {
return; return;
} }
queryMetrics(); queryMetrics();
} }
async function queryMetrics() { async function queryMetrics() {
const params = useQueryProcessor(states); if (states.isList) {
return;
}
const { standard } = dashboardStore.selectedGrid;
const params = useQueryProcessor({ ...states, standard });
if (!params) { if (!params) {
emit("update", {}); emit("update", {});
return; return;
@ -266,11 +338,12 @@ async function queryMetrics() {
ElMessage.error(json.errors); ElMessage.error(json.errors);
return; return;
} }
const source = useSourceProcessor(json, states); const source = useSourceProcessor(json, { ...states, standard });
emit("update", source); emit("update", source);
} }
function changeDashboard() { function changeDashboard(opt: any) {
states.dashboardName = opt[0].value;
const graph = { const graph = {
...dashboardStore.selectedGrid.graph, ...dashboardStore.selectedGrid.graph,
dashboardName: states.dashboardName, dashboardName: states.dashboardName,
@ -282,7 +355,7 @@ function changeDashboard() {
} }
function addMetric() { function addMetric() {
states.metrics.push(""); states.metrics.push("");
if (!states.isTable) { if (!states.isList) {
states.metricTypes.push(states.metricTypes[0]); states.metricTypes.push(states.metricTypes[0]);
states.metricTypeList.push(states.metricTypeList[0]); states.metricTypeList.push(states.metricTypeList[0]);
return; return;
@ -293,6 +366,21 @@ function deleteMetric(index: number) {
states.metrics.splice(index, 1); states.metrics.splice(index, 1);
states.metricTypes.splice(index, 1); states.metricTypes.splice(index, 1);
} }
function setMetricTypeList(type: string) {
if (type !== "REGULAR_VALUE") {
return MetricTypes[type];
}
if (states.isList || dashboardStore.selectedGrid.graph.type === "Table") {
return [
{ label: "read all values in the duration", value: "readMetricsValues" },
{
label: "read the single value in the duration",
value: "readMetricsValue",
},
];
}
return MetricTypes[type];
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.ds-name { .ds-name {

View File

@ -17,132 +17,148 @@ limitations under the License. -->
<span class="label">{{ t("unit") }}</span> <span class="label">{{ t("unit") }}</span>
<el-input <el-input
class="input" class="input"
v-model="state.unit" v-model="selectedGrid.standard.unit"
size="small" size="small"
placeholder="Please input Unit" placeholder="Please input Unit"
@change="changeStandardOpt({ unit: state.unit })"
/> />
</div> </div>
<div class="item"> <div class="item">
<span class="label">{{ t("sortOrder") }}</span> <span class="label">{{ t("sortOrder") }}</span>
<Selector <Selector
:value="state.sortOrder" :value="sortOrder"
:options="SortOrder" :options="SortOrder"
size="small" size="small"
placeholder="Select a sort order" placeholder="Select a sort order"
class="selector" class="selector"
@change="changeStandardOpt({ sortOrder: state.sortOrder })" @change="changeStandardOpt({ sortOrder })"
/> />
</div> </div>
<div class="item"> <div class="item" v-show="percentile">
<span class="label">{{ t("max") }}</span> <span class="label">{{ t("labels") }}</span>
<el-input <el-input
class="input" class="input"
v-model="state.max" v-model="selectedGrid.standard.metricLabels"
size="small" size="small"
placeholder="auto" placeholder="auto"
@change="changeStandardOpt({ max: state.max })" @change="changeStandardOpt"
/> />
</div> </div>
<div class="item"> <div class="item" v-show="percentile">
<span class="label">{{ t("min") }}</span> <span class="label">{{ t("labelsIndex") }}</span>
<el-input <el-input
class="input" class="input"
v-model="state.min" v-model="selectedGrid.standard.labelsIndex"
size="small" size="small"
placeholder="auto" placeholder="auto"
@change="changeStandardOpt({ min: state.min })" @change="changeStandardOpt"
/> />
</div> </div>
<div class="item"> <div class="item">
<span class="label">{{ t("plus") }}</span> <span class="label">{{ t("plus") }}</span>
<el-input <el-input
class="input" class="input"
v-model="state.plus" v-model="selectedGrid.standard.plus"
size="small" size="small"
placeholder="none" placeholder="none"
@change="changeStandardOpt({ plus: state.plus })" @change="changeStandardOpt"
/> />
</div> </div>
<div class="item"> <div class="item">
<span class="label">{{ t("minus") }}</span> <span class="label">{{ t("minus") }}</span>
<el-input <el-input
class="input" class="input"
v-model="state.minus" v-model="selectedGrid.standard.minus"
size="small" size="small"
placeholder="none" placeholder="none"
@change="changeStandardOpt({ minus: state.minus })" @change="changeStandardOpt"
/> />
</div> </div>
<div class="item"> <div class="item">
<span class="label">{{ t("multiply") }}</span> <span class="label">{{ t("multiply") }}</span>
<el-input <el-input
class="input" class="input"
v-model="state.multiply" v-model="selectedGrid.standard.multiply"
size="small" size="small"
placeholder="none" placeholder="none"
@change="changeStandardOpt({ multiply: state.multiply })" @change="changeStandardOpt"
/> />
</div> </div>
<div class="item"> <div class="item">
<span class="label">{{ t("divide") }}</span> <span class="label">{{ t("divide") }}</span>
<el-input <el-input
class="input" class="input"
v-model="state.divide" v-model="selectedGrid.standard.divide"
size="small" size="small"
placeholder="none" placeholder="none"
@change="changeStandardOpt({ divide: state.divide })" @change="changeStandardOpt"
/> />
</div> </div>
<div class="item"> <div class="item">
<span class="label">{{ t("convertToMilliseconds") }}</span> <span class="label">{{ t("convertToMilliseconds") }}</span>
<el-input <el-input
class="input" class="input"
v-model="state.milliseconds" v-model="selectedGrid.standard.milliseconds"
size="small" size="small"
placeholder="none" placeholder="none"
@change="changeStandardOpt({ milliseconds: state.milliseconds })" @change="changeStandardOpt"
/> />
</div> </div>
<div class="item"> <div class="item">
<span class="label">{{ t("convertToSeconds") }}</span> <span class="label">{{ t("convertToSeconds") }}</span>
<el-input <el-input
class="input" class="input"
v-model="state.seconds" v-model="selectedGrid.standard.seconds"
size="small" size="small"
placeholder="none" placeholder="none"
@change="changeStandardOpt({ seconds: state.seconds })" @change="changeStandardOpt"
/> />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive } from "vue"; import { ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { SortOrder } from "../../data"; import { SortOrder } from "../../data";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor";
import { ElMessage } from "element-plus";
/*global defineEmits */
const { t } = useI18n();
const emit = defineEmits(["update", "loading"]);
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore; const { selectedGrid } = dashboardStore;
const { t } = useI18n(); const percentile = ref<boolean>(
const state = reactive({ selectedGrid.metricTypes.includes("readLabeledMetricsValues")
unit: selectedGrid.standard.unit, );
max: "", const sortOrder = ref<string>(selectedGrid.standard.sortOrder || "DES");
min: "",
plus: "",
minus: "",
multiply: "",
divide: "",
milliseconds: "",
seconds: "",
sortOrder: selectedGrid.standard.sortOrder,
});
function changeStandardOpt(param: { [key: string]: unknown }) { function changeStandardOpt(param?: any) {
const standard = { let standard = dashboardStore.selectedGrid.standard;
...selectedGrid.standard, if (param) {
standard = {
...dashboardStore.selectedGrid.standard,
...param, ...param,
}; };
dashboardStore.selectWidget({ ...selectedGrid, standard }); dashboardStore.selectWidget({ ...dashboardStore.selectedGrid, standard });
}
queryMetrics();
}
async function queryMetrics() {
const params = useQueryProcessor(dashboardStore.selectedGrid);
if (!params) {
emit("update", {});
return;
}
emit("loading", true);
const json = await dashboardStore.fetchMetricValue(params);
emit("loading", false);
if (json.errors) {
ElMessage.error(json.errors);
return;
}
const source = useSourceProcessor(json, dashboardStore.selectedGrid);
emit("update", source);
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -20,8 +20,8 @@ limitations under the License. -->
v-model="fontSize" v-model="fontSize"
show-input show-input
input-size="small" input-size="small"
:min="10" :min="12"
:max="20" :max="50"
:step="1" :step="1"
@change="updateConfig({ fontSize })" @change="updateConfig({ fontSize })"
/> />

View File

@ -13,6 +13,24 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div>
<span class="label">{{ t("showXAxis") }}</span>
<el-switch
v-model="selectedGrid.graph.showXAxis"
active-text="Yes"
inactive-text="No"
@change="updateConfig({ showXAxis: selectedGrid.graph.showXAxis })"
/>
</div>
<div>
<span class="label">{{ t("showYAxis") }}</span>
<el-switch
v-model="selectedGrid.graph.showYAxis"
active-text="Yes"
inactive-text="No"
@change="updateConfig({ showYAxis: selectedGrid.graph.showYAxis })"
/>
</div>
<div> <div>
<span class="label">{{ t("smooth") }}</span> <span class="label">{{ t("smooth") }}</span>
<el-switch <el-switch

View File

@ -13,7 +13,16 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div> <div class="item">
<span class="label">{{ t("showGroup") }}</span>
<el-switch
v-model="selectedGrid.graph.showGroup"
active-text="Yes"
inactive-text="No"
@change="updateConfig({ showGroup: selectedGrid.graph.showGroup })"
/>
</div>
<div class="item">
<span class="label">{{ t("fontSize") }}</span> <span class="label">{{ t("fontSize") }}</span>
<el-slider <el-slider
class="slider" class="slider"
@ -38,6 +47,7 @@ const { selectedGrid } = dashboardStore;
const fontSize = ref(selectedGrid.graph.fontSize); const fontSize = ref(selectedGrid.graph.fontSize);
function updateConfig(param: { [key: string]: unknown }) { function updateConfig(param: { [key: string]: unknown }) {
const { selectedGrid } = dashboardStore;
const graph = { const graph = {
...selectedGrid.graph, ...selectedGrid.graph,
...param, ...param,
@ -57,4 +67,8 @@ function updateConfig(param: { [key: string]: unknown }) {
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
} }
.item {
margin-top: 5px;
}
</style> </style>

View File

@ -75,4 +75,8 @@ function updateConfig(param: { [key: string]: unknown }) {
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
} }
.item {
margin-top: 10px;
}
</style> </style>

View File

@ -14,7 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div class="log-wrapper flex-v"> <div class="log-wrapper flex-v">
<el-popover placement="bottom" trigger="click" :width="100"> <el-popover
placement="bottom"
trigger="click"
:width="100"
v-if="routeParams.entity"
>
<template #reference> <template #reference>
<span class="delete cp"> <span class="delete cp">
<Icon iconName="ellipsis_v" size="middle" class="operation" /> <Icon iconName="ellipsis_v" size="middle" class="operation" />
@ -34,6 +39,7 @@ limitations under the License. -->
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import Header from "../related/log/Header.vue"; import Header from "../related/log/Header.vue";
import List from "../related/log/List.vue"; import List from "../related/log/List.vue";
@ -47,6 +53,7 @@ const props = defineProps({
activeIndex: { type: String, default: "" }, activeIndex: { type: String, default: "" },
}); });
const { t } = useI18n(); const { t } = useI18n();
const routeParams = useRoute().params;
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
function removeWidget() { function removeWidget() {

View File

@ -14,7 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div class="profile-wrapper flex-v"> <div class="profile-wrapper flex-v">
<el-popover placement="bottom" trigger="click" :width="100"> <el-popover
placement="bottom"
trigger="click"
:width="100"
v-if="routeParams.entity"
>
<template #reference> <template #reference>
<span class="delete cp"> <span class="delete cp">
<Icon iconName="ellipsis_v" size="middle" class="operation" /> <Icon iconName="ellipsis_v" size="middle" class="operation" />
@ -34,6 +39,7 @@ import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import Header from "../related/profile/Header.vue"; import Header from "../related/profile/Header.vue";
import Content from "../related/profile/Content.vue"; import Content from "../related/profile/Content.vue";
import { useRoute } from "vue-router";
/*global defineProps */ /*global defineProps */
const props = defineProps({ const props = defineProps({
@ -44,6 +50,7 @@ const props = defineProps({
activeIndex: { type: String, default: "" }, activeIndex: { type: String, default: "" },
}); });
const { t } = useI18n(); const { t } = useI18n();
const routeParams = useRoute().params;
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
function removeWidget() { function removeWidget() {
dashboardStore.removeControls(props.data); dashboardStore.removeControls(props.data);

View File

@ -34,9 +34,10 @@ limitations under the License. -->
size="sm" size="sm"
iconName="cancel" iconName="cancel"
@click="deleteTabItem($event, idx)" @click="deleteTabItem($event, idx)"
v-if="routeParams.entity"
/> />
</span> </span>
<span class="tab-icons"> <span class="tab-icons" v-if="routeParams.entity">
<el-tooltip content="Add tab items" placement="bottom"> <el-tooltip content="Add tab items" placement="bottom">
<i @click="addTabItem"> <i @click="addTabItem">
<Icon size="middle" iconName="add" /> <Icon size="middle" iconName="add" />
@ -44,7 +45,7 @@ limitations under the License. -->
</el-tooltip> </el-tooltip>
</span> </span>
</div> </div>
<div class="operations"> <div class="operations" v-if="routeParams.entity">
<el-popover <el-popover
placement="bottom" placement="bottom"
trigger="click" trigger="click"
@ -84,7 +85,6 @@ limitations under the License. -->
:row-height="10" :row-height="10"
:is-draggable="true" :is-draggable="true"
:is-resizable="true" :is-resizable="true"
:responsive="true"
@layout-updated="layoutUpdatedEvent" @layout-updated="layoutUpdatedEvent"
> >
<grid-item <grid-item
@ -106,12 +106,13 @@ limitations under the License. -->
/> />
</grid-item> </grid-item>
</grid-layout> </grid-layout>
<div class="no-data-tips" v-else>Please add widgets.</div> <div class="no-data-tips" v-else>{{ t("noWidget") }}</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, watch, defineComponent, toRefs } from "vue"; import { ref, watch, defineComponent, toRefs } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import type { PropType } from "vue"; import type { PropType } from "vue";
import { LayoutConfig } from "@/types/dashboard"; import { LayoutConfig } from "@/types/dashboard";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
@ -134,6 +135,7 @@ export default defineComponent({
props, props,
setup(props) { setup(props) {
const { t } = useI18n(); const { t } = useI18n();
const routeParams = useRoute().params;
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const activeTabIndex = ref<number>(0); const activeTabIndex = ref<number>(0);
const activeTabWidget = ref<string>(""); const activeTabWidget = ref<string>("");
@ -144,9 +146,11 @@ export default defineComponent({
const l = dashboardStore.layout.findIndex( const l = dashboardStore.layout.findIndex(
(d: LayoutConfig) => d.i === props.data.i (d: LayoutConfig) => d.i === props.data.i
); );
if (dashboardStore.layout[l].children.length) {
dashboardStore.setCurrentTabItems( dashboardStore.setCurrentTabItems(
dashboardStore.layout[l].children[activeTabIndex.value].children dashboardStore.layout[l].children[activeTabIndex.value].children
); );
}
function clickTabs(e: Event, idx: number) { function clickTabs(e: Event, idx: number) {
e.stopPropagation(); e.stopPropagation();
@ -169,6 +173,12 @@ export default defineComponent({
function deleteTabItem(e: Event, idx: number) { function deleteTabItem(e: Event, idx: number) {
e.stopPropagation(); e.stopPropagation();
dashboardStore.removeTabItem(props.data, idx); dashboardStore.removeTabItem(props.data, idx);
const kids = dashboardStore.layout[l].children[0];
const arr = (kids && kids.children) || [];
dashboardStore.setCurrentTabItems(arr);
dashboardStore.activeGridItem(0);
activeTabIndex.value = 0;
needQuery.value = true;
} }
function addTabItem() { function addTabItem() {
dashboardStore.addTabItem(props.data); dashboardStore.addTabItem(props.data);
@ -236,6 +246,7 @@ export default defineComponent({
needQuery, needQuery,
canEditTabName, canEditTabName,
showTools, showTools,
routeParams,
t, t,
}; };
}, },
@ -256,7 +267,7 @@ export default defineComponent({
} }
.tab-name { .tab-name {
max-width: 80px; max-width: 130px;
height: 20px; height: 20px;
line-height: 20px; line-height: 20px;
outline: none; outline: none;

View File

@ -27,7 +27,12 @@ limitations under the License. -->
/> />
</span> </span>
</el-tooltip> </el-tooltip>
<el-popover placement="bottom" trigger="click" :width="100"> <el-popover
placement="bottom"
trigger="click"
:width="100"
v-if="routeParams.entity"
>
<template #reference> <template #reference>
<span> <span>
<Icon iconName="ellipsis_v" size="middle" class="operation" /> <Icon iconName="ellipsis_v" size="middle" class="operation" />
@ -64,6 +69,7 @@ limitations under the License. -->
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { PropType } from "vue"; import type { PropType } from "vue";
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { Colors } from "../data"; import { Colors } from "../data";
@ -76,6 +82,7 @@ const props = defineProps({
activeIndex: { type: String, default: "" }, activeIndex: { type: String, default: "" },
}); });
const { t } = useI18n(); const { t } = useI18n();
const routeParams = useRoute().params;
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
function editConfig() { function editConfig() {

View File

@ -20,7 +20,7 @@ limitations under the License. -->
<Icon iconName="ellipsis_v" size="middle" class="operation" /> <Icon iconName="ellipsis_v" size="middle" class="operation" />
</span> </span>
</template> </template>
<div class="tools" @click="removeWidget"> <div class="tools" @click="removeWidget" v-if="routeParams.entity">
<span>{{ t("delete") }}</span> <span>{{ t("delete") }}</span>
</div> </div>
</el-popover> </el-popover>
@ -35,6 +35,7 @@ limitations under the License. -->
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { PropType } from "vue"; import type { PropType } from "vue";
import { useRoute } from "vue-router";
import Filter from "../related/trace/Filter.vue"; import Filter from "../related/trace/Filter.vue";
import TraceList from "../related/trace/TraceList.vue"; import TraceList from "../related/trace/TraceList.vue";
import TraceDetail from "../related/trace/Detail.vue"; import TraceDetail from "../related/trace/Detail.vue";
@ -50,6 +51,7 @@ const props = defineProps({
activeIndex: { type: String, default: "" }, activeIndex: { type: String, default: "" },
}); });
const { t } = useI18n(); const { t } = useI18n();
const routeParams = useRoute().params;
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
function removeWidget() { function removeWidget() {
dashboardStore.removeControls(props.data); dashboardStore.removeControls(props.data);

View File

@ -15,7 +15,14 @@ limitations under the License. -->
<template> <template>
<div class="widget"> <div class="widget">
<div class="header flex-h"> <div class="header flex-h">
<div>{{ data.widget?.title || "" }}</div> <div>
<span>
{{ data.widget?.title || "" }}
</span>
<span class="unit" v-show="data.standard?.unit">
({{ data.standard?.unit }})
</span>
</div>
<div> <div>
<el-tooltip :content="data.widget?.tips"> <el-tooltip :content="data.widget?.tips">
<span> <span>
@ -27,7 +34,12 @@ limitations under the License. -->
/> />
</span> </span>
</el-tooltip> </el-tooltip>
<el-popover placement="bottom" trigger="click" :width="100"> <el-popover
placement="bottom"
trigger="click"
:width="100"
v-if="routeParams.entity"
>
<template #reference> <template #reference>
<span> <span>
<Icon iconName="ellipsis_v" size="middle" class="operation" /> <Icon iconName="ellipsis_v" size="middle" class="operation" />
@ -62,6 +74,7 @@ limitations under the License. -->
<script lang="ts"> <script lang="ts">
import { toRefs, reactive, defineComponent, ref, watch } from "vue"; import { toRefs, reactive, defineComponent, ref, watch } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import { useRoute } from "vue-router";
import { LayoutConfig } from "@/types/dashboard"; import { LayoutConfig } from "@/types/dashboard";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
@ -69,7 +82,7 @@ import { useSelectorStore } from "@/store/modules/selectors";
import graphs from "../graphs"; import graphs from "../graphs";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor"; import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor";
import { EntityType, TableChartTypes } from "../data"; import { EntityType, ListChartTypes } from "../data";
const props = { const props = {
data: { data: {
@ -85,6 +98,7 @@ export default defineComponent({
props, props,
setup(props) { setup(props) {
const { t } = useI18n(); const { t } = useI18n();
const routeParams = useRoute().params;
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const state = reactive<{ source: { [key: string]: unknown } }>({ const state = reactive<{ source: { [key: string]: unknown } }>({
source: {}, source: {},
@ -94,7 +108,11 @@ export default defineComponent({
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore(); const selectorStore = useSelectorStore();
if (dashboardStore.entity === EntityType[1].value || props.needQuery) { if (
dashboardStore.entity === EntityType[1].value ||
props.needQuery ||
!dashboardStore.currentDashboard.id
) {
queryMetrics(); queryMetrics();
} }
@ -111,7 +129,12 @@ export default defineComponent({
if (!json) { if (!json) {
return; return;
} }
state.source = useSourceProcessor(json, props.data); const d = {
metrics: props.data.metrics,
metricTypes: props.data.metricTypes,
standard: props.data.standard,
};
state.source = useSourceProcessor(json, d);
} }
function removeWidget() { function removeWidget() {
@ -127,22 +150,26 @@ export default defineComponent({
} }
} }
watch( watch(
() => [props.data.metricTypes, props.data.metrics], () => [props.data.metricTypes, props.data.metrics, props.data.standard],
() => { () => {
if ( if (!dashboardStore.selectedGrid) {
dashboardStore.selectedGrid &&
props.data.i !== dashboardStore.selectedGrid.i
) {
return; return;
} }
if (TableChartTypes.includes(dashboardStore.selectedGrid.graph.type)) { if (props.data.i !== dashboardStore.selectedGrid.i) {
return;
}
if (ListChartTypes.includes(dashboardStore.selectedGrid.graph.type)) {
return; return;
} }
queryMetrics(); queryMetrics();
} }
); );
watch( watch(
() => [selectorStore.currentService, selectorStore.currentDestService], () => [
selectorStore.currentService,
selectorStore.currentDestService,
appStore.durationTime,
],
() => { () => {
if ( if (
dashboardStore.entity === EntityType[0].value || dashboardStore.entity === EntityType[0].value ||
@ -169,6 +196,7 @@ export default defineComponent({
editConfig, editConfig,
data, data,
loading, loading,
routeParams,
t, t,
}; };
}, },
@ -218,4 +246,9 @@ export default defineComponent({
text-align: center; text-align: center;
padding-top: 20px; padding-top: 20px;
} }
.unit {
display: inline-block;
margin-left: 5px;
}
</style> </style>

View File

@ -17,7 +17,7 @@
export const PodsChartTypes = ["EndpointList", "InstanceList"]; export const PodsChartTypes = ["EndpointList", "InstanceList"];
export const TableChartTypes = ["EndpointList", "InstanceList", "ServiceList"]; export const ListChartTypes = ["EndpointList", "InstanceList", "ServiceList"];
export const ChartTypes = [ export const ChartTypes = [
{ label: "Bar", value: "Bar" }, { label: "Bar", value: "Bar" },
@ -87,11 +87,16 @@ export const DefaultGraphConfig: { [key: string]: any } = {
type: "EndpointList", type: "EndpointList",
dashboardName: "", dashboardName: "",
fontSize: 12, fontSize: 12,
showXAxis: false,
showYAxis: false,
}, },
ServiceList: { ServiceList: {
type: "ServiceList", type: "ServiceList",
dashboardName: "", dashboardName: "",
fontSize: 12, fontSize: 12,
showXAxis: false,
showYAxis: false,
showGroup: true,
}, },
HeatMap: { HeatMap: {
type: "HeatMap", type: "HeatMap",
@ -152,8 +157,7 @@ export const EntityType = [
}, },
{ value: "EndpointRelation", label: "Endpoint Relation", key: 4 }, { value: "EndpointRelation", label: "Endpoint Relation", key: 4 },
]; ];
export const hasTopology = ["All", "Service", "ServiceRelation", "Endpoint"]; export const ListEntity: any = {
export const TableEntity: any = {
InstanceList: EntityType[3].value, InstanceList: EntityType[3].value,
EndpointList: EntityType[2].value, EndpointList: EntityType[2].value,
ServiceList: EntityType[0].value, ServiceList: EntityType[0].value,
@ -162,18 +166,51 @@ export const SortOrder = [
{ label: "DES", value: "DES" }, { label: "DES", value: "DES" },
{ label: "ASC", value: "ASC" }, { label: "ASC", value: "ASC" },
]; ];
export const ToolIcons = [ export const AllTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
{ name: "merge", content: "Add Trace", id: "addTrace" },
{ name: "assignment", content: "Add Log", id: "addLog" },
{ name: "save", content: "Apply", id: "apply" },
];
export const ServiceTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" }, { name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" }, { name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "device_hub", content: "Add Topology", id: "addTopology" }, { name: "device_hub", content: "Add Topology", id: "addTopology" },
{ name: "merge", content: "Add Trace", id: "addTrace" }, { name: "merge", content: "Add Trace", id: "addTrace" },
{ name: "timeline", content: "Add Profile", id: "addProfile" }, { name: "timeline", content: "Add Profile", id: "addProfile" },
{ name: "assignment", content: "Add Log", id: "addLog" }, { name: "assignment", content: "Add Log", id: "addLog" },
// { name: "save_alt", content: "Export", id: "export" }, { name: "save", content: "Apply", id: "apply" },
// { name: "folder_open", content: "Import", id: "import" },
// { name: "settings", content: "Settings", id: "settings" },
// { name: "save", content: "Apply", id: "apply" },
]; ];
export const InstanceTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
{ name: "merge", content: "Add Trace", id: "addTrace" },
{ name: "assignment", content: "Add Log", id: "addLog" },
{ name: "save", content: "Apply", id: "apply" },
];
export const EndpointTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
{ name: "merge", content: "Add Trace", id: "addTrace" },
{ name: "assignment", content: "Add Log", id: "addLog" },
{ name: "save", content: "Apply", id: "apply" },
];
export const ServiceRelationTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
{ name: "save", content: "Apply", id: "apply" },
];
export const PodRelationTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "save", content: "Apply", id: "apply" },
];
export const ScopeType = [ export const ScopeType = [
{ value: "Service", label: "Service", key: 1 }, { value: "Service", label: "Service", key: 1 },
{ value: "Endpoint", label: "Endpoint", key: 3 }, { value: "Endpoint", label: "Endpoint", key: 3 },
@ -214,4 +251,3 @@ export const QueryOrders = [
{ label: "Start Time", value: "BY_START_TIME" }, { label: "Start Time", value: "BY_START_TIME" },
{ label: "Duration", value: "BY_DURATION" }, { label: "Duration", value: "BY_DURATION" },
]; ];
export const TraceEntitys = ["All", "Service", "ServiceInstance", "Endpoint"];

View File

@ -16,6 +16,7 @@ limitations under the License. -->
<template> <template>
<div <div
class="chart-card" class="chart-card"
:class="{ center: config.textAlign === 'center' }"
:style="{ fontSize: `${config.fontSize}px`, textAlign: config.textAlign }" :style="{ fontSize: `${config.fontSize}px`, textAlign: config.textAlign }"
> >
{{ {{
@ -52,12 +53,15 @@ const singleVal = computed(() => props.data[key.value]);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chart-card { .chart-card {
box-sizing: border-box;
color: #333; color: #333;
height: 100%;
}
.center {
box-sizing: border-box;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: horizontal; -webkit-box-orient: horizontal;
-webkit-box-pack: center; -webkit-box-pack: center;
-webkit-box-align: center; -webkit-box-align: center;
height: 100%;
} }
</style> </style>

View File

@ -18,17 +18,18 @@ limitations under the License. -->
<el-input <el-input
v-model="searchText" v-model="searchText"
placeholder="Please input endpoint name" placeholder="Please input endpoint name"
class="input-with-search"
size="small" size="small"
@change="searchList" @change="searchList"
class="inputs"
> >
<template #append> <template #append>
<el-button size="small" @click="searchList"> <el-button size="small" @click="searchList">
<Icon size="lg" iconName="search" /> <Icon size="sm" iconName="search" />
</el-button> </el-button>
</template> </template>
</el-input> </el-input>
</div> </div>
<div class="list">
<el-table v-loading="chartLoading" :data="endpoints" style="width: 100%"> <el-table v-loading="chartLoading" :data="endpoints" style="width: 100%">
<el-table-column label="Endpoints"> <el-table-column label="Endpoints">
<template #default="scope"> <template #default="scope">
@ -63,9 +64,11 @@ limitations under the License. -->
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</div>
<el-pagination <el-pagination
class="pagination" class="pagination"
background background
small
layout="prev, pager, next" layout="prev, pager, next"
:page-size="pageSize" :page-size="pageSize"
:total="selectorStore.pods.length" :total="selectorStore.pods.length"
@ -99,6 +102,7 @@ const props = defineProps({
i: string; i: string;
metrics: string[]; metrics: string[];
metricTypes: string[]; metricTypes: string[];
isEdit: boolean;
} }
>, >,
default: () => ({ dashboardName: "", fontSize: 12, i: "" }), default: () => ({ dashboardName: "", fontSize: 12, i: "" }),
@ -126,6 +130,9 @@ async function queryEndpoints() {
} }
searchEndpoints.value = selectorStore.pods; searchEndpoints.value = selectorStore.pods;
endpoints.value = selectorStore.pods.splice(0, pageSize); endpoints.value = selectorStore.pods.splice(0, pageSize);
if (props.config.isEdit) {
return;
}
queryEndpointMetrics(endpoints.value); queryEndpointMetrics(endpoints.value);
} }
async function queryEndpointMetrics(currentPods: Endpoint[]) { async function queryEndpointMetrics(currentPods: Endpoint[]) {
@ -134,7 +141,7 @@ async function queryEndpointMetrics(currentPods: Endpoint[]) {
if (metrics.length && metrics[0]) { if (metrics.length && metrics[0]) {
const params = await useQueryPodsMetrics( const params = await useQueryPodsMetrics(
currentPods, currentPods,
dashboardStore.selectedGrid, props.config,
EntityType[2].value EntityType[2].value
); );
const json = await dashboardStore.fetchMetricValue(params); const json = await dashboardStore.fetchMetricValue(params);
@ -143,11 +150,7 @@ async function queryEndpointMetrics(currentPods: Endpoint[]) {
ElMessage.error(json.errors); ElMessage.error(json.errors);
return; return;
} }
endpoints.value = usePodsSource( endpoints.value = usePodsSource(currentPods, json, props.config);
currentPods,
json,
dashboardStore.selectedGrid
);
return; return;
} }
endpoints.value = currentPods; endpoints.value = currentPods;
@ -177,4 +180,8 @@ watch(
.chart { .chart {
height: 39px; height: 39px;
} }
.inputs {
width: 300px;
}
</style> </style>

View File

@ -18,17 +18,18 @@ limitations under the License. -->
<el-input <el-input
v-model="searchText" v-model="searchText"
placeholder="Please input instance name" placeholder="Please input instance name"
class="input-with-search"
size="small" size="small"
@change="searchList" @change="searchList"
class="inputs"
> >
<template #append> <template #append>
<el-button size="small" @click="searchList"> <el-button size="small" @click="searchList">
<Icon size="lg" iconName="search" /> <Icon size="sm" iconName="search" />
</el-button> </el-button>
</template> </template>
</el-input> </el-input>
</div> </div>
<div class="list">
<el-table v-loading="chartLoading" :data="instances" style="width: 100%"> <el-table v-loading="chartLoading" :data="instances" style="width: 100%">
<el-table-column label="Service Instances"> <el-table-column label="Service Instances">
<template #default="scope"> <template #default="scope">
@ -63,9 +64,11 @@ limitations under the License. -->
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</div>
<el-pagination <el-pagination
class="pagination" class="pagination"
background background
small
layout="prev, pager, next" layout="prev, pager, next"
:page-size="pageSize" :page-size="pageSize"
:total="searchInstances.length" :total="searchInstances.length"
@ -96,6 +99,7 @@ const props = defineProps({
i: string; i: string;
metrics: string[]; metrics: string[];
metricTypes: string[]; metricTypes: string[];
isEdit: boolean;
} }
>, >,
default: () => ({ default: () => ({
@ -129,6 +133,9 @@ async function queryInstance() {
} }
searchInstances.value = selectorStore.pods; searchInstances.value = selectorStore.pods;
instances.value = searchInstances.value.splice(0, pageSize); instances.value = searchInstances.value.splice(0, pageSize);
if (props.config.isEdit) {
return;
}
queryInstanceMetrics(instances.value); queryInstanceMetrics(instances.value);
} }
@ -138,7 +145,7 @@ async function queryInstanceMetrics(currentInstances: Instance[]) {
if (metrics.length && metrics[0]) { if (metrics.length && metrics[0]) {
const params = await useQueryPodsMetrics( const params = await useQueryPodsMetrics(
currentInstances, currentInstances,
dashboardStore.selectedGrid, props.config,
EntityType[3].value EntityType[3].value
); );
const json = await dashboardStore.fetchMetricValue(params); const json = await dashboardStore.fetchMetricValue(params);
@ -147,11 +154,7 @@ async function queryInstanceMetrics(currentInstances: Instance[]) {
ElMessage.error(json.errors); ElMessage.error(json.errors);
return; return;
} }
instances.value = usePodsSource( instances.value = usePodsSource(currentInstances, json, props.config);
currentInstances,
json,
dashboardStore.selectedGrid
);
return; return;
} }
instances.value = currentInstances; instances.value = currentInstances;
@ -182,4 +185,8 @@ watch(
.chart { .chart {
height: 40px; height: 40px;
} }
.inputs {
width: 300px;
}
</style> </style>

View File

@ -78,11 +78,11 @@ function getOption() {
]), ]),
name: i, name: i,
type: "line", type: "line",
symbol: "none", symbol: "circle",
barMaxWidth: 10, symbolSize: 8,
showSymbol: props.config.showSymbol,
step: props.config.step, step: props.config.step,
smooth: props.config.smooth, smooth: props.config.smooth,
showSymbol: true,
lineStyle: { lineStyle: {
width: 1.5, width: 1.5,
type: "solid", type: "solid",

View File

@ -18,23 +18,38 @@ limitations under the License. -->
<el-input <el-input
v-model="searchText" v-model="searchText"
placeholder="Please input service name" placeholder="Please input service name"
class="input-with-search"
size="small" size="small"
@change="searchList" @change="searchList"
class="inputs mt-5"
> >
<template #append> <template #append>
<el-button size="small" @click="searchList"> <el-button size="small" @click="searchList">
<Icon size="lg" iconName="search" /> <Icon size="sm" iconName="search" />
</el-button> </el-button>
</template> </template>
</el-input> </el-input>
</div> </div>
<el-table v-loading="chartLoading" :data="services" style="width: 100%"> <div class="list">
<el-table-column label="Services"> <el-table
v-loading="chartLoading"
:data="services"
style="width: 100%"
:span-method="objectSpanMethod"
:border="true"
:style="{ fontSize: '14px' }"
>
<el-table-column label="Service Groups" v-if="config.showGroup">
<template #default="scope">
{{ scope.row.group }}
</template>
</el-table-column>
<el-table-column label="Service Names">
<template #default="scope"> <template #default="scope">
<router-link <router-link
class="link" class="link"
:to="`/dashboard/${dashboardStore.layerId}/${EntityType[0].value}/${scope.row.id}/${config.dashboardName}`" :to="`/dashboard/${dashboardStore.layerId}/${
EntityType[0].value
}/${scope.row.id}/${config.dashboardName.split(' ').join('-')}`"
:key="1" :key="1"
:style="{ fontSize: `${config.fontSize}px` }" :style="{ fontSize: `${config.fontSize}px` }"
> >
@ -64,9 +79,11 @@ limitations under the License. -->
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</div>
<el-pagination <el-pagination
class="pagination" class="pagination"
background background
small
layout="prev, pager, next" layout="prev, pager, next"
:page-size="pageSize" :page-size="pageSize"
:total="selectorStore.services.length" :total="selectorStore.services.length"
@ -100,6 +117,7 @@ const props = defineProps({
i: string; i: string;
metrics: string[]; metrics: string[];
metricTypes: string[]; metricTypes: string[];
isEdit: boolean;
} }
>, >,
default: () => ({ dashboardName: "", fontSize: 12 }), default: () => ({ dashboardName: "", fontSize: 12 }),
@ -113,6 +131,7 @@ const pageSize = 5;
const services = ref<Service[]>([]); const services = ref<Service[]>([]);
const searchServices = ref<Service[]>([]); const searchServices = ref<Service[]>([]);
const searchText = ref<string>(""); const searchText = ref<string>("");
const groups = ref<any>({});
queryServices(); queryServices();
@ -124,7 +143,34 @@ async function queryServices() {
if (resp.errors) { if (resp.errors) {
ElMessage.error(resp.errors); ElMessage.error(resp.errors);
} }
services.value = selectorStore.services.splice(0, pageSize); const map: { [key: string]: any[] } = selectorStore.services.reduce(
(result: { [key: string]: any[] }, item: any) => {
item.group = item.group || "";
if (result[item.group]) {
item.merge = true;
} else {
item.merge = false;
result[item.group] = [];
}
result[item.group].push(item);
return result;
},
{}
);
services.value = Object.values(map).flat(1).splice(0, pageSize);
const obj = {} as any;
for (const s of services.value) {
s.group = s.group || "";
if (!obj[s.group]) {
obj[s.group] = 1;
} else {
obj[s.group]++;
}
groups.value[s.group] = obj[s.group];
}
if (props.config.isEdit) {
return;
}
queryServiceMetrics(services.value); queryServiceMetrics(services.value);
} }
async function queryServiceMetrics(currentServices: Service[]) { async function queryServiceMetrics(currentServices: Service[]) {
@ -133,7 +179,7 @@ async function queryServiceMetrics(currentServices: Service[]) {
if (metrics.length && metrics[0]) { if (metrics.length && metrics[0]) {
const params = await useQueryPodsMetrics( const params = await useQueryPodsMetrics(
currentServices, currentServices,
dashboardStore.selectedGrid, props.config,
EntityType[0].value EntityType[0].value
); );
const json = await dashboardStore.fetchMetricValue(params); const json = await dashboardStore.fetchMetricValue(params);
@ -142,15 +188,27 @@ async function queryServiceMetrics(currentServices: Service[]) {
ElMessage.error(json.errors); ElMessage.error(json.errors);
return; return;
} }
services.value = usePodsSource( services.value = usePodsSource(currentServices, json, props.config);
currentServices,
json,
dashboardStore.selectedGrid
);
return; return;
} }
services.value = currentServices; 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,
};
} else {
return { rowspan: groups.value[param.row.group], colspan: 1 };
}
}
function changePage(pageIndex: number) { function changePage(pageIndex: number) {
services.value = selectorStore.services.splice(pageIndex - 1, pageSize); services.value = selectorStore.services.splice(pageIndex - 1, pageSize);
} }
@ -175,4 +233,8 @@ watch(
.chart { .chart {
height: 39px; height: 39px;
} }
.inputs {
width: 300px;
}
</style> </style>

View File

@ -21,13 +21,13 @@ limitations under the License. -->
:style="`width: ${nameWidth + initWidth}px`" :style="`width: ${nameWidth + initWidth}px`"
> >
<div class="name" :style="`width: ${nameWidth}px`"> <div class="name" :style="`width: ${nameWidth}px`">
{{ config.tableHeaderCol1 || $t("name") }} {{ config.graph.tableHeaderCol1 || t("name") }}
<i class="r cp" ref="draggerName"> <i class="r cp" ref="draggerName">
<Icon iconName="settings_ethernet" size="middle" /> <Icon iconName="settings_ethernet" size="middle" />
</i> </i>
</div> </div>
<div class="value-col" v-if="config.showTableValues"> <div class="value-col" v-if="showTableValues">
{{ config.tableHeaderCol2 || $t("value") }} {{ config.graph.tableHeaderCol2 || t("value") }}
</div> </div>
</div> </div>
<div <div
@ -37,8 +37,12 @@ limitations under the License. -->
:style="`width: ${nameWidth + initWidth}px`" :style="`width: ${nameWidth + initWidth}px`"
> >
<div :style="`width: ${nameWidth}px`">{{ key }}</div> <div :style="`width: ${nameWidth}px`">{{ key }}</div>
<div class="value-col" v-if="config.showTableValues"> <div class="value-col" v-if="showTableValues">
{{ data[key][data[key].length - 1 || 0] }} {{
config.metricTypes[0] === "readMetricsValue"
? data[key]
: data[key][data[key].length - 1 || 0]
}}
</div> </div>
</div> </div>
</div> </div>
@ -47,6 +51,7 @@ limitations under the License. -->
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, onMounted } from "vue"; import { computed, ref, onMounted } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
/*global defineProps */ /*global defineProps */
const props = defineProps({ const props = defineProps({
data: { data: {
@ -55,26 +60,32 @@ const props = defineProps({
}, },
config: { config: {
type: Object as PropType<{ type: Object as PropType<{
graph: {
showTableValues: boolean; showTableValues: boolean;
tableHeaderCol2: string; tableHeaderCol2: string;
tableHeaderCol1: string; tableHeaderCol1: string;
};
metricTypes: string[];
}>, }>,
default: () => ({}), default: () => ({ showTableValues: true }),
}, },
}); });
/*global Nullable*/ /*global Nullable*/
const { t } = useI18n();
const chartTable = ref<Nullable<HTMLElement>>(null); const chartTable = ref<Nullable<HTMLElement>>(null);
const initWidth = ref<number>(0); const initWidth = ref<number>(0);
const nameWidth = ref<number>(0); const nameWidth = ref<number>(0);
const draggerName = ref<Nullable<HTMLElement>>(null); const draggerName = ref<Nullable<HTMLElement>>(null);
const showTableValues = ref<boolean>(props.config.graph.showTableValues);
onMounted(() => { onMounted(() => {
if (!chartTable.value) { if (!chartTable.value) {
return; return;
} }
const width = props.config.showTableValues const width = props.config.graph.showTableValues
? chartTable.value.offsetWidth / 2 ? chartTable.value.offsetWidth / 2
: chartTable.value.offsetWidth; : chartTable.value.offsetWidth;
initWidth.value = props.config.showTableValues initWidth.value = props.config.graph.showTableValues
? chartTable.value.offsetWidth / 2 ? chartTable.value.offsetWidth / 2
: 0; : 0;
nameWidth.value = width - 5; nameWidth.value = width - 5;
@ -95,8 +106,12 @@ onMounted(() => {
}; };
}); });
const dataKeys = computed(() => { const dataKeys = computed(() => {
if (props.config.metricTypes[0] === "readMetricsValue") {
const keys = Object.keys(props.data || {});
return keys;
}
const keys = Object.keys(props.data || {}).filter( const keys = Object.keys(props.data || {}).filter(
(i: any) => Array.isArray(props.data[i]) && props.data[i].length (i: string) => Array.isArray(props.data[i]) && props.data[i].length
); );
return keys; return keys;
}); });

View File

@ -20,6 +20,11 @@
padding: 0 10px 5px 0; padding: 0 10px 5px 0;
} }
.list {
margin-top: 10px;
margin-bottom: 10px;
}
.pagination { .pagination {
width: 100%; width: 100%;
text-align: center; text-align: center;

View File

@ -19,7 +19,7 @@ limitations under the License. -->
:row-height="10" :row-height="10"
:is-draggable="true" :is-draggable="true"
:is-resizable="true" :is-resizable="true"
@layout-updated="layoutUpdatedEvent" v-if="dashboardStore.layout.length"
> >
<grid-item <grid-item
v-for="item in dashboardStore.layout" v-for="item in dashboardStore.layout"
@ -36,10 +36,13 @@ limitations under the License. -->
<component :is="item.type" :data="item" /> <component :is="item.type" :data="item" />
</grid-item> </grid-item>
</grid-layout> </grid-layout>
<div class="no-data-tips" v-else>{{ t("noWidget") }}</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent, onBeforeUnmount } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import { LayoutConfig } from "@/types/dashboard"; import { LayoutConfig } from "@/types/dashboard";
import controls from "../controls/index"; import controls from "../controls/index";
@ -47,7 +50,9 @@ export default defineComponent({
name: "Layout", name: "Layout",
components: { ...controls }, components: { ...controls },
setup() { setup() {
const { t } = useI18n();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore();
function layoutUpdatedEvent(newLayout: LayoutConfig[]) { function layoutUpdatedEvent(newLayout: LayoutConfig[]) {
dashboardStore.setLayout(newLayout); dashboardStore.setLayout(newLayout);
} }
@ -55,10 +60,16 @@ export default defineComponent({
dashboardStore.activeGridItem(item.i); dashboardStore.activeGridItem(item.i);
dashboardStore.selectWidget(item); dashboardStore.selectWidget(item);
} }
onBeforeUnmount(() => {
dashboardStore.setLayout([]);
selectorStore.setCurrentService(null);
selectorStore.setCurrentPod(null);
});
return { return {
dashboardStore, dashboardStore,
layoutUpdatedEvent, layoutUpdatedEvent,
clickGrid, clickGrid,
t,
}; };
}, },
}); });
@ -78,4 +89,12 @@ export default defineComponent({
.vue-grid-item.active { .vue-grid-item.active {
border: 1px solid #409eff; border: 1px solid #409eff;
} }
.no-data-tips {
width: 100%;
text-align: center;
font-size: 14px;
padding-top: 30px;
color: #888;
}
</style> </style>

View File

@ -75,35 +75,30 @@ limitations under the License. -->
<div class="tool-icons"> <div class="tool-icons">
<span <span
@click="clickIcons(t)" @click="clickIcons(t)"
v-for="(t, index) in ToolIcons" v-for="(t, index) in toolIcons"
:key="index" :key="index"
:title="t.content" :title="t.content"
> >
<Icon <Icon class="icon-btn" size="sm" :iconName="t.name" />
class="icon-btn"
size="sm"
:iconName="t.name"
v-if="
!['topology', 'trace', 'profile'].includes(t.id) ||
(t.id === 'topology' &&
hasTopology.includes(dashboardStore.entity)) ||
(t.id === 'trace' &&
TraceEntitys.includes(dashboardStore.entity)) ||
(t.id === 'profile' &&
dashboardStore.entity === EntityType[0].value)
"
/>
</span> </span>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, watch } from "vue"; import { reactive, ref } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { EntityType, ToolIcons, hasTopology, TraceEntitys } from "../data"; import {
EntityType,
AllTools,
ServiceTools,
InstanceTools,
EndpointTools,
PodRelationTools,
ServiceRelationTools,
} from "../data";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { Option } from "@/types/app"; import { Option } from "@/types/app";
@ -113,6 +108,8 @@ const selectorStore = useSelectorStore();
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const params = useRoute().params; const params = useRoute().params;
const type = EntityType.filter((d: Option) => d.value === params.entity)[0]; const type = EntityType.filter((d: Option) => d.value === params.entity)[0];
const toolIcons =
ref<{ name: string; content: string; id: string }[]>(PodRelationTools);
const states = reactive<{ const states = reactive<{
destService: string; destService: string;
destPod: string; destPod: string;
@ -131,12 +128,14 @@ const states = reactive<{
currentDestPod: "", currentDestPod: "",
}); });
dashboardStore.setLayer(String(params.layerId)); dashboardStore.setLayer(params.layerId);
dashboardStore.setEntity(String(params.entity)); dashboardStore.setEntity(params.entity);
appStore.setEventStack([initSelector]);
initSelector(); initSelector();
function initSelector() { function initSelector() {
getTools();
if (params.serviceId) { if (params.serviceId) {
setSelector(); setSelector();
} else { } else {
@ -180,7 +179,8 @@ async function setSelector() {
selectorStore.setCurrentService(currentService); selectorStore.setCurrentService(currentService);
selectorStore.setCurrentDestService(currentDestService); selectorStore.setCurrentDestService(currentDestService);
states.currentService = selectorStore.currentService.value; states.currentService = selectorStore.currentService.value;
states.currentDestService = selectorStore.currentDestService.value; states.currentDestService =
selectorStore.currentDestService && selectorStore.currentDestService.value;
} }
async function setSourceSelector() { async function setSourceSelector() {
@ -318,6 +318,9 @@ function setTabControls(id: string) {
case "addTopology": case "addTopology":
dashboardStore.addTabControls("Topology"); dashboardStore.addTabControls("Topology");
break; break;
case "apply":
dashboardStore.saveDashboard();
break;
default: default:
ElMessage.info("Don't support this control"); ElMessage.info("Don't support this control");
break; break;
@ -344,8 +347,8 @@ function setControls(id: string) {
case "addTopology": case "addTopology":
dashboardStore.addControl("Topology"); dashboardStore.addControl("Topology");
break; break;
case "settings": case "apply":
dashboardStore.setConfigPanel(true); dashboardStore.saveDashboard();
break; break;
default: default:
dashboardStore.addControl("Widget"); dashboardStore.addControl("Widget");
@ -401,12 +404,27 @@ async function fetchPods(type: string, serviceId: string, setPod: boolean) {
ElMessage.error(resp.errors); ElMessage.error(resp.errors);
} }
} }
watch( function getTools() {
() => appStore.durationTime, switch (params.entity) {
() => { case EntityType[1].value:
initSelector(); toolIcons.value = AllTools;
break;
case EntityType[0].value:
toolIcons.value = ServiceTools;
break;
case EntityType[2].value:
toolIcons.value = EndpointTools;
break;
case EntityType[3].value:
toolIcons.value = InstanceTools;
break;
case EntityType[4].value:
toolIcons.value = ServiceRelationTools;
break;
default:
toolIcons.value = PodRelationTools;
}
} }
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.dashboard-tool { .dashboard-tool {

View File

@ -148,7 +148,7 @@ const state = reactive<any>({
init(); init();
async function init() { async function init() {
const resp = await logStore.queryLogsByKeywords(); const resp = await logStore.getLogsByKeywords();
if (resp.errors) { if (resp.errors) {
ElMessage.error(resp.errors); ElMessage.error(resp.errors);

View File

@ -83,7 +83,8 @@ searchTasks();
// } // }
async function searchTasks() { async function searchTasks() {
profileStore.setConditions({ profileStore.setConditions({
serviceId: selectorStore.currentService.id, serviceId:
(selectorStore.currentService && selectorStore.currentService.id) || "",
endpointName: endpointName.value, endpointName: endpointName.value,
}); });
const res = await profileStore.getTaskList(); const res = await profileStore.getTaskList();

View File

@ -1,23 +0,0 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="enpoints">This is a enpoint page</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
div {
padding: 15px;
}
</style>

View File

@ -1,24 +0,0 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div>This is the Metrics page</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
div {
padding: 15px;
text-align: center;
}
</style>

View File

@ -1,110 +0,0 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="service-detail">
<div class="title">
<span>{{ state.serviceID }}</span>
<span>Types</span>
<span>Technologies</span>
</div>
<div class="tabs">
<router-link
class="tab cp"
v-for="tab in tabs"
:key="tab"
@click="handleClick(tab)"
:class="{ active: tab === activeName }"
:to="`${state.path}/${state.serviceID}/${tab}`"
>
{{ t(tab) }}
</router-link>
</div>
<Endpoints v-if="state.type === tabs[2]" />
<Metrics v-else-if="state.type === tabs[0]" />
<Topology
v-else-if="state.type === tabs[1]"
msg="This is the Topology page"
/>
<Traces v-else-if="state.type === tabs[3]" msg="This is the Trace page" />
<Profiles v-else msg="This is the Profiles page" />
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import Metrics from "./Metrics.vue";
import Endpoints from "./Endpoints.vue";
import Topology from "./Topology.vue";
import Traces from "./Traces.vue";
import Profiles from "./Profiles.vue";
import { useAppStoreWithOut } from "@/store/modules/app";
const appStore = useAppStoreWithOut();
appStore.setPageTitle("General Service");
const route = useRoute();
const { t } = useI18n();
const tabs = ["metrics", "topologies", "endpoints", "traces", "profiles"];
const activeName = ref<string>(tabs[0]);
const state = reactive({
serviceID: route.params.id,
type: route.params.type,
path: route.meta.headPath,
});
function handleClick(tab: string) {
activeName.value = tab;
state.type = tab;
}
</script>
<style lang="scss" scoped>
.service-detail {
text-align: left;
}
.tabs {
padding: 15px 15px 0 15px;
border-bottom: 1px solid var(--el-border-color-light);
}
.tab {
display: inline-block;
margin-right: 30px;
font-size: 13px;
font-weight: 400;
height: 30px;
&:hover {
color: var(--el-color-primary);
}
&.active {
color: var(--el-color-primary);
border-bottom: 1px solid var(--el-color-primary);
}
}
.title {
padding: 5px 0 5px 15px;
font-size: 14px;
font-weight: 400;
border-bottom: 1px solid #dfe4e8;
background-color: #c4c8e133;
span {
display: inline-block;
margin-right: 10px;
}
}
</style>

View File

@ -1,31 +0,0 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div>{{ msg }}</div>
</template>
<script lang="ts" setup>
/*global defineProps */
defineProps({
msg: { type: String },
});
// props.msg
</script>
<style scoped>
div {
padding: 15px;
}
</style>

View File

@ -1,131 +0,0 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="service-table">
<el-table :data="tableData" :span-method="objectSpanMethod" border>
<el-table-column
v-for="(h, index) in tableHeader"
:label="t(h)"
:key="h + index"
>
<template #default="scope">
<router-link
:to="`${state.path}/${scope.row.serviceName}/metrics`"
v-if="h === tableHeader[1] && index !== 0"
>
<span class="service-name cp">{{ scope.row[h] }}</span>
</router-link>
<span v-else>{{ scope.row[h] }}</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script lang="ts" setup>
import { reactive, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { ElTable, ElTableColumn } from "element-plus";
const route = useRoute();
const { t } = useI18n();
const tableHeader = [
"groupName",
"serviceName",
"types",
"technologies",
"endpoints",
"health",
];
const tableData = [
{
endpoints: 2,
groupName: "group 1",
serviceName: "discount",
types: "HTTP",
health: true,
technologies: "Spring Boot",
},
{
endpoints: 3,
groupName: "group 1",
serviceName: "frontend",
types: "HTTP",
health: true,
technologies: "Node.js",
},
{
endpoints: 3,
groupName: "group 2",
serviceName: "web",
types: "",
health: true,
technologies: "Nginx",
},
{
endpoints: 3,
groupName: "group 2",
serviceName: "shipping",
types: "HTTP",
health: true,
technologies: "JVM",
},
{
endpoints: 3,
groupName: "group 3",
serviceName: "payment",
types: "HTTP MESSAGING",
health: true,
technologies: "RabbitMQ Python",
},
];
const state = reactive({
path: route.meta.headPath,
});
const objectSpanMethod = (item: { columnIndex: number; rowIndex: number }) => {
if (item.columnIndex === 0) {
if (item.rowIndex % 2 === 0) {
return {
rowspan: 2,
colspan: 1,
};
} else {
return {
rowspan: 0,
colspan: 0,
};
}
}
};
watch(
() => route.meta.headPath,
(path: unknown) => {
if (!path) {
return;
}
state.path = path;
}
);
</script>
<style lang="scss" scoped>
.service-name {
color: #448edf;
cursor: pointer;
}
.service-table {
padding: 15px;
}
</style>

View File

@ -1,30 +0,0 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div>Topology</div>
</template>
<script lang="ts" setup>
/*global defineProps */
defineProps({
msg: { type: String },
});
// props.msg
</script>
<style scoped>
div {
padding: 15px;
}
</style>

View File

@ -1,31 +0,0 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div>Traces</div>
</template>
<script lang="ts" setup>
/*global defineProps */
defineProps({
msg: { type: String },
});
// props.msg
</script>
<style scoped>
div {
padding: 15px;
}
</style>

View File

@ -1,41 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const TabsConfig: { [key: string]: any } = {
GeneralService: [
{ name: "metrics", path: "/generalService/metrics" },
{ name: "traces", path: "/generalService/traces" },
{ name: "profiles", path: "/generalService/profiles" },
{ name: "services", path: "/generalService" },
],
ServiceMesh: [
{ name: "services", path: "/serviceMesh" },
{ name: "metrics", path: "/serviceMesh/metrics" },
{ name: "traces", path: "/serviceMesh/traces" },
{ name: "profiles", path: "/serviceMesh/profiles" },
],
};
export const PagesConfig = [
{ label: "generalService", name: "GeneralService" },
{ label: "serviceMesh", name: "ServiceMesh" },
{ label: "virtualMachine", name: "VirtualMachine" },
{ label: "dashboardHome", name: "DashboardHome" },
{ label: "dashboardList", name: "DashboardList" },
{ label: "logs", name: "Logs" },
{ label: "settings", name: "Settings" },
{ label: "events", name: "Events" },
{ label: "alerts", name: "Alerts" },
];