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>
<router-view :key="$route.fullPath" />
</template>
<style>
#app {
color: #2c3e50;

View File

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

View File

@ -18,6 +18,7 @@ export const TypeOfMetrics = {
variable: "$name: String!",
query: `typeOfMetrics(name: $name)`,
};
export const listMetrics = {
variable: "$regex: String",
query: `
@ -30,17 +31,40 @@ export const listMetrics = {
`,
};
export const queryHeatMap = {
variable: ["$condition: MetricsCondition!, $duration: Duration!"],
export const getAllTemplates = {
query: `
readHeatMap: readHeatMap(condition: $condition, duration: $duration) {
values {
getAllTemplates {
id,
configuration,
}
`,
};
export const addTemplate = {
variable: "$setting: NewDashboardSetting!",
query: `
addTemplate(setting: $setting) {
id
values
}
buckets {
min
max
}
status
message
}`,
};
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 {
TypeOfMetrics,
queryHeatMap,
listMetrics,
getAllTemplates,
addTemplate,
changeTemplate,
deleteTemplate,
} from "../fragments/dashboard";
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 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
}`,
};
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
* limitations under the License.
*/
import dayjs from "dayjs";
import { RespFields, MetricQueryTypes } from "./data";
import { ElMessage } from "element-plus";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import { useAppStoreWithOut } from "@/store/modules/app";
import { Instance, Endpoint, Service } from "@/types/selector";
import { StandardConfig } from "@/types/dashboard";
export function useQueryProcessor(config: any) {
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 metricType = config.metricTypes[index] || "";
const labels = ["0", "1", "2", "3", "4"];
if (
[
MetricQueryTypes.ReadSampledRecords,
@ -62,10 +63,13 @@ export function useQueryProcessor(config: any) {
normal: selectorStore.currentService.normal,
scope: dashboardStore.entity,
topN: 10,
order: "DES",
order: config.standard.sortOrder || "DES",
};
} else {
if (metricType === MetricQueryTypes.ReadLabeledMetricsValues) {
const labels = (config.labelsIndex || "")
.split(",")
.map((item: string) => item.replace(/^\s*|\s*$/g, ""));
variables.push(`$labels${index}: [String!]!`);
conditions[`labels${index}`] = labels;
}
@ -121,7 +125,11 @@ export function useQueryProcessor(config: any) {
}
export function useSourceProcessor(
resp: { errors: string; data: { [key: string]: any } },
config: { metrics: string[]; metricTypes: string[] }
config: {
metrics: string[];
metricTypes: string[];
standard: StandardConfig;
}
) {
if (resp.errors) {
ElMessage.error(resp.errors);
@ -135,16 +143,20 @@ export function useSourceProcessor(
if (type === MetricQueryTypes.ReadMetricsValues) {
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) {
const resVal = Object.values(resp.data)[0] || [];
const labelsIdx = ["0", "1", "2", "3", "4"];
const labels = ["P50", "P75", "P90", "P95", "P99"];
const labels = (config.standard.metricLabels || "")
.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) {
const values = item.values.values.map(
(d: { value: number }) => d.value
const values = item.values.values.map((d: { value: number }) =>
aggregation(Number(d.value), config.standard)
);
const indexNum = labelsIdx.findIndex((d: string) => d === item.label);
@ -156,13 +168,22 @@ export function useSourceProcessor(
}
}
if (type === MetricQueryTypes.ReadMetricsValue) {
source[m] = Object.values(resp.data)[0];
source[m] = aggregation(
Number(Object.values(resp.data)[0]),
config.standard
);
}
if (
type === MetricQueryTypes.SortMetrics ||
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) {
const resVal = Object.values(resp.data)[0] || {};
@ -205,7 +226,6 @@ export function useQueryPodsMetrics(
};
const variables: string[] = [`$duration: Duration!`];
const { currentService } = selectorStore;
const fragmentList = pods.map(
(
d: (Instance | Endpoint | Service) & { normal: boolean },
@ -286,3 +306,34 @@ export function useQueryTopologyMetrics(metrics: string[], ids: string[]) {
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
limitations under the License. -->
<template>
<section class="app-main">
<section class="app-main flex-v">
<router-view v-slot="{ Component }" :key="$route.fullPath">
<keep-alive>
<component :is="Component" />
@ -21,7 +21,19 @@ limitations under the License. -->
</router-view>
</section>
</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>
.app-main {
height: calc(100% - 40px);

View File

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

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
const msg = {
generalService: "General Service",
general: "General Service",
services: "Services",
service: "Service",
traces: "Traces",
@ -96,6 +96,22 @@ const msg = {
taskList: "Task List",
sampledTraces: "Sampled Traces",
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",
minuteTip: "Select Minute",
secondTip: "Select Second",
@ -234,7 +250,7 @@ const msg = {
parentService: "Parent Service",
isParentService: "Set Parent Service",
noneParentService: "No Parent Service",
serviceGroup: "Service Group",
group: "Service Group",
endpointFilter: "Endpoint Filter",
databaseView: "Database",
browserView: "Browser",
@ -331,8 +347,6 @@ const msg = {
conditionNotice:
"Notice: Please press Enter after inputting a tag, key of content, exclude key of content(key=value).",
cacheModalTitle: "Clear cache reminder",
yes: "Yes",
no: "No",
cacheReminderContent:
"SkyWalking detected dashboard template updates, do you want to update?",
language: "Language",

View File

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

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ export const routesMesh: Array<RouteRecordRaw> = [
title: "services",
headPath: "/mesh/services",
},
component: () => import("@/views/service/Service.vue"),
component: () => import("@/views/Layer.vue"),
},
{
path: "/mesh/controlPanel",
@ -45,7 +45,7 @@ export const routesMesh: Array<RouteRecordRaw> = [
title: "controlPanel",
headPath: "/mesh/controlPanel",
},
component: () => import("@/views/service/Service.vue"),
component: () => import("@/views/Layer.vue"),
},
{
path: "/mesh/dataPanel",
@ -54,40 +54,7 @@ export const routesMesh: Array<RouteRecordRaw> = [
title: "dataPanel",
headPath: "/mesh/dataPanel",
},
component: () => import("@/views/service/Service.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"),
component: () => import("@/views/Layer.vue"),
},
],
},

View File

@ -29,161 +29,3 @@ export const NewControl = {
metrics: [""],
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 { store } from "@/store";
import graphql from "@/graphql";
import { Duration, DurationTime } from "@/types/app";
import getLocalTime from "@/utils/localtime";
import getDurationRow from "@/utils/dateTime";
import { AxiosResponse } from "axios";
import dateFormatStep, { dateFormatTime } from "@/utils/dateFormat";
import { TimeType } from "@/constants/data";
/*global Nullable*/
interface AppState {
durationRow: any;
@ -35,7 +37,11 @@ interface AppState {
export const appStore = defineStore({
id: "app",
state: (): AppState => ({
durationRow: getDurationRow(),
durationRow: {
start: new Date(new Date().getTime() - 1800000),
end: new Date(),
step: TimeType.MINUTE_TIME,
},
utc: "",
utcHour: 0,
utcMin: 0,
@ -102,7 +108,6 @@ export const appStore = defineStore({
actions: {
setDuration(data: Duration): void {
this.durationRow = data;
localStorage.setItem("durationRow", JSON.stringify(data, null, 0));
if ((window as any).axiosCancel.length !== 0) {
for (const event of (window as any).axiosCancel) {
setTimeout(event(), 0);
@ -116,7 +121,6 @@ export const appStore = defineStore({
this.utcMin = utcMin;
this.utcHour = utcHour;
this.utc = `${utcHour}:${utcMin}`;
localStorage.setItem("utc", this.utc);
},
setEventStack(funcs: (() => void)[]): void {
this.eventStack = funcs;
@ -139,6 +143,18 @@ export const appStore = defineStore({
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 {

View File

@ -19,20 +19,14 @@ import { store } from "@/store";
import { LayoutConfig } from "@/types/dashboard";
import graphql from "@/graphql";
import query from "@/graphql/fetch";
import {
ConfigData,
ConfigData1,
ConfigData2,
ConfigData3,
ConfigData4,
ConfigData5,
ConfigData6,
} from "../data";
import { DashboardItem } from "@/types/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import { NewControl } from "../data";
import { Duration } from "@/types/app";
import { AxiosResponse } from "axios";
import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
interface DashboardState {
showConfig: boolean;
layout: LayoutConfig[];
@ -44,12 +38,14 @@ interface DashboardState {
selectorStore: any;
showTopology: boolean;
currentTabItems: LayoutConfig[];
dashboards: DashboardItem[];
currentDashboard: Nullable<DashboardItem>;
}
export const dashboardStore = defineStore({
id: "dashboard",
state: (): DashboardState => ({
layout: [ConfigData],
layout: [],
showConfig: false,
selectedGrid: null,
entity: "",
@ -59,11 +55,20 @@ export const dashboardStore = defineStore({
selectorStore: useSelectorStore(),
showTopology: false,
currentTabItems: [],
dashboards: [],
currentDashboard: null,
}),
actions: {
setLayout(data: LayoutConfig[]) {
this.layout = data;
},
resetDashboards(list: DashboardItem[]) {
this.dashboards = list;
sessionStorage.setItem("dashboards", JSON.stringify(list));
},
setCurrentDashboard(item: DashboardItem) {
this.currentDashboard = item;
},
addControl(type: string) {
const newItem: LayoutConfig = {
...NewControl,
@ -129,7 +134,7 @@ export const dashboardStore = defineStore({
if (idx < 0) {
return;
}
const tabIndex = this.layout[idx].activedTabIndex;
const tabIndex = this.layout[idx].activedTabIndex || 0;
const { children } = this.layout[idx].children[tabIndex];
const newItem: LayoutConfig = {
...NewControl,
@ -229,28 +234,6 @@ export const dashboardStore = defineStore({
},
setEntity(type: string) {
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) {
this.showTopology = show;
@ -303,6 +286,154 @@ export const dashboardStore = defineStore({
const res: AxiosResponse = await query(param);
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({
id: "trace",
id: "log",
state: (): LogState => ({
services: [{ value: "0", label: "All" }],
instances: [{ value: "0", label: "All" }],
@ -101,7 +101,7 @@ export const logStore = defineStore({
] || [{ value: "0", label: "All" }];
return res.data;
},
async queryLogsByKeywords() {
async getLogsByKeywords() {
const res: AxiosResponse = await graphql
.query("queryLogsByKeywords")
.params({});

View File

@ -44,7 +44,7 @@ interface ProfileState {
highlightTop: boolean;
}
export const traceStore = defineStore({
export const profileStore = defineStore({
id: "profile",
state: (): ProfileState => ({
services: [{ value: "0", label: "All" }],
@ -208,5 +208,5 @@ export const traceStore = defineStore({
});
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
* limitations under the License.
*/
export type DashboardItem = {
id?: string;
entity: string;
layer: string;
isRoot: boolean;
name: string;
};
export interface LayoutConfig {
x: number;
y: number;
@ -38,8 +46,8 @@ export interface WidgetConfig {
export interface StandardConfig {
sortOrder?: string;
unit?: string;
max?: string;
min?: string;
labelsIndex?: string;
metricLabels?: string;
plus?: string;
minus?: string;
multiply?: string;
@ -99,6 +107,7 @@ export interface ServiceListConfig {
type?: string;
dashboardName: string;
fontSize: number;
showGroup: boolean;
}
export interface InstanceListConfig {

View File

@ -14,31 +14,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Duration } from "@/types/app";
import { TimeType } from "@/constants/data";
/**
* init or generate durationRow Obj and save localStorage.
*/
const getDurationRow = (): Duration => {
const durationRowString = localStorage.getItem("durationRow");
let durationRow: Duration;
if (durationRowString && durationRowString !== "") {
durationRow = JSON.parse(durationRowString);
durationRow = {
start: new Date(durationRow.start),
end: new Date(durationRow.end),
step: durationRow.step,
};
} else {
durationRow = {
start: new Date(new Date().getTime() - 900000),
end: new Date(),
step: TimeType.MINUTE_TIME,
};
localStorage.setItem("durationRow", JSON.stringify(durationRow, null, 0));
export const readFile = (event: any) => {
return new Promise((resolve) => {
const { files } = event.target;
if (files.length < 1) {
return;
}
const file = files[0];
const reader: FileReader = new FileReader();
reader.readAsText(file);
reader.onload = function () {
if (typeof this.result === "string") {
resolve(JSON.parse(this.result));
}
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
* limitations under the License.
*/
import graphql from "@/graphql";
import { AxiosResponse } from "axios";
const getLocalTime = (utc: string, time: Date): Date => {
const utcArr = utc.split(":");
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);
};
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;

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

View File

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

View File

@ -17,13 +17,14 @@ limitations under the License. -->
<div class="flex-h header" style="margin: 10px 0">
<el-input
v-model="searchText"
placeholder="Please input"
placeholder="Please input name"
class="input-with-search"
size="small"
@change="searchDashboards"
>
<template #append>
<el-button size="small">
<Icon size="lg" iconName="search" />
<Icon size="sm" iconName="search" />
</el-button>
</template>
</el-input>
@ -33,38 +34,106 @@ limitations under the License. -->
</el-button>
</router-link>
</div>
<el-table :data="tableData" style="width: 100%" max-height="550">
<el-table-column fixed prop="name" label="Name" />
<el-table-column prop="type" label="Type" />
<el-table-column prop="date" label="Date" />
<div class="table">
<el-table
:data="dashboards"
: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">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.$index, scope.row)">
<el-button size="small" @click="handleView(scope.row)">
{{ t("view") }}
</el-button>
<el-button size="small" @click="handleEdit(scope.$index, scope.row)">
<el-button size="small" @click="handleEdit(scope.row)">
{{ t("edit") }}
</el-button>
<el-button
size="small"
type="danger"
@click="handleDelete(scope.$index, scope.row)"
<el-popconfirm
title="Are you sure to delete this?"
@confirm="handleDelete(scope.row)"
>
<template #reference>
<el-button size="small" type="danger">
{{ t("delete") }}
</el-button>
</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>
<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>
</template>
<script lang="ts" setup>
import { ref } from "vue";
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 { 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();
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
// # - k8s
// # - general(agent-installed)
@ -76,36 +145,218 @@ appStore.setPageTitle("Dashboard List");
// # - cache
// # - browser
// # - skywalking
const { t } = useI18n();
const searchText = ref<string>("");
const tableData = [
{
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);
appStore.setPageTitle("Dashboard List");
const handleSelectionChange = (val: DashboardItem[]) => {
multipleSelection.value = val;
};
const handleDelete = (index: number, row: any) => {
console.log(index, row);
setList();
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>
<style lang="scss" scoped>
.header {
@ -120,4 +371,32 @@ const handleDelete = (index: number, row: any) => {
width: 300px;
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>

View File

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

View File

@ -17,6 +17,9 @@ limitations under the License. -->
<div class="graph" v-loading="loading">
<div class="header">
<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">
<el-tooltip :content="dashboardStore.selectedGrid.widget.tips">
<Icon iconName="info_outline" size="sm" />
@ -33,6 +36,8 @@ limitations under the License. -->
i: dashboardStore.selectedGrid.i,
metrics: dashboardStore.selectedGrid.metrics,
metricTypes: dashboardStore.selectedGrid.metricTypes,
standard: dashboardStore.selectedGrid.standard,
isEdit: true,
}"
/>
<div v-show="!dashboardStore.selectedGrid.graph.type" class="no-data">
@ -55,7 +60,7 @@ limitations under the License. -->
<WidgetOptions />
</el-collapse-item>
<el-collapse-item :title="t('standardOptions')" name="4">
<StandardOptions />
<StandardOptions @update="getSource" @loading="setLoading" />
</el-collapse-item>
</el-collapse>
</div>
@ -98,7 +103,7 @@ export default defineComponent({
const loading = ref<boolean>(false);
const states = reactive<{
activeNames: string;
source: any;
source: unknown;
index: string;
visType: Option[];
}>({
@ -209,4 +214,9 @@ export default defineComponent({
.ds-name {
margin-bottom: 10px;
}
.unit {
display: inline-block;
margin-left: 5px;
}
</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
limitations under the License. -->
<template>
<div v-show="states.isTable" class="ds-name">
<div v-if="states.isList && states.dashboardList.length" class="ds-name">
<div>{{ t("dashboards") }}</div>
<el-input
v-model="states.dashboardName"
placeholder="Please input dashboard name"
<Selector
:value="states.dashboardName"
:options="states.dashboardList"
size="small"
placeholder="Please select a dashboard name"
@change="changeDashboard"
class="selectors"
size="small"
/>
</div>
<div>{{ t("metrics") }}</div>
@ -42,13 +43,13 @@ limitations under the License. -->
:options="states.metricTypeList[index]"
size="small"
:disabled="
dashboardStore.selectedGrid.graph.type && !states.isTable && index !== 0
dashboardStore.selectedGrid.graph.type && !states.isList && index !== 0
"
@change="changeMetricType(index, $event)"
class="selectors"
/>
<span
v-show="states.isTable || states.metricTypes[0] === 'readMetricsValues'"
v-show="states.isList || states.metricTypes[0] === 'readMetricsValues'"
>
<Icon
class="cp mr-5"
@ -88,18 +89,19 @@ import { Option } from "@/types/app";
import { useDashboardStore } from "@/store/modules/dashboard";
import {
MetricTypes,
TableChartTypes,
ListChartTypes,
MetricCatalog,
DefaultGraphConfig,
EntityType,
ChartTypes,
PodsChartTypes,
TableEntity,
ListEntity,
} from "../../data";
import { ElMessage } from "element-plus";
import Icon from "@/components/Icon.vue";
import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor";
import { useI18n } from "vue-i18n";
import { DashboardItem } from "@/types/dashboard";
/*global defineEmits */
const { t } = useI18n();
@ -111,26 +113,31 @@ const states = reactive<{
metricTypes: string[];
metricTypeList: Option[][];
visTypes: Option[];
isTable: boolean;
isList: boolean;
metricList: (Option & { type: string })[];
dashboardName: string;
dashboardList: (DashboardItem & { label: string; value: string })[];
}>({
metrics: metrics && metrics.length ? metrics : [""],
metricTypes: metricTypes && metricTypes.length ? metricTypes : [""],
metricTypeList: [],
visTypes: [],
isTable: false,
isList: false,
metricList: [],
dashboardName: graph.dashboardName,
dashboardList: [],
});
states.isTable = TableChartTypes.includes(graph.type);
states.isList = ListChartTypes.includes(graph.type);
states.visTypes = setVisTypes();
setDashboards();
setMetricType();
async function setMetricType(catalog?: string) {
if (states.isTable) {
catalog = catalog || TableEntity[graph.type];
const { graph } = dashboardStore.selectedGrid;
if (states.isList) {
catalog = catalog || ListEntity[graph.type];
} else {
catalog = catalog || dashboardStore.entity;
}
@ -140,24 +147,82 @@ async function setMetricType(catalog?: string) {
return;
}
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(
(d: { value: string; type: string }) => {
const metric = states.metrics.filter((m: string) => m === d.value)[0];
if (metric) {
const index = states.metrics.findIndex((m: string) => m === d.value);
if (index > -1) {
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) {
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]) {
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() {
let graphs = [];
if (dashboardStore.entity === EntityType[0].value) {
@ -168,7 +233,7 @@ function setVisTypes() {
);
} else {
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) {
const graph = DefaultGraphConfig[item.value];
states.isTable = TableChartTypes.includes(graph.type);
dashboardStore.selectWidget({ ...dashboardStore.selectedGrid, graph });
states.isTable = TableChartTypes.includes(graph.type);
if (states.isTable) {
states.isList = ListChartTypes.includes(graph.type);
if (states.isList) {
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
metrics: [""],
@ -194,12 +258,15 @@ function changeChartType(item: Option) {
EndpointList: EntityType[2].value,
ServiceList: EntityType[0].value,
};
if (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) {
states.metricTypeList = [];
states.metricTypes = [];
@ -212,33 +279,34 @@ function changeMetrics(index: number, arr: (Option & { type: string })[]) {
states.metrics[index] = arr[0].value;
const typeOfMetrics = arr[0].type;
states.metricTypeList[index] = MetricTypes[typeOfMetrics];
states.metricTypeList[index] = setMetricTypeList(typeOfMetrics);
states.metricTypes[index] = MetricTypes[typeOfMetrics][0].value;
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
...{ metricTypes: states.metricTypes, metrics: states.metrics },
});
if (states.isTable) {
if (states.isList) {
return;
}
queryMetrics();
}
function changeMetricType(index: number, opt: Option[]) {
function changeMetricType(index: number, opt: Option[] | any) {
const metric =
states.metricList.filter(
(d: Option) => states.metrics[index] === d.value
)[0] || {};
if (states.isTable) {
const l = setMetricTypeList(metric.type);
if (states.isList) {
states.metricTypes[index] = opt[0].value;
states.metricTypeList[index] = (MetricTypes as any)[metric.type];
states.metricTypeList[index] = l;
} else {
states.metricTypes = states.metricTypes.map((d: string) => {
d = opt[0].value;
return d;
});
states.metricTypeList = states.metricTypeList.map((d: Option[]) => {
d = (MetricTypes as any)[metric.type];
d = l;
return d;
});
@ -247,13 +315,17 @@ function changeMetricType(index: number, opt: Option[]) {
...dashboardStore.selectedGrid,
...{ metricTypes: states.metricTypes },
});
if (states.isTable) {
if (states.isList) {
return;
}
queryMetrics();
}
async function queryMetrics() {
const params = useQueryProcessor(states);
if (states.isList) {
return;
}
const { standard } = dashboardStore.selectedGrid;
const params = useQueryProcessor({ ...states, standard });
if (!params) {
emit("update", {});
return;
@ -266,11 +338,12 @@ async function queryMetrics() {
ElMessage.error(json.errors);
return;
}
const source = useSourceProcessor(json, states);
const source = useSourceProcessor(json, { ...states, standard });
emit("update", source);
}
function changeDashboard() {
function changeDashboard(opt: any) {
states.dashboardName = opt[0].value;
const graph = {
...dashboardStore.selectedGrid.graph,
dashboardName: states.dashboardName,
@ -282,7 +355,7 @@ function changeDashboard() {
}
function addMetric() {
states.metrics.push("");
if (!states.isTable) {
if (!states.isList) {
states.metricTypes.push(states.metricTypes[0]);
states.metricTypeList.push(states.metricTypeList[0]);
return;
@ -293,6 +366,21 @@ function deleteMetric(index: number) {
states.metrics.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>
<style lang="scss" scoped>
.ds-name {

View File

@ -17,132 +17,148 @@ limitations under the License. -->
<span class="label">{{ t("unit") }}</span>
<el-input
class="input"
v-model="state.unit"
v-model="selectedGrid.standard.unit"
size="small"
placeholder="Please input Unit"
@change="changeStandardOpt({ unit: state.unit })"
/>
</div>
<div class="item">
<span class="label">{{ t("sortOrder") }}</span>
<Selector
:value="state.sortOrder"
:value="sortOrder"
:options="SortOrder"
size="small"
placeholder="Select a sort order"
class="selector"
@change="changeStandardOpt({ sortOrder: state.sortOrder })"
@change="changeStandardOpt({ sortOrder })"
/>
</div>
<div class="item">
<span class="label">{{ t("max") }}</span>
<div class="item" v-show="percentile">
<span class="label">{{ t("labels") }}</span>
<el-input
class="input"
v-model="state.max"
v-model="selectedGrid.standard.metricLabels"
size="small"
placeholder="auto"
@change="changeStandardOpt({ max: state.max })"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("min") }}</span>
<div class="item" v-show="percentile">
<span class="label">{{ t("labelsIndex") }}</span>
<el-input
class="input"
v-model="state.min"
v-model="selectedGrid.standard.labelsIndex"
size="small"
placeholder="auto"
@change="changeStandardOpt({ min: state.min })"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("plus") }}</span>
<el-input
class="input"
v-model="state.plus"
v-model="selectedGrid.standard.plus"
size="small"
placeholder="none"
@change="changeStandardOpt({ plus: state.plus })"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("minus") }}</span>
<el-input
class="input"
v-model="state.minus"
v-model="selectedGrid.standard.minus"
size="small"
placeholder="none"
@change="changeStandardOpt({ minus: state.minus })"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("multiply") }}</span>
<el-input
class="input"
v-model="state.multiply"
v-model="selectedGrid.standard.multiply"
size="small"
placeholder="none"
@change="changeStandardOpt({ multiply: state.multiply })"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("divide") }}</span>
<el-input
class="input"
v-model="state.divide"
v-model="selectedGrid.standard.divide"
size="small"
placeholder="none"
@change="changeStandardOpt({ divide: state.divide })"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("convertToMilliseconds") }}</span>
<el-input
class="input"
v-model="state.milliseconds"
v-model="selectedGrid.standard.milliseconds"
size="small"
placeholder="none"
@change="changeStandardOpt({ milliseconds: state.milliseconds })"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("convertToSeconds") }}</span>
<el-input
class="input"
v-model="state.seconds"
v-model="selectedGrid.standard.seconds"
size="small"
placeholder="none"
@change="changeStandardOpt({ seconds: state.seconds })"
@change="changeStandardOpt"
/>
</div>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { SortOrder } from "../../data";
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 { selectedGrid } = dashboardStore;
const { t } = useI18n();
const state = reactive({
unit: selectedGrid.standard.unit,
max: "",
min: "",
plus: "",
minus: "",
multiply: "",
divide: "",
milliseconds: "",
seconds: "",
sortOrder: selectedGrid.standard.sortOrder,
});
const percentile = ref<boolean>(
selectedGrid.metricTypes.includes("readLabeledMetricsValues")
);
const sortOrder = ref<string>(selectedGrid.standard.sortOrder || "DES");
function changeStandardOpt(param: { [key: string]: unknown }) {
const standard = {
...selectedGrid.standard,
function changeStandardOpt(param?: any) {
let standard = dashboardStore.selectedGrid.standard;
if (param) {
standard = {
...dashboardStore.selectedGrid.standard,
...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>
<style lang="scss" scoped>

View File

@ -20,8 +20,8 @@ limitations under the License. -->
v-model="fontSize"
show-input
input-size="small"
:min="10"
:max="20"
:min="12"
:max="50"
:step="1"
@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
limitations under the License. -->
<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>
<span class="label">{{ t("smooth") }}</span>
<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
limitations under the License. -->
<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>
<el-slider
class="slider"
@ -38,6 +47,7 @@ const { selectedGrid } = dashboardStore;
const fontSize = ref(selectedGrid.graph.fontSize);
function updateConfig(param: { [key: string]: unknown }) {
const { selectedGrid } = dashboardStore;
const graph = {
...selectedGrid.graph,
...param,
@ -57,4 +67,8 @@ function updateConfig(param: { [key: string]: unknown }) {
display: block;
margin-bottom: 5px;
}
.item {
margin-top: 5px;
}
</style>

View File

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

View File

@ -14,7 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<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>
<span class="delete cp">
<Icon iconName="ellipsis_v" size="middle" class="operation" />
@ -34,6 +39,7 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useDashboardStore } from "@/store/modules/dashboard";
import Header from "../related/log/Header.vue";
import List from "../related/log/List.vue";
@ -47,6 +53,7 @@ const props = defineProps({
activeIndex: { type: String, default: "" },
});
const { t } = useI18n();
const routeParams = useRoute().params;
const dashboardStore = useDashboardStore();
function removeWidget() {

View File

@ -14,7 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<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>
<span class="delete cp">
<Icon iconName="ellipsis_v" size="middle" class="operation" />
@ -34,6 +39,7 @@ import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import Header from "../related/profile/Header.vue";
import Content from "../related/profile/Content.vue";
import { useRoute } from "vue-router";
/*global defineProps */
const props = defineProps({
@ -44,6 +50,7 @@ const props = defineProps({
activeIndex: { type: String, default: "" },
});
const { t } = useI18n();
const routeParams = useRoute().params;
const dashboardStore = useDashboardStore();
function removeWidget() {
dashboardStore.removeControls(props.data);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -83,7 +83,8 @@ searchTasks();
// }
async function searchTasks() {
profileStore.setConditions({
serviceId: selectorStore.currentService.id,
serviceId:
(selectorStore.currentService && selectorStore.currentService.id) || "",
endpointName: endpointName.value,
});
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" },
];