diff --git a/src/assets/icons/logo-light.svg b/src/assets/icons/logo-light.svg new file mode 100644 index 00000000..b2a04196 --- /dev/null +++ b/src/assets/icons/logo-light.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/graphql/fragments/topology.ts b/src/graphql/fragments/topology.ts index 327c1408..bdbf9158 100644 --- a/src/graphql/fragments/topology.ts +++ b/src/graphql/fragments/topology.ts @@ -23,6 +23,7 @@ export const ServicesTopology = { name type isReal + layers } calls { id diff --git a/src/hooks/useDashboardsSession.ts b/src/hooks/useDashboardsSession.ts index a97d7c33..3fc9d1af 100644 --- a/src/hooks/useDashboardsSession.ts +++ b/src/hooks/useDashboardsSession.ts @@ -24,7 +24,7 @@ export default function getDashboard(param?: { name?: string; layer: string; ent const dashboardStore = useDashboardStore(); const opt = param || dashboardStore.currentDashboard; const list = JSON.parse(sessionStorage.getItem("dashboards") || "[]"); - let dashboard; + let dashboard: Recordable; if (type === ConfigFieldTypes.NAME) { dashboard = list.find( (d: { name: string; layer: string; entity: string }) => @@ -62,6 +62,9 @@ export default function getDashboard(param?: { name?: string; layer: string; ent filters, }; dashboardStore.setWidget(item); + if (widget.id === sourceId) { + return; + } const targetTabIndex = (widget.id || "").split("-"); const sourceTabindex = (sourceId || "").split("-") || []; let container: Nullable; diff --git a/src/router/index.ts b/src/router/index.ts index 2cfd6c9d..819539e7 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -21,6 +21,7 @@ import { routesMarketplace } from "./marketplace"; import { routesAlarm } from "./alarm"; import routesLayers from "./layer"; import { routesSettings } from "./settings"; +import { routesNotFound } from "./notFound"; const routes: RouteRecordRaw[] = [ ...routesMarketplace, @@ -28,6 +29,7 @@ const routes: RouteRecordRaw[] = [ ...routesAlarm, ...routesDashboard, ...routesSettings, + ...routesNotFound, ]; const router = createRouter({ diff --git a/src/router/notFound.ts b/src/router/notFound.ts new file mode 100644 index 00000000..8efe0da0 --- /dev/null +++ b/src/router/notFound.ts @@ -0,0 +1,26 @@ +/** + * 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. + */ +import type { RouteRecordRaw } from "vue-router"; +import NotFound from "@/views/NotFound.vue"; + +export const routesNotFound: Array = [ + { + path: "/:pathMatch(.*)*", + name: "NotFound", + component: NotFound, + }, +]; diff --git a/src/store/modules/dashboard.ts b/src/store/modules/dashboard.ts index c784acee..46f6cac3 100644 --- a/src/store/modules/dashboard.ts +++ b/src/store/modules/dashboard.ts @@ -24,7 +24,6 @@ import { useSelectorStore } from "@/store/modules/selectors"; import { NewControl, TextConfig, TimeRangeConfig, ControlsTypes } from "../data"; import type { AxiosResponse } from "axios"; import { ElMessage } from "element-plus"; -import { useI18n } from "vue-i18n"; import { EntityType, MetricModes, WidgetType } from "@/views/dashboard/data"; interface DashboardState { showConfig: boolean; diff --git a/src/store/modules/topology.ts b/src/store/modules/topology.ts index b14e09fd..dee037ae 100644 --- a/src/store/modules/topology.ts +++ b/src/store/modules/topology.ts @@ -16,7 +16,6 @@ */ import { defineStore } from "pinia"; import { store } from "@/store"; -import type { Service } from "@/types/selector"; import type { Node, Call, HierarchyNode, ServiceHierarchy, InstanceHierarchy } from "@/types/topology"; import graphql from "@/graphql"; import { useSelectorStore } from "@/store/modules/selectors"; @@ -88,12 +87,9 @@ export const topologyStore = defineStore({ }, setTopology(data: { nodes: Node[]; calls: Call[] }) { const obj = {} as Recordable; - const services = useSelectorStore().services; const nodes = (data.nodes || []).reduce((prev: Node[], next: Node) => { if (!obj[next.id]) { obj[next.id] = true; - const s = services.filter((d: Service) => d.id === next.id)[0] || {}; - next.layer = s.layers ? s.layers[0] : null; prev.push(next); } return prev; @@ -603,7 +599,12 @@ export const topologyStore = defineStore({ const dashboardStore = useDashboardStore(); const { currentService } = useSelectorStore(); const id = this.node ? this.node.id : (currentService || {}).id; - const layer = this.node ? this.node.layer : dashboardStore.layerId; + let layer = dashboardStore.layerId; + if (this.node) { + layer = this.node.layers.includes(dashboardStore.layerId) + ? dashboardStore.layerId + : this.node.layers.filter((d: string) => d !== dashboardStore.layerId)[0]; + } if (!(id && layer)) { return new Promise((resolve) => resolve({})); } @@ -659,7 +660,7 @@ export const topologyStore = defineStore({ return metrics; }, async queryHierarchyNodeExpressions(expressions: string[], layer: string) { - const nodes = this.hierarchyServiceNodes.filter((n: Node) => n.layer === layer); + const nodes = this.hierarchyServiceNodes.filter((n: HierarchyNode) => n.layer === layer); if (!nodes.length) { this.setHierarchyNodeMetricValue({}, layer); return; @@ -672,7 +673,7 @@ export const topologyStore = defineStore({ this.setHierarchyNodeMetricValue(metrics, layer); }, async queryHierarchyInstanceNodeExpressions(expressions: string[], layer: string) { - const nodes = this.hierarchyInstanceNodes.filter((n: Node) => n.layer === layer); + const nodes = this.hierarchyInstanceNodes.filter((n: HierarchyNode) => n.layer === layer); if (!expressions.length) { this.setHierarchyInstanceNodeMetricValue({}, layer); diff --git a/src/types/topology.d.ts b/src/types/topology.d.ts index d2e4c4e3..ec7feb11 100644 --- a/src/types/topology.d.ts +++ b/src/types/topology.d.ts @@ -43,7 +43,7 @@ export interface Node { name: string; type: string; isReal: boolean; - layer?: string; + layers: string[]; serviceName?: string; height?: number; width?: number; @@ -52,6 +52,7 @@ export interface Node { level?: number; l?: number; key?: string; + layer?: string; } export interface ServiceHierarchy { diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts new file mode 100644 index 00000000..83cc8b0f --- /dev/null +++ b/src/utils/debounce.ts @@ -0,0 +1,29 @@ +/** + * 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 function debounce(callback: Function, dur: number) { + let timer: any; + + return function () { + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(function () { + callback(); + }, dur); + }; +} diff --git a/src/views/NotFound.vue b/src/views/NotFound.vue new file mode 100644 index 00000000..fe7bcee9 --- /dev/null +++ b/src/views/NotFound.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/views/dashboard/controls/Trace.vue b/src/views/dashboard/controls/Trace.vue index d717b496..4daa614b 100644 --- a/src/views/dashboard/controls/Trace.vue +++ b/src/views/dashboard/controls/Trace.vue @@ -81,7 +81,7 @@ limitations under the License. --> padding: 10px; font-size: $font-size-smaller; border-bottom: 1px solid $border-color; - min-width: 1200px; + min-width: 1000px; } .tools { @@ -101,6 +101,6 @@ limitations under the License. --> min-height: calc(100% - 150px); width: 100%; overflow: auto; - min-width: 1200px; + min-width: 1000px; } diff --git a/src/views/dashboard/related/topology/service/ServiceMap.vue b/src/views/dashboard/related/topology/service/ServiceMap.vue index b4eb5f83..111f2380 100644 --- a/src/views/dashboard/related/topology/service/ServiceMap.vue +++ b/src/views/dashboard/related/topology/service/ServiceMap.vue @@ -124,11 +124,17 @@ limitations under the License. --> left: operationsPos.x + 5 + 'px', }" > - + {{ item.title }} - + @@ -160,6 +166,7 @@ limitations under the License. --> import { layout, computeLevels, changeNode } from "../components/utils/layout"; import zoom from "@/views/dashboard/related/components/utils/zoom"; import { useQueryTopologyExpressionsProcessor } from "@/hooks/useExpressionsProcessor"; + import { ConfigFieldTypes } from "@/views/dashboard/data"; /*global Nullable, defineProps */ const props = defineProps({ config: { @@ -268,6 +275,20 @@ limitations under the License. --> currentNode.value = null; } + function getHierarchyTitle() { + if (!currentNode.value) { + return; + } + if (currentNode.value.layers.includes(dashboardStore.layerId)) { + return `${dashboardStore.layerId} --> ${currentNode.value.name}`; + } + const layer = currentNode.value.layers.filter((d: string) => d !== dashboardStore.layerId); + if (layer.length) { + return `${layer[0]} --> ${currentNode.value.name}`; + } + return ""; + } + async function initLegendMetrics() { if (!topologyStore.nodes.length) { return; @@ -410,19 +431,15 @@ limitations under the License. --> topologyStore.setLink(null); operationsPos.x = event.offsetX; operationsPos.y = event.offsetY; - if (d.layer === String(dashboardStore.layerId)) { + if (d.layers.includes(dashboardStore.layerId)) { setNodeTools(settings.value.nodeDashboard); return; } - items.value = [ - { id: "hierarchyServices", title: "Hierarchy Services", func: handleHierarchyRelatedServices }, - { id: "inspect", title: "Inspect", func: handleInspect }, - { id: "alerting", title: "Alerting", func: handleGoAlerting }, - ]; + initNodeMenus(); } function handleLinkClick(event: MouseEvent, d: Call) { event.stopPropagation(); - if (d.sourceObj.layer !== dashboardStore.layerId || d.targetObj.layer !== dashboardStore.layerId) { + if (!d.sourceObj.layers.includes(dashboardStore.layerId) || !d.targetObj.layers.includes(dashboardStore.layerId)) { return; } topologyStore.setNode(null); @@ -462,25 +479,34 @@ limitations under the License. --> topologyStore.setNode(null); topologyStore.setLink(null); } - function handleGoEndpoint(name: string) { + function handleGoEndpoint(params: { dashboard: string }) { + if (!params.dashboard) { + return; + } const origin = dashboardStore.entity; - const path = `/dashboard/${dashboardStore.layerId}/${EntityType[2].value}/${topologyStore.node.id}/${name}`; + const path = `/dashboard/${dashboardStore.layerId}/${EntityType[2].value}/${topologyStore.node.id}/${params.dashboard}`; const routeUrl = router.resolve({ path }); window.open(routeUrl.href, "_blank"); dashboardStore.setEntity(origin); } - function handleGoInstance(name: string) { + function handleGoInstance(params: { dashboard: string }) { + if (!params.dashboard) { + return; + } const origin = dashboardStore.entity; - const path = `/dashboard/${dashboardStore.layerId}/${EntityType[3].value}/${topologyStore.node.id}/${name}`; + const path = `/dashboard/${dashboardStore.layerId}/${EntityType[3].value}/${topologyStore.node.id}/${params.dashboard}`; const routeUrl = router.resolve({ path }); window.open(routeUrl.href, "_blank"); dashboardStore.setEntity(origin); } - function handleGoDashboard(name: string) { + function handleGoDashboard(params: { dashboard: string }) { + if (!params.dashboard) { + return; + } const origin = dashboardStore.entity; - const path = `/dashboard/${dashboardStore.layerId}/${EntityType[0].value}/${topologyStore.node.id}/${name}`; + const path = `/dashboard/${dashboardStore.layerId}/${EntityType[0].value}/${topologyStore.node.id}/${params.dashboard}`; const routeUrl = router.resolve({ path }); window.open(routeUrl.href, "_blank"); @@ -492,6 +518,28 @@ limitations under the License. --> window.open(routeUrl.href, "_blank"); } + function handleGoLayerDashboard(param: { id: string }) { + if (!(param.id && currentNode.value)) { + return; + } + const origin = dashboardStore.entity; + const { dashboard } = getDashboard( + { + layer: param.id, + entity: EntityType[0].value, + }, + ConfigFieldTypes.ISDEFAULT, + ); + if (!dashboard) { + return ElMessage.info("No Dashboard"); + } + + const path = `/dashboard/${param.id}/${EntityType[0].value}/${currentNode.value.id}/${dashboard.name}`; + const routeUrl = router.resolve({ path }); + + window.open(routeUrl.href, "_blank"); + dashboardStore.setEntity(origin); + } async function backToTopology() { loading.value = true; await freshNodes(); @@ -520,12 +568,26 @@ limitations under the License. --> settings.value = config; setNodeTools(config.nodeDashboard); } - function setNodeTools(nodeDashboard: any) { + function initNodeMenus() { items.value = [ { id: "hierarchyServices", title: "Hierarchy Services", func: handleHierarchyRelatedServices }, { id: "inspect", title: "Inspect", func: handleInspect }, { id: "alerting", title: "Alerting", func: handleGoAlerting }, ]; + if (!currentNode.value) { + return; + } + const diffLayers = currentNode.value.layers.filter((l: string) => l !== dashboardStore.layerId); + for (const l of diffLayers) { + items.value.push({ + id: l, + title: `${l} Dashboard`, + func: handleGoLayerDashboard, + }); + } + } + function setNodeTools(nodeDashboard: any) { + initNodeMenus(); if (!(nodeDashboard && nodeDashboard.length)) { return; } diff --git a/src/views/dashboard/related/trace/Detail.vue b/src/views/dashboard/related/trace/Detail.vue index f58f80fc..c5c1ea2a 100644 --- a/src/views/dashboard/related/trace/Detail.vue +++ b/src/views/dashboard/related/trace/Detail.vue @@ -172,9 +172,8 @@ limitations under the License. --> } .trace-chart { - height: calc(100% - 100px); + height: calc(100% - 95px); overflow: auto; - padding-bottom: 20px; } .trace-detail-wrapper { diff --git a/src/views/dashboard/related/trace/Filter.vue b/src/views/dashboard/related/trace/Filter.vue index b9eeb38f..0ca285a1 100644 --- a/src/views/dashboard/related/trace/Filter.vue +++ b/src/views/dashboard/related/trace/Filter.vue @@ -150,7 +150,13 @@ limitations under the License. --> ElMessage.error(resp.errors); return; } - state.service = getCurrentNode(traceStore.services) || traceStore.services[0]; + if (props.data.filters && props.data.filters.id === "0") { + state.service = { value: "", label: "" }; + return; + } else { + state.service = getCurrentNode(traceStore.services) || traceStore.services[0]; + } + emits("get", state.service.id); getEndpoints(state.service.id); @@ -198,7 +204,7 @@ limitations under the License. --> if (props.data.filters && props.data.filters.id) { param = { ...param, - serviceId: props.data.filters.id || undefined, + serviceId: props.data.filters.id && props.data.filters.id !== "0" ? props.data.filters.id : undefined, endpointId: state.endpoint.id || undefined, serviceInstanceId: state.instance.id || undefined, }; diff --git a/src/views/dashboard/related/trace/TraceList.vue b/src/views/dashboard/related/trace/TraceList.vue index 2ec6016d..ba2991a8 100644 --- a/src/views/dashboard/related/trace/TraceList.vue +++ b/src/views/dashboard/related/trace/TraceList.vue @@ -159,6 +159,7 @@ limitations under the License. --> .selectors { margin: 2px 2px 0 0; + width: 120px; } .trace-t-wrapper { @@ -182,11 +183,11 @@ limitations under the License. --> } .trace-t { - width: 420px; + width: 300px; } .list { - width: 300px; + width: 280px; } .trace-tr { @@ -226,7 +227,8 @@ limitations under the License. --> .no-data { padding-top: 50px; - width: 100%; + width: 280px; text-align: center; + height: 100px; } diff --git a/src/views/dashboard/related/trace/components/D3Graph/Index.vue b/src/views/dashboard/related/trace/components/D3Graph/Index.vue index f848f8c2..ff4705e7 100644 --- a/src/views/dashboard/related/trace/components/D3Graph/Index.vue +++ b/src/views/dashboard/related/trace/components/D3Graph/Index.vue @@ -29,6 +29,7 @@ limitations under the License. --> import type { Span, Ref } from "@/types/trace"; import SpanDetail from "./SpanDetail.vue"; import { useAppStoreWithOut } from "@/store/modules/app"; + import { debounce } from "@/utils/debounce"; /* global defineProps, Nullable, defineExpose,Recordable*/ const props = defineProps({ @@ -45,6 +46,8 @@ limitations under the License. --> const refSpans = ref>([]); const tree = ref>(null); const traceGraph = ref>(null); + const debounceFunc = debounce(draw, 500); + defineExpose({ tree, }); @@ -55,6 +58,15 @@ limitations under the License. --> loading.value = false; return; } + draw(); + loading.value = false; + window.addEventListener("resize", debounceFunc); + }); + + function draw() { + if (!traceGraph.value) { + return; + } if (props.type === "List") { tree.value = new ListGraph(traceGraph.value, handleSelectSpan); tree.value.init({ label: "TRACE_ROOT", children: segmentId.value }, props.data, fixSpansSize.value); @@ -63,11 +75,6 @@ limitations under the License. --> tree.value = new TreeGraph(traceGraph.value, handleSelectSpan); tree.value.init({ label: `${props.traceId}`, children: segmentId.value }, props.data); } - loading.value = false; - window.addEventListener("resize", resize); - }); - function resize() { - tree.value.resize(); } function handleSelectSpan(i: Recordable) { currentSpan.value = i.data; @@ -132,28 +139,61 @@ limitations under the License. --> } segmentHeaders.forEach((span: Span) => { if (span.refs.length) { + let exit = 0; span.refs.forEach((ref) => { - const index = props.data.findIndex( + const i = props.data.findIndex( (i: Recordable) => ref.parentSegmentId === i.segmentId && ref.parentSpanId === i.spanId, ); - if (index === -1) { - // create a known broken node. - const i = ref.parentSpanId; - const fixSpanKeyContent = { + if (i > -1) { + exit = 1; + } + }); + + if (!exit) { + const ref = span.refs[0]; + // create a known broken node. + const i = ref.parentSpanId; + const fixSpanKeyContent = { + traceId: ref.traceId, + segmentId: ref.parentSegmentId, + spanId: i, + parentSpanId: i > -1 ? 0 : -1, + }; + if (!_.find(fixSpans, fixSpanKeyContent)) { + fixSpans.push({ + ...fixSpanKeyContent, + refs: [], + endpointName: `VNode: ${ref.parentSegmentId}`, + serviceCode: "VirtualNode", + type: `[Broken] ${ref.type}`, + peer: "", + component: `VirtualNode: #${i}`, + isError: true, + isBroken: true, + layer: "Broken", + tags: [], + logs: [], + startTime: 0, + endTime: 0, + }); + } + // if root broken node is not exist, create a root broken node. + if (fixSpanKeyContent.parentSpanId > -1) { + const fixRootSpanKeyContent = { traceId: ref.traceId, segmentId: ref.parentSegmentId, - spanId: i, - parentSpanId: i > -1 ? 0 : -1, + spanId: 0, + parentSpanId: -1, }; - if (!_.find(fixSpans, fixSpanKeyContent)) { + if (!_.find(fixSpans, fixRootSpanKeyContent)) { fixSpans.push({ - ...fixSpanKeyContent, + ...fixRootSpanKeyContent, refs: [], endpointName: `VNode: ${ref.parentSegmentId}`, serviceCode: "VirtualNode", type: `[Broken] ${ref.type}`, peer: "", - component: `VirtualNode: #${i}`, + component: `VirtualNode: #0`, isError: true, isBroken: true, layer: "Broken", @@ -163,44 +203,16 @@ limitations under the License. --> endTime: 0, }); } - // if root broken node is not exist, create a root broken node. - if (fixSpanKeyContent.parentSpanId > -1) { - const fixRootSpanKeyContent = { - traceId: ref.traceId, - segmentId: ref.parentSegmentId, - spanId: 0, - parentSpanId: -1, - }; - if (!_.find(fixSpans, fixRootSpanKeyContent)) { - fixSpans.push({ - ...fixRootSpanKeyContent, - refs: [], - endpointName: `VNode: ${ref.parentSegmentId}`, - serviceCode: "VirtualNode", - type: `[Broken] ${ref.type}`, - peer: "", - component: `VirtualNode: #0`, - isError: true, - isBroken: true, - layer: "Broken", - tags: [], - logs: [], - startTime: 0, - endTime: 0, - }); - } - } } - }); + } } }); [...fixSpans, ...props.data].forEach((i) => { i.label = i.endpointName || "no operation name"; i.children = []; - if (segmentGroup[i.segmentId] === undefined) { + if (!segmentGroup[i.segmentId]) { segmentIdGroup.push(i.segmentId); - segmentGroup[i.segmentId] = []; - segmentGroup[i.segmentId].push(i); + segmentGroup[i.segmentId] = [i]; } else { segmentGroup[i.segmentId].push(i); } @@ -273,7 +285,7 @@ limitations under the License. --> } onBeforeUnmount(() => { d3.selectAll(".d3-tip").remove(); - window.removeEventListener("resize", resize); + window.removeEventListener("resize", debounceFunc); }); watch( () => props.data, diff --git a/src/views/dashboard/related/trace/components/D3Graph/SpanDetail.vue b/src/views/dashboard/related/trace/components/D3Graph/SpanDetail.vue index a9d54711..eebdd416 100644 --- a/src/views/dashboard/related/trace/components/D3Graph/SpanDetail.vue +++ b/src/views/dashboard/related/trace/components/D3Graph/SpanDetail.vue @@ -42,6 +42,12 @@ limitations under the License. --> {{ t("isError") }}: {{ currentSpan.isError }} +
{{ t("traceID") }}.
+
+ + {{ item.traceId }} + +
{{ t("tags") }}.
{{ i.key }}: @@ -127,7 +133,7 @@ limitations under the License. -->