mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-10-14 20:01:28 +00:00
feat: adapt new trace protocol and implement new trace view (#499)
This commit is contained in:
17
src/assets/icons/download.svg
Normal file
17
src/assets/icons/download.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!-- 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. -->
|
||||||
|
<svg t="1758874892311" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M731.428571 341.333333h73.142858a73.142857 73.142857 0 0 1 73.142857 73.142857v414.476191a73.142857 73.142857 0 0 1-73.142857 73.142857H219.428571a73.142857 73.142857 0 0 1-73.142857-73.142857V414.47619a73.142857 73.142857 0 0 1 73.142857-73.142857h73.142858v73.142857H219.428571v414.476191h585.142858V414.47619h-73.142858v-73.142857z m-176.90819-242.590476l0.048762 397.092572 84.577524-84.601905 51.687619 51.712-172.373334 172.397714-172.397714-172.373333 51.712-51.736381 83.626667 83.626666V98.742857h73.142857z" p-id="4697"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
@@ -310,6 +310,9 @@ limitations under the License. -->
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
dates.value = [start, end];
|
dates.value = [start, end];
|
||||||
|
if (!props.showButtons) {
|
||||||
|
ok(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
inputDates.value = dates.value;
|
inputDates.value = dates.value;
|
||||||
|
@@ -163,3 +163,73 @@ export const TraceSpansFromColdStage = {
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
export const HasQueryTracesV2Support = {
|
||||||
|
query: `
|
||||||
|
hasQueryTracesV2Support
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QueryV2Traces = {
|
||||||
|
variable: "$condition: TraceQueryCondition",
|
||||||
|
query: `
|
||||||
|
queryTraces(condition: $condition) {
|
||||||
|
traces {
|
||||||
|
spans {
|
||||||
|
traceId
|
||||||
|
segmentId
|
||||||
|
spanId
|
||||||
|
parentSpanId
|
||||||
|
refs {
|
||||||
|
traceId
|
||||||
|
parentSegmentId
|
||||||
|
parentSpanId
|
||||||
|
type
|
||||||
|
}
|
||||||
|
serviceCode
|
||||||
|
serviceInstanceName
|
||||||
|
startTime
|
||||||
|
endTime
|
||||||
|
endpointName
|
||||||
|
type
|
||||||
|
peer
|
||||||
|
component
|
||||||
|
isError
|
||||||
|
layer
|
||||||
|
tags {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
logs {
|
||||||
|
time
|
||||||
|
data {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attachedEvents {
|
||||||
|
startTime {
|
||||||
|
seconds
|
||||||
|
nanos
|
||||||
|
}
|
||||||
|
event
|
||||||
|
endTime {
|
||||||
|
seconds
|
||||||
|
nanos
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
retrievedTimeRange {
|
||||||
|
startTime
|
||||||
|
endTime
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
@@ -17,11 +17,45 @@
|
|||||||
import { httpQuery } from "../base";
|
import { httpQuery } from "../base";
|
||||||
import { HttpURL } from "./url";
|
import { HttpURL } from "./url";
|
||||||
|
|
||||||
export default async function fetchQuery({ method, json, path }: { method: string; json?: unknown; path: string }) {
|
export default async function fetchQuery({
|
||||||
|
method,
|
||||||
|
json,
|
||||||
|
path,
|
||||||
|
}: {
|
||||||
|
method: string;
|
||||||
|
json?: Record<string, unknown>;
|
||||||
|
path: string;
|
||||||
|
}) {
|
||||||
|
const upperMethod = method.toUpperCase();
|
||||||
|
let url = (HttpURL as Record<string, string>)[path];
|
||||||
|
let body: unknown | undefined = json;
|
||||||
|
|
||||||
|
if (upperMethod === "GET" && json && typeof json === "object") {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
const stringifyValue = (val: unknown): string => {
|
||||||
|
if (val instanceof Date) return val.toISOString();
|
||||||
|
if (typeof val === "object") return JSON.stringify(val);
|
||||||
|
return String(val);
|
||||||
|
};
|
||||||
|
for (const [key, value] of Object.entries(json)) {
|
||||||
|
if (value === undefined || value === null) continue;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const v of value as unknown[]) params.append(key, stringifyValue(v));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
params.append(key, stringifyValue(value));
|
||||||
|
}
|
||||||
|
const queryString = params.toString();
|
||||||
|
if (queryString) {
|
||||||
|
url += (url.includes("?") ? "&" : "?") + queryString;
|
||||||
|
}
|
||||||
|
body = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await httpQuery({
|
const response = await httpQuery({
|
||||||
method,
|
method: upperMethod,
|
||||||
json,
|
json: body,
|
||||||
url: (HttpURL as { [key: string]: string })[path],
|
url,
|
||||||
});
|
});
|
||||||
if (response.errors) {
|
if (response.errors) {
|
||||||
response.errors = response.errors.map((e: { message: string }) => e.message).join(" ");
|
response.errors = response.errors.map((e: { message: string }) => e.message).join(" ");
|
||||||
|
@@ -15,7 +15,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Traces, TraceSpans, TraceTagKeys, TraceTagValues, TraceSpansFromColdStage } from "../fragments/trace";
|
import {
|
||||||
|
Traces,
|
||||||
|
TraceSpans,
|
||||||
|
TraceTagKeys,
|
||||||
|
TraceTagValues,
|
||||||
|
TraceSpansFromColdStage,
|
||||||
|
HasQueryTracesV2Support,
|
||||||
|
QueryV2Traces,
|
||||||
|
} from "../fragments/trace";
|
||||||
|
|
||||||
export const queryTraces = `query queryTraces(${Traces.variable}) {${Traces.query}}`;
|
export const queryTraces = `query queryTraces(${Traces.variable}) {${Traces.query}}`;
|
||||||
|
|
||||||
@@ -26,3 +34,7 @@ export const queryTraceTagKeys = `query queryTraceTagKeys(${TraceTagKeys.variabl
|
|||||||
export const queryTraceTagValues = `query queryTraceTagValues(${TraceTagValues.variable}) {${TraceTagValues.query}}`;
|
export const queryTraceTagValues = `query queryTraceTagValues(${TraceTagValues.variable}) {${TraceTagValues.query}}`;
|
||||||
|
|
||||||
export const queryTraceSpansFromColdStage = `query queryTraceSpansFromColdStage(${TraceSpansFromColdStage.variable}) {${TraceSpansFromColdStage.query}}`;
|
export const queryTraceSpansFromColdStage = `query queryTraceSpansFromColdStage(${TraceSpansFromColdStage.variable}) {${TraceSpansFromColdStage.query}}`;
|
||||||
|
|
||||||
|
export const queryHasQueryTracesV2Support = `query queryHasQueryTracesV2Support {${HasQueryTracesV2Support.query}}`;
|
||||||
|
|
||||||
|
export const queryV2Traces = `query queryV2Traces(${QueryV2Traces.variable}) {${QueryV2Traces.query}}`;
|
||||||
|
@@ -89,6 +89,7 @@ limitations under the License. -->
|
|||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import { useAppStoreWithOut, InitializationDurationRow } from "@/store/modules/app";
|
import { useAppStoreWithOut, InitializationDurationRow } from "@/store/modules/app";
|
||||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||||
|
import { useTraceStore } from "@/store/modules/trace";
|
||||||
import type { DashboardItem } from "@/types/dashboard";
|
import type { DashboardItem } from "@/types/dashboard";
|
||||||
import timeFormat from "@/utils/timeFormat";
|
import timeFormat from "@/utils/timeFormat";
|
||||||
import { MetricCatalog } from "@/views/dashboard/data";
|
import { MetricCatalog } from "@/views/dashboard/data";
|
||||||
@@ -98,10 +99,10 @@ limitations under the License. -->
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
/*global Indexable */
|
|
||||||
const { t, te } = useI18n();
|
const { t, te } = useI18n();
|
||||||
const appStore = useAppStoreWithOut();
|
const appStore = useAppStoreWithOut();
|
||||||
const dashboardStore = useDashboardStore();
|
const dashboardStore = useDashboardStore();
|
||||||
|
const traceStore = useTraceStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const pathNames = ref<{ path?: string; name: string; selected: boolean }[][]>([]);
|
const pathNames = ref<{ path?: string; name: string; selected: boolean }[][]>([]);
|
||||||
const showTimeRangeTips = ref<boolean>(false);
|
const showTimeRangeTips = ref<boolean>(false);
|
||||||
@@ -124,6 +125,7 @@ limitations under the License. -->
|
|||||||
getVersion();
|
getVersion();
|
||||||
getNavPaths();
|
getNavPaths();
|
||||||
setTTL();
|
setTTL();
|
||||||
|
traceStore.getHasQueryTracesV2Support();
|
||||||
|
|
||||||
function changeTheme() {
|
function changeTheme() {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
@@ -391,7 +393,7 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetDuration() {
|
function resetDuration() {
|
||||||
const { duration }: Indexable = route.params;
|
const { duration } = route.params as { duration: string };
|
||||||
if (duration) {
|
if (duration) {
|
||||||
const d = JSON.parse(duration);
|
const d = JSON.parse(duration);
|
||||||
|
|
||||||
|
@@ -17,7 +17,7 @@ limitations under the License. -->
|
|||||||
<div :class="isCollapse ? 'logo-icon-collapse' : 'logo-icon'">
|
<div :class="isCollapse ? 'logo-icon-collapse' : 'logo-icon'">
|
||||||
<Icon :size="isCollapse ? 'xl' : 'logo'" :iconName="isCollapse ? 'logo' : 'logo-sw'" />
|
<Icon :size="isCollapse ? 'xl' : 'logo'" :iconName="isCollapse ? 'logo' : 'logo-sw'" />
|
||||||
</div>
|
</div>
|
||||||
<div class="menu scroll_bar_dark" :style="isCollapse ? {} : { width: '220px' }">
|
<div class="menu scroll_bar_style" :style="isCollapse ? {} : { width: '220px' }">
|
||||||
<el-menu
|
<el-menu
|
||||||
active-text-color="#448dfe"
|
active-text-color="#448dfe"
|
||||||
background-color="#252a2f"
|
background-color="#252a2f"
|
||||||
|
@@ -31,7 +31,7 @@ const msg = {
|
|||||||
profiles: "Profiles",
|
profiles: "Profiles",
|
||||||
database: "Database",
|
database: "Database",
|
||||||
mySQL: "MySQL/MariaDB",
|
mySQL: "MySQL/MariaDB",
|
||||||
serviceName: "Service Name",
|
serviceName: "Service name",
|
||||||
technologies: "Technologies",
|
technologies: "Technologies",
|
||||||
health: "Health",
|
health: "Health",
|
||||||
groupName: "Group Name",
|
groupName: "Group Name",
|
||||||
@@ -406,5 +406,11 @@ const msg = {
|
|||||||
minutes: "Minutes",
|
minutes: "Minutes",
|
||||||
invalidProfilingDurationRange: "Please enter a valid duration between 1 and 900 seconds",
|
invalidProfilingDurationRange: "Please enter a valid duration between 1 and 900 seconds",
|
||||||
taskCreatedSuccessfully: "Task created successfully",
|
taskCreatedSuccessfully: "Task created successfully",
|
||||||
|
runQuery: "Run Query",
|
||||||
|
spansTable: "Spans Table",
|
||||||
|
download: "Download",
|
||||||
|
totalSpans: "Total Spans",
|
||||||
|
spanName: "Span name",
|
||||||
|
parentId: "Parent ID",
|
||||||
};
|
};
|
||||||
export default msg;
|
export default msg;
|
||||||
|
@@ -406,5 +406,11 @@ const msg = {
|
|||||||
minutes: "Minutos",
|
minutes: "Minutos",
|
||||||
invalidProfilingDurationRange: "Por favor ingrese una duración válida entre 1 y 900 segundos",
|
invalidProfilingDurationRange: "Por favor ingrese una duración válida entre 1 y 900 segundos",
|
||||||
taskCreatedSuccessfully: "Tarea creada exitosamente",
|
taskCreatedSuccessfully: "Tarea creada exitosamente",
|
||||||
|
runQuery: "Ejecutar Consulta",
|
||||||
|
spansTable: "Tabla de Lapso",
|
||||||
|
download: "Descargar",
|
||||||
|
totalSpans: "Total Lapso",
|
||||||
|
spanName: "Nombre de Lapso",
|
||||||
|
parentId: "ID Padre",
|
||||||
};
|
};
|
||||||
export default msg;
|
export default msg;
|
||||||
|
@@ -404,5 +404,11 @@ const msg = {
|
|||||||
minutes: "分钟",
|
minutes: "分钟",
|
||||||
invalidProfilingDurationRange: "请输入1到900秒之间的有效时长",
|
invalidProfilingDurationRange: "请输入1到900秒之间的有效时长",
|
||||||
taskCreatedSuccessfully: "任务创建成功",
|
taskCreatedSuccessfully: "任务创建成功",
|
||||||
|
runQuery: "运行查询",
|
||||||
|
spansTable: "Spans表格",
|
||||||
|
download: "下载",
|
||||||
|
totalSpans: "总跨度",
|
||||||
|
spanName: "跨度名称",
|
||||||
|
parentId: "父ID",
|
||||||
};
|
};
|
||||||
export default msg;
|
export default msg;
|
||||||
|
@@ -37,9 +37,15 @@ interface TraceState {
|
|||||||
selectorStore: ReturnType<typeof useSelectorStore>;
|
selectorStore: ReturnType<typeof useSelectorStore>;
|
||||||
selectedSpan: Nullable<Span>;
|
selectedSpan: Nullable<Span>;
|
||||||
serviceList: string[];
|
serviceList: string[];
|
||||||
|
currentSpan: Nullable<Span>;
|
||||||
|
hasQueryTracesV2Support: boolean;
|
||||||
|
v2Traces: Trace[];
|
||||||
|
loading: boolean;
|
||||||
}
|
}
|
||||||
const { getDurationTime } = useDuration();
|
const { getDurationTime } = useDuration();
|
||||||
|
|
||||||
|
export const PageSize = 20;
|
||||||
|
|
||||||
export const traceStore = defineStore({
|
export const traceStore = defineStore({
|
||||||
id: "trace",
|
id: "trace",
|
||||||
state: (): TraceState => ({
|
state: (): TraceState => ({
|
||||||
@@ -54,24 +60,39 @@ export const traceStore = defineStore({
|
|||||||
queryDuration: getDurationTime(),
|
queryDuration: getDurationTime(),
|
||||||
traceState: "ALL",
|
traceState: "ALL",
|
||||||
queryOrder: QueryOrders[0].value,
|
queryOrder: QueryOrders[0].value,
|
||||||
paging: { pageNum: 1, pageSize: 20 },
|
paging: { pageNum: 1, pageSize: PageSize },
|
||||||
},
|
},
|
||||||
traceSpanLogs: [],
|
traceSpanLogs: [],
|
||||||
selectorStore: useSelectorStore(),
|
selectorStore: useSelectorStore(),
|
||||||
serviceList: [],
|
serviceList: [],
|
||||||
|
currentSpan: null,
|
||||||
|
hasQueryTracesV2Support: false,
|
||||||
|
v2Traces: [],
|
||||||
|
loading: false,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setTraceCondition(data: Recordable) {
|
setTraceCondition(data: Recordable) {
|
||||||
this.conditions = { ...this.conditions, ...data };
|
this.conditions = { ...this.conditions, ...data };
|
||||||
},
|
},
|
||||||
setCurrentTrace(trace: Trace) {
|
setCurrentTrace(trace: Nullable<Trace>) {
|
||||||
this.currentTrace = trace;
|
this.currentTrace = trace || {};
|
||||||
},
|
},
|
||||||
setTraceSpans(spans: Span[]) {
|
setTraceSpans(spans: Span[]) {
|
||||||
this.traceSpans = spans;
|
this.traceSpans = spans;
|
||||||
},
|
},
|
||||||
setSelectedSpan(span: Span) {
|
setSelectedSpan(span: Nullable<Span>) {
|
||||||
this.selectedSpan = span;
|
this.selectedSpan = span || {};
|
||||||
|
},
|
||||||
|
setCurrentSpan(span: Nullable<Span>) {
|
||||||
|
this.currentSpan = span || {};
|
||||||
|
},
|
||||||
|
setV2Spans(traceId: string) {
|
||||||
|
const trace = this.traceList.find((d: Trace) => d.traceId === traceId);
|
||||||
|
this.setTraceSpans(trace?.spans || []);
|
||||||
|
this.serviceList = Array.from(new Set(trace?.spans.map((i: Span) => i.serviceCode)));
|
||||||
|
},
|
||||||
|
setTraceList(traces: Trace[]) {
|
||||||
|
this.traceList = traces;
|
||||||
},
|
},
|
||||||
resetState() {
|
resetState() {
|
||||||
this.traceSpans = [];
|
this.traceSpans = [];
|
||||||
@@ -156,14 +177,20 @@ export const traceStore = defineStore({
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
async getTraces() {
|
async getTraces() {
|
||||||
|
if (this.hasQueryTracesV2Support) {
|
||||||
|
return this.fetchV2Traces();
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
const response = await graphql.query("queryTraces").params({ condition: this.conditions });
|
const response = await graphql.query("queryTraces").params({ condition: this.conditions });
|
||||||
if (response.errors) {
|
if (response.errors) {
|
||||||
|
this.loading = false;
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
if (!response.data.data.traces.length) {
|
if (!response.data.data.traces.length) {
|
||||||
this.traceList = [];
|
this.traceList = [];
|
||||||
this.setCurrentTrace({});
|
this.setCurrentTrace({});
|
||||||
this.setTraceSpans([]);
|
this.setTraceSpans([]);
|
||||||
|
this.loading = false;
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
this.getTraceSpans({ traceId: response.data.data.traces[0].traceIds[0] });
|
this.getTraceSpans({ traceId: response.data.data.traces[0].traceIds[0] });
|
||||||
@@ -177,8 +204,13 @@ export const traceStore = defineStore({
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
async getTraceSpans(params: { traceId: string }) {
|
async getTraceSpans(params: { traceId: string }) {
|
||||||
|
if (this.hasQueryTracesV2Support) {
|
||||||
|
this.setV2Spans(params.traceId);
|
||||||
|
return new Promise((resolve) => resolve({}));
|
||||||
|
}
|
||||||
const appStore = useAppStoreWithOut();
|
const appStore = useAppStoreWithOut();
|
||||||
let response;
|
let response;
|
||||||
|
this.loading = true;
|
||||||
if (appStore.coldStageMode) {
|
if (appStore.coldStageMode) {
|
||||||
response = await graphql
|
response = await graphql
|
||||||
.query("queryTraceSpansFromColdStage")
|
.query("queryTraceSpansFromColdStage")
|
||||||
@@ -186,6 +218,7 @@ export const traceStore = defineStore({
|
|||||||
} else {
|
} else {
|
||||||
response = await graphql.query("querySpans").params(params);
|
response = await graphql.query("querySpans").params(params);
|
||||||
}
|
}
|
||||||
|
this.loading = false;
|
||||||
if (response.errors) {
|
if (response.errors) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -209,6 +242,61 @@ export const traceStore = defineStore({
|
|||||||
async getTagValues(tagKey: string) {
|
async getTagValues(tagKey: string) {
|
||||||
return await graphql.query("queryTraceTagValues").params({ tagKey, duration: useAppStoreWithOut().durationTime });
|
return await graphql.query("queryTraceTagValues").params({ tagKey, duration: useAppStoreWithOut().durationTime });
|
||||||
},
|
},
|
||||||
|
async getHasQueryTracesV2Support() {
|
||||||
|
const response = await graphql.query("queryHasQueryTracesV2Support").params({});
|
||||||
|
this.hasQueryTracesV2Support = response.data.hasQueryTracesV2Support;
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
async fetchV2Traces() {
|
||||||
|
this.loading = true;
|
||||||
|
const response = await graphql.query("queryV2Traces").params({ condition: this.conditions });
|
||||||
|
this.loading = false;
|
||||||
|
if (response.errors) {
|
||||||
|
this.traceList = [];
|
||||||
|
this.setCurrentTrace({});
|
||||||
|
this.setTraceSpans([]);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
this.v2Traces = response.data.queryTraces.traces || [];
|
||||||
|
this.traceList = this.v2Traces
|
||||||
|
.map((d: Trace) => {
|
||||||
|
const newSpans = d.spans.map((span: Span) => {
|
||||||
|
return {
|
||||||
|
...span,
|
||||||
|
traceId: span.traceId,
|
||||||
|
duration: span.endTime - span.startTime,
|
||||||
|
label: `${span.serviceCode}: ${span.endpointName}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const trace =
|
||||||
|
newSpans.find((span: Span) => span.parentSpanId === -1 && span.refs.length === 0) || newSpans[0];
|
||||||
|
return {
|
||||||
|
endpointNames: trace.endpointName ? [trace.endpointName] : [],
|
||||||
|
traceIds: trace.traceId ? [{ value: trace.traceId, label: trace.traceId }] : [],
|
||||||
|
start: trace.startTime,
|
||||||
|
duration: trace.endTime - trace.startTime,
|
||||||
|
isError: trace.isError,
|
||||||
|
spans: newSpans,
|
||||||
|
traceId: trace.traceId,
|
||||||
|
key: trace.traceId,
|
||||||
|
serviceCode: trace.serviceCode,
|
||||||
|
label: `${trace.serviceCode}: ${trace.endpointName}`,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a: Trace, b: Trace) => b.duration - a.duration);
|
||||||
|
const trace = this.traceList[0];
|
||||||
|
if (!trace) {
|
||||||
|
this.traceList = [];
|
||||||
|
this.setCurrentTrace({});
|
||||||
|
this.setTraceSpans([]);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.serviceList = Array.from(new Set(trace.spans.map((i: Span) => i.serviceCode)));
|
||||||
|
this.setTraceSpans(trace.spans);
|
||||||
|
this.setCurrentTrace(trace || {});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -185,13 +185,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.scroll_bar_style::-webkit-scrollbar {
|
.scroll_bar_style::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 5px;
|
||||||
height: 4px;
|
height: 5px;
|
||||||
background-color: #eee;
|
background-color: var(--sw-scrollbar-track);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll_bar_style::-webkit-scrollbar-track {
|
.scroll_bar_style::-webkit-scrollbar-track {
|
||||||
background-color: #eee;
|
background-color: var(--sw-scrollbar-track);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: inset 0 0 6px $disabled-color;
|
box-shadow: inset 0 0 6px $disabled-color;
|
||||||
}
|
}
|
||||||
@@ -199,26 +199,9 @@
|
|||||||
.scroll_bar_style::-webkit-scrollbar-thumb {
|
.scroll_bar_style::-webkit-scrollbar-thumb {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: inset 0 0 6px $disabled-color;
|
box-shadow: inset 0 0 6px $disabled-color;
|
||||||
background-color: #aaa;
|
background-color: var(--sw-scrollbar-thumb);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll_bar_dark::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
background-color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll_bar_dark::-webkit-scrollbar-track {
|
|
||||||
background-color: #252a2f;
|
|
||||||
border-radius: 3px;
|
|
||||||
box-shadow: inset 0 0 6px #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll_bar_dark::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 3px;
|
|
||||||
box-shadow: inset 0 0 6px #888;
|
|
||||||
background-color: #999;
|
|
||||||
}
|
|
||||||
.d3-tip {
|
.d3-tip {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
@@ -71,6 +71,11 @@ html {
|
|||||||
--sw-marketplace-border: #dedfe0;
|
--sw-marketplace-border: #dedfe0;
|
||||||
--sw-grid-item-active: #d4d7de;
|
--sw-grid-item-active: #d4d7de;
|
||||||
--sw-trace-line: #999;
|
--sw-trace-line: #999;
|
||||||
|
--sw-scrollbar-track: #eee;
|
||||||
|
--sw-scrollbar-thumb: #aaa;
|
||||||
|
--sw-font-grey-color: #a7aebb;
|
||||||
|
--sw-trace-list-path: rgba(0, 0, 0, 0.1);
|
||||||
|
--sw-trace-table-selected: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark {
|
html.dark {
|
||||||
@@ -81,7 +86,7 @@ html.dark {
|
|||||||
--disabled-color: #999;
|
--disabled-color: #999;
|
||||||
--dashboard-tool-bg: #000;
|
--dashboard-tool-bg: #000;
|
||||||
--text-color-placeholder: #ccc;
|
--text-color-placeholder: #ccc;
|
||||||
--border-color: #262629;
|
--border-color: #333;
|
||||||
--border-color-primary: #4b4b52;
|
--border-color-primary: #4b4b52;
|
||||||
--layout-background: #000;
|
--layout-background: #000;
|
||||||
--box-shadow-color: #606266;
|
--box-shadow-color: #606266;
|
||||||
@@ -114,6 +119,11 @@ html.dark {
|
|||||||
--sw-marketplace-border: #606266;
|
--sw-marketplace-border: #606266;
|
||||||
--sw-grid-item-active: #73767a;
|
--sw-grid-item-active: #73767a;
|
||||||
--sw-trace-line: #e8e8e8;
|
--sw-trace-line: #e8e8e8;
|
||||||
|
--sw-scrollbar-track: #252a2f;
|
||||||
|
--sw-scrollbar-thumb: #888;
|
||||||
|
--sw-font-grey-color: #a7aebb;
|
||||||
|
--sw-trace-list-path: rgba(244, 244, 244, 0.4);
|
||||||
|
--sw-trace-table-selected: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-drawer__header {
|
.el-drawer__header {
|
||||||
|
@@ -27,6 +27,10 @@ export interface Trace {
|
|||||||
spans: Span[];
|
spans: Span[];
|
||||||
endpointNames: string[];
|
endpointNames: string[];
|
||||||
traceId: string;
|
traceId: string;
|
||||||
|
serviceCode: string;
|
||||||
|
label: string;
|
||||||
|
id: string;
|
||||||
|
parentId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Span {
|
export interface Span {
|
||||||
@@ -46,6 +50,7 @@ export interface Span {
|
|||||||
refs: Array<Ref>;
|
refs: Array<Ref>;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
endTime: number;
|
endTime: number;
|
||||||
|
duration?: number;
|
||||||
dur?: number;
|
dur?: number;
|
||||||
children?: Span[];
|
children?: Span[];
|
||||||
tags?: { value: string; key: string }[];
|
tags?: { value: string; key: string }[];
|
||||||
@@ -63,6 +68,8 @@ export interface Span {
|
|||||||
avgTime?: number;
|
avgTime?: number;
|
||||||
count?: number;
|
count?: number;
|
||||||
profiled?: boolean;
|
profiled?: boolean;
|
||||||
|
startTimeNanos?: number;
|
||||||
|
event?: string;
|
||||||
}
|
}
|
||||||
export type Ref = {
|
export type Ref = {
|
||||||
type?: string;
|
type?: string;
|
||||||
|
70
src/utils/color.ts
Normal file
70
src/utils/color.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Service color palette for consistent coloring across trace visualizations
|
||||||
|
export const ServicePalette = [
|
||||||
|
"#10b981", // emerald-500
|
||||||
|
"#f59e0b", // amber-500
|
||||||
|
"#8b5cf6", // violet-500
|
||||||
|
"#06b6d4", // cyan-500
|
||||||
|
"#84cc16", // lime-500
|
||||||
|
"#f97316", // orange-500
|
||||||
|
"#ec4899", // pink-500
|
||||||
|
"#6366f1", // indigo-500
|
||||||
|
"#14b8a6", // teal-500
|
||||||
|
"#a855f7", // purple-500
|
||||||
|
"#22c55e", // green-500
|
||||||
|
"#eab308", // yellow-500
|
||||||
|
"#f43f5e", // rose-500
|
||||||
|
"#0ea5e9", // sky-500
|
||||||
|
"#8b5a2b", // brown-500
|
||||||
|
"#64748b", // slate-500
|
||||||
|
"#dc2626", // red-600
|
||||||
|
"#059669", // emerald-600
|
||||||
|
"#d97706", // amber-600
|
||||||
|
"#7c3aed", // violet-600
|
||||||
|
"#0891b2", // cyan-600
|
||||||
|
"#65a30d", // lime-600
|
||||||
|
"#ea580ce6", // orange-600
|
||||||
|
"#db2777", // pink-600
|
||||||
|
"#4f46e5", // indigo-600
|
||||||
|
"#0d9488", // teal-600
|
||||||
|
"#9333ea", // purple-600
|
||||||
|
"#16a34a", // green-600
|
||||||
|
"#ca8a04", // yellow-600
|
||||||
|
"#e11d48", // rose-600
|
||||||
|
"#0284c7", // sky-600
|
||||||
|
"#92400e", // brown-600
|
||||||
|
"#475569", // slate-600
|
||||||
|
];
|
||||||
|
// Reuse the same service color function from TracesTable
|
||||||
|
function generateHash(str: string) {
|
||||||
|
let hash = 0;
|
||||||
|
if (str.length === 0) return hash;
|
||||||
|
for (let i = 0; i < str.length; i += 1) {
|
||||||
|
const c = str.charCodeAt(i);
|
||||||
|
hash = (hash << 5) - hash + c;
|
||||||
|
hash |= 0; // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
return Math.abs(hash); // Only positive number.
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServiceColor(serviceName: string): string {
|
||||||
|
if (!serviceName) return "#eee";
|
||||||
|
const hash = generateHash(serviceName);
|
||||||
|
return ServicePalette[hash % ServicePalette.length];
|
||||||
|
}
|
@@ -31,16 +31,15 @@ export const readFile = (event: any) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
export const saveFile = (data: any, name: string) => {
|
export const saveFileAsJSON = (data: unknown, filename: string) => {
|
||||||
const newData = JSON.stringify(data);
|
const jsonContent = JSON.stringify(data, null, 2);
|
||||||
const tagA = document.createElement("a");
|
const blob = new Blob([jsonContent], { type: "application/json;charset=utf-8;" });
|
||||||
tagA.download = name;
|
const link = document.createElement("a");
|
||||||
tagA.style.display = "none";
|
link.href = URL.createObjectURL(blob);
|
||||||
const blob = new Blob([newData]);
|
link.download = filename;
|
||||||
const url = URL.createObjectURL(blob);
|
link.style.display = "none";
|
||||||
tagA.href = url;
|
document.body.appendChild(link);
|
||||||
document.body.appendChild(tagA);
|
link.click();
|
||||||
tagA.click();
|
document.body.removeChild(link);
|
||||||
document.body.removeChild(tagA);
|
URL.revokeObjectURL(link.href);
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
};
|
||||||
|
@@ -179,7 +179,7 @@ limitations under the License. -->
|
|||||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import type { DashboardItem, LayoutConfig } from "@/types/dashboard";
|
import type { DashboardItem, LayoutConfig } from "@/types/dashboard";
|
||||||
import { saveFile, readFile } from "@/utils/file";
|
import { saveFileAsJSON, readFile } from "@/utils/file";
|
||||||
import { EntityType } from "./data";
|
import { EntityType } from "./data";
|
||||||
import { isEmptyObject } from "@/utils/is";
|
import { isEmptyObject } from "@/utils/is";
|
||||||
import { WidgetType } from "@/views/dashboard/data";
|
import { WidgetType } from "@/views/dashboard/data";
|
||||||
@@ -356,7 +356,7 @@ limitations under the License. -->
|
|||||||
optimizeTemplate(item.configuration.children);
|
optimizeTemplate(item.configuration.children);
|
||||||
}
|
}
|
||||||
const name = `dashboards.json`;
|
const name = `dashboards.json`;
|
||||||
saveFile(templates, name);
|
saveFileAsJSON(templates, name);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
multipleTableRef.value!.clearSelection();
|
multipleTableRef.value!.clearSelection();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
@@ -24,36 +24,16 @@ limitations under the License. -->
|
|||||||
<span>{{ t("delete") }}</span>
|
<span>{{ t("delete") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
<div class="header">
|
<Content :data="data" />
|
||||||
<Filter :needQuery="needQuery" :data="data" @get="getService" @search="popSegmentList" />
|
|
||||||
</div>
|
|
||||||
<div class="trace flex-h">
|
|
||||||
<TraceList class="trace-list" :style="`width: ${currentWidth}px;`" />
|
|
||||||
<div
|
|
||||||
@mouseover="showIcon = true"
|
|
||||||
@mouseout="showIcon = false"
|
|
||||||
@mousedown="mousedown($event)"
|
|
||||||
@mouseup="mouseup($event)"
|
|
||||||
>
|
|
||||||
<div class="trace-line" />
|
|
||||||
<span class="trace-icon" v-show="showIcon" @mousedown="triggerArrow" @mouseup="stopObserve($event)">
|
|
||||||
<Icon class="trace-arrow" :icon-name="isLeft ? 'chevron-left' : 'chevron-right'" size="lg" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<TraceDetail :serviceId="serviceId" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { provide, ref, onMounted, onUnmounted } from "vue";
|
import { provide } from "vue";
|
||||||
import type { PropType } from "vue";
|
import type { PropType } from "vue";
|
||||||
import Filter from "../related/trace/Filter.vue";
|
|
||||||
import TraceList from "../related/trace/TraceList.vue";
|
|
||||||
import TraceDetail from "../related/trace/Detail.vue";
|
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||||
import { mutationObserver } from "@/utils/mutation";
|
|
||||||
import type { LayoutConfig } from "@/types/dashboard";
|
import type { LayoutConfig } from "@/types/dashboard";
|
||||||
|
import Content from "@/views/dashboard/related/trace/Content.vue";
|
||||||
|
|
||||||
/* global defineProps */
|
/* global defineProps */
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -65,76 +45,12 @@ limitations under the License. -->
|
|||||||
needQuery: { type: Boolean, default: true },
|
needQuery: { type: Boolean, default: true },
|
||||||
});
|
});
|
||||||
provide("options", props.data);
|
provide("options", props.data);
|
||||||
const defaultWidth = 280,
|
|
||||||
minArrowLeftWidth = 120;
|
|
||||||
const serviceId = ref<string>("");
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const dashboardStore = useDashboardStore();
|
const dashboardStore = useDashboardStore();
|
||||||
const isLeft = ref<boolean>(true);
|
|
||||||
const showIcon = ref<boolean>(false);
|
|
||||||
const currentWidth = ref<number>(defaultWidth);
|
|
||||||
const isDrag = ref<boolean>(false);
|
|
||||||
|
|
||||||
function removeWidget() {
|
function removeWidget() {
|
||||||
dashboardStore.removeControls(props.data);
|
dashboardStore.removeControls(props.data);
|
||||||
}
|
}
|
||||||
function getService(id: string) {
|
|
||||||
serviceId.value = id;
|
|
||||||
}
|
|
||||||
// When click the arrow, the width of the segment list is determined by the direction it points to.
|
|
||||||
function triggerArrow() {
|
|
||||||
currentWidth.value = isLeft.value ? 0 : defaultWidth;
|
|
||||||
isLeft.value = !isLeft.value;
|
|
||||||
startObserve();
|
|
||||||
}
|
|
||||||
function popSegmentList() {
|
|
||||||
if (currentWidth.value >= defaultWidth) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
currentWidth.value = defaultWidth;
|
|
||||||
isLeft.value = true;
|
|
||||||
}
|
|
||||||
function startObserve() {
|
|
||||||
mutationObserver.observe("trigger-resize", document.querySelector(".trace-list")!, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ["style"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function stopObserve(event: MouseEvent) {
|
|
||||||
mutationObserver.disconnect("trigger-resize");
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
const mousemove = (event: MouseEvent) => {
|
|
||||||
if (!isDrag.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const diffX = event.clientX;
|
|
||||||
let leftWidth = document.querySelector(".trace-list")!.getBoundingClientRect();
|
|
||||||
currentWidth.value = diffX - leftWidth.left;
|
|
||||||
isLeft.value = currentWidth.value >= minArrowLeftWidth;
|
|
||||||
};
|
|
||||||
const mouseup = (event: MouseEvent) => {
|
|
||||||
showIcon.value = false;
|
|
||||||
isDrag.value = false;
|
|
||||||
stopObserve(event);
|
|
||||||
};
|
|
||||||
const mousedown = (event: MouseEvent) => {
|
|
||||||
if ((event.target as HTMLDivElement)?.className === "trace-line") {
|
|
||||||
isDrag.value = true;
|
|
||||||
startObserve();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener("mousedown", mousedown);
|
|
||||||
document.addEventListener("mousemove", mousemove);
|
|
||||||
document.addEventListener("mouseup", mouseup);
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener("mousedown", mousedown);
|
|
||||||
document.removeEventListener("mousemove", mousemove);
|
|
||||||
document.removeEventListener("mouseup", mouseup);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.trace-wrapper {
|
.trace-wrapper {
|
||||||
@@ -150,13 +66,6 @@ limitations under the License. -->
|
|||||||
right: 3px;
|
right: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
|
||||||
padding: 10px;
|
|
||||||
font-size: $font-size-smaller;
|
|
||||||
border-bottom: 1px solid $border-color;
|
|
||||||
min-width: 1000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tools {
|
.tools {
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
color: #999;
|
color: #999;
|
||||||
@@ -169,47 +78,4 @@ limitations under the License. -->
|
|||||||
background-color: $popper-hover-bg-color;
|
background-color: $popper-hover-bg-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace {
|
|
||||||
min-height: calc(100% - 150px);
|
|
||||||
width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
min-width: 1000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trace-list {
|
|
||||||
max-width: 480px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trace-line {
|
|
||||||
position: relative;
|
|
||||||
width: 2px;
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--sw-trace-line);
|
|
||||||
cursor: ew-resize;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $active-color;
|
|
||||||
background-color: $active-background;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.trace-icon {
|
|
||||||
position: absolute;
|
|
||||||
cursor: pointer;
|
|
||||||
top: calc(50% - 15px);
|
|
||||||
text-align: center;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
transform: translateX(-11px);
|
|
||||||
line-height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: $layout-background;
|
|
||||||
box-shadow: 0 3px 5px rgb(45 60 80 / 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.trace-arrow {
|
|
||||||
padding-bottom: 1px;
|
|
||||||
color: $active-color;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
export const dragIgnoreFrom =
|
export const dragIgnoreFrom =
|
||||||
"svg.d3-trace-tree, .dragger, .micro-topo-chart, .schedules, .vis-item, .vis-timeline, .process-svg";
|
"svg.d3-trace-tree, .dragger, .micro-topo-chart, .schedules, .vis-item, .vis-timeline, .process-svg, .trace-query-content";
|
||||||
|
|
||||||
export const PodsChartTypes = ["EndpointList", "InstanceList"];
|
export const PodsChartTypes = ["EndpointList", "InstanceList"];
|
||||||
|
|
||||||
|
@@ -16,7 +16,7 @@ limitations under the License. -->
|
|||||||
<div class="flex-h content">
|
<div class="flex-h content">
|
||||||
<TaskList />
|
<TaskList />
|
||||||
<div class="vis-graph ml-5">
|
<div class="vis-graph ml-5">
|
||||||
<div class="mb-20">
|
<div class="mb-20 mt-10">
|
||||||
<Filter />
|
<Filter />
|
||||||
</div>
|
</div>
|
||||||
<div class="stack" v-loading="asyncProfilingStore.loadingTree">
|
<div class="stack" v-loading="asyncProfilingStore.loadingTree">
|
||||||
|
@@ -41,7 +41,7 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
<div class="profile-table">
|
<div class="profile-table">
|
||||||
<Table
|
<Table
|
||||||
:data="(profileStore.segmentSpans as SegmentSpan[])"
|
:tableData="(profileStore.segmentSpans as SegmentSpan[])"
|
||||||
:traceId="profileStore.currentSegment?.traceId"
|
:traceId="profileStore.currentSegment?.traceId"
|
||||||
:headerType="WidgetType.Profile"
|
:headerType="WidgetType.Profile"
|
||||||
@select="selectSpan"
|
@select="selectSpan"
|
||||||
@@ -52,7 +52,7 @@ limitations under the License. -->
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import Table from "../../trace/components/Table.vue";
|
import Table from "@/views/dashboard/related/trace/components/Table/TableContainer.vue";
|
||||||
import { useProfileStore } from "@/store/modules/profile";
|
import { useProfileStore } from "@/store/modules/profile";
|
||||||
import Selector from "@/components/Selector.vue";
|
import Selector from "@/components/Selector.vue";
|
||||||
import type { SegmentSpan, ProfileTimeRange, ProfileAnalyzeParams } from "@/types/profile";
|
import type { SegmentSpan, ProfileTimeRange, ProfileAnalyzeParams } from "@/types/profile";
|
||||||
|
193
src/views/dashboard/related/trace/Content.vue
Normal file
193
src/views/dashboard/related/trace/Content.vue
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<!-- 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="search-bar">
|
||||||
|
<Filter :needQuery="needQuery" :data="data" @get="getService" @search="popSegmentList" />
|
||||||
|
<div class="filter-row flex-h mt-10" v-if="traceStore.hasQueryTracesV2Support">
|
||||||
|
<div class="grey mr-10 label">{{ t("limit") }}</div>
|
||||||
|
<el-input-number size="small" v-model="limit" :min="10" @change="changeLimit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TraceQuery v-if="traceStore.hasQueryTracesV2Support" style="height: 100%" />
|
||||||
|
<div v-else class="trace flex-h">
|
||||||
|
<SegmentList class="trace-list" :style="`width: ${currentWidth}px;`" />
|
||||||
|
<div
|
||||||
|
@mouseover="showIcon = true"
|
||||||
|
@mouseout="showIcon = false"
|
||||||
|
@mousedown="mousedown($event)"
|
||||||
|
@mouseup="mouseup($event)"
|
||||||
|
>
|
||||||
|
<div class="trace-line" />
|
||||||
|
<span class="trace-icon" v-show="showIcon" @mousedown="triggerArrow" @mouseup="stopObserve($event)">
|
||||||
|
<Icon class="trace-arrow" :icon-name="isLeft ? 'chevron-left' : 'chevron-right'" size="lg" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<SpanList :serviceId="serviceId" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { provide, ref, onMounted, onUnmounted } from "vue";
|
||||||
|
import type { PropType } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useTraceStore, PageSize } from "@/store/modules/trace";
|
||||||
|
import Filter from "./components/TraceList/Filter.vue";
|
||||||
|
import SegmentList from "./components/TraceList/SegmentList.vue";
|
||||||
|
import SpanList from "./components/TraceList/SpanList.vue";
|
||||||
|
import type { LayoutConfig } from "@/types/dashboard";
|
||||||
|
import { mutationObserver } from "@/utils/mutation";
|
||||||
|
import TraceQuery from "./components/TraceQuery/Index.vue";
|
||||||
|
/*global defineProps */
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object as PropType<LayoutConfig>,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
provide("options", props.data);
|
||||||
|
const { t } = useI18n();
|
||||||
|
const traceStore = useTraceStore();
|
||||||
|
const serviceId = ref<string>("");
|
||||||
|
const showIcon = ref<boolean>(false);
|
||||||
|
const isLeft = ref<boolean>(true);
|
||||||
|
const currentWidth = ref<number>(280);
|
||||||
|
const needQuery = ref<boolean>(true);
|
||||||
|
const isDrag = ref<boolean>(false);
|
||||||
|
const limit = ref(PageSize);
|
||||||
|
const defaultWidth = 280;
|
||||||
|
const minArrowLeftWidth = 120;
|
||||||
|
|
||||||
|
function getService(id: string) {
|
||||||
|
serviceId.value = id;
|
||||||
|
}
|
||||||
|
function changeLimit(val: number | undefined) {
|
||||||
|
if (!val) return;
|
||||||
|
traceStore.setTraceCondition({
|
||||||
|
paging: { pageNum: 1, pageSize: val },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// When click the arrow, the width of the segment list is determined by the direction it points to.
|
||||||
|
function triggerArrow() {
|
||||||
|
currentWidth.value = isLeft.value ? 0 : defaultWidth;
|
||||||
|
isLeft.value = !isLeft.value;
|
||||||
|
startObserve();
|
||||||
|
}
|
||||||
|
function popSegmentList() {
|
||||||
|
if (currentWidth.value >= defaultWidth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentWidth.value = defaultWidth;
|
||||||
|
isLeft.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startObserve() {
|
||||||
|
mutationObserver.observe("trigger-resize", document.querySelector(".trace-list")!, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["style"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function stopObserve(event: MouseEvent) {
|
||||||
|
mutationObserver.disconnect("trigger-resize");
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
const mousemove = (event: MouseEvent) => {
|
||||||
|
if (!isDrag.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const diffX = event.clientX;
|
||||||
|
let leftWidth = document.querySelector(".trace-list")!.getBoundingClientRect();
|
||||||
|
currentWidth.value = diffX - leftWidth.left;
|
||||||
|
isLeft.value = currentWidth.value >= minArrowLeftWidth;
|
||||||
|
};
|
||||||
|
const mouseup = (event: MouseEvent) => {
|
||||||
|
showIcon.value = false;
|
||||||
|
isDrag.value = false;
|
||||||
|
stopObserve(event);
|
||||||
|
};
|
||||||
|
const mousedown = (event: MouseEvent) => {
|
||||||
|
if ((event.target as HTMLDivElement)?.className === "trace-line") {
|
||||||
|
isDrag.value = true;
|
||||||
|
startObserve();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener("mousedown", mousedown);
|
||||||
|
document.addEventListener("mousemove", mousemove);
|
||||||
|
document.addEventListener("mouseup", mouseup);
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener("mousedown", mousedown);
|
||||||
|
document.removeEventListener("mousemove", mousemove);
|
||||||
|
document.removeEventListener("mouseup", mouseup);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-bar {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: $font-size-smaller;
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
min-width: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace {
|
||||||
|
min-height: calc(100% - 160px);
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
min-width: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-list {
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-line {
|
||||||
|
position: relative;
|
||||||
|
width: 2px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--sw-trace-line);
|
||||||
|
cursor: ew-resize;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $active-color;
|
||||||
|
background-color: $active-background;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-icon {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: calc(50% - 15px);
|
||||||
|
text-align: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
transform: translateX(-11px);
|
||||||
|
line-height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: $layout-background;
|
||||||
|
box-shadow: 0 3px 5px rgb(45 60 80 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-arrow {
|
||||||
|
padding-bottom: 1px;
|
||||||
|
color: $active-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -14,27 +14,27 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License. -->
|
limitations under the License. -->
|
||||||
<template>
|
<template>
|
||||||
<div class="trace-wrapper flex-v">
|
<div class="trace-wrapper flex-v">
|
||||||
<div class="header">
|
<div class="search-bar">
|
||||||
<Header :data="data" />
|
<SearchBar :data="data" />
|
||||||
</div>
|
</div>
|
||||||
<div class="trace flex-h">
|
<div class="trace flex-h">
|
||||||
<TraceList />
|
<SegmentList />
|
||||||
<TraceDetail />
|
<SpanList />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { provide } from "vue";
|
import { provide } from "vue";
|
||||||
import type { PropType } from "vue";
|
import type { PropType } from "vue";
|
||||||
import Header from "./Header.vue";
|
|
||||||
import TraceList from "./TraceList.vue";
|
|
||||||
import TraceDetail from "./Detail.vue";
|
|
||||||
import type { LayoutConfig } from "@/types/dashboard";
|
import type { LayoutConfig } from "@/types/dashboard";
|
||||||
|
import SearchBar from "./components/TraceList/SearchBar.vue";
|
||||||
|
import SegmentList from "./components/TraceList/SegmentList.vue";
|
||||||
|
import SpanList from "./components/TraceList/SpanList.vue";
|
||||||
/*global defineProps */
|
/*global defineProps */
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
type: Object as PropType<LayoutConfig>,
|
type: Object as PropType<LayoutConfig>,
|
||||||
default: () => ({ graph: {} }),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
provide("options", props.data);
|
provide("options", props.data);
|
||||||
@@ -47,18 +47,4 @@ limitations under the License. -->
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
|
||||||
padding: 10px;
|
|
||||||
font-size: $font-size-smaller;
|
|
||||||
border-bottom: 1px solid $border-color;
|
|
||||||
min-width: 1200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trace {
|
|
||||||
width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
min-width: 1200px;
|
|
||||||
height: calc(100% - 100px);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -20,6 +20,8 @@ limitations under the License. -->
|
|||||||
:type="type"
|
:type="type"
|
||||||
:headerType="headerType"
|
:headerType="headerType"
|
||||||
:traceId="traceId"
|
:traceId="traceId"
|
||||||
|
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||||
|
:selectedMinTimestamp="selectedMinTimestamp"
|
||||||
@select="handleSelectSpan"
|
@select="handleSelectSpan"
|
||||||
>
|
>
|
||||||
<div class="trace-tips" v-if="!segmentId.length">{{ $t("noData") }}</div>
|
<div class="trace-tips" v-if="!segmentId.length">{{ $t("noData") }}</div>
|
||||||
@@ -47,47 +49,74 @@ limitations under the License. -->
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch, onBeforeUnmount, onMounted } from "vue";
|
import { ref, watch, onBeforeUnmount, onMounted, nextTick } from "vue";
|
||||||
import type { PropType } from "vue";
|
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import ListGraph from "../../utils/d3-trace-list";
|
import ListGraph from "./utils/d3-trace-list";
|
||||||
import TreeGraph from "../../utils/d3-trace-tree";
|
import TreeGraph from "./utils/d3-trace-tree";
|
||||||
import type { Span, Ref } from "@/types/trace";
|
import type { Span, Ref } from "@/types/trace";
|
||||||
import SpanDetail from "./SpanDetail.vue";
|
import SpanDetail from "./SpanDetail.vue";
|
||||||
import TableContainer from "../Table/TableContainer.vue";
|
import TableContainer from "../Table/TableContainer.vue";
|
||||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||||
|
import { useTraceStore } from "@/store/modules/trace";
|
||||||
import { debounce } from "@/utils/debounce";
|
import { debounce } from "@/utils/debounce";
|
||||||
import { mutationObserver } from "@/utils/mutation";
|
import { mutationObserver } from "@/utils/mutation";
|
||||||
import { TraceGraphType } from "../constant";
|
import { TraceGraphType } from "../VisGraph/constant";
|
||||||
import { Themes } from "@/constants/data";
|
|
||||||
import type { SegmentSpan } from "@/types/profile";
|
import type { SegmentSpan } from "@/types/profile";
|
||||||
|
import { buildSegmentForest, collapseTree, getRefsAllNodes } from "./utils/helper";
|
||||||
|
|
||||||
/* global Recordable, Nullable */
|
/* global Nullable */
|
||||||
const props = defineProps({
|
type Props = {
|
||||||
data: { type: Array as PropType<(Span | SegmentSpan)[]>, default: () => [] },
|
data: (Span | SegmentSpan)[];
|
||||||
traceId: { type: String, default: "" },
|
traceId: string;
|
||||||
type: { type: String, default: TraceGraphType.LIST },
|
type: string;
|
||||||
headerType: { type: String, default: "" },
|
headerType?: string;
|
||||||
});
|
selectedMaxTimestamp?: number;
|
||||||
const emits = defineEmits(["select"]);
|
selectedMinTimestamp?: number;
|
||||||
|
minTimestamp: number;
|
||||||
|
maxTimestamp: number;
|
||||||
|
};
|
||||||
|
type Emits = {
|
||||||
|
(e: "select", value: Span): void;
|
||||||
|
};
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emits = defineEmits<Emits>();
|
||||||
const appStore = useAppStoreWithOut();
|
const appStore = useAppStoreWithOut();
|
||||||
const loading = ref<boolean>(false);
|
const loading = ref<boolean>(false);
|
||||||
const showDetail = ref<boolean>(false);
|
const showDetail = ref<boolean>(false);
|
||||||
const fixSpansSize = ref<number>(0);
|
const fixSpansSize = ref<number>(0);
|
||||||
const segmentId = ref<Recordable[]>([]);
|
const segmentId = ref<Span[]>([]);
|
||||||
const currentSpan = ref<Nullable<Span>>(null);
|
const currentSpan = ref<Nullable<Span>>(null);
|
||||||
const refSpans = ref<Array<Ref>>([]);
|
const refSpans = ref<Array<Ref>>([]);
|
||||||
const tree = ref<Nullable<any>>(null);
|
const tree = ref<Nullable<any>>(null);
|
||||||
const traceGraph = ref<Nullable<HTMLDivElement>>(null);
|
const traceGraph = ref<Nullable<HTMLDivElement>>(null);
|
||||||
const parentSpans = ref<Array<Span | SegmentSpan>>([]);
|
const parentSpans = ref<Array<Span | SegmentSpan>>([]);
|
||||||
const refParentSpans = ref<Array<Span | SegmentSpan>>([]);
|
const refParentSpans = ref<Array<Span | SegmentSpan>>([]);
|
||||||
|
const traceStore = useTraceStore();
|
||||||
const debounceFunc = debounce(draw, 500);
|
const debounceFunc = debounce(draw, 500);
|
||||||
const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") => dayjs(date).format(pattern);
|
// Store previous timestamp values to check for significant changes
|
||||||
|
const prevSelectedMaxTimestamp = ref<number>(props.selectedMaxTimestamp || 0);
|
||||||
|
const prevSelectedMinTimestamp = ref<number>(props.selectedMinTimestamp || 0);
|
||||||
|
|
||||||
onMounted(() => {
|
const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") => dayjs(date).format(pattern);
|
||||||
|
// Debounced version of onSpanPanelToggled to prevent excessive re-renders
|
||||||
|
const debouncedOnSpanPanelToggled = debounce(draw, 150);
|
||||||
|
|
||||||
|
// Check if timestamp change is significant enough to warrant a redraw
|
||||||
|
function isTimestampChangeSignificant(newMax: number, newMin: number): boolean {
|
||||||
|
const maxDiff = Math.abs(newMax - prevSelectedMaxTimestamp.value);
|
||||||
|
const minDiff = Math.abs(newMin - prevSelectedMinTimestamp.value);
|
||||||
|
const totalRange = props.maxTimestamp - props.minTimestamp;
|
||||||
|
|
||||||
|
// Consider change significant if it's more than 0.1% of the total range
|
||||||
|
const threshold = totalRange * 0.001;
|
||||||
|
|
||||||
|
return maxDiff > threshold || minDiff > threshold;
|
||||||
|
}
|
||||||
|
onMounted(async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
changeTree();
|
changeTree();
|
||||||
|
await nextTick();
|
||||||
draw();
|
draw();
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
// monitor segment list width changes.
|
// monitor segment list width changes.
|
||||||
@@ -96,11 +125,12 @@ limitations under the License. -->
|
|||||||
debounceFunc();
|
debounceFunc();
|
||||||
});
|
});
|
||||||
window.addEventListener("resize", debounceFunc);
|
window.addEventListener("resize", debounceFunc);
|
||||||
|
window.addEventListener("spanPanelToggled", draw);
|
||||||
});
|
});
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
if (props.type === TraceGraphType.TABLE) {
|
if (props.type === TraceGraphType.TABLE) {
|
||||||
segmentId.value = setLevel(segmentId.value);
|
segmentId.value = setLevel(segmentId.value) as Span[];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!traceGraph.value) {
|
if (!traceGraph.value) {
|
||||||
@@ -108,21 +138,35 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
d3.selectAll(".d3-tip").remove();
|
d3.selectAll(".d3-tip").remove();
|
||||||
if (props.type === TraceGraphType.LIST) {
|
if (props.type === TraceGraphType.LIST) {
|
||||||
tree.value = new ListGraph(traceGraph.value, handleSelectSpan);
|
tree.value = new ListGraph({ el: traceGraph.value, handleSelectSpan: handleSelectSpan });
|
||||||
tree.value.init(
|
tree.value.init({
|
||||||
{ label: "TRACE_ROOT", children: segmentId.value },
|
data: { label: "TRACE_ROOT", children: segmentId.value },
|
||||||
getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
row: getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
||||||
fixSpansSize.value,
|
fixSpansSize: fixSpansSize.value,
|
||||||
);
|
selectedMaxTimestamp: props.selectedMaxTimestamp,
|
||||||
|
selectedMinTimestamp: props.selectedMinTimestamp,
|
||||||
|
});
|
||||||
tree.value.draw();
|
tree.value.draw();
|
||||||
|
selectInitialSpan();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (props.type === TraceGraphType.TREE) {
|
if (props.type === TraceGraphType.TREE) {
|
||||||
tree.value = new TreeGraph(traceGraph.value, handleSelectSpan);
|
tree.value = new TreeGraph({ el: traceGraph.value, handleSelectSpan });
|
||||||
tree.value.init(
|
tree.value.init({
|
||||||
{ label: `${props.traceId}`, children: segmentId.value },
|
data: { label: `${props.traceId}`, children: segmentId.value },
|
||||||
getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
row: getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
||||||
);
|
selectedMaxTimestamp: props.selectedMaxTimestamp,
|
||||||
|
selectedMinTimestamp: props.selectedMinTimestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function selectInitialSpan() {
|
||||||
|
if (segmentId.value && segmentId.value.length > 0) {
|
||||||
|
const root = segmentId.value[0];
|
||||||
|
traceStore.setCurrentSpan(root);
|
||||||
|
if (tree.value && typeof tree.value.highlightSpan === "function") {
|
||||||
|
tree.value.highlightSpan(root as any);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function handleSelectSpan(i: any) {
|
function handleSelectSpan(i: any) {
|
||||||
@@ -162,7 +206,7 @@ limitations under the License. -->
|
|||||||
item && parentSpans.value.push(item);
|
item && parentSpans.value.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function viewParentSpan(span: Recordable) {
|
function viewParentSpan(span: Span) {
|
||||||
if (props.type === TraceGraphType.TABLE) {
|
if (props.type === TraceGraphType.TABLE) {
|
||||||
setTableSpanStyle(span);
|
setTableSpanStyle(span);
|
||||||
return;
|
return;
|
||||||
@@ -173,234 +217,34 @@ limitations under the License. -->
|
|||||||
showDetail.value = true;
|
showDetail.value = true;
|
||||||
hideActionBox();
|
hideActionBox();
|
||||||
}
|
}
|
||||||
function setTableSpanStyle(span: Recordable) {
|
function setTableSpanStyle(span: Span) {
|
||||||
const itemDom: any = document.querySelector(`.trace-item-${span.key}`);
|
const itemDom: HTMLSpanElement | null = document.querySelector(`.trace-item-${span.key}`);
|
||||||
const items: any = document.querySelectorAll(".trace-item");
|
const items: HTMLSpanElement[] = Array.from(document.querySelectorAll(".trace-item")) as HTMLSpanElement[];
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
item.style.background = appStore.theme === Themes.Dark ? "#212224" : "#fff";
|
item.style.background = "transparent";
|
||||||
|
}
|
||||||
|
if (itemDom) {
|
||||||
|
itemDom.style.background = "var(--sw-trace-table-selected)";
|
||||||
}
|
}
|
||||||
itemDom.style.background = appStore.theme === Themes.Dark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)";
|
|
||||||
hideActionBox();
|
hideActionBox();
|
||||||
}
|
}
|
||||||
function hideActionBox() {
|
function hideActionBox() {
|
||||||
const box: any = document.querySelector("#trace-action-box");
|
const box: any = document.querySelector("#trace-action-box");
|
||||||
box.style.display = "none";
|
box.style.display = "none";
|
||||||
}
|
}
|
||||||
function traverseTree(node: Recordable, spanId: string, segmentId: string, data: Recordable) {
|
|
||||||
if (!node || node.isBroken) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (node.spanId === spanId && node.segmentId === segmentId) {
|
|
||||||
node.children.push(data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const nodeItem of node.children || []) {
|
|
||||||
traverseTree(nodeItem, spanId, segmentId, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function changeTree() {
|
function changeTree() {
|
||||||
if (props.data.length === 0) {
|
if (!props.data.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
segmentId.value = [];
|
const { roots, fixSpansSize: fixSize, refSpans: refs } = buildSegmentForest(props.data as Span[], props.traceId);
|
||||||
const segmentGroup: Recordable = {};
|
segmentId.value = roots;
|
||||||
const segmentIdGroup: string[] = [];
|
fixSpansSize.value = fixSize;
|
||||||
const fixSpans: Span[] = [];
|
refSpans.value = refs;
|
||||||
const segmentHeaders: Span[] = [];
|
for (const root of segmentId.value) {
|
||||||
for (const span of props.data) {
|
collapseTree(root, refSpans.value);
|
||||||
if (span.refs.length) {
|
|
||||||
refSpans.value.push(...span.refs);
|
|
||||||
}
|
|
||||||
if (span.parentSpanId === -1) {
|
|
||||||
segmentHeaders.push(span);
|
|
||||||
} else {
|
|
||||||
const item = props.data.find(
|
|
||||||
(i: Span) => i.traceId === span.traceId && i.segmentId === span.segmentId && i.spanId === span.spanId - 1,
|
|
||||||
);
|
|
||||||
const content = fixSpans.find(
|
|
||||||
(i: Span) =>
|
|
||||||
i.traceId === span.traceId &&
|
|
||||||
i.segmentId === span.segmentId &&
|
|
||||||
i.spanId === span.spanId - 1 &&
|
|
||||||
i.parentSpanId === span.spanId - 2,
|
|
||||||
);
|
|
||||||
if (!item && !content) {
|
|
||||||
fixSpans.push({
|
|
||||||
traceId: span.traceId,
|
|
||||||
segmentId: span.segmentId,
|
|
||||||
spanId: span.spanId - 1,
|
|
||||||
parentSpanId: span.spanId - 2,
|
|
||||||
refs: [],
|
|
||||||
endpointName: `VNode: ${span.segmentId}`,
|
|
||||||
serviceCode: "VirtualNode",
|
|
||||||
type: `[Broken] ${span.type}`,
|
|
||||||
peer: "",
|
|
||||||
component: `VirtualNode: #${span.spanId - 1}`,
|
|
||||||
isError: true,
|
|
||||||
isBroken: true,
|
|
||||||
layer: "Broken",
|
|
||||||
tags: [],
|
|
||||||
logs: [],
|
|
||||||
startTime: 0,
|
|
||||||
endTime: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const span of segmentHeaders) {
|
|
||||||
if (span.refs.length) {
|
|
||||||
let exit = null;
|
|
||||||
for (const ref of span.refs) {
|
|
||||||
const e = props.data.find(
|
|
||||||
(i: Recordable) =>
|
|
||||||
ref.traceId === i.traceId && ref.parentSegmentId === i.segmentId && ref.parentSpanId === i.spanId,
|
|
||||||
);
|
|
||||||
if (e) {
|
|
||||||
exit = e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!exit) {
|
|
||||||
const ref = span.refs[0];
|
|
||||||
// create a known broken node.
|
|
||||||
const parentSpanId = ref.parentSpanId > -1 ? 0 : -1;
|
|
||||||
const content = fixSpans.find(
|
|
||||||
(i: Span) =>
|
|
||||||
i.traceId === ref.traceId &&
|
|
||||||
i.segmentId === ref.parentSegmentId &&
|
|
||||||
i.spanId === ref.parentSpanId &&
|
|
||||||
i.parentSpanId === parentSpanId,
|
|
||||||
);
|
|
||||||
if (!content) {
|
|
||||||
fixSpans.push({
|
|
||||||
traceId: ref.traceId,
|
|
||||||
segmentId: ref.parentSegmentId,
|
|
||||||
spanId: ref.parentSpanId,
|
|
||||||
parentSpanId,
|
|
||||||
refs: [],
|
|
||||||
endpointName: `VNode: ${ref.parentSegmentId}`,
|
|
||||||
serviceCode: "VirtualNode",
|
|
||||||
type: `[Broken] ${ref.type}`,
|
|
||||||
peer: "",
|
|
||||||
component: `VirtualNode: #${ref.parentSpanId}`,
|
|
||||||
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 (parentSpanId > -1) {
|
|
||||||
const content = fixSpans.find(
|
|
||||||
(i: Span) =>
|
|
||||||
i.traceId === ref.traceId &&
|
|
||||||
i.segmentId === ref.parentSegmentId &&
|
|
||||||
i.spanId === 0 &&
|
|
||||||
i.parentSpanId === -1,
|
|
||||||
);
|
|
||||||
if (!content) {
|
|
||||||
fixSpans.push({
|
|
||||||
traceId: ref.traceId,
|
|
||||||
segmentId: ref.parentSegmentId,
|
|
||||||
spanId: 0,
|
|
||||||
parentSpanId: -1,
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const i of [...fixSpans, ...props.data]) {
|
|
||||||
i.label = i.endpointName || "no operation name";
|
|
||||||
i.key = Math.random().toString(36).substring(2, 36);
|
|
||||||
i.children = [];
|
|
||||||
if (segmentGroup[i.segmentId]) {
|
|
||||||
segmentGroup[i.segmentId].push(i);
|
|
||||||
} else {
|
|
||||||
segmentIdGroup.push(i.segmentId);
|
|
||||||
segmentGroup[i.segmentId] = [i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fixSpansSize.value = fixSpans.length;
|
|
||||||
for (const id of segmentIdGroup) {
|
|
||||||
const currentSegment = segmentGroup[id].sort((a: Span, b: Span) => b.parentSpanId - a.parentSpanId);
|
|
||||||
for (const s of currentSegment) {
|
|
||||||
const index = currentSegment.findIndex((i: Span) => i.spanId === s.parentSpanId);
|
|
||||||
if (index > -1) {
|
|
||||||
if (
|
|
||||||
(currentSegment[index].isBroken && currentSegment[index].parentSpanId === -1) ||
|
|
||||||
!currentSegment[index].isBroken
|
|
||||||
) {
|
|
||||||
currentSegment[index].children.push(s);
|
|
||||||
currentSegment[index].children.sort((a: Span, b: Span) => a.spanId - b.spanId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (s.isBroken) {
|
|
||||||
const children = props.data.filter((span: Span) =>
|
|
||||||
span.refs.find(
|
|
||||||
(d) => d.traceId === s.traceId && d.parentSegmentId === s.segmentId && d.parentSpanId === s.spanId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (children.length) {
|
|
||||||
s.children.push(...children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
segmentGroup[id] = currentSegment[currentSegment.length - 1];
|
|
||||||
}
|
|
||||||
for (const id of segmentIdGroup) {
|
|
||||||
for (const ref of segmentGroup[id].refs) {
|
|
||||||
if (ref.traceId === props.traceId) {
|
|
||||||
traverseTree(segmentGroup[ref.parentSegmentId], ref.parentSpanId, ref.parentSegmentId, segmentGroup[id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const i in segmentGroup) {
|
|
||||||
for (const ref of segmentGroup[i].refs) {
|
|
||||||
if (!segmentGroup[ref.parentSegmentId]) {
|
|
||||||
segmentId.value.push(segmentGroup[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!segmentGroup[i].refs.length && segmentGroup[i].parentSpanId === -1) {
|
|
||||||
segmentId.value.push(segmentGroup[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const i of segmentId.value) {
|
|
||||||
collapse(i);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function collapse(d: Span | Recordable) {
|
function setLevel(arr: Span[], level = 1, totalExec?: number) {
|
||||||
if (d.children) {
|
|
||||||
const item = refSpans.value.find((s: Ref) => s.parentSpanId === d.spanId && s.parentSegmentId === d.segmentId);
|
|
||||||
let dur = d.endTime - d.startTime;
|
|
||||||
for (const i of d.children) {
|
|
||||||
dur -= i.endTime - i.startTime;
|
|
||||||
}
|
|
||||||
d.dur = dur < 0 ? 0 : dur;
|
|
||||||
if (item) {
|
|
||||||
d.children = d.children.sort(compare("startTime"));
|
|
||||||
}
|
|
||||||
for (const i of d.children) {
|
|
||||||
collapse(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function setLevel(arr: Recordable[], level = 1, totalExec?: number) {
|
|
||||||
for (const item of arr) {
|
for (const item of arr) {
|
||||||
item.level = level;
|
item.level = level;
|
||||||
totalExec = totalExec || item.endTime - item.startTime;
|
totalExec = totalExec || item.endTime - item.startTime;
|
||||||
@@ -411,34 +255,12 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
function getRefsAllNodes(tree: Recordable) {
|
|
||||||
let nodes = [];
|
|
||||||
let stack = [tree];
|
|
||||||
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const node = stack.pop();
|
|
||||||
nodes.push(node);
|
|
||||||
|
|
||||||
if (node?.children && node.children.length > 0) {
|
|
||||||
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
||||||
stack.push(node.children[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
function compare(p: string) {
|
|
||||||
return (m: Recordable, n: Recordable) => {
|
|
||||||
const a = m[p];
|
|
||||||
const b = n[p];
|
|
||||||
return a - b;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
d3.selectAll(".d3-tip").remove();
|
d3.selectAll(".d3-tip").remove();
|
||||||
window.removeEventListener("resize", debounceFunc);
|
window.removeEventListener("resize", debounceFunc);
|
||||||
mutationObserver.deleteObserve("trigger-resize");
|
mutationObserver.deleteObserve("trigger-resize");
|
||||||
|
window.removeEventListener("spanPanelToggled", draw);
|
||||||
});
|
});
|
||||||
watch(
|
watch(
|
||||||
() => props.data,
|
() => props.data,
|
||||||
@@ -455,7 +277,22 @@ limitations under the License. -->
|
|||||||
watch(
|
watch(
|
||||||
() => appStore.theme,
|
() => appStore.theme,
|
||||||
() => {
|
() => {
|
||||||
tree.value.init({ label: "TRACE_ROOT", children: segmentId.value }, props.data, fixSpansSize.value);
|
if (props.type === TraceGraphType.LIST) {
|
||||||
|
tree.value.init({
|
||||||
|
data: { label: "TRACE_ROOT", children: segmentId.value },
|
||||||
|
row: getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
||||||
|
fixSpansSize: fixSpansSize.value,
|
||||||
|
selectedMaxTimestamp: props.selectedMaxTimestamp,
|
||||||
|
selectedMinTimestamp: props.selectedMinTimestamp,
|
||||||
|
});
|
||||||
|
} else if (props.type === TraceGraphType.TREE) {
|
||||||
|
tree.value.init({
|
||||||
|
data: { label: `${props.traceId}`, children: segmentId.value },
|
||||||
|
row: getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
||||||
|
selectedMaxTimestamp: props.selectedMaxTimestamp,
|
||||||
|
selectedMinTimestamp: props.selectedMinTimestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
tree.value.draw(() => {
|
tree.value.draw(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -463,6 +300,20 @@ limitations under the License. -->
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
watch(
|
||||||
|
() => [props.selectedMaxTimestamp, props.selectedMinTimestamp],
|
||||||
|
([newMax, newMin]) => {
|
||||||
|
// Only trigger redraw if the change is significant
|
||||||
|
if (isTimestampChangeSignificant(newMax as number, newMin as number)) {
|
||||||
|
// Update previous values
|
||||||
|
prevSelectedMaxTimestamp.value = newMax as number;
|
||||||
|
prevSelectedMinTimestamp.value = newMin as number;
|
||||||
|
|
||||||
|
// Use debounced version to prevent excessive re-renders
|
||||||
|
debouncedOnSpanPanelToggled();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.d3-graph {
|
.d3-graph {
|
||||||
|
@@ -83,7 +83,7 @@ limitations under the License. -->
|
|||||||
ref="eventGraph"
|
ref="eventGraph"
|
||||||
v-if="currentSpan.attachedEvents && currentSpan.attachedEvents.length"
|
v-if="currentSpan.attachedEvents && currentSpan.attachedEvents.length"
|
||||||
></div>
|
></div>
|
||||||
<el-button class="popup-btn" type="primary" @click="getTaceLogs">
|
<el-button class="popup-btn" @click="getTaceLogs">
|
||||||
{{ t("relatedTraceLogs") }}
|
{{ t("relatedTraceLogs") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,9 +149,8 @@ limitations under the License. -->
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, onMounted, inject } from "vue";
|
import { ref, computed, onMounted, inject } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import type { PropType } from "vue";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import ListGraph from "../../utils/d3-trace-list";
|
import ListGraph from "./utils/d3-trace-list";
|
||||||
import copy from "@/utils/copy";
|
import copy from "@/utils/copy";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { dateFormat } from "@/utils/dateFormat";
|
import { dateFormat } from "@/utils/dateFormat";
|
||||||
@@ -163,10 +162,11 @@ limitations under the License. -->
|
|||||||
import { WidgetType } from "@/views/dashboard/data";
|
import { WidgetType } from "@/views/dashboard/data";
|
||||||
import type { LayoutConfig, DashboardItem } from "@/types/dashboard";
|
import type { LayoutConfig, DashboardItem } from "@/types/dashboard";
|
||||||
/*global defineProps, Nullable, Recordable */
|
/*global defineProps, Nullable, Recordable */
|
||||||
const props = defineProps({
|
type Props = {
|
||||||
currentSpan: { type: Object as PropType<Span>, default: () => ({}) },
|
currentSpan: Span;
|
||||||
traceId: { type: String, default: "" },
|
traceId?: string;
|
||||||
});
|
};
|
||||||
|
const props = defineProps<Props>();
|
||||||
const options: LayoutConfig | null = inject("options") || null;
|
const options: LayoutConfig | null = inject("options") || null;
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const traceStore = useTraceStore();
|
const traceStore = useTraceStore();
|
||||||
@@ -233,7 +233,7 @@ limitations under the License. -->
|
|||||||
return a.startTime - b.startTime;
|
return a.startTime - b.startTime;
|
||||||
});
|
});
|
||||||
|
|
||||||
tree.value = new ListGraph(eventGraph.value, selectEvent);
|
tree.value = new ListGraph({ el: eventGraph.value, handleSelectSpan: selectEvent });
|
||||||
tree.value.init(
|
tree.value.init(
|
||||||
{
|
{
|
||||||
children: events,
|
children: events,
|
||||||
@@ -287,9 +287,9 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
|
|
||||||
.popup-btn {
|
.popup-btn {
|
||||||
margin-top: 40px;
|
margin-top: 20px;
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 0 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item span {
|
.item span {
|
||||||
|
@@ -17,13 +17,12 @@
|
|||||||
|
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import d3tip from "d3-tip";
|
import d3tip from "d3-tip";
|
||||||
import type { Trace } from "@/types/trace";
|
import type { Trace, Span } from "@/types/trace";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import icons from "@/assets/img/icons";
|
import icons from "@/assets/img/icons";
|
||||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
import { getServiceColor } from "@/utils/color";
|
||||||
import { Themes } from "@/constants/data";
|
|
||||||
import { useTraceStore } from "@/store/modules/trace";
|
|
||||||
|
|
||||||
|
const xScaleWidth = 0.6;
|
||||||
export default class ListGraph {
|
export default class ListGraph {
|
||||||
private barHeight = 48;
|
private barHeight = 48;
|
||||||
private handleSelectSpan: Nullable<(i: Trace) => void> = null;
|
private handleSelectSpan: Nullable<(i: Trace) => void> = null;
|
||||||
@@ -36,19 +35,16 @@ export default class ListGraph {
|
|||||||
private prompt: any = null;
|
private prompt: any = null;
|
||||||
private row: any[] = [];
|
private row: any[] = [];
|
||||||
private data: any = [];
|
private data: any = [];
|
||||||
private min = 0;
|
private minTimestamp = 0;
|
||||||
private max = 0;
|
private maxTimestamp = 0;
|
||||||
private list: any[] = [];
|
|
||||||
private xScale: any = null;
|
private xScale: any = null;
|
||||||
private xAxis: any = null;
|
private xAxis: any = null;
|
||||||
private sequentialScale: any = null;
|
|
||||||
private root: any = null;
|
private root: any = null;
|
||||||
private selectedNode: any = null;
|
private selectedNode: any = null;
|
||||||
constructor(el: HTMLDivElement, handleSelectSpan: (i: Trace) => void) {
|
constructor({ el, handleSelectSpan }: { el: HTMLDivElement; handleSelectSpan: (i: Trace) => void }) {
|
||||||
this.handleSelectSpan = handleSelectSpan;
|
this.handleSelectSpan = handleSelectSpan;
|
||||||
this.el = el;
|
this.el = el;
|
||||||
this.width = el.getBoundingClientRect().width - 10;
|
this.width = el.getBoundingClientRect().width - 10;
|
||||||
this.height = el.getBoundingClientRect().height - 10;
|
|
||||||
d3.select(`.${this.el?.className} .trace-list`).remove();
|
d3.select(`.${this.el?.className} .trace-list`).remove();
|
||||||
this.svg = d3
|
this.svg = d3
|
||||||
.select(this.el)
|
.select(this.el)
|
||||||
@@ -80,39 +76,48 @@ export default class ListGraph {
|
|||||||
this.svg.call(this.tip);
|
this.svg.call(this.tip);
|
||||||
this.svg.call(this.prompt);
|
this.svg.call(this.prompt);
|
||||||
}
|
}
|
||||||
diagonal(d: Recordable) {
|
diagonal(d: Indexable) {
|
||||||
return `M ${d.source.y} ${d.source.x + 5}
|
return `M ${d.source.y} ${d.source.x + 5}
|
||||||
L ${d.source.y} ${d.target.x - 30}
|
L ${d.source.y} ${d.target.x - 30}
|
||||||
L${d.target.y} ${d.target.x - 20}
|
L${d.target.y} ${d.target.x - 20}
|
||||||
L${d.target.y} ${d.target.x - 5}`;
|
L${d.target.y} ${d.target.x - 5}`;
|
||||||
}
|
}
|
||||||
init(data: Recordable, row: Recordable[], fixSpansSize: number) {
|
init({
|
||||||
|
data,
|
||||||
|
row,
|
||||||
|
fixSpansSize,
|
||||||
|
selectedMaxTimestamp,
|
||||||
|
selectedMinTimestamp,
|
||||||
|
}: {
|
||||||
|
data: Indexable;
|
||||||
|
row: Indexable[];
|
||||||
|
fixSpansSize: number;
|
||||||
|
selectedMaxTimestamp?: number;
|
||||||
|
selectedMinTimestamp?: number;
|
||||||
|
}) {
|
||||||
d3.select(`.${this.el?.className} .trace-xaxis`).remove();
|
d3.select(`.${this.el?.className} .trace-xaxis`).remove();
|
||||||
d3.select("#trace-action-box").style("display", "none");
|
d3.select("#trace-action-box").style("display", "none");
|
||||||
this.row = row;
|
this.row = row;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.min = d3.min(this.row.map((i) => i.startTime));
|
const min = d3.min(this.row.map((i) => i.startTime));
|
||||||
this.max = d3.max(this.row.map((i) => i.endTime - this.min)) || 0;
|
const max = d3.max(this.row.map((i) => i.endTime));
|
||||||
this.list = useTraceStore().serviceList || [];
|
this.minTimestamp = selectedMinTimestamp ?? min;
|
||||||
|
this.maxTimestamp = selectedMaxTimestamp ?? max;
|
||||||
this.xScale = d3
|
this.xScale = d3
|
||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
.range([0, this.width * 0.387])
|
.range([0, this.width * xScaleWidth])
|
||||||
.domain([0, this.max]);
|
.domain([this.minTimestamp - min, this.maxTimestamp - min]);
|
||||||
this.xAxis = d3.axisTop(this.xScale).tickFormat((d: any) => {
|
this.xAxis = d3.axisTop(this.xScale).tickFormat((d: any) => {
|
||||||
if (d === 0) return 0;
|
if (d === 0) return d.toFixed(2);
|
||||||
if (d >= 1000) return d / 1000 + "s";
|
if (d >= 1000) return (d / 1000).toFixed(2) + "s";
|
||||||
return d;
|
return d.toFixed(2);
|
||||||
});
|
});
|
||||||
this.svg.attr("height", (this.row.length + fixSpansSize + 1) * this.barHeight);
|
this.svg.attr("height", (this.row.length + fixSpansSize + 1) * this.barHeight);
|
||||||
this.svg
|
this.svg
|
||||||
.append("g")
|
.append("g")
|
||||||
.attr("class", "trace-xaxis")
|
.attr("class", "trace-xaxis")
|
||||||
.attr("transform", `translate(${this.width * 0.618 - 20},${30})`)
|
.attr("transform", `translate(${this.width * (1 - xScaleWidth) - 20},${30})`)
|
||||||
.call(this.xAxis);
|
.call(this.xAxis);
|
||||||
this.sequentialScale = d3
|
|
||||||
.scaleSequential()
|
|
||||||
.domain([0, this.list.length + 1])
|
|
||||||
.interpolator(d3.interpolateCool);
|
|
||||||
this.root = d3.hierarchy(this.data, (d) => d.children);
|
this.root = d3.hierarchy(this.data, (d) => d.children);
|
||||||
this.root.x0 = 0;
|
this.root.x0 = 0;
|
||||||
this.root.y0 = 0;
|
this.root.y0 = 0;
|
||||||
@@ -141,7 +146,6 @@ export default class ListGraph {
|
|||||||
}
|
}
|
||||||
update(source: Recordable, callback: Function) {
|
update(source: Recordable, callback: Function) {
|
||||||
const t = this;
|
const t = this;
|
||||||
const appStore = useAppStoreWithOut();
|
|
||||||
const nodes = this.root.descendants();
|
const nodes = this.root.descendants();
|
||||||
let index = -1;
|
let index = -1;
|
||||||
this.root.eachBefore((n: Recordable) => {
|
this.root.eachBefore((n: Recordable) => {
|
||||||
@@ -186,12 +190,12 @@ export default class ListGraph {
|
|||||||
});
|
});
|
||||||
nodeEnter
|
nodeEnter
|
||||||
.append("rect")
|
.append("rect")
|
||||||
.attr("height", 42)
|
.attr("height", this.barHeight)
|
||||||
.attr("ry", 2)
|
.attr("ry", 2)
|
||||||
.attr("rx", 2)
|
.attr("rx", 2)
|
||||||
.attr("y", -22)
|
.attr("y", -this.barHeight / 2)
|
||||||
.attr("x", 20)
|
.attr("x", 20)
|
||||||
.attr("width", "100%")
|
.attr("width", "200px")
|
||||||
.attr("fill", "rgba(0,0,0,0)");
|
.attr("fill", "rgba(0,0,0,0)");
|
||||||
nodeEnter
|
nodeEnter
|
||||||
.append("image")
|
.append("image")
|
||||||
@@ -242,24 +246,18 @@ export default class ListGraph {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
t.prompt.hide(d, this);
|
t.prompt.hide(d, this);
|
||||||
});
|
});
|
||||||
nodeEnter
|
|
||||||
.append("text")
|
|
||||||
.attr("x", 13)
|
|
||||||
.attr("y", 5)
|
|
||||||
.attr("fill", `#e54c17`)
|
|
||||||
.html((d: Recordable) => (d.data.isError ? "◉" : ""));
|
|
||||||
nodeEnter
|
nodeEnter
|
||||||
.append("text")
|
.append("text")
|
||||||
.attr("class", "node-text")
|
.attr("class", "node-text")
|
||||||
.attr("x", 35)
|
.attr("x", 35)
|
||||||
.attr("y", -6)
|
.attr("y", -6)
|
||||||
.attr("fill", (d: Recordable) => (d.data.isError ? `#e54c17` : appStore.theme === Themes.Dark ? "#eee" : "#333"))
|
.attr("fill", (d: { data: Span }) => (d.data.isError ? `var(--el-color-danger)` : `var(--font-color)`))
|
||||||
.html((d: Recordable) => {
|
.html((d: { data: Span }) => {
|
||||||
|
const { label } = d.data;
|
||||||
if (d.data.label === "TRACE_ROOT") {
|
if (d.data.label === "TRACE_ROOT") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const label = d.data.label.length > 30 ? `${d.data.label.slice(0, 30)}...` : `${d.data.label}`;
|
return (label || "").length > 30 ? `${label?.slice(0, 30)}...` : `${label}`;
|
||||||
return label;
|
|
||||||
});
|
});
|
||||||
nodeEnter
|
nodeEnter
|
||||||
.append("circle")
|
.append("circle")
|
||||||
@@ -289,7 +287,7 @@ export default class ListGraph {
|
|||||||
.attr("y", -1)
|
.attr("y", -1)
|
||||||
.attr("fill", "#e66")
|
.attr("fill", "#e66")
|
||||||
.style("font-size", "10px")
|
.style("font-size", "10px")
|
||||||
.text((d: Recordable) => {
|
.text((d: { data: Span }) => {
|
||||||
const events = d.data.attachedEvents;
|
const events = d.data.attachedEvents;
|
||||||
if (events && events.length) {
|
if (events && events.length) {
|
||||||
return `${events.length}`;
|
return `${events.length}`;
|
||||||
@@ -302,18 +300,15 @@ export default class ListGraph {
|
|||||||
.attr("class", "node-text")
|
.attr("class", "node-text")
|
||||||
.attr("x", 35)
|
.attr("x", 35)
|
||||||
.attr("y", 12)
|
.attr("y", 12)
|
||||||
.attr("fill", appStore.theme === Themes.Dark ? "#777" : "#ccc")
|
.attr("fill", (d: { data: Span }) => (d.data.isError ? `var(--el-color-danger)` : `var(--disabled-color)`))
|
||||||
.style("font-size", "11px")
|
.style("font-size", "11px")
|
||||||
.text(
|
.text((d: { data: Span }) => {
|
||||||
(d: Recordable) =>
|
const { layer, component, event, startTime, startTimeNanos } = d.data;
|
||||||
`${d.data.layer || ""} ${
|
const text = `${layer || ""} ${
|
||||||
d.data.component
|
component ? "- " + component : event ? this.visDate(startTime) + ":" + startTimeNanos : ""
|
||||||
? "- " + d.data.component
|
}`;
|
||||||
: d.data.event
|
return text.length > 20 ? `${text?.slice(0, 20)}...` : `${text}`;
|
||||||
? this.visDate(d.data.startTime) + ":" + d.data.startTimeNanos
|
});
|
||||||
: ""
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
nodeEnter
|
nodeEnter
|
||||||
.append("rect")
|
.append("rect")
|
||||||
.attr("rx", 2)
|
.attr("rx", 2)
|
||||||
@@ -321,15 +316,33 @@ export default class ListGraph {
|
|||||||
.attr("height", 4)
|
.attr("height", 4)
|
||||||
.attr("width", (d: Recordable) => {
|
.attr("width", (d: Recordable) => {
|
||||||
if (!d.data.endTime || !d.data.startTime) return 0;
|
if (!d.data.endTime || !d.data.startTime) return 0;
|
||||||
return this.xScale(d.data.endTime - d.data.startTime) + 1 || 0;
|
// Calculate the actual start and end times within the visible range
|
||||||
|
let spanStart = d.data.startTime;
|
||||||
|
let spanEnd = d.data.endTime;
|
||||||
|
|
||||||
|
const isIn = d.data.startTime > this.maxTimestamp || d.data.endTime < this.minTimestamp;
|
||||||
|
if (isIn) return 0;
|
||||||
|
|
||||||
|
// If the span is completely outside the visible range, don't show it
|
||||||
|
if (spanStart >= spanEnd) return 0;
|
||||||
|
if (spanStart < this.minTimestamp) spanStart = this.minTimestamp;
|
||||||
|
if (spanEnd > this.maxTimestamp) spanEnd = this.maxTimestamp;
|
||||||
|
if (spanStart >= spanEnd) return 0;
|
||||||
|
const min = d3.min(this.row.map((i) => i.startTime));
|
||||||
|
return this.xScale(spanEnd - min) - this.xScale(spanStart - min) + 1 || 0;
|
||||||
|
})
|
||||||
|
.attr("x", (d: Recordable) => {
|
||||||
|
if (!d.data.endTime || !d.data.startTime) return 0;
|
||||||
|
const isIn = d.data.startTime > this.maxTimestamp || d.data.endTime < this.minTimestamp;
|
||||||
|
if (isIn) return 0;
|
||||||
|
// Calculate the actual start time within the visible range
|
||||||
|
let spanStart = d.data.startTime;
|
||||||
|
if (spanStart < this.minTimestamp) spanStart = this.minTimestamp;
|
||||||
|
const min = d3.min(this.row.map((i) => i.startTime));
|
||||||
|
return this.width * (1 - xScaleWidth) - d.y - 25 + this.xScale(spanStart - min) || 0;
|
||||||
})
|
})
|
||||||
.attr("x", (d: Recordable) =>
|
|
||||||
!d.data.endTime || !d.data.startTime
|
|
||||||
? 0
|
|
||||||
: this.width * 0.618 - 20 - d.y + this.xScale(d.data.startTime - this.min) || 0,
|
|
||||||
)
|
|
||||||
.attr("y", -2)
|
.attr("y", -2)
|
||||||
.style("fill", (d: Recordable) => `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`);
|
.style("fill", (d: Recordable) => `${getServiceColor(d.data.serviceCode || "")}`);
|
||||||
const nodeUpdate = nodeEnter.merge(node);
|
const nodeUpdate = nodeEnter.merge(node);
|
||||||
nodeUpdate
|
nodeUpdate
|
||||||
.transition()
|
.transition()
|
||||||
@@ -338,14 +351,12 @@ export default class ListGraph {
|
|||||||
.style("opacity", 1);
|
.style("opacity", 1);
|
||||||
nodeUpdate
|
nodeUpdate
|
||||||
.append("circle")
|
.append("circle")
|
||||||
.attr("r", appStore.theme === Themes.Dark ? 4 : 3)
|
.attr("r", 3)
|
||||||
.style("cursor", "pointer")
|
.style("cursor", "pointer")
|
||||||
.attr("stroke-width", appStore.theme === Themes.Dark ? 3 : 2.5)
|
.attr("stroke-width", 3)
|
||||||
.style("fill", (d: Recordable) =>
|
.style("fill", (d: Recordable) => (d._children ? `${getServiceColor(d.data.serviceCode)}` : "#eee"))
|
||||||
d._children ? `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}` : "#eee",
|
|
||||||
)
|
|
||||||
.style("stroke", (d: Recordable) =>
|
.style("stroke", (d: Recordable) =>
|
||||||
d.data.label === "TRACE_ROOT" ? "" : `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`,
|
d.data.label === "TRACE_ROOT" ? "" : `${getServiceColor(d.data.serviceCode)}`,
|
||||||
)
|
)
|
||||||
.on("click", (event: any, d: Recordable) => {
|
.on("click", (event: any, d: Recordable) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -368,8 +379,8 @@ export default class ListGraph {
|
|||||||
.enter()
|
.enter()
|
||||||
.insert("path", "g")
|
.insert("path", "g")
|
||||||
.attr("class", "trace-link")
|
.attr("class", "trace-link")
|
||||||
.attr("fill", appStore.theme === Themes.Dark ? "rgba(244,244,244,0)" : "rgba(0,0,0,0)")
|
.attr("fill", "none")
|
||||||
.attr("stroke", appStore.theme === Themes.Dark ? "rgba(244,244,244,0.4)" : "rgba(0, 0, 0, 0.1)")
|
.attr("stroke", "var(--sw-trace-list-path)")
|
||||||
.attr("stroke-width", 2)
|
.attr("stroke-width", 2)
|
||||||
.attr("transform", `translate(5, 0)`)
|
.attr("transform", `translate(5, 0)`)
|
||||||
.attr("d", () => {
|
.attr("d", () => {
|
||||||
@@ -432,6 +443,23 @@ export default class ListGraph {
|
|||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
highlightSpan(span: Recordable) {
|
||||||
|
if (!span) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nodes = this.root?.descendants ? this.root.descendants() : [];
|
||||||
|
const targetNode = nodes.find(
|
||||||
|
(node: { data: Span }) =>
|
||||||
|
span.spanId === node.data.spanId &&
|
||||||
|
span.segmentId === node.data.segmentId &&
|
||||||
|
span.traceId === node.data.traceId,
|
||||||
|
);
|
||||||
|
if (!targetNode) return;
|
||||||
|
this.selectedNode?.classed("highlighted", false);
|
||||||
|
const sel = d3.select(`#list-node-${targetNode.id}`);
|
||||||
|
sel.classed("highlighted", true);
|
||||||
|
this.selectedNode = sel;
|
||||||
|
}
|
||||||
visDate(date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") {
|
visDate(date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") {
|
||||||
return dayjs(date).format(pattern);
|
return dayjs(date).format(pattern);
|
||||||
}
|
}
|
@@ -19,8 +19,8 @@ import * as d3 from "d3";
|
|||||||
import d3tip from "d3-tip";
|
import d3tip from "d3-tip";
|
||||||
import type { Trace, Span } from "@/types/trace";
|
import type { Trace, Span } from "@/types/trace";
|
||||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||||
import { useTraceStore } from "@/store/modules/trace";
|
|
||||||
import { Themes } from "@/constants/data";
|
import { Themes } from "@/constants/data";
|
||||||
|
import { getServiceColor } from "@/utils/color";
|
||||||
|
|
||||||
export default class TraceMap {
|
export default class TraceMap {
|
||||||
private i = 0;
|
private i = 0;
|
||||||
@@ -36,11 +36,9 @@ export default class TraceMap {
|
|||||||
private treemap: Nullable<any> = null;
|
private treemap: Nullable<any> = null;
|
||||||
private data: Nullable<any> = null;
|
private data: Nullable<any> = null;
|
||||||
private row: Nullable<any> = null;
|
private row: Nullable<any> = null;
|
||||||
private min = 0;
|
private minTimestamp = 0;
|
||||||
private max = 0;
|
private maxTimestamp = 0;
|
||||||
private list: string[] = [];
|
|
||||||
private xScale: Nullable<any> = null;
|
private xScale: Nullable<any> = null;
|
||||||
private sequentialScale: Nullable<any> = null;
|
|
||||||
private root: Nullable<any> = null;
|
private root: Nullable<any> = null;
|
||||||
private topSlowMax: number[] = [];
|
private topSlowMax: number[] = [];
|
||||||
private topSlowMin: number[] = [];
|
private topSlowMin: number[] = [];
|
||||||
@@ -49,14 +47,14 @@ export default class TraceMap {
|
|||||||
private nodeUpdate: Nullable<any> = null;
|
private nodeUpdate: Nullable<any> = null;
|
||||||
private selectedNode: any = null;
|
private selectedNode: any = null;
|
||||||
|
|
||||||
constructor(el: HTMLDivElement, handleSelectSpan: (i: Trace) => void) {
|
constructor({ el, handleSelectSpan }: { el: HTMLDivElement; handleSelectSpan: (i: Trace) => void }) {
|
||||||
this.el = el;
|
this.el = el;
|
||||||
this.handleSelectSpan = handleSelectSpan;
|
this.handleSelectSpan = handleSelectSpan;
|
||||||
this.i = 0;
|
this.i = 0;
|
||||||
this.topSlow = [];
|
this.topSlow = [];
|
||||||
this.topChild = [];
|
this.topChild = [];
|
||||||
this.width = el.clientWidth - 20;
|
this.width = el.getBoundingClientRect().width - 10;
|
||||||
this.height = el.clientHeight - 30;
|
this.height = el.getBoundingClientRect().height - 10;
|
||||||
d3.select(`.${this.el?.className} .d3-trace-tree`).remove();
|
d3.select(`.${this.el?.className} .d3-trace-tree`).remove();
|
||||||
this.body = d3
|
this.body = d3
|
||||||
.select(this.el)
|
.select(this.el)
|
||||||
@@ -81,19 +79,28 @@ export default class TraceMap {
|
|||||||
this.svg = this.body.append("g").attr("transform", () => `translate(120, 0)`);
|
this.svg = this.body.append("g").attr("transform", () => `translate(120, 0)`);
|
||||||
this.svg.call(this.tip);
|
this.svg.call(this.tip);
|
||||||
}
|
}
|
||||||
init(data: Recordable, row: Recordable) {
|
init({
|
||||||
|
data,
|
||||||
|
row,
|
||||||
|
selectedMaxTimestamp,
|
||||||
|
selectedMinTimestamp,
|
||||||
|
}: {
|
||||||
|
data: Span;
|
||||||
|
row: Span[];
|
||||||
|
selectedMaxTimestamp?: number;
|
||||||
|
selectedMinTimestamp?: number;
|
||||||
|
}) {
|
||||||
d3.select("#trace-action-box").style("display", "none");
|
d3.select("#trace-action-box").style("display", "none");
|
||||||
this.treemap = d3.tree().size([row.length * 35, this.width]);
|
this.treemap = d3.tree().size([row.length * 35, this.width]);
|
||||||
this.row = row;
|
this.row = row;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.min = Number(d3.min(this.row.map((i: Span) => i.startTime)));
|
this.minTimestamp = selectedMinTimestamp ?? Number(d3.min(this.row.map((i: Span) => i.startTime)));
|
||||||
this.max = Number(d3.max(this.row.map((i: Span) => i.endTime - this.min)));
|
this.maxTimestamp =
|
||||||
this.list = useTraceStore().serviceList || [];
|
selectedMaxTimestamp ?? Number(d3.max(this.row.map((i: Span) => i.endTime - this.minTimestamp)));
|
||||||
this.xScale = d3.scaleLinear().range([0, 100]).domain([0, this.max]);
|
this.xScale = d3
|
||||||
this.sequentialScale = d3
|
.scaleLinear()
|
||||||
.scaleSequential()
|
.range([0, 100])
|
||||||
.domain([0, this.list.length + 1])
|
.domain([0, this.maxTimestamp - this.minTimestamp]);
|
||||||
.interpolator(d3.interpolateCool);
|
|
||||||
|
|
||||||
this.body.call(this.getZoomBehavior(this.svg));
|
this.body.call(this.getZoomBehavior(this.svg));
|
||||||
this.root = d3.hierarchy(this.data, (d) => d.children);
|
this.root = d3.hierarchy(this.data, (d) => d.children);
|
||||||
@@ -102,7 +109,7 @@ export default class TraceMap {
|
|||||||
this.topSlow = [];
|
this.topSlow = [];
|
||||||
this.topChild = [];
|
this.topChild = [];
|
||||||
const that = this;
|
const that = this;
|
||||||
this.root.children.forEach(collapse);
|
this.root.children?.forEach(collapse);
|
||||||
this.topSlowMax = this.topSlow.sort((a: number, b: number) => b - a)[0];
|
this.topSlowMax = this.topSlow.sort((a: number, b: number) => b - a)[0];
|
||||||
this.topSlowMin = this.topSlow.sort((a: number, b: number) => b - a)[4];
|
this.topSlowMin = this.topSlow.sort((a: number, b: number) => b - a)[4];
|
||||||
this.topChildMax = this.topChild.sort((a: number, b: number) => b - a)[0];
|
this.topChildMax = this.topChild.sort((a: number, b: number) => b - a)[0];
|
||||||
@@ -218,11 +225,9 @@ export default class TraceMap {
|
|||||||
.append("circle")
|
.append("circle")
|
||||||
.attr("class", "node")
|
.attr("class", "node")
|
||||||
.attr("r", 2)
|
.attr("r", 2)
|
||||||
.attr("stroke", (d: Recordable) => this.sequentialScale(this.list.indexOf(d.data.serviceCode)))
|
.attr("stroke", (d: Recordable) => getServiceColor(d.data.serviceCode))
|
||||||
.attr("stroke-width", 2.5)
|
.attr("stroke-width", 2.5)
|
||||||
.attr("fill", (d: Recordable) =>
|
.attr("fill", (d: Recordable) => (d.data.children.length ? getServiceColor(d.data.serviceCode) : "#fff"));
|
||||||
d.data.children.length ? this.sequentialScale(this.list.indexOf(d.data.serviceCode)) : "#fff",
|
|
||||||
);
|
|
||||||
nodeEnter
|
nodeEnter
|
||||||
.append("text")
|
.append("text")
|
||||||
.attr("class", "trace-node-text")
|
.attr("class", "trace-node-text")
|
||||||
@@ -276,21 +281,31 @@ export default class TraceMap {
|
|||||||
.attr("rx", 1)
|
.attr("rx", 1)
|
||||||
.attr("ry", 1)
|
.attr("ry", 1)
|
||||||
.attr("height", 2)
|
.attr("height", 2)
|
||||||
.attr("width", (d: Recordable) => {
|
.attr("width", (d: { data: Span }) => {
|
||||||
if (!d.data.endTime || !d.data.startTime) return 0;
|
let spanStart = d.data.startTime;
|
||||||
return this.xScale(d.data.endTime - d.data.startTime) + 1 || 0;
|
let spanEnd = d.data.endTime;
|
||||||
|
if (!spanEnd || !spanStart) return 0;
|
||||||
|
if (spanStart > this.maxTimestamp || spanEnd < this.minTimestamp) return 0;
|
||||||
|
if (spanStart < this.minTimestamp) spanStart = this.minTimestamp;
|
||||||
|
if (spanEnd > this.maxTimestamp) spanEnd = this.maxTimestamp;
|
||||||
|
return this.xScale(spanEnd - spanStart) + 1 || 0;
|
||||||
})
|
})
|
||||||
.attr("x", (d: Recordable) => {
|
.attr("x", (d: Indexable) => {
|
||||||
if (!d.data.endTime || !d.data.startTime) {
|
let spanStart = d.data.startTime;
|
||||||
|
let spanEnd = d.data.endTime;
|
||||||
|
if (!spanEnd || !spanStart) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
if (spanStart > this.maxTimestamp || spanEnd < this.minTimestamp) return 0;
|
||||||
|
if (spanStart < this.minTimestamp) spanStart = this.minTimestamp;
|
||||||
|
if (spanEnd > this.maxTimestamp) spanEnd = this.maxTimestamp;
|
||||||
if (d.children || d._children) {
|
if (d.children || d._children) {
|
||||||
return -110 + this.xScale(d.data.startTime - this.min);
|
return -110 + this.xScale(spanStart - this.minTimestamp);
|
||||||
}
|
}
|
||||||
return 10 + this.xScale(d.data.startTime - this.min);
|
return 10 + this.xScale(spanStart - this.minTimestamp);
|
||||||
})
|
})
|
||||||
.attr("y", -1)
|
.attr("y", -1)
|
||||||
.style("fill", (d: Recordable) => this.sequentialScale(this.list.indexOf(d.data.serviceCode)));
|
.style("fill", (d: { data: Span }) => getServiceColor(d.data.serviceCode));
|
||||||
const nodeUpdate = nodeEnter.merge(node);
|
const nodeUpdate = nodeEnter.merge(node);
|
||||||
this.nodeUpdate = nodeUpdate;
|
this.nodeUpdate = nodeUpdate;
|
||||||
nodeUpdate
|
nodeUpdate
|
@@ -0,0 +1,281 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Span, Ref } from "@/types/trace";
|
||||||
|
|
||||||
|
/* global Indexable */
|
||||||
|
|
||||||
|
export interface BuildTreeResult {
|
||||||
|
roots: Span[];
|
||||||
|
fixSpansSize: number;
|
||||||
|
refSpans: Ref[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSegmentForest(data: Span[], traceId: string): BuildTreeResult {
|
||||||
|
const refSpans: Ref[] = [];
|
||||||
|
const segmentGroup: { [key: string]: any } = {};
|
||||||
|
const segmentIdGroup: string[] = [];
|
||||||
|
const fixSpans: Span[] = [];
|
||||||
|
const segmentHeaders: Span[] = [];
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return { roots: [], fixSpansSize: 0, refSpans };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const span of data) {
|
||||||
|
if (span.refs && span.refs.length) {
|
||||||
|
refSpans.push(...span.refs);
|
||||||
|
}
|
||||||
|
if (span.parentSpanId === -1) {
|
||||||
|
segmentHeaders.push(span);
|
||||||
|
} else {
|
||||||
|
const item = data.find(
|
||||||
|
(i: Span) => i.traceId === span.traceId && i.segmentId === span.segmentId && i.spanId === span.spanId - 1,
|
||||||
|
);
|
||||||
|
const content = fixSpans.find(
|
||||||
|
(i: Span) =>
|
||||||
|
i.traceId === span.traceId &&
|
||||||
|
i.segmentId === span.segmentId &&
|
||||||
|
i.spanId === span.spanId - 1 &&
|
||||||
|
i.parentSpanId === span.spanId - 2,
|
||||||
|
);
|
||||||
|
if (!item && !content) {
|
||||||
|
fixSpans.push({
|
||||||
|
traceId: span.traceId,
|
||||||
|
segmentId: span.segmentId,
|
||||||
|
spanId: span.spanId - 1,
|
||||||
|
parentSpanId: span.spanId - 2,
|
||||||
|
refs: [],
|
||||||
|
endpointName: `VNode: ${span.segmentId}`,
|
||||||
|
serviceCode: "VirtualNode",
|
||||||
|
type: `[Broken] ${span.type}`,
|
||||||
|
peer: "",
|
||||||
|
component: `VirtualNode: #${span.spanId - 1}`,
|
||||||
|
isError: true,
|
||||||
|
isBroken: true,
|
||||||
|
layer: "Broken",
|
||||||
|
tags: [],
|
||||||
|
logs: [],
|
||||||
|
startTime: 0,
|
||||||
|
endTime: 0,
|
||||||
|
} as Span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const span of segmentHeaders) {
|
||||||
|
if (span.refs && span.refs.length) {
|
||||||
|
let exit: Span | null = null;
|
||||||
|
for (const ref of span.refs) {
|
||||||
|
const e = data.find(
|
||||||
|
(i: Span) =>
|
||||||
|
ref.traceId === i.traceId && ref.parentSegmentId === i.segmentId && ref.parentSpanId === i.spanId,
|
||||||
|
);
|
||||||
|
if (e) {
|
||||||
|
exit = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!exit) {
|
||||||
|
const ref = span.refs[0];
|
||||||
|
const parentSpanId = ref.parentSpanId > -1 ? 0 : -1;
|
||||||
|
const content = fixSpans.find(
|
||||||
|
(i: Span) =>
|
||||||
|
i.traceId === ref.traceId &&
|
||||||
|
i.segmentId === ref.parentSegmentId &&
|
||||||
|
i.spanId === ref.parentSpanId &&
|
||||||
|
i.parentSpanId === parentSpanId,
|
||||||
|
);
|
||||||
|
if (!content) {
|
||||||
|
fixSpans.push({
|
||||||
|
traceId: ref.traceId,
|
||||||
|
segmentId: ref.parentSegmentId,
|
||||||
|
spanId: ref.parentSpanId,
|
||||||
|
parentSpanId,
|
||||||
|
refs: [],
|
||||||
|
endpointName: `VNode: ${ref.parentSegmentId}`,
|
||||||
|
serviceCode: "VirtualNode",
|
||||||
|
type: `[Broken] ${ref.type}`,
|
||||||
|
peer: "",
|
||||||
|
component: `VirtualNode: #${ref.parentSpanId}`,
|
||||||
|
isError: true,
|
||||||
|
isBroken: true,
|
||||||
|
layer: "Broken",
|
||||||
|
tags: [],
|
||||||
|
logs: [],
|
||||||
|
startTime: 0,
|
||||||
|
endTime: 0,
|
||||||
|
} as Span);
|
||||||
|
}
|
||||||
|
if (parentSpanId > -1) {
|
||||||
|
const exists = fixSpans.find(
|
||||||
|
(i: Span) =>
|
||||||
|
i.traceId === ref.traceId &&
|
||||||
|
i.segmentId === ref.parentSegmentId &&
|
||||||
|
i.spanId === 0 &&
|
||||||
|
i.parentSpanId === -1,
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
fixSpans.push({
|
||||||
|
traceId: ref.traceId,
|
||||||
|
segmentId: ref.parentSegmentId,
|
||||||
|
spanId: 0,
|
||||||
|
parentSpanId: -1,
|
||||||
|
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,
|
||||||
|
} as Span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const i of [...fixSpans, ...data]) {
|
||||||
|
i.label = i.endpointName || "no operation name";
|
||||||
|
i.key = Math.random().toString(36).substring(2, 36);
|
||||||
|
i.children = [];
|
||||||
|
if (segmentGroup[i.segmentId]) {
|
||||||
|
segmentGroup[i.segmentId].push(i);
|
||||||
|
} else {
|
||||||
|
segmentIdGroup.push(i.segmentId);
|
||||||
|
segmentGroup[i.segmentId] = [i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of segmentIdGroup) {
|
||||||
|
const currentSegment = segmentGroup[id].sort((a: Span, b: Span) => b.parentSpanId - a.parentSpanId);
|
||||||
|
for (const s of currentSegment) {
|
||||||
|
const index = currentSegment.findIndex((i: Span) => i.spanId === s.parentSpanId);
|
||||||
|
if (index > -1) {
|
||||||
|
if (
|
||||||
|
(currentSegment[index].isBroken && currentSegment[index].parentSpanId === -1) ||
|
||||||
|
!currentSegment[index].isBroken
|
||||||
|
) {
|
||||||
|
currentSegment[index].children?.push(s);
|
||||||
|
currentSegment[index].children?.sort((a: Span, b: Span) => a.spanId - b.spanId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (s.isBroken) {
|
||||||
|
const children = data.filter(
|
||||||
|
(span: Span) =>
|
||||||
|
!!span.refs?.find(
|
||||||
|
(d) => d.traceId === s.traceId && d.parentSegmentId === s.segmentId && d.parentSpanId === s.spanId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (children.length) {
|
||||||
|
s.children?.push(...children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segmentGroup[id] = currentSegment[currentSegment.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of segmentIdGroup) {
|
||||||
|
for (const ref of segmentGroup[id].refs || []) {
|
||||||
|
if (ref.traceId === traceId) {
|
||||||
|
traverseTree(segmentGroup[ref.parentSegmentId], ref.parentSpanId, ref.parentSegmentId, segmentGroup[id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const roots: Span[] = [];
|
||||||
|
for (const i in segmentGroup) {
|
||||||
|
let pushed = false;
|
||||||
|
for (const ref of segmentGroup[i].refs || []) {
|
||||||
|
if (!segmentGroup[ref.parentSegmentId]) {
|
||||||
|
roots.push(segmentGroup[i]);
|
||||||
|
pushed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!pushed &&
|
||||||
|
(!segmentGroup[i].refs || segmentGroup[i].refs.length === 0) &&
|
||||||
|
segmentGroup[i].parentSpanId === -1
|
||||||
|
) {
|
||||||
|
roots.push(segmentGroup[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { roots, fixSpansSize: fixSpans.length, refSpans };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collapseTree(d: Span, refSpans: Ref[]): void {
|
||||||
|
if (d.children) {
|
||||||
|
const item = refSpans.find((s: Ref) => s.parentSpanId === d.spanId && s.parentSegmentId === d.segmentId);
|
||||||
|
let dur = d.endTime - d.startTime;
|
||||||
|
for (const i of d.children) {
|
||||||
|
dur -= i.endTime - i.startTime;
|
||||||
|
}
|
||||||
|
d.dur = dur < 0 ? 0 : dur;
|
||||||
|
if (item) {
|
||||||
|
d.children = d.children?.sort(compare("startTime"));
|
||||||
|
}
|
||||||
|
for (const i of d.children) {
|
||||||
|
collapseTree(i, refSpans);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function traverseTree(node: Span, spanId: number, segmentId: string, data: Span) {
|
||||||
|
if (!node || node.isBroken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node.spanId === spanId && node.segmentId === segmentId) {
|
||||||
|
node.children?.push(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const nodeItem of (node as Span).children || []) {
|
||||||
|
traverseTree(nodeItem, spanId, segmentId, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(p: string) {
|
||||||
|
return (m: Span, n: Span) => {
|
||||||
|
const a = (m as Indexable)[p];
|
||||||
|
const b = (n as Indexable)[p];
|
||||||
|
return a - b;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRefsAllNodes(tree: Indexable) {
|
||||||
|
const nodes = [];
|
||||||
|
const stack = [tree];
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const node = stack.pop();
|
||||||
|
nodes.push(node);
|
||||||
|
|
||||||
|
if (node?.children && node.children.length > 0) {
|
||||||
|
for (let i = node.children.length - 1; i >= 0; i--) {
|
||||||
|
stack.push(node.children[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
@@ -16,11 +16,11 @@ limitations under the License. -->
|
|||||||
<template>
|
<template>
|
||||||
<div class="trace-table">
|
<div class="trace-table">
|
||||||
<div class="trace-table-header" v-if="type === TraceGraphType.STATISTICS">
|
<div class="trace-table-header" v-if="type === TraceGraphType.STATISTICS">
|
||||||
<div :class="item.label" v-for="(item, index) in headerData" :key="index">
|
<div :class="item.label" v-for="(item, index) in headerData as typeof StatisticsConstant" :key="index">
|
||||||
{{ item.value }}
|
{{ item.value }}
|
||||||
<span
|
<span
|
||||||
class="r cp"
|
class="r cp"
|
||||||
@click="sortStatistics(item.key)"
|
@click="sortStatistics(item.key || '')"
|
||||||
:key="componentKey"
|
:key="componentKey"
|
||||||
v-if="item.key !== 'endpointName' && item.key !== 'type'"
|
v-if="item.key !== 'endpointName' && item.key !== 'type'"
|
||||||
>
|
>
|
||||||
@@ -47,34 +47,41 @@ limitations under the License. -->
|
|||||||
:key="`key${index}`"
|
:key="`key${index}`"
|
||||||
:type="type"
|
:type="type"
|
||||||
:headerType="headerType"
|
:headerType="headerType"
|
||||||
@click="selectItem"
|
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||||
|
:selectedMinTimestamp="selectedMinTimestamp"
|
||||||
|
@selectedSpan="selectItem"
|
||||||
/>
|
/>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import type { PropType } from "vue";
|
import type { Span } from "@/types/trace";
|
||||||
import { useTraceStore } from "@/store/modules/trace";
|
|
||||||
import TableItem from "./TableItem.vue";
|
import TableItem from "./TableItem.vue";
|
||||||
import { ProfileConstant, TraceConstant, StatisticsConstant } from "./data";
|
import { ProfileConstant, TraceConstant, StatisticsConstant } from "./data";
|
||||||
import { TraceGraphType } from "../constant";
|
import { TraceGraphType } from "../VisGraph/constant";
|
||||||
import { WidgetType } from "@/views/dashboard/data";
|
import { WidgetType } from "@/views/dashboard/data";
|
||||||
|
|
||||||
/* global defineProps, Nullable, defineEmits, Recordable*/
|
/* global defineProps, Nullable, defineEmits*/
|
||||||
const props = defineProps({
|
type Props = {
|
||||||
tableData: { type: Array as PropType<Recordable>, default: () => [] },
|
tableData: Span[];
|
||||||
type: { type: String, default: "" },
|
type?: string;
|
||||||
headerType: { type: String, default: "" },
|
headerType?: string;
|
||||||
traceId: { type: String, default: "" },
|
traceId: string;
|
||||||
});
|
selectedMaxTimestamp?: number;
|
||||||
const emits = defineEmits(["select"]);
|
selectedMinTimestamp?: number;
|
||||||
const traceStore = useTraceStore();
|
};
|
||||||
|
type Emits = {
|
||||||
|
(e: "select", value: Span): void;
|
||||||
|
};
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emits = defineEmits<Emits>();
|
||||||
|
|
||||||
const method = ref<number>(300);
|
const method = ref<number>(300);
|
||||||
const componentKey = ref<number>(300);
|
const componentKey = ref<number>(300);
|
||||||
const flag = ref<boolean>(true);
|
const flag = ref<boolean>(true);
|
||||||
const dragger = ref<Nullable<HTMLSpanElement>>(null);
|
const dragger = ref<Nullable<HTMLSpanElement>>(null);
|
||||||
let headerData: Recordable[] = TraceConstant;
|
let headerData: typeof TraceConstant | typeof ProfileConstant | typeof StatisticsConstant = TraceConstant;
|
||||||
|
|
||||||
if (props.headerType === WidgetType.Profile) {
|
if (props.headerType === WidgetType.Profile) {
|
||||||
headerData = ProfileConstant;
|
headerData = ProfileConstant;
|
||||||
@@ -104,31 +111,15 @@ limitations under the License. -->
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
function selectItem(event: MouseEvent) {
|
function selectItem(span: Span) {
|
||||||
emits("select", traceStore.selectedSpan);
|
emits("select", span);
|
||||||
if (props.headerType === WidgetType.Profile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (props.type === TraceGraphType.STATISTICS) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const item: any = document.querySelector("#trace-action-box");
|
|
||||||
const tableBox = document.querySelector(".trace-table-charts")?.getBoundingClientRect();
|
|
||||||
if (!tableBox) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const offsetX = event.x - tableBox.x;
|
|
||||||
const offsetY = event.y - tableBox.y;
|
|
||||||
item.style.display = "block";
|
|
||||||
item.style.top = `${offsetY + 20}px`;
|
|
||||||
item.style.left = `${offsetX + 10}px`;
|
|
||||||
}
|
}
|
||||||
function sortStatistics(key: string) {
|
function sortStatistics(key: string) {
|
||||||
const element = props.tableData;
|
const element = props.tableData;
|
||||||
for (let i = 0; i < element.length; i++) {
|
for (let i = 0; i < element.length; i++) {
|
||||||
for (let j = 0; j < element.length - i - 1; j++) {
|
for (let j = 0; j < element.length - i - 1; j++) {
|
||||||
let val1;
|
let val1: number | undefined;
|
||||||
let val2;
|
let val2: number | undefined;
|
||||||
if (key === "maxTime") {
|
if (key === "maxTime") {
|
||||||
val1 = element[j].maxTime;
|
val1 = element[j].maxTime;
|
||||||
val2 = element[j + 1].maxTime;
|
val2 = element[j + 1].maxTime;
|
||||||
@@ -150,13 +141,13 @@ limitations under the License. -->
|
|||||||
val2 = element[j + 1].count;
|
val2 = element[j + 1].count;
|
||||||
}
|
}
|
||||||
if (flag.value) {
|
if (flag.value) {
|
||||||
if (val1 < val2) {
|
if (val1 && val2 && val1 < val2) {
|
||||||
const tmp = element[j];
|
const tmp = element[j];
|
||||||
element[j] = element[j + 1];
|
element[j] = element[j + 1];
|
||||||
element[j + 1] = tmp;
|
element[j + 1] = tmp;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (val1 > val2) {
|
if (val1 && val2 && val1 > val2) {
|
||||||
const tmp = element[j];
|
const tmp = element[j];
|
||||||
element[j] = element[j + 1];
|
element[j] = element[j + 1];
|
||||||
element[j + 1] = tmp;
|
element[j + 1] = tmp;
|
||||||
|
@@ -49,15 +49,16 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
@click="selectSpan"
|
|
||||||
:class="[
|
:class="[
|
||||||
'trace-item',
|
'trace-item',
|
||||||
'level' + ((data.level || 0) - 1),
|
'level' + ((data.level || 0) - 1),
|
||||||
{ 'trace-item-error': data.isError },
|
{ 'trace-item-error': data.isError },
|
||||||
{ profiled: data.profiled === false },
|
{ profiled: data.profiled === false },
|
||||||
`trace-item-${data.key}`,
|
`trace-item-${data.key}`,
|
||||||
|
{ highlighted: inTimeRange },
|
||||||
]"
|
]"
|
||||||
:data-text="data.profiled === false ? 'No Thread Dump' : ''"
|
:data-text="data.profiled === false ? 'No Thread Dump' : ''"
|
||||||
|
@click="hideActionBox"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:class="['method', 'level' + ((data.level || 0) - 1)]"
|
:class="['method', 'level' + ((data.level || 0) - 1)]"
|
||||||
@@ -65,6 +66,8 @@ limitations under the License. -->
|
|||||||
'text-indent': ((data.level || 0) - 1) * 10 + 'px',
|
'text-indent': ((data.level || 0) - 1) * 10 + 'px',
|
||||||
width: `${method}px`,
|
width: `${method}px`,
|
||||||
}"
|
}"
|
||||||
|
@click="selectSpan"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
:style="!displayChildren ? 'transform: rotate(-90deg);' : ''"
|
:style="!displayChildren ? 'transform: rotate(-90deg);' : ''"
|
||||||
@@ -73,6 +76,7 @@ limitations under the License. -->
|
|||||||
iconName="arrow-down"
|
iconName="arrow-down"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="mr-5"
|
class="mr-5"
|
||||||
|
@click="hideActionBox"
|
||||||
/>
|
/>
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
:content="data.type === 'Entry' ? 'Entry' : 'Exit'"
|
:content="data.type === 'Entry' ? 'Entry' : 'Exit'"
|
||||||
@@ -90,7 +94,7 @@ limitations under the License. -->
|
|||||||
</span>
|
</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip :content="data.endpointName" placement="top" :show-after="300">
|
<el-tooltip :content="data.endpointName" placement="top" :show-after="300">
|
||||||
<span>
|
<span class="link-span">
|
||||||
{{ data.endpointName }}
|
{{ data.endpointName }}
|
||||||
</span>
|
</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@@ -116,7 +120,7 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
<div class="application">
|
<div class="application">
|
||||||
<el-tooltip :show-after="300" :content="data.serviceCode || '-'" placement="top">
|
<el-tooltip :show-after="300" :content="data.serviceCode || '-'" placement="top">
|
||||||
<span>{{ data.serviceCode }}</span>
|
<span :style="{ color: getServiceColor(data.serviceCode || '') }">{{ data.serviceCode }}</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="application" v-show="headerType === WidgetType.Profile" @click="viewSpan($event)">
|
<div class="application" v-show="headerType === WidgetType.Profile" @click="viewSpan($event)">
|
||||||
@@ -128,12 +132,15 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
<div v-show="data.children && data.children.length > 0 && displayChildren" class="children-trace">
|
<div v-show="data.children && data.children.length > 0 && displayChildren" class="children-trace">
|
||||||
<table-item
|
<table-item
|
||||||
:method="method"
|
|
||||||
v-for="(child, index) in data.children"
|
v-for="(child, index) in data.children"
|
||||||
|
:method="method"
|
||||||
:key="index"
|
:key="index"
|
||||||
:data="child"
|
:data="child"
|
||||||
:type="type"
|
:type="type"
|
||||||
:headerType="headerType"
|
:headerType="headerType"
|
||||||
|
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||||
|
:selectedMinTimestamp="selectedMinTimestamp"
|
||||||
|
@selectedSpan="selectItem"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<el-dialog v-model="showDetail" :destroy-on-close="true" fullscreen @closed="showDetail = false">
|
<el-dialog v-model="showDetail" :destroy-on-close="true" fullscreen @closed="showDetail = false">
|
||||||
@@ -141,136 +148,150 @@ limitations under the License. -->
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { ref, computed, defineComponent, watch } from "vue";
|
import { ref, computed, watch } from "vue";
|
||||||
import type { PropType } from "vue";
|
|
||||||
import SpanDetail from "../D3Graph/SpanDetail.vue";
|
import SpanDetail from "../D3Graph/SpanDetail.vue";
|
||||||
import { dateFormat } from "@/utils/dateFormat";
|
import { dateFormat } from "@/utils/dateFormat";
|
||||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||||
import { useTraceStore } from "@/store/modules/trace";
|
import { useTraceStore } from "@/store/modules/trace";
|
||||||
import { Themes } from "@/constants/data";
|
import { TraceGraphType } from "../VisGraph/constant";
|
||||||
import { TraceGraphType } from "../constant";
|
|
||||||
import { WidgetType } from "@/views/dashboard/data";
|
import { WidgetType } from "@/views/dashboard/data";
|
||||||
import type { Span, Ref } from "@/types/trace";
|
import type { Span, Ref } from "@/types/trace";
|
||||||
|
import { getServiceColor } from "@/utils/color";
|
||||||
|
|
||||||
/*global Recordable*/
|
interface Props {
|
||||||
const props = {
|
data: Span;
|
||||||
data: { type: Object as PropType<Span>, default: () => ({}) },
|
method: number;
|
||||||
method: { type: Number, default: 0 },
|
type?: string;
|
||||||
type: { type: String, default: "" },
|
headerType?: string;
|
||||||
headerType: { type: String, default: "" },
|
traceId?: string;
|
||||||
traceId: { type: String, traceId: "" },
|
selectedMaxTimestamp?: number;
|
||||||
};
|
selectedMinTimestamp?: number;
|
||||||
export default defineComponent({
|
}
|
||||||
name: "TableItem",
|
interface Emits {
|
||||||
props,
|
(e: "selectedSpan", value: Span): void;
|
||||||
components: { SpanDetail },
|
}
|
||||||
setup(props) {
|
const emits = defineEmits<Emits>();
|
||||||
const appStore = useAppStoreWithOut();
|
const props = defineProps<Props>();
|
||||||
const traceStore = useTraceStore();
|
const appStore = useAppStoreWithOut();
|
||||||
const displayChildren = ref<boolean>(true);
|
const traceStore = useTraceStore();
|
||||||
const showDetail = ref<boolean>(false);
|
const displayChildren = ref<boolean>(true);
|
||||||
const { t } = useI18n();
|
const showDetail = ref<boolean>(false);
|
||||||
const selfTime = computed(() => (props.data.dur ? props.data.dur : 0));
|
const { t } = useI18n();
|
||||||
const execTime = computed(() =>
|
const selfTime = computed(() => (props.data.dur ? props.data.dur : 0));
|
||||||
props.data.endTime - props.data.startTime ? props.data.endTime - props.data.startTime : 0,
|
const execTime = computed(() =>
|
||||||
);
|
props.data.endTime - props.data.startTime > 0 ? props.data.endTime - props.data.startTime : 0,
|
||||||
const outterPercent = computed(() => {
|
);
|
||||||
if (props.data.level === 1) {
|
const outterPercent = computed(() => {
|
||||||
return "100%";
|
if (props.data.level === 1) {
|
||||||
} else {
|
return "100%";
|
||||||
const data = props.data;
|
} else {
|
||||||
const exec = data.endTime - data.startTime ? data.endTime - data.startTime : 0;
|
const { data } = props;
|
||||||
let result = (exec / (data.totalExec || 0)) * 100;
|
let result = (execTime.value / (data.totalExec || 0)) * 100;
|
||||||
result = result > 100 ? 100 : result;
|
result = result > 100 ? 100 : result;
|
||||||
const resultStr = result.toFixed(4) + "%";
|
const resultStr = result.toFixed(4) + "%";
|
||||||
return resultStr === "0.0000%" ? "0.9%" : resultStr;
|
return resultStr === "0.0000%" ? "0.9%" : resultStr;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
const innerPercent = computed(() => {
|
|
||||||
const result = (selfTime.value / execTime.value) * 100;
|
|
||||||
const resultStr = result.toFixed(4) + "%";
|
|
||||||
return resultStr === "0.0000%" ? "0.9%" : resultStr;
|
|
||||||
});
|
|
||||||
const isCrossThread = computed(() => {
|
|
||||||
const key = props.data.refs?.findIndex((d: Ref) => d.type === "CROSS_THREAD") ?? -1;
|
|
||||||
return key > -1 ? true : false;
|
|
||||||
});
|
|
||||||
function toggle() {
|
|
||||||
displayChildren.value = !displayChildren.value;
|
|
||||||
}
|
|
||||||
function showSelectSpan(dom: HTMLSpanElement) {
|
|
||||||
if (!dom) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const items: any = document.querySelectorAll(".trace-item");
|
|
||||||
for (const item of items) {
|
|
||||||
item.style.background = appStore.theme === Themes.Dark ? "#212224" : "#fff";
|
|
||||||
}
|
|
||||||
dom.style.background = appStore.theme === Themes.Dark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)";
|
|
||||||
const p: any = document.getElementsByClassName("profiled")[0];
|
|
||||||
if (p) {
|
|
||||||
p.style.background = appStore.theme === Themes.Dark ? "#333" : "#eee";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function selectSpan(event: Recordable) {
|
|
||||||
const dom = event.composedPath().find((d: Recordable) => d.className.includes("trace-item"));
|
|
||||||
selectedItem(props.data);
|
|
||||||
if (props.headerType === WidgetType.Profile) {
|
|
||||||
showSelectSpan(dom);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
viewSpanDetail(dom);
|
|
||||||
}
|
|
||||||
function viewSpan(event: Recordable) {
|
|
||||||
showDetail.value = true;
|
|
||||||
const dom = event.composedPath().find((d: Recordable) => d.className.includes("trace-item"));
|
|
||||||
selectedItem(props.data);
|
|
||||||
viewSpanDetail(dom);
|
|
||||||
}
|
|
||||||
function selectedItem(span: Span) {
|
|
||||||
traceStore.setSelectedSpan(span);
|
|
||||||
}
|
|
||||||
function viewSpanDetail(dom: HTMLSpanElement) {
|
|
||||||
showSelectSpan(dom);
|
|
||||||
if (props.type === TraceGraphType.STATISTICS) {
|
|
||||||
showDetail.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
watch(
|
|
||||||
() => appStore.theme,
|
|
||||||
() => {
|
|
||||||
const items: any = document.querySelectorAll(".trace-item");
|
|
||||||
for (const item of items) {
|
|
||||||
item.style.background = appStore.theme === Themes.Dark ? "#212224" : "#fff";
|
|
||||||
}
|
|
||||||
const p: any = document.getElementsByClassName("profiled")[0];
|
|
||||||
if (p) {
|
|
||||||
p.style.background = appStore.theme === Themes.Dark ? "#333" : "#eee";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
displayChildren,
|
|
||||||
outterPercent,
|
|
||||||
innerPercent,
|
|
||||||
isCrossThread,
|
|
||||||
viewSpanDetail,
|
|
||||||
toggle,
|
|
||||||
dateFormat,
|
|
||||||
showSelectSpan,
|
|
||||||
showDetail,
|
|
||||||
selectSpan,
|
|
||||||
selectedItem,
|
|
||||||
viewSpan,
|
|
||||||
t,
|
|
||||||
appStore,
|
|
||||||
TraceGraphType,
|
|
||||||
WidgetType,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
const innerPercent = computed(() => {
|
||||||
|
const result = (selfTime.value / execTime.value) * 100;
|
||||||
|
const resultStr = result.toFixed(4) + "%";
|
||||||
|
return resultStr === "0.0000%" ? "0.9%" : resultStr;
|
||||||
|
});
|
||||||
|
const isCrossThread = computed(() => {
|
||||||
|
const key = props.data.refs?.findIndex((d: Ref) => d.type === "CROSS_THREAD") ?? -1;
|
||||||
|
return key > -1 ? true : false;
|
||||||
|
});
|
||||||
|
const inTimeRange = computed(() => {
|
||||||
|
if (props.selectedMinTimestamp === undefined || props.selectedMaxTimestamp === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.data.startTime <= props.selectedMaxTimestamp && props.data.endTime >= props.selectedMinTimestamp;
|
||||||
|
});
|
||||||
|
function toggle() {
|
||||||
|
displayChildren.value = !displayChildren.value;
|
||||||
|
}
|
||||||
|
function selectItem(span: Span) {
|
||||||
|
emits("selectedSpan", span);
|
||||||
|
}
|
||||||
|
function showSelectSpan(dom: HTMLSpanElement) {
|
||||||
|
if (!dom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const items: HTMLSpanElement[] = Array.from(document.querySelectorAll(".trace-item")) as HTMLSpanElement[];
|
||||||
|
for (const item of items) {
|
||||||
|
item.style.background = "transparent";
|
||||||
|
}
|
||||||
|
dom.style.background = "var(--sw-trace-table-selected)";
|
||||||
|
const p = document.getElementsByClassName("profiled")[0] as HTMLSpanElement | null;
|
||||||
|
if (p) {
|
||||||
|
p.style.background = "var(--border-color-primary)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function selectSpan(event: MouseEvent) {
|
||||||
|
emits("selectedSpan", props.data);
|
||||||
|
const dom = event
|
||||||
|
.composedPath()
|
||||||
|
.find((d: EventTarget) => (d as HTMLElement).className.includes("trace-item")) as HTMLSpanElement;
|
||||||
|
selectedItem(props.data);
|
||||||
|
if (props.headerType === WidgetType.Profile) {
|
||||||
|
showSelectSpan(dom);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
viewSpanDetail(dom);
|
||||||
|
if (props.type === TraceGraphType.STATISTICS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const item: HTMLSpanElement | null = document.querySelector("#trace-action-box");
|
||||||
|
const tableBox = document.querySelector(".trace-table-charts")?.getBoundingClientRect();
|
||||||
|
if (!tableBox || !item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const offsetX = event.x - tableBox.x;
|
||||||
|
const offsetY = event.y - tableBox.y;
|
||||||
|
item.style.display = "block";
|
||||||
|
item.style.top = `${offsetY + 20}px`;
|
||||||
|
item.style.left = `${offsetX + 10}px`;
|
||||||
|
}
|
||||||
|
function viewSpan(event: MouseEvent) {
|
||||||
|
showDetail.value = true;
|
||||||
|
const dom = event
|
||||||
|
.composedPath()
|
||||||
|
.find((d: EventTarget) => (d as HTMLElement).className.includes("trace-item")) as HTMLSpanElement;
|
||||||
|
selectedItem(props.data);
|
||||||
|
viewSpanDetail(dom);
|
||||||
|
}
|
||||||
|
function selectedItem(span: Span) {
|
||||||
|
traceStore.setSelectedSpan(span);
|
||||||
|
emits("selectedSpan", span);
|
||||||
|
}
|
||||||
|
function viewSpanDetail(dom: HTMLSpanElement) {
|
||||||
|
showSelectSpan(dom);
|
||||||
|
if (props.type === TraceGraphType.STATISTICS) {
|
||||||
|
showDetail.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function hideActionBox() {
|
||||||
|
const item: HTMLSpanElement | null = document.querySelector("#trace-action-box");
|
||||||
|
if (item) {
|
||||||
|
item.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => appStore.theme,
|
||||||
|
() => {
|
||||||
|
const items: HTMLSpanElement[] = Array.from(document.querySelectorAll(".trace-item")) as HTMLSpanElement[];
|
||||||
|
for (const item of items) {
|
||||||
|
item.style.background = "transparent";
|
||||||
|
}
|
||||||
|
const p = document.getElementsByClassName("profiled")[0] as HTMLSpanElement | null;
|
||||||
|
if (p) {
|
||||||
|
p.style.background = "var(--border-color-primary)";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import url("./table.scss");
|
@import url("./table.scss");
|
||||||
@@ -290,6 +311,10 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlighted {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.profiled {
|
.profiled {
|
||||||
background-color: var(--sw-table-header);
|
background-color: var(--sw-table-header);
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -334,7 +359,6 @@ limitations under the License. -->
|
|||||||
.trace-item {
|
.trace-item {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-item.selected {
|
.trace-item.selected {
|
||||||
@@ -358,6 +382,7 @@ limitations under the License. -->
|
|||||||
|
|
||||||
.trace-item > div.method {
|
.trace-item > div.method {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-item div.exec-percent {
|
.trace-item div.exec-percent {
|
||||||
@@ -385,4 +410,8 @@ limitations under the License. -->
|
|||||||
top: 1px;
|
top: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link-span {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -48,7 +48,7 @@ export const ProfileConstant = [
|
|||||||
label: "application",
|
label: "application",
|
||||||
value: "Operation",
|
value: "Operation",
|
||||||
},
|
},
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
export const TraceConstant = [
|
export const TraceConstant = [
|
||||||
{
|
{
|
||||||
@@ -83,7 +83,7 @@ export const TraceConstant = [
|
|||||||
label: "application",
|
label: "application",
|
||||||
value: "Attached Events",
|
value: "Attached Events",
|
||||||
},
|
},
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
export const StatisticsConstant = [
|
export const StatisticsConstant = [
|
||||||
{
|
{
|
||||||
@@ -121,4 +121,4 @@ export const StatisticsConstant = [
|
|||||||
value: "Hits",
|
value: "Hits",
|
||||||
key: "count",
|
key: "count",
|
||||||
},
|
},
|
||||||
];
|
] as const;
|
||||||
|
@@ -13,52 +13,56 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License. -->
|
limitations under the License. -->
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-h row">
|
<div class="flex-h row" style="justify-content: space-between">
|
||||||
<div class="mr-10 flex-h" v-if="dashboardStore.entity === EntityType[1].value">
|
<div class="flex-h">
|
||||||
<span class="grey mr-5 label">{{ t("service") }}:</span>
|
<div class="mr-10 flex-h" v-if="dashboardStore.entity === EntityType[1].value">
|
||||||
<Selector
|
<span class="grey mr-5 label">{{ t("service") }}:</span>
|
||||||
size="small"
|
<Selector
|
||||||
:value="state.service.value"
|
size="small"
|
||||||
:options="traceStore.services"
|
:value="state.service.value"
|
||||||
placeholder="Select a service"
|
:options="traceStore.services"
|
||||||
@change="changeField('service', $event)"
|
placeholder="Select a service"
|
||||||
/>
|
@change="changeField('service', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mr-10 flex-h" v-if="dashboardStore.entity !== EntityType[3].value">
|
||||||
|
<span class="grey mr-5 label">{{ t("instance") }}:</span>
|
||||||
|
<Selector
|
||||||
|
size="small"
|
||||||
|
:value="state.instance.value"
|
||||||
|
:options="traceStore.instances"
|
||||||
|
placeholder="Select a instance"
|
||||||
|
@change="changeField('instance', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mr-10 flex-h" v-if="dashboardStore.entity !== EntityType[2].value">
|
||||||
|
<span class="grey mr-5 label">{{ t("endpoint") }}:</span>
|
||||||
|
<Selector
|
||||||
|
size="small"
|
||||||
|
:value="state.endpoint.value"
|
||||||
|
:options="traceStore.endpoints"
|
||||||
|
placeholder="Select a endpoint"
|
||||||
|
:isRemote="true"
|
||||||
|
@change="changeField('endpoint', $event)"
|
||||||
|
@query="searchEndpoints"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mr-10 flex-h">
|
||||||
|
<span class="grey mr-5 label">{{ t("status") }}:</span>
|
||||||
|
<Selector
|
||||||
|
size="small"
|
||||||
|
:value="state.status.value"
|
||||||
|
:options="Status"
|
||||||
|
placeholder="Select a status"
|
||||||
|
@change="changeField('status', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-10 flex-h" v-if="dashboardStore.entity !== EntityType[3].value">
|
<div class="mr-10">
|
||||||
<span class="grey mr-5 label">{{ t("instance") }}:</span>
|
<el-button type="primary" @click="searchTraces" :loading="traceStore.loading">
|
||||||
<Selector
|
{{ t("runQuery") }}
|
||||||
size="small"
|
</el-button>
|
||||||
:value="state.instance.value"
|
|
||||||
:options="traceStore.instances"
|
|
||||||
placeholder="Select a instance"
|
|
||||||
@change="changeField('instance', $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-10 flex-h" v-if="dashboardStore.entity !== EntityType[2].value">
|
|
||||||
<span class="grey mr-5 label">{{ t("endpoint") }}:</span>
|
|
||||||
<Selector
|
|
||||||
size="small"
|
|
||||||
:value="state.endpoint.value"
|
|
||||||
:options="traceStore.endpoints"
|
|
||||||
placeholder="Select a endpoint"
|
|
||||||
:isRemote="true"
|
|
||||||
@change="changeField('endpoint', $event)"
|
|
||||||
@query="searchEndpoints"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mr-10 flex-h">
|
|
||||||
<span class="grey mr-5 label">{{ t("status") }}:</span>
|
|
||||||
<Selector
|
|
||||||
size="small"
|
|
||||||
:value="state.status.value"
|
|
||||||
:options="Status"
|
|
||||||
placeholder="Select a status"
|
|
||||||
@change="changeField('status', $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<el-button size="small" type="primary" @click="searchTraces" class="search-btn">
|
|
||||||
{{ t("search") }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-h row">
|
<div class="flex-h row">
|
||||||
<div class="mr-10">
|
<div class="mr-10">
|
||||||
@@ -72,7 +76,7 @@ limitations under the License. -->
|
|||||||
<el-input size="small" class="inputs" v-model="maxTraceDuration" type="number" />
|
<el-input size="small" class="inputs" v-model="maxTraceDuration" type="number" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="sm b grey mr-5">{{ t("timeRange") }}:</span>
|
<span class="sm b grey mr-5">{{ t("timeRange") }}</span>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
:value="[durationRow.start, durationRow.end]"
|
:value="[durationRow.start, durationRow.end]"
|
||||||
:maxRange="maxRange"
|
:maxRange="maxRange"
|
||||||
@@ -91,15 +95,15 @@ limitations under the License. -->
|
|||||||
import type { PropType } from "vue";
|
import type { PropType } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import type { Option, DurationTime, Duration } from "@/types/app";
|
import type { Option, DurationTime, Duration } from "@/types/app";
|
||||||
import { useTraceStore } from "@/store/modules/trace";
|
import { useTraceStore, PageSize } from "@/store/modules/trace";
|
||||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||||
import { useAppStoreWithOut, InitializationDurationRow } from "@/store/modules/app";
|
import { useAppStoreWithOut, InitializationDurationRow } from "@/store/modules/app";
|
||||||
import { useSelectorStore } from "@/store/modules/selectors";
|
import { useSelectorStore } from "@/store/modules/selectors";
|
||||||
import timeFormat from "@/utils/timeFormat";
|
import timeFormat from "@/utils/timeFormat";
|
||||||
import ConditionTags from "@/views/components/ConditionTags.vue";
|
import ConditionTags from "@/views/components/ConditionTags.vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { EntityType, QueryOrders, Status } from "../../data";
|
import { EntityType, QueryOrders, Status } from "@/views/dashboard/data";
|
||||||
import type { LayoutConfig } from "@/types/dashboard";
|
import type { LayoutConfig, FilterDuration } from "@/types/dashboard";
|
||||||
import { useDuration } from "@/hooks/useDuration";
|
import { useDuration } from "@/hooks/useDuration";
|
||||||
|
|
||||||
/*global defineProps, defineEmits, Recordable */
|
/*global defineProps, defineEmits, Recordable */
|
||||||
@@ -108,7 +112,7 @@ limitations under the License. -->
|
|||||||
needQuery: { type: Boolean, default: true },
|
needQuery: { type: Boolean, default: true },
|
||||||
data: {
|
data: {
|
||||||
type: Object as PropType<LayoutConfig>,
|
type: Object as PropType<LayoutConfig>,
|
||||||
default: () => ({ graph: {} }),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -117,20 +121,19 @@ limitations under the License. -->
|
|||||||
const dashboardStore = useDashboardStore();
|
const dashboardStore = useDashboardStore();
|
||||||
const traceStore: ReturnType<typeof useTraceStore> = useTraceStore();
|
const traceStore: ReturnType<typeof useTraceStore> = useTraceStore();
|
||||||
const { setDurationRow, getDurationTime, getMaxRange } = useDuration();
|
const { setDurationRow, getDurationTime, getMaxRange } = useDuration();
|
||||||
const filters = reactive<Recordable>(props.data.filters || {});
|
const filters = computed(() => props.data.filters || {});
|
||||||
const traceId = ref<string>(filters.traceId || "");
|
const traceId = ref<string>(filters.value.traceId || "");
|
||||||
const { duration: filtersDuration } = filters;
|
const { duration: filtersDuration } = filters.value;
|
||||||
const duration = ref<DurationTime>(
|
const duration = ref<DurationTime | FilterDuration>(
|
||||||
filtersDuration
|
filtersDuration
|
||||||
? { start: filtersDuration.startTime || "", end: filtersDuration.endTime || "", step: filtersDuration.step || "" }
|
? { start: filtersDuration.startTime || "", end: filtersDuration.endTime || "", step: filtersDuration.step || "" }
|
||||||
: getDurationTime(),
|
: getDurationTime(),
|
||||||
);
|
);
|
||||||
const minTraceDuration = ref<number>();
|
const minTraceDuration = ref<number>();
|
||||||
const maxTraceDuration = ref<number>();
|
const maxTraceDuration = ref<number>();
|
||||||
const tagsList = ref<string[]>([]);
|
|
||||||
const tagsMap = ref<Option[]>([]);
|
const tagsMap = ref<Option[]>([]);
|
||||||
const state = reactive<Recordable>({
|
const state = reactive<Recordable>({
|
||||||
status: filters.status === "ERROR" ? Status[2] : Status[0],
|
status: filters.value.status === "ERROR" ? Status[2] : Status[0],
|
||||||
instance: { value: "0", label: "All" },
|
instance: { value: "0", label: "All" },
|
||||||
endpoint: { value: "0", label: "All" },
|
endpoint: { value: "0", label: "All" },
|
||||||
service: { value: "", label: "" },
|
service: { value: "", label: "" },
|
||||||
@@ -139,9 +142,9 @@ limitations under the License. -->
|
|||||||
const maxRange = computed(() =>
|
const maxRange = computed(() =>
|
||||||
getMaxRange(appStore.coldStageMode ? appStore.recordsTTL?.coldTrace || 0 : appStore.recordsTTL?.trace || 0),
|
getMaxRange(appStore.coldStageMode ? appStore.recordsTTL?.coldTrace || 0 : appStore.recordsTTL?.trace || 0),
|
||||||
);
|
);
|
||||||
if (filters.queryOrder) {
|
if (filters.value.queryOrder) {
|
||||||
traceStore.setTraceCondition({
|
traceStore.setTraceCondition({
|
||||||
queryOrder: filters.queryOrder,
|
queryOrder: filters.value.queryOrder,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (props.needQuery) {
|
if (props.needQuery) {
|
||||||
@@ -149,7 +152,7 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
duration.value = filters.duration || appStore.durationTime;
|
duration.value = filters.value.duration || appStore.durationTime;
|
||||||
if (dashboardStore.entity === EntityType[1].value) {
|
if (dashboardStore.entity === EntityType[1].value) {
|
||||||
await getServices();
|
await getServices();
|
||||||
}
|
}
|
||||||
@@ -221,8 +224,10 @@ limitations under the License. -->
|
|||||||
minTraceDuration: Number(minTraceDuration.value),
|
minTraceDuration: Number(minTraceDuration.value),
|
||||||
maxTraceDuration: Number(maxTraceDuration.value),
|
maxTraceDuration: Number(maxTraceDuration.value),
|
||||||
traceId: traceId.value || undefined,
|
traceId: traceId.value || undefined,
|
||||||
paging: { pageNum: 1, pageSize: 20 },
|
|
||||||
};
|
};
|
||||||
|
if (!traceStore.hasQueryTracesV2Support) {
|
||||||
|
param.paging = { pageNum: 1, pageSize: PageSize };
|
||||||
|
}
|
||||||
if (props.data.filters && props.data.filters.id) {
|
if (props.data.filters && props.data.filters.id) {
|
||||||
param = {
|
param = {
|
||||||
...param,
|
...param,
|
||||||
@@ -267,8 +272,7 @@ limitations under the License. -->
|
|||||||
emits("get", state.service.id);
|
emits("get", state.service.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function updateTags(data: { tagsMap: Array<Option>; tagsList: string[] }) {
|
function updateTags(data: { tagsMap: Array<Option> }) {
|
||||||
tagsList.value = data.tagsList;
|
|
||||||
tagsMap.value = data.tagsMap;
|
tagsMap.value = data.tagsMap;
|
||||||
}
|
}
|
||||||
async function searchEndpoints(keyword: string) {
|
async function searchEndpoints(keyword: string) {
|
||||||
@@ -345,21 +349,12 @@ limitations under the License. -->
|
|||||||
|
|
||||||
.row {
|
.row {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.traceId {
|
.traceId {
|
||||||
width: 270px;
|
width: 270px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-btn {
|
|
||||||
cursor: pointer;
|
|
||||||
width: 80px;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
@@ -91,7 +91,7 @@ limitations under the License. -->
|
|||||||
import timeFormat from "@/utils/timeFormat";
|
import timeFormat from "@/utils/timeFormat";
|
||||||
import ConditionTags from "@/views/components/ConditionTags.vue";
|
import ConditionTags from "@/views/components/ConditionTags.vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { EntityType, QueryOrders, Status } from "../../data";
|
import { EntityType, QueryOrders, Status } from "@/views/dashboard/data";
|
||||||
import type { LayoutConfig } from "@/types/dashboard";
|
import type { LayoutConfig } from "@/types/dashboard";
|
||||||
|
|
||||||
const FiltersKeys: { [key: string]: string } = {
|
const FiltersKeys: { [key: string]: string } = {
|
@@ -83,9 +83,9 @@ limitations under the License. -->
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useTraceStore } from "@/store/modules/trace";
|
import { useTraceStore, PageSize } from "@/store/modules/trace";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { QueryOrders } from "../../data";
|
import { QueryOrders } from "@/views/dashboard/data";
|
||||||
import type { Option } from "@/types/app";
|
import type { Option } from "@/types/app";
|
||||||
import type { Trace } from "@/types/trace";
|
import type { Trace } from "@/types/trace";
|
||||||
import { dateFormat } from "@/utils/dateFormat";
|
import { dateFormat } from "@/utils/dateFormat";
|
||||||
@@ -94,7 +94,7 @@ limitations under the License. -->
|
|||||||
const traceStore = useTraceStore();
|
const traceStore = useTraceStore();
|
||||||
const loading = ref<boolean>(false);
|
const loading = ref<boolean>(false);
|
||||||
const selectedKey = ref<string>("");
|
const selectedKey = ref<string>("");
|
||||||
const pageSize = 20;
|
const pageSize = PageSize;
|
||||||
const total = computed(() =>
|
const total = computed(() =>
|
||||||
traceStore.traceList.length === pageSize
|
traceStore.traceList.length === pageSize
|
||||||
? pageSize * traceStore.conditions.paging.pageNum + 1
|
? pageSize * traceStore.conditions.paging.pageNum + 1
|
@@ -13,14 +13,16 @@ limitations under the License. -->
|
|||||||
<template>
|
<template>
|
||||||
<div class="trace-detail" v-loading="loading">
|
<div class="trace-detail" v-loading="loading">
|
||||||
<div class="trace-detail-wrapper clear" v-if="traceStore.currentTrace?.endpointNames">
|
<div class="trace-detail-wrapper clear" v-if="traceStore.currentTrace?.endpointNames">
|
||||||
<h5 class="mb-5 mt-0">
|
<div class="flex-h" style="justify-content: space-between">
|
||||||
<span class="vm">{{ traceStore.currentTrace?.endpointNames?.[0] }}</span>
|
<h5 class="mb-5 mt-0">
|
||||||
<div class="trace-log-btn">
|
<span class="vm">{{ traceStore.currentTrace?.endpointNames?.[0] }}</span>
|
||||||
<el-button size="small" class="mr-10" type="primary" @click="searchTraceLogs">
|
</h5>
|
||||||
|
<div>
|
||||||
|
<el-button size="small" class="mr-10" @click="searchTraceLogs">
|
||||||
{{ t("viewLogs") }}
|
{{ t("viewLogs") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</h5>
|
</div>
|
||||||
<div class="mb-5 blue">
|
<div class="mb-5 blue">
|
||||||
<Selector
|
<Selector
|
||||||
size="small"
|
size="small"
|
||||||
@@ -46,33 +48,13 @@ limitations under the License. -->
|
|||||||
<div class="tag mr-5">{{ t("spans") }}</div>
|
<div class="tag mr-5">{{ t("spans") }}</div>
|
||||||
<span class="sm">{{ traceStore.traceSpans.length }}</span>
|
<span class="sm">{{ traceStore.traceSpans.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex-h trace-type item">
|
||||||
<el-button class="grey" size="small" :class="{ ghost: displayMode !== 'List' }" @click="displayMode = 'List'">
|
<el-radio-group v-model="spansGraphType" size="small">
|
||||||
<Icon class="mr-5" size="sm" iconName="list-bulleted" />
|
<el-radio-button v-for="option in GraphTypeOptions" :key="option.value" :value="option.value">
|
||||||
{{ t("list") }}
|
<Icon :iconName="option.icon" />
|
||||||
</el-button>
|
{{ t(option.label) }}
|
||||||
<el-button class="grey" size="small" :class="{ ghost: displayMode !== 'Tree' }" @click="displayMode = 'Tree'">
|
</el-radio-button>
|
||||||
<Icon class="mr-5" size="sm" iconName="issue-child" />
|
</el-radio-group>
|
||||||
{{ t("tree") }}
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
class="grey"
|
|
||||||
size="small"
|
|
||||||
:class="{ ghost: displayMode !== 'Table' }"
|
|
||||||
@click="displayMode = 'Table'"
|
|
||||||
>
|
|
||||||
<Icon class="mr-5" size="sm" iconName="table" />
|
|
||||||
{{ t("table") }}
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
class="grey"
|
|
||||||
size="small"
|
|
||||||
:class="{ ghost: displayMode !== 'Statistics' }"
|
|
||||||
@click="displayMode = 'Statistics'"
|
|
||||||
>
|
|
||||||
<Icon class="mr-5" size="sm" iconName="statistics-bulleted" />
|
|
||||||
{{ t("statistics") }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +62,7 @@ limitations under the License. -->
|
|||||||
<div class="trace-chart">
|
<div class="trace-chart">
|
||||||
<component
|
<component
|
||||||
v-if="traceStore.currentTrace?.endpointNames"
|
v-if="traceStore.currentTrace?.endpointNames"
|
||||||
:is="displayMode"
|
:is="spansGraphType"
|
||||||
:data="traceStore.traceSpans"
|
:data="traceStore.traceSpans"
|
||||||
:traceId="traceStore.currentTrace?.traceIds?.[0]?.value"
|
:traceId="traceStore.currentTrace?.traceIds?.[0]?.value"
|
||||||
:showBtnDetail="false"
|
:showBtnDetail="false"
|
||||||
@@ -95,13 +77,14 @@ limitations under the License. -->
|
|||||||
import { useTraceStore } from "@/store/modules/trace";
|
import { useTraceStore } from "@/store/modules/trace";
|
||||||
import type { Option } from "@/types/app";
|
import type { Option } from "@/types/app";
|
||||||
import copy from "@/utils/copy";
|
import copy from "@/utils/copy";
|
||||||
import graphs from "./components/index";
|
import graphs from "../VisGraph/index";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import getDashboard from "@/hooks/useDashboardsSession";
|
import getDashboard from "@/hooks/useDashboardsSession";
|
||||||
import type { LayoutConfig } from "@/types/dashboard";
|
import type { LayoutConfig } from "@/types/dashboard";
|
||||||
import { dateFormat } from "@/utils/dateFormat";
|
import { dateFormat } from "@/utils/dateFormat";
|
||||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||||
import { WidgetType } from "@/views/dashboard/data";
|
import { WidgetType } from "@/views/dashboard/data";
|
||||||
|
import { GraphTypeOptions } from "../VisGraph/constant";
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
serviceId: { type: String, default: "" },
|
serviceId: { type: String, default: "" },
|
||||||
@@ -120,7 +103,7 @@ limitations under the License. -->
|
|||||||
const traceStore = useTraceStore();
|
const traceStore = useTraceStore();
|
||||||
const loading = ref<boolean>(false);
|
const loading = ref<boolean>(false);
|
||||||
const traceId = ref<string>("");
|
const traceId = ref<string>("");
|
||||||
const displayMode = ref<string>("List");
|
const spansGraphType = ref<string>("List");
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
copy(traceId.value || traceStore.currentTrace?.traceIds?.[0]?.value);
|
copy(traceId.value || traceStore.currentTrace?.traceIds?.[0]?.value);
|
||||||
@@ -150,7 +133,6 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
traceStore,
|
traceStore,
|
||||||
displayMode,
|
|
||||||
dateFormat,
|
dateFormat,
|
||||||
changeTraceId,
|
changeTraceId,
|
||||||
handleClick,
|
handleClick,
|
||||||
@@ -160,6 +142,8 @@ limitations under the License. -->
|
|||||||
loading,
|
loading,
|
||||||
traceId,
|
traceId,
|
||||||
WidgetType,
|
WidgetType,
|
||||||
|
spansGraphType,
|
||||||
|
GraphTypeOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -196,6 +180,7 @@ limitations under the License. -->
|
|||||||
|
|
||||||
.item {
|
.item {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-detail-ids {
|
.trace-detail-ids {
|
||||||
@@ -208,10 +193,6 @@ limitations under the License. -->
|
|||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-log-btn {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -225,4 +206,16 @@ limitations under the License. -->
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trace-type {
|
||||||
|
// padding: 10px 0 10px 10px;
|
||||||
|
border-bottom: 1px solid $border-color-primary;
|
||||||
|
|
||||||
|
/* Make radio buttons content align nicely with icons */
|
||||||
|
:deep(.el-radio-button__inner) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@@ -0,0 +1,47 @@
|
|||||||
|
<!-- 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="trace-query flex-v">
|
||||||
|
<TracesTable />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onUnmounted } from "vue";
|
||||||
|
import type { PropType } from "vue";
|
||||||
|
import type { LayoutConfig } from "@/types/dashboard";
|
||||||
|
import { useTraceStore } from "@/store/modules/trace";
|
||||||
|
import TracesTable from "./TracesTable.vue";
|
||||||
|
|
||||||
|
/*global defineProps */
|
||||||
|
defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object as PropType<LayoutConfig>,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const traceStore = useTraceStore();
|
||||||
|
onUnmounted(() => {
|
||||||
|
traceStore.setTraceList([]);
|
||||||
|
traceStore.setSelectedSpan(null);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.trace-query {
|
||||||
|
width: 100%;
|
||||||
|
font-size: $font-size-smaller;
|
||||||
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,107 @@
|
|||||||
|
<!-- 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="trace-min-timeline">
|
||||||
|
<div class="timeline-marker-fixed">
|
||||||
|
<svg width="100%" height="20px">
|
||||||
|
<MinTimelineMarker :minTimestamp="minTimestamp" :maxTimestamp="maxTimestamp" :lineHeight="20" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="timeline-content scroll_bar_style"
|
||||||
|
:style="{ paddingRight: (trace.spans.length + 1) * rowHeight < 200 ? '20px' : '14px' }"
|
||||||
|
>
|
||||||
|
<svg ref="svgEle" width="100%" :height="`${(trace.spans.length + 1) * rowHeight}px`">
|
||||||
|
<MinTimelineOverlay
|
||||||
|
:minTimestamp="minTimestamp"
|
||||||
|
:maxTimestamp="maxTimestamp"
|
||||||
|
@setSelectedMinTimestamp="setSelectedMinTimestamp"
|
||||||
|
@setSelectedMaxTimestamp="setSelectedMaxTimestamp"
|
||||||
|
/>
|
||||||
|
<MinTimelineSelector
|
||||||
|
:minTimestamp="minTimestamp"
|
||||||
|
:maxTimestamp="maxTimestamp"
|
||||||
|
:selectedMinTimestamp="selectedMinTimestamp"
|
||||||
|
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||||
|
@setSelectedMinTimestamp="setSelectedMinTimestamp"
|
||||||
|
@setSelectedMaxTimestamp="setSelectedMaxTimestamp"
|
||||||
|
/>
|
||||||
|
<g
|
||||||
|
v-for="(item, index) in trace.spans"
|
||||||
|
:key="index"
|
||||||
|
:transform="`translate(0, ${(index + 1) * rowHeight + 3})`"
|
||||||
|
>
|
||||||
|
<SpanNode :span="item" :minTimestamp="minTimestamp" :maxTimestamp="maxTimestamp" :depth="index + 1" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import type { Trace } from "@/types/trace";
|
||||||
|
import SpanNode from "./SpanNode.vue";
|
||||||
|
import MinTimelineMarker from "./MinTimelineMarker.vue";
|
||||||
|
import MinTimelineOverlay from "./MinTimelineOverlay.vue";
|
||||||
|
import MinTimelineSelector from "./MinTimelineSelector.vue";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
trace: Trace;
|
||||||
|
minTimestamp: number;
|
||||||
|
maxTimestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const svgEle = ref<SVGSVGElement | null>(null);
|
||||||
|
const rowHeight = 12;
|
||||||
|
|
||||||
|
const selectedMinTimestamp = ref<number>(props.minTimestamp);
|
||||||
|
const selectedMaxTimestamp = ref<number>(props.maxTimestamp);
|
||||||
|
|
||||||
|
const emit = defineEmits(["updateSelectedMaxTimestamp", "updateSelectedMinTimestamp"]);
|
||||||
|
|
||||||
|
const setSelectedMinTimestamp = (value: number) => {
|
||||||
|
selectedMinTimestamp.value = value;
|
||||||
|
emit("updateSelectedMinTimestamp", value);
|
||||||
|
};
|
||||||
|
const setSelectedMaxTimestamp = (value: number) => {
|
||||||
|
selectedMaxTimestamp.value = value;
|
||||||
|
emit("updateSelectedMaxTimestamp", value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.trace-min-timeline {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-marker-fixed {
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-top: 5px;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,57 @@
|
|||||||
|
<!-- 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>
|
||||||
|
<g v-for="(marker, index) in markers" :key="marker.duration">
|
||||||
|
<line
|
||||||
|
:x1="`${marker.position}%`"
|
||||||
|
:y1="0"
|
||||||
|
:x2="`${marker.position}%`"
|
||||||
|
:y2="lineHeight ? `${lineHeight}` : '100%'"
|
||||||
|
stroke="var(--el-border-color-light)"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
:key="`label-${marker.duration}`"
|
||||||
|
:x="`${marker.position}%`"
|
||||||
|
:y="12"
|
||||||
|
font-size="10"
|
||||||
|
fill="var(--sw-font-grey-color)"
|
||||||
|
text-anchor="right"
|
||||||
|
:transform="`translate(${index === markers.length - 1 ? -50 : 5}, 0)`"
|
||||||
|
>
|
||||||
|
{{ marker.duration }}ms
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
minTimestamp: number;
|
||||||
|
maxTimestamp: number;
|
||||||
|
lineHeight?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const markers = computed(() => {
|
||||||
|
const maxDuration = props.maxTimestamp - props.minTimestamp;
|
||||||
|
const markerDurations = [0, (maxDuration * 1) / 3, (maxDuration * 2) / 3, maxDuration];
|
||||||
|
|
||||||
|
return markerDurations.map((duration) => ({
|
||||||
|
duration: duration.toFixed(2),
|
||||||
|
position: maxDuration > 0 ? (duration / maxDuration) * 100 : 0,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@@ -0,0 +1,147 @@
|
|||||||
|
<!-- 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>
|
||||||
|
<g>
|
||||||
|
<rect
|
||||||
|
v-if="mouseDownX !== undefined && currentX !== undefined"
|
||||||
|
:x="`${Math.min(mouseDownX, currentX)}%`"
|
||||||
|
y="0"
|
||||||
|
:width="`${Math.abs(mouseDownX - currentX)}%`"
|
||||||
|
height="100%"
|
||||||
|
fill="var(--el-color-primary-light-5)"
|
||||||
|
fill-opacity="0.2"
|
||||||
|
pointer-events="none"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
ref="rootEl"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
@mousedown="handleMouseDown"
|
||||||
|
@mousemove="handleMouseHoverMove"
|
||||||
|
@mouseleave="handleMouseHoverLeave"
|
||||||
|
fill-opacity="0"
|
||||||
|
cursor="col-resize"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
v-if="hoverX"
|
||||||
|
:x1="`${hoverX}%`"
|
||||||
|
:y1="0"
|
||||||
|
:x2="`${hoverX}%`"
|
||||||
|
y2="100%"
|
||||||
|
stroke="var(--el-color-primary-light-5)"
|
||||||
|
stroke-width="1"
|
||||||
|
pointer-events="none"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
minTimestamp: number;
|
||||||
|
maxTimestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: "setSelectedMaxTimestamp", value: number): void;
|
||||||
|
(e: "setSelectedMinTimestamp", value: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
const rootEl = ref<SVGRectElement | null>(null);
|
||||||
|
const mouseDownX = ref<number | undefined>(undefined);
|
||||||
|
const currentX = ref<number | undefined>(undefined);
|
||||||
|
const hoverX = ref<number | undefined>(undefined);
|
||||||
|
const mouseDownXRef = ref<number | undefined>(undefined);
|
||||||
|
const isDragging = ref(false);
|
||||||
|
|
||||||
|
function handleMouseMove(e: MouseEvent) {
|
||||||
|
if (!rootEl.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const x = calculateX(rootEl.value.getBoundingClientRect(), e.pageX);
|
||||||
|
currentX.value = x;
|
||||||
|
}
|
||||||
|
function handleMouseUp(e: MouseEvent) {
|
||||||
|
if (!isDragging.value || !rootEl.value || mouseDownXRef.value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = calculateX(rootEl.value.getBoundingClientRect(), e.pageX);
|
||||||
|
const adjustedX = Math.abs(x - mouseDownXRef.value) < 1 ? x + 1 : x;
|
||||||
|
|
||||||
|
const t1 = (mouseDownXRef.value / 100) * (props.maxTimestamp - props.minTimestamp) + props.minTimestamp;
|
||||||
|
const t2 = (adjustedX / 100) * (props.maxTimestamp - props.minTimestamp) + props.minTimestamp;
|
||||||
|
const newMinTimestmap = Math.min(t1, t2);
|
||||||
|
const newMaxTimestamp = Math.max(t1, t2);
|
||||||
|
|
||||||
|
emit("setSelectedMinTimestamp", newMinTimestmap);
|
||||||
|
emit("setSelectedMaxTimestamp", newMaxTimestamp);
|
||||||
|
|
||||||
|
currentX.value = undefined;
|
||||||
|
mouseDownX.value = undefined;
|
||||||
|
mouseDownXRef.value = undefined;
|
||||||
|
isDragging.value = false;
|
||||||
|
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateX = (parentRect: DOMRect, x: number) => {
|
||||||
|
const value = ((x - parentRect.left) / (parentRect.right - parentRect.left)) * 100;
|
||||||
|
if (value <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (value >= 100) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleMouseDown(e: MouseEvent) {
|
||||||
|
if (!rootEl.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const x = calculateX(rootEl.value.getBoundingClientRect(), e.pageX);
|
||||||
|
currentX.value = x;
|
||||||
|
mouseDownX.value = x;
|
||||||
|
mouseDownXRef.value = x;
|
||||||
|
isDragging.value = true;
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseHoverMove(e: MouseEvent) {
|
||||||
|
if (e.buttons !== 0 || !rootEl.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const x = calculateX(rootEl.value.getBoundingClientRect(), e.pageX);
|
||||||
|
hoverX.value = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseHoverLeave() {
|
||||||
|
hoverX.value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
isDragging.value = false;
|
||||||
|
});
|
||||||
|
</script>
|
@@ -0,0 +1,157 @@
|
|||||||
|
<!-- 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>
|
||||||
|
<g>
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
:width="`${boundaryLeft}%`"
|
||||||
|
height="100%"
|
||||||
|
fill="var(--el-color-primary-light-8)"
|
||||||
|
fill-opacity="0.6"
|
||||||
|
pointer-events="none"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
:x="`${boundaryRight}%`"
|
||||||
|
y="0"
|
||||||
|
:width="`${100 - boundaryRight}%`"
|
||||||
|
height="100%"
|
||||||
|
fill="var(--el-color-primary-light-8)"
|
||||||
|
fill-opacity="0.6"
|
||||||
|
pointer-events="none"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
:x="`${boundaryLeft}%`"
|
||||||
|
y="0"
|
||||||
|
width="3"
|
||||||
|
height="100%"
|
||||||
|
fill="var(--el-color-primary-light-5)"
|
||||||
|
transform="translate(-1)"
|
||||||
|
pointer-events="none"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
:x="`${boundaryRight}%`"
|
||||||
|
y="0"
|
||||||
|
width="3"
|
||||||
|
height="100%"
|
||||||
|
fill="var(--el-color-primary-light-5)"
|
||||||
|
transform="translate(-1)"
|
||||||
|
pointer-events="none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<rect
|
||||||
|
v-if="minMouseDownX !== undefined && minCurrentX !== undefined"
|
||||||
|
:x="`${Math.min(minMouseDownX, minCurrentX)}%`"
|
||||||
|
y="0"
|
||||||
|
:width="`${Math.abs(minMouseDownX - minCurrentX)}%`"
|
||||||
|
height="100%"
|
||||||
|
fill="var(--el-color-primary-light-6)"
|
||||||
|
fill-opacity="0.4"
|
||||||
|
pointer-events="none"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
v-if="maxMouseDownX !== undefined && maxCurrentX !== undefined"
|
||||||
|
:x="`${Math.min(maxMouseDownX, maxCurrentX)}%`"
|
||||||
|
y="0"
|
||||||
|
:width="`${Math.abs(maxMouseDownX - maxCurrentX)}%`"
|
||||||
|
height="100%"
|
||||||
|
fill="var(--el-color-primary-light-6)"
|
||||||
|
fill-opacity="0.4"
|
||||||
|
pointer-events="none"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
:x="`${boundaryLeft}%`"
|
||||||
|
y="0"
|
||||||
|
width="6"
|
||||||
|
height="40%"
|
||||||
|
fill="var(--el-color-primary)"
|
||||||
|
@mousedown="minRangeHandler.onMouseDown"
|
||||||
|
cursor="pointer"
|
||||||
|
transform="translate(-3)"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
:x="`${boundaryRight}%`"
|
||||||
|
y="0"
|
||||||
|
width="6"
|
||||||
|
height="40%"
|
||||||
|
fill="var(--el-color-primary)"
|
||||||
|
@mousedown="maxRangeHandler.onMouseDown"
|
||||||
|
cursor="pointer"
|
||||||
|
transform="translate(-3)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, onMounted } from "vue";
|
||||||
|
import { useRangeTimestampHandler, adjustPercentValue } from "./useHooks";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
minTimestamp: number;
|
||||||
|
maxTimestamp: number;
|
||||||
|
selectedMinTimestamp: number;
|
||||||
|
selectedMaxTimestamp: number;
|
||||||
|
}
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "setSelectedMinTimestamp", value: number): void;
|
||||||
|
(e: "setSelectedMaxTimestamp", value: number): void;
|
||||||
|
}>();
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const svgEle = ref<SVGSVGElement | null>(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const element = document.querySelector(".trace-min-timeline svg") as SVGSVGElement;
|
||||||
|
if (element) {
|
||||||
|
svgEle.value = element;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const maxOpositeX = computed(
|
||||||
|
() => ((props.selectedMaxTimestamp - props.minTimestamp) / (props.maxTimestamp - props.minTimestamp)) * 100,
|
||||||
|
);
|
||||||
|
const minOpositeX = computed(
|
||||||
|
() => ((props.selectedMinTimestamp - props.minTimestamp) / (props.maxTimestamp - props.minTimestamp)) * 100,
|
||||||
|
);
|
||||||
|
|
||||||
|
const minRangeHandler = computed(() => {
|
||||||
|
return useRangeTimestampHandler({
|
||||||
|
rootEl: svgEle.value,
|
||||||
|
minTimestamp: props.minTimestamp,
|
||||||
|
maxTimestamp: props.maxTimestamp,
|
||||||
|
selectedTimestamp: props.selectedMaxTimestamp,
|
||||||
|
isSmallerThanOpositeX: true,
|
||||||
|
setTimestamp: (value) => emit("setSelectedMinTimestamp", value),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const maxRangeHandler = computed(() =>
|
||||||
|
useRangeTimestampHandler({
|
||||||
|
rootEl: svgEle.value,
|
||||||
|
minTimestamp: props.minTimestamp,
|
||||||
|
maxTimestamp: props.maxTimestamp,
|
||||||
|
selectedTimestamp: props.selectedMinTimestamp,
|
||||||
|
isSmallerThanOpositeX: false,
|
||||||
|
setTimestamp: (value) => emit("setSelectedMaxTimestamp", value),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const boundaryLeft = computed(() => {
|
||||||
|
return adjustPercentValue(minOpositeX.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const boundaryRight = computed(() => adjustPercentValue(maxOpositeX.value));
|
||||||
|
|
||||||
|
const minMouseDownX = computed(() => minRangeHandler.value.mouseDownX.value);
|
||||||
|
const minCurrentX = computed(() => minRangeHandler.value.currentX.value);
|
||||||
|
const maxMouseDownX = computed(() => maxRangeHandler.value.mouseDownX.value);
|
||||||
|
const maxCurrentX = computed(() => maxRangeHandler.value.currentX.value);
|
||||||
|
</script>
|
@@ -0,0 +1,120 @@
|
|||||||
|
<!-- 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>
|
||||||
|
<rect :x="`${startPct}%`" :y="0" :width="`${widthPct}%`" :height="barHeight" :fill="barColor" rx="2" ry="2" />
|
||||||
|
<!-- Label and Duration Text -->
|
||||||
|
<text
|
||||||
|
v-if="showLabel"
|
||||||
|
:x="0"
|
||||||
|
:y="barHeight - 7"
|
||||||
|
font-size="10"
|
||||||
|
fill="var(--sw-font-grey-color)"
|
||||||
|
text-anchor="start"
|
||||||
|
class="span-label"
|
||||||
|
>
|
||||||
|
{{ span.label || "Unknown" }}
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
v-if="showDuration"
|
||||||
|
:x="`${100}%`"
|
||||||
|
:y="barHeight - 7"
|
||||||
|
font-size="10"
|
||||||
|
fill="var(--sw-font-grey-color)"
|
||||||
|
text-anchor="end"
|
||||||
|
class="span-duration"
|
||||||
|
>
|
||||||
|
{{ span.duration }}ms
|
||||||
|
</text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type { Span } from "@/types/trace";
|
||||||
|
import { getServiceColor } from "@/utils/color";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
span: Span;
|
||||||
|
minTimestamp: number;
|
||||||
|
maxTimestamp: number;
|
||||||
|
depth: number;
|
||||||
|
showDuration?: boolean;
|
||||||
|
showLabel?: boolean;
|
||||||
|
selectedMaxTimestamp?: number;
|
||||||
|
selectedMinTimestamp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const barHeight = 3;
|
||||||
|
|
||||||
|
const widthScale = computed(() => {
|
||||||
|
const { selectedMinTimestamp, selectedMaxTimestamp, minTimestamp, maxTimestamp } = props;
|
||||||
|
let max = maxTimestamp - minTimestamp;
|
||||||
|
if (selectedMaxTimestamp !== undefined && selectedMinTimestamp !== undefined) {
|
||||||
|
max = selectedMaxTimestamp - selectedMinTimestamp;
|
||||||
|
}
|
||||||
|
return (duration: number | undefined | null) => {
|
||||||
|
const d = Math.max(0, duration || 0);
|
||||||
|
return (d / max) * 100;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const startPct = computed(() => {
|
||||||
|
const { span, selectedMinTimestamp, minTimestamp } = props;
|
||||||
|
const end = span.endTime;
|
||||||
|
let start = span.startTime;
|
||||||
|
if (selectedMinTimestamp !== undefined) {
|
||||||
|
start = selectedMinTimestamp > start ? (end < selectedMinTimestamp ? 0 : selectedMinTimestamp) : start;
|
||||||
|
}
|
||||||
|
const dur = start - (selectedMinTimestamp || minTimestamp);
|
||||||
|
|
||||||
|
return Math.max(0, widthScale.value(dur));
|
||||||
|
});
|
||||||
|
|
||||||
|
const widthPct = computed(() => {
|
||||||
|
const { span, selectedMinTimestamp, selectedMaxTimestamp } = props;
|
||||||
|
let start = span.startTime;
|
||||||
|
let end = span.endTime;
|
||||||
|
if (selectedMinTimestamp !== undefined) {
|
||||||
|
start = selectedMinTimestamp > start ? selectedMinTimestamp : start;
|
||||||
|
if (end < selectedMinTimestamp) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selectedMaxTimestamp !== undefined) {
|
||||||
|
end = selectedMaxTimestamp < end ? selectedMaxTimestamp : end;
|
||||||
|
if (span.startTime > selectedMaxTimestamp) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dur = end - start;
|
||||||
|
return Math.max(0, widthScale.value(dur));
|
||||||
|
});
|
||||||
|
|
||||||
|
const barColor = computed(() => {
|
||||||
|
const serviceName = props.span.serviceCode || "";
|
||||||
|
return getServiceColor(serviceName);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.span-label {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-duration {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,59 @@
|
|||||||
|
<!-- 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="timeline-tool flex-h">
|
||||||
|
<div class="flex-h trace-type item">
|
||||||
|
<el-radio-group v-model="spansGraphType" size="small">
|
||||||
|
<el-radio-button v-for="option in GraphTypeOptions" :key="option.value" :value="option.value">
|
||||||
|
<Icon :iconName="option.icon" />
|
||||||
|
{{ t(option.label) }}
|
||||||
|
</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-button size="small" @click="onToggleMinTimeline">
|
||||||
|
<Icon iconName="sort" size="middle" />
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { GraphTypeOptions } from "../VisGraph/constant";
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "toggleMinTimeline"): void;
|
||||||
|
(e: "updateSpansGraphType", value: string): void;
|
||||||
|
}>();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const spansGraphType = ref<string>(GraphTypeOptions[2].value);
|
||||||
|
|
||||||
|
function onToggleMinTimeline() {
|
||||||
|
emit("toggleMinTimeline");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for changes in spansGraphType and emit to parent
|
||||||
|
watch(spansGraphType, (newValue) => {
|
||||||
|
emit("updateSpansGraphType", newValue);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.timeline-tool {
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 5px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,175 @@
|
|||||||
|
<!-- 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="trace-query-content">
|
||||||
|
<div class="trace-info">
|
||||||
|
<div class="flex-h" style="justify-content: space-between">
|
||||||
|
<h3>{{ trace.label }}</h3>
|
||||||
|
<div>
|
||||||
|
<el-dropdown @command="handleDownload" trigger="click">
|
||||||
|
<el-button size="small">
|
||||||
|
{{ t("download") }}
|
||||||
|
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="json">Download JSON</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="trace-meta flex-h">
|
||||||
|
<div>
|
||||||
|
<span class="grey mr-5">{{ t("duration") }}</span>
|
||||||
|
<span class="value">{{ trace.duration }}ms</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="grey mr-5">{{ t("services") }}</span>
|
||||||
|
<span class="value">{{ trace.serviceCode || "Unknown" }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="grey mr-5">{{ t("totalSpans") }}</span>
|
||||||
|
<span class="value">{{ trace.spans?.length || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="grey mr-5">{{ t("traceID") }}</span>
|
||||||
|
<span class="value">{{ trace.traceId }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-h">
|
||||||
|
<div class="detail-section-timeline flex-v">
|
||||||
|
<MinTimeline
|
||||||
|
v-show="minTimelineVisible"
|
||||||
|
:trace="trace"
|
||||||
|
:minTimestamp="minTimestamp"
|
||||||
|
:maxTimestamp="maxTimestamp"
|
||||||
|
@updateSelectedMaxTimestamp="handleSelectedMaxTimestamp"
|
||||||
|
@updateSelectedMinTimestamp="handleSelectedMinTimestamp"
|
||||||
|
/>
|
||||||
|
<TimelineTool @toggleMinTimeline="toggleMinTimeline" @updateSpansGraphType="handleSpansGraphTypeUpdate" />
|
||||||
|
<component
|
||||||
|
v-if="traceStore.currentTrace?.endpointNames"
|
||||||
|
:is="graphs[spansGraphType as keyof typeof graphs]"
|
||||||
|
:data="trace.spans"
|
||||||
|
:traceId="traceStore.currentTrace?.traceId"
|
||||||
|
:showBtnDetail="false"
|
||||||
|
:headerType="WidgetType.Trace"
|
||||||
|
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||||
|
:selectedMinTimestamp="selectedMinTimestamp"
|
||||||
|
:minTimestamp="minTimestamp"
|
||||||
|
:maxTimestamp="maxTimestamp"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { ArrowDown } from "@element-plus/icons-vue";
|
||||||
|
import type { Trace } from "@/types/trace";
|
||||||
|
import MinTimeline from "./MinTimeline.vue";
|
||||||
|
import { saveFileAsJSON } from "@/utils/file";
|
||||||
|
import { useTraceStore } from "@/store/modules/trace";
|
||||||
|
import TimelineTool from "./TimelineTool.vue";
|
||||||
|
import graphs from "../VisGraph/index";
|
||||||
|
import { WidgetType } from "@/views/dashboard/data";
|
||||||
|
import { GraphTypeOptions } from "../VisGraph/constant";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
trace: Trace;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const traceStore = useTraceStore();
|
||||||
|
// Time range like xScale domain [0, max]
|
||||||
|
const minTimestamp = computed(() => {
|
||||||
|
if (!props.trace.spans.length) return 0;
|
||||||
|
return Math.min(...props.trace.spans.map((s) => s.startTime || 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxTimestamp = computed(() => {
|
||||||
|
const timestamps = props.trace.spans.map((span) => span.endTime || 0);
|
||||||
|
if (timestamps.length === 0) return 0;
|
||||||
|
|
||||||
|
return Math.max(...timestamps);
|
||||||
|
});
|
||||||
|
const selectedMaxTimestamp = ref<number>(maxTimestamp.value);
|
||||||
|
const selectedMinTimestamp = ref<number>(minTimestamp.value);
|
||||||
|
const minTimelineVisible = ref<boolean>(true);
|
||||||
|
const spansGraphType = ref<string>(GraphTypeOptions[2].value);
|
||||||
|
|
||||||
|
function handleSelectedMaxTimestamp(value: number) {
|
||||||
|
selectedMaxTimestamp.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectedMinTimestamp(value: number) {
|
||||||
|
selectedMinTimestamp.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMinTimeline() {
|
||||||
|
minTimelineVisible.value = !minTimelineVisible.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSpansGraphTypeUpdate(value: string) {
|
||||||
|
spansGraphType.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDownload() {
|
||||||
|
const trace = props.trace;
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
|
const baseFilename = `trace-${trace.traceId}-${timestamp}`;
|
||||||
|
const spans = trace.spans.map((span) => {
|
||||||
|
const { duration, label, ...newSpan } = span;
|
||||||
|
return newSpan;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
saveFileAsJSON(spans, `${baseFilename}.json`);
|
||||||
|
ElMessage.success("Trace data downloaded as JSON");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Download error:", error);
|
||||||
|
ElMessage.error("Failed to download file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.trace-info {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-info h3 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section-timeline {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,379 @@
|
|||||||
|
<!-- 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="flex-h result-header">
|
||||||
|
<div style="align-items: center"> {{ filteredTraces.length }} of {{ totalTraces }} Results </div>
|
||||||
|
<div class="flex-h" style="align-items: center">
|
||||||
|
<el-switch
|
||||||
|
v-model="expandAll"
|
||||||
|
size="large"
|
||||||
|
active-text="Expand All"
|
||||||
|
inactive-text="Collapse All"
|
||||||
|
class="mr-20"
|
||||||
|
@change="toggleAllExpansion"
|
||||||
|
/>
|
||||||
|
<Selector
|
||||||
|
placeholder="Service filters"
|
||||||
|
@change="changeServiceFilters"
|
||||||
|
:value="selectedServiceNames"
|
||||||
|
:options="serviceNames"
|
||||||
|
:multiple="true"
|
||||||
|
style="width: 500px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="trace-query-table scroll_bar_style">
|
||||||
|
<el-table
|
||||||
|
ref="tableRef"
|
||||||
|
:data="filteredTraces"
|
||||||
|
:border="false"
|
||||||
|
:preserve-expanded-content="true"
|
||||||
|
:default-sort="{ prop: 'duration', order: 'descending' }"
|
||||||
|
style="width: 100%"
|
||||||
|
v-loading="traceStore.loading"
|
||||||
|
>
|
||||||
|
<el-table-column type="expand">
|
||||||
|
<template #default="props">
|
||||||
|
<div class="flex-h service-tags">
|
||||||
|
<el-tag
|
||||||
|
v-for="(value, key) in getEndpoints(props.row.spans)"
|
||||||
|
:key="key"
|
||||||
|
class="mr-5 cp"
|
||||||
|
disable-transitions
|
||||||
|
:style="{ backgroundColor: getServiceColor(value[0]), color: 'white', border: 'none' }"
|
||||||
|
@click="toggleServiceTags(value[0], props.row)"
|
||||||
|
>
|
||||||
|
{{ value[0] }}: {{ value[1] }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Root" prop="label" />
|
||||||
|
<el-table-column label="Start Time" prop="timestamp" width="220">
|
||||||
|
<template #default="props">
|
||||||
|
{{ dateFormat(props.row.start) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Spans" prop="spans.length" width="100" />
|
||||||
|
<el-table-column label="Duration (ms)" prop="duration" width="200" sortable>
|
||||||
|
<template #default="props">
|
||||||
|
<div class="duration-cell">
|
||||||
|
<el-progress
|
||||||
|
:percentage="getDurationProgress(props.row.duration)"
|
||||||
|
:stroke-width="22"
|
||||||
|
:text-inside="true"
|
||||||
|
color="rgba(64, 158, 255, 0.4)"
|
||||||
|
>
|
||||||
|
<div class="duration-value">{{ props.row.duration || 0 }}ms</div>
|
||||||
|
</el-progress>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column fixed="right" label="Operations" width="120">
|
||||||
|
<template #default="props">
|
||||||
|
<el-button size="small" @click="(e: MouseEvent) => handleShowTrace(e, props.row)"> Show </el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- Loading indicator -->
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<el-icon class="is-loading">
|
||||||
|
<Loading />
|
||||||
|
</el-icon>
|
||||||
|
<span>Loading more traces...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Load more button (fallback) -->
|
||||||
|
<div v-else-if="hasMoreItems" class="load-more-container">
|
||||||
|
<el-button @click="loadMoreItems" :loading="loading">
|
||||||
|
Load More ({{ totalTraces - filteredTraces.length }} remaining)
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Trace Details Dialog -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
fullscreen
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="true"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<TraceContent v-if="selectedTrace" :trace="selectedTrace" />
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, nextTick, watch, onMounted, onUnmounted } from "vue";
|
||||||
|
import { ElTable, ElDialog, ElIcon } from "element-plus";
|
||||||
|
import { Loading } from "@element-plus/icons-vue";
|
||||||
|
import { dateFormat } from "@/utils/dateFormat";
|
||||||
|
import { useTraceStore } from "@/store/modules/trace";
|
||||||
|
import type { Trace } from "@/types/trace";
|
||||||
|
import type { Option } from "@/types/app";
|
||||||
|
import { getServiceColor } from "@/utils/color";
|
||||||
|
import TraceContent from "./TraceContent.vue";
|
||||||
|
|
||||||
|
const PageSize = 10;
|
||||||
|
const traceStore = useTraceStore();
|
||||||
|
const expandAll = ref<boolean>(true);
|
||||||
|
const tableRef = ref<InstanceType<typeof ElTable>>();
|
||||||
|
const selectedServiceNames = ref<string[]>([]);
|
||||||
|
const dialogVisible = ref<boolean>(false);
|
||||||
|
const selectedTrace = ref<Trace | null>(null);
|
||||||
|
// Infinite scroll state
|
||||||
|
const loadedItemsCount = ref<number>(PageSize);
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
const loadMoreThreshold = 100; // pixels from bottom to trigger load
|
||||||
|
|
||||||
|
// Calculate max duration for progress bar scaling
|
||||||
|
const maxDuration = computed(() => {
|
||||||
|
if (!traceStore.traceList.length) return 1;
|
||||||
|
const durations = traceStore.traceList.map((trace: Trace) => trace.duration || 0);
|
||||||
|
return Math.max(...durations);
|
||||||
|
});
|
||||||
|
|
||||||
|
// All filtered traces based on selected service names
|
||||||
|
const allFilteredTraces = computed<Trace[]>(() => {
|
||||||
|
const rows = traceStore.traceList as Trace[];
|
||||||
|
if (!selectedServiceNames.value.length) return rows;
|
||||||
|
const selected = new Set(selectedServiceNames.value);
|
||||||
|
return rows.filter((row) => {
|
||||||
|
const rowService = row?.serviceCode;
|
||||||
|
if (rowService && selected.has(rowService)) return true;
|
||||||
|
for (const s of row.spans || []) {
|
||||||
|
const name = s?.serviceCode;
|
||||||
|
if (name && selected.has(name)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredTraces = computed<Trace[]>(() => {
|
||||||
|
return allFilteredTraces.value.slice(0, loadedItemsCount.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalTraces = computed<number>(() => allFilteredTraces.value.length);
|
||||||
|
|
||||||
|
const hasMoreItems = computed<boolean>(() => {
|
||||||
|
return loadedItemsCount.value < allFilteredTraces.value.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Service names options for the header Selector
|
||||||
|
const serviceNames = computed<Option[]>(() => {
|
||||||
|
const names = new Set<string>();
|
||||||
|
const rows = traceStore.traceList as Trace[];
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row?.serviceCode) {
|
||||||
|
names.add(row.serviceCode);
|
||||||
|
}
|
||||||
|
for (const s of row.spans || []) {
|
||||||
|
if (s?.serviceCode) {
|
||||||
|
names.add(s.serviceCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(names)
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map((n) => ({ label: n, value: n }));
|
||||||
|
});
|
||||||
|
|
||||||
|
function getEndpoints(spans: Trace[]) {
|
||||||
|
const endpoints = new Map<string, number>();
|
||||||
|
for (const d of spans) {
|
||||||
|
endpoints.set(d.serviceCode, (endpoints.get(d.serviceCode) || 0) + 1);
|
||||||
|
}
|
||||||
|
return endpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleShowTrace(e: MouseEvent, row: Trace) {
|
||||||
|
traceStore.setCurrentTrace(row);
|
||||||
|
selectedTrace.value = row;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDurationProgress(duration: number): number {
|
||||||
|
if (maxDuration.value === 0) return 0;
|
||||||
|
return Math.round((duration / maxDuration.value) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleServiceTags(serviceName: string, row: Trace) {
|
||||||
|
selectedServiceNames.value = selectedServiceNames.value.includes(serviceName)
|
||||||
|
? selectedServiceNames.value.filter((name) => name !== serviceName)
|
||||||
|
: [...selectedServiceNames.value, serviceName];
|
||||||
|
|
||||||
|
// Expand the row to show the service tags
|
||||||
|
nextTick(() => {
|
||||||
|
if (tableRef.value) {
|
||||||
|
tableRef.value.toggleRowExpansion(row, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeServiceFilters(selected: Option[]) {
|
||||||
|
selectedServiceNames.value = selected.map((o) => String(o.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infinite scroll handlers
|
||||||
|
function loadMoreItems() {
|
||||||
|
if (loading.value || !hasMoreItems.value) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
// Simulate loading delay for better UX
|
||||||
|
setTimeout(() => {
|
||||||
|
const nextBatch = Math.min(PageSize, allFilteredTraces.value.length - loadedItemsCount.value);
|
||||||
|
loadedItemsCount.value += nextBatch;
|
||||||
|
|
||||||
|
// Apply current expand state to all currently visible items
|
||||||
|
nextTick(() => {
|
||||||
|
if (tableRef.value) {
|
||||||
|
const allVisibleItems = filteredTraces.value;
|
||||||
|
for (const row of allVisibleItems) {
|
||||||
|
tableRef.value.toggleRowExpansion(row, expandAll.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScroll(event: Event) {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = target;
|
||||||
|
|
||||||
|
// Check if user is near the bottom
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < loadMoreThreshold && hasMoreItems.value && !loading.value) {
|
||||||
|
loadMoreItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset loaded items when filters change
|
||||||
|
watch(selectedServiceNames, () => {
|
||||||
|
loadedItemsCount.value = PageSize;
|
||||||
|
// Apply expand state to newly visible items after filter change
|
||||||
|
nextTick(() => {
|
||||||
|
if (tableRef.value) {
|
||||||
|
const visibleItems = filteredTraces.value;
|
||||||
|
for (const row of visibleItems) {
|
||||||
|
tableRef.value.toggleRowExpansion(row, expandAll.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for expandAll state changes to apply to all visible items
|
||||||
|
watch(expandAll, () => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (tableRef.value) {
|
||||||
|
const visibleItems = filteredTraces.value;
|
||||||
|
for (const row of visibleItems) {
|
||||||
|
tableRef.value.toggleRowExpansion(row, expandAll.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for trace list changes (when new query is executed) to reapply expand state
|
||||||
|
watch(
|
||||||
|
() => traceStore.traceList,
|
||||||
|
() => {
|
||||||
|
// Reset loaded items count when new data is loaded
|
||||||
|
loadedItemsCount.value = PageSize;
|
||||||
|
|
||||||
|
// Reapply expand state to newly loaded traces
|
||||||
|
nextTick(() => {
|
||||||
|
if (tableRef.value) {
|
||||||
|
const visibleItems = filteredTraces.value;
|
||||||
|
for (const row of visibleItems) {
|
||||||
|
tableRef.value.toggleRowExpansion(row, expandAll.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Setup scroll listener
|
||||||
|
onMounted(() => {
|
||||||
|
const tableContainer = document.querySelector(".trace-query-table");
|
||||||
|
if (tableContainer) {
|
||||||
|
tableContainer.addEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
const tableContainer = document.querySelector(".trace-query-table");
|
||||||
|
if (tableContainer) {
|
||||||
|
tableContainer.removeEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle all table row expansions (only for loaded items)
|
||||||
|
function toggleAllExpansion() {
|
||||||
|
// The expandAll watcher will handle applying the state to all visible items
|
||||||
|
// This function just needs to trigger the change
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.trace-query-table {
|
||||||
|
width: 100%;
|
||||||
|
font-size: $font-size-smaller;
|
||||||
|
overflow: auto;
|
||||||
|
height: calc(100% - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-tags {
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-value {
|
||||||
|
font-size: $font-size-smaller;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header {
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 20px 0;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: $font-size-smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-top: 1px solid var(--el-border-color-light);
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* 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 { ref, computed } from "vue";
|
||||||
|
|
||||||
|
export const adjustPercentValue = (value: number) => {
|
||||||
|
if (value <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (value >= 100) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateX = ({
|
||||||
|
parentRect,
|
||||||
|
x,
|
||||||
|
opositeX,
|
||||||
|
isSmallerThanOpositeX,
|
||||||
|
}: {
|
||||||
|
parentRect: DOMRect;
|
||||||
|
x: number;
|
||||||
|
opositeX: number;
|
||||||
|
isSmallerThanOpositeX: boolean;
|
||||||
|
}) => {
|
||||||
|
let value = ((x - parentRect.left) / (parentRect.right - parentRect.left)) * 100;
|
||||||
|
if (isSmallerThanOpositeX) {
|
||||||
|
if (value >= opositeX) {
|
||||||
|
value = opositeX - 1;
|
||||||
|
}
|
||||||
|
} else if (value <= opositeX) {
|
||||||
|
value = opositeX + 1;
|
||||||
|
}
|
||||||
|
return adjustPercentValue(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRangeTimestampHandler = ({
|
||||||
|
rootEl,
|
||||||
|
minTimestamp,
|
||||||
|
maxTimestamp,
|
||||||
|
selectedTimestamp,
|
||||||
|
isSmallerThanOpositeX,
|
||||||
|
setTimestamp,
|
||||||
|
}: {
|
||||||
|
rootEl: SVGSVGElement | null;
|
||||||
|
minTimestamp: number;
|
||||||
|
maxTimestamp: number;
|
||||||
|
selectedTimestamp: number;
|
||||||
|
isSmallerThanOpositeX: boolean;
|
||||||
|
setTimestamp: (value: number) => void;
|
||||||
|
}) => {
|
||||||
|
const currentX = ref<number>();
|
||||||
|
const mouseDownX = ref<number>();
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const selectedTimestampComputed = ref(selectedTimestamp);
|
||||||
|
const opositeX = computed(() => {
|
||||||
|
return ((selectedTimestampComputed.value - minTimestamp) / (maxTimestamp - minTimestamp)) * 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!rootEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const x = calculateX({
|
||||||
|
parentRect: rootEl.getBoundingClientRect(),
|
||||||
|
x: e.pageX,
|
||||||
|
opositeX: opositeX.value,
|
||||||
|
isSmallerThanOpositeX,
|
||||||
|
});
|
||||||
|
currentX.value = x;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = (e: MouseEvent) => {
|
||||||
|
if (!rootEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = calculateX({
|
||||||
|
parentRect: rootEl.getBoundingClientRect(),
|
||||||
|
x: e.pageX,
|
||||||
|
opositeX: opositeX.value,
|
||||||
|
isSmallerThanOpositeX,
|
||||||
|
});
|
||||||
|
const timestamp = (x / 100) * (maxTimestamp - minTimestamp) + minTimestamp;
|
||||||
|
selectedTimestampComputed.value = timestamp;
|
||||||
|
setTimestamp(timestamp);
|
||||||
|
currentX.value = undefined;
|
||||||
|
mouseDownX.value = undefined;
|
||||||
|
isDragging.value = false;
|
||||||
|
|
||||||
|
document.removeEventListener("mousemove", onMouseMove);
|
||||||
|
document.removeEventListener("mouseup", onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
|
if (!rootEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const x = calculateX({
|
||||||
|
parentRect: rootEl.getBoundingClientRect(),
|
||||||
|
x: (e.currentTarget as SVGRectElement).getBoundingClientRect().x + 3,
|
||||||
|
opositeX: opositeX.value,
|
||||||
|
isSmallerThanOpositeX,
|
||||||
|
});
|
||||||
|
currentX.value = x;
|
||||||
|
mouseDownX.value = x;
|
||||||
|
isDragging.value = true;
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", onMouseMove);
|
||||||
|
document.addEventListener("mouseup", onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { currentX, mouseDownX, onMouseDown, isDragging };
|
||||||
|
};
|
@@ -17,56 +17,59 @@ limitations under the License. -->
|
|||||||
class="charts-item mr-5"
|
class="charts-item mr-5"
|
||||||
v-for="(i, index) in traceStore.serviceList"
|
v-for="(i, index) in traceStore.serviceList"
|
||||||
:key="index"
|
:key="index"
|
||||||
:style="`color:${computedScale(index)}`"
|
:style="`color:${getServiceColor(i)}`"
|
||||||
>
|
>
|
||||||
<Icon iconName="issue-open-m" class="mr-5" size="sm" />
|
<Icon iconName="issue-open-m" class="mr-5" size="sm" />
|
||||||
<span>{{ i }}</span>
|
<span>{{ i }}</span>
|
||||||
</span>
|
</span>
|
||||||
<el-button class="btn" size="small" type="primary" @click="downloadTrace">
|
<el-button class="btn" size="small" @click="downloadTrace">
|
||||||
{{ t("exportImage") }}
|
<Icon iconName="download" size="sm" />
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="list">
|
<div class="list">
|
||||||
<Graph :data="data" :traceId="traceId" :type="TraceGraphType.LIST" />
|
<Graph
|
||||||
|
:data="data"
|
||||||
|
:traceId="traceId"
|
||||||
|
:type="TraceGraphType.LIST"
|
||||||
|
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||||
|
:selectedMinTimestamp="selectedMinTimestamp"
|
||||||
|
:minTimestamp="minTimestamp"
|
||||||
|
:maxTimestamp="maxTimestamp"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { PropType } from "vue";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||||
import { useTraceStore } from "@/store/modules/trace";
|
import { useTraceStore } from "@/store/modules/trace";
|
||||||
import type { Span } from "@/types/trace";
|
import type { Span } from "@/types/trace";
|
||||||
import Graph from "./D3Graph/Index.vue";
|
import Graph from "../D3Graph/Index.vue";
|
||||||
import { Themes } from "@/constants/data";
|
import { Themes } from "@/constants/data";
|
||||||
import { TraceGraphType } from "./constant";
|
import { TraceGraphType } from "./constant";
|
||||||
|
import { getServiceColor } from "@/utils/color";
|
||||||
|
|
||||||
/* global defineProps, Recordable*/
|
/* global defineProps, Indexable*/
|
||||||
defineProps({
|
type Props = {
|
||||||
data: { type: Array as PropType<Span[]>, default: () => [] },
|
data: Span[];
|
||||||
traceId: { type: String, default: "" },
|
traceId: string;
|
||||||
});
|
selectedMaxTimestamp?: number;
|
||||||
const { t } = useI18n();
|
selectedMinTimestamp?: number;
|
||||||
|
minTimestamp: number;
|
||||||
|
maxTimestamp: number;
|
||||||
|
};
|
||||||
|
defineProps<Props>();
|
||||||
const appStore = useAppStoreWithOut();
|
const appStore = useAppStoreWithOut();
|
||||||
const traceStore = useTraceStore();
|
const traceStore = useTraceStore();
|
||||||
|
|
||||||
function computedScale(i: number) {
|
|
||||||
const sequentialScale = d3
|
|
||||||
.scaleSequential()
|
|
||||||
.domain([0, traceStore.serviceList.length + 1])
|
|
||||||
.interpolator(d3.interpolateCool);
|
|
||||||
return sequentialScale(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadTrace() {
|
function downloadTrace() {
|
||||||
const serializer = new XMLSerializer();
|
const serializer = new XMLSerializer();
|
||||||
const svgNode: any = d3.select(".trace-list").node();
|
const svgNode: any = d3.select(".trace-list").node();
|
||||||
const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(svgNode)}`;
|
const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(svgNode)}`;
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const context: any = canvas.getContext("2d");
|
const context: any = canvas.getContext("2d");
|
||||||
canvas.width = (d3.select(".trace-list") as Recordable)._groups[0][0].clientWidth;
|
canvas.width = (d3.select(".trace-list") as Indexable)._groups[0][0].clientWidth;
|
||||||
canvas.height = (d3.select(".trace-list") as Recordable)._groups[0][0].clientHeight;
|
canvas.height = (d3.select(".trace-list") as Indexable)._groups[0][0].clientHeight;
|
||||||
context.fillStyle = appStore.theme === Themes.Dark ? "#212224" : `#fff`;
|
context.fillStyle = appStore.theme === Themes.Dark ? "#212224" : `#fff`;
|
||||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
@@ -94,6 +97,7 @@ limitations under the License. -->
|
|||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
margin: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
@@ -17,29 +17,39 @@ limitations under the License. -->
|
|||||||
<div class="trace-t-loading" v-show="loading">
|
<div class="trace-t-loading" v-show="loading">
|
||||||
<Icon iconName="spinner" size="sm" />
|
<Icon iconName="spinner" size="sm" />
|
||||||
</div>
|
</div>
|
||||||
<TableContainer :tableData="tableData" :type="TraceGraphType.STATISTICS" :headerType="headerType">
|
<TableContainer
|
||||||
|
:tableData="tableData"
|
||||||
|
:type="TraceGraphType.STATISTICS"
|
||||||
|
:headerType="headerType"
|
||||||
|
:traceId="traceId"
|
||||||
|
>
|
||||||
<div class="trace-tips" v-if="!tableData.length">{{ $t("noData") }}</div>
|
<div class="trace-tips" v-if="!tableData.length">{{ $t("noData") }}</div>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch, onMounted } from "vue";
|
import { ref, watch, onMounted } from "vue";
|
||||||
import type { PropType } from "vue";
|
import TableContainer from "../Table/TableContainer.vue";
|
||||||
import TableContainer from "./Table/TableContainer.vue";
|
import traceTable from "../D3Graph/utils/trace-table";
|
||||||
import traceTable from "../utils/trace-table";
|
|
||||||
import type { StatisticsSpan, Span, StatisticsGroupRef } from "@/types/trace";
|
import type { StatisticsSpan, Span, StatisticsGroupRef } from "@/types/trace";
|
||||||
import { TraceGraphType } from "./constant";
|
import { TraceGraphType } from "./constant";
|
||||||
|
|
||||||
/* global defineProps, defineEmits, Recordable*/
|
/* global defineProps, defineEmits*/
|
||||||
const props = defineProps({
|
type Props = {
|
||||||
data: { type: Array as PropType<Span[]>, default: () => [] },
|
data: Span[];
|
||||||
traceId: { type: String, default: "" },
|
traceId: string;
|
||||||
showBtnDetail: { type: Boolean, default: false },
|
showBtnDetail: boolean;
|
||||||
headerType: { type: String, default: "" },
|
headerType: string;
|
||||||
});
|
selectedMaxTimestamp: number;
|
||||||
const emit = defineEmits(["load"]);
|
selectedMinTimestamp: number;
|
||||||
|
};
|
||||||
|
type Emits = {
|
||||||
|
(e: "load", callback: () => void): void;
|
||||||
|
};
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
const loading = ref<boolean>(true);
|
const loading = ref<boolean>(true);
|
||||||
const tableData = ref<Recordable>([]);
|
const tableData = ref<any[]>([]);
|
||||||
const list = ref<any[]>([]);
|
const list = ref<any[]>([]);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
@@ -18,24 +18,34 @@ limitations under the License. -->
|
|||||||
:type="TraceGraphType.TABLE"
|
:type="TraceGraphType.TABLE"
|
||||||
:headerType="headerType"
|
:headerType="headerType"
|
||||||
@select="getSelectedSpan"
|
@select="getSelectedSpan"
|
||||||
|
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||||
|
:selectedMinTimestamp="selectedMinTimestamp"
|
||||||
|
:minTimestamp="minTimestamp"
|
||||||
|
:maxTimestamp="maxTimestamp"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { PropType } from "vue";
|
|
||||||
import type { Span } from "@/types/trace";
|
|
||||||
import type { SegmentSpan } from "@/types/profile";
|
import type { SegmentSpan } from "@/types/profile";
|
||||||
import Graph from "./D3Graph/Index.vue";
|
import Graph from "../D3Graph/Index.vue";
|
||||||
import { TraceGraphType } from "./constant";
|
import { TraceGraphType } from "../VisGraph/constant";
|
||||||
|
|
||||||
defineProps({
|
type Props = {
|
||||||
data: { type: Array as PropType<(Span | SegmentSpan)[]>, default: () => [] },
|
data: SegmentSpan[];
|
||||||
traceId: { type: String, default: "" },
|
traceId: string;
|
||||||
headerType: { type: String, default: "" },
|
selectedMaxTimestamp?: number;
|
||||||
});
|
selectedMinTimestamp?: number;
|
||||||
const emits = defineEmits(["select"]);
|
minTimestamp: number;
|
||||||
|
maxTimestamp: number;
|
||||||
|
headerType?: string;
|
||||||
|
};
|
||||||
|
type Emits = {
|
||||||
|
(e: "select", value: SegmentSpan): void;
|
||||||
|
};
|
||||||
|
defineProps<Props>();
|
||||||
|
const emits = defineEmits<Emits>();
|
||||||
|
|
||||||
function getSelectedSpan(span: Span) {
|
function getSelectedSpan(span: SegmentSpan) {
|
||||||
emits("select", span);
|
emits("select", span);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@@ -17,7 +17,7 @@ limitations under the License. -->
|
|||||||
class="time-charts-item mr-5"
|
class="time-charts-item mr-5"
|
||||||
v-for="(i, index) in traceStore.serviceList"
|
v-for="(i, index) in traceStore.serviceList"
|
||||||
:key="index"
|
:key="index"
|
||||||
:style="`color:${computedScale(index)}`"
|
:style="`color:${getServiceColor(i)}`"
|
||||||
>
|
>
|
||||||
<Icon iconName="issue-open-m" class="mr-5" size="sm" />
|
<Icon iconName="issue-open-m" class="mr-5" size="sm" />
|
||||||
<span>{{ i }}</span>
|
<span>{{ i }}</span>
|
||||||
@@ -35,40 +35,44 @@ limitations under the License. -->
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="trace-tree">
|
<div class="trace-tree">
|
||||||
<Graph ref="charts" :data="data" :traceId="traceId" :type="TraceGraphType.TREE" />
|
<Graph
|
||||||
|
ref="charts"
|
||||||
|
:data="data"
|
||||||
|
:traceId="traceId"
|
||||||
|
:type="TraceGraphType.TREE"
|
||||||
|
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||||
|
:selectedMinTimestamp="selectedMinTimestamp"
|
||||||
|
:minTimestamp="minTimestamp"
|
||||||
|
:maxTimestamp="maxTimestamp"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as d3 from "d3";
|
import Graph from "../D3Graph/Index.vue";
|
||||||
import Graph from "./D3Graph/Index.vue";
|
|
||||||
import type { PropType } from "vue";
|
|
||||||
import type { Span } from "@/types/trace";
|
import type { Span } from "@/types/trace";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { TraceGraphType } from "./constant";
|
import { TraceGraphType } from "./constant";
|
||||||
import { useTraceStore } from "@/store/modules/trace";
|
import { useTraceStore } from "@/store/modules/trace";
|
||||||
|
import { getServiceColor } from "@/utils/color";
|
||||||
|
|
||||||
/* global defineProps */
|
type Props = {
|
||||||
defineProps({
|
data: Span[];
|
||||||
data: { type: Array as PropType<Span[]>, default: () => [] },
|
traceId: string;
|
||||||
traceId: { type: String, default: "" },
|
selectedMaxTimestamp?: number;
|
||||||
});
|
selectedMinTimestamp?: number;
|
||||||
|
minTimestamp: number;
|
||||||
|
maxTimestamp: number;
|
||||||
|
};
|
||||||
|
defineProps<Props>();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const traceStore = useTraceStore();
|
const traceStore = useTraceStore();
|
||||||
const charts = ref<any>(null);
|
const charts = ref<any>(null);
|
||||||
|
|
||||||
function computedScale(i: number) {
|
|
||||||
const sequentialScale = d3
|
|
||||||
.scaleSequential()
|
|
||||||
.domain([0, traceStore.serviceList.length + 1])
|
|
||||||
.interpolator(d3.interpolateCool);
|
|
||||||
return sequentialScale(i);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.trace-tree {
|
.trace-tree {
|
||||||
height: 100%;
|
min-height: 400px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +88,7 @@ limitations under the License. -->
|
|||||||
.trace-tree-charts {
|
.trace-tree-charts {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
position: relative;
|
// position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -95,5 +99,6 @@ limitations under the License. -->
|
|||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
margin: 3px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@@ -21,3 +21,9 @@ export enum TraceGraphType {
|
|||||||
TABLE = "Table",
|
TABLE = "Table",
|
||||||
STATISTICS = "Statistics",
|
STATISTICS = "Statistics",
|
||||||
}
|
}
|
||||||
|
export const GraphTypeOptions = [
|
||||||
|
{ value: "List", icon: "list-bulleted", label: "list" },
|
||||||
|
{ value: "Tree", icon: "issue-child", label: "tree" },
|
||||||
|
{ value: "Table", icon: "table", label: "table" },
|
||||||
|
{ value: "Statistics", icon: "statistics-bulleted", label: "statistics" },
|
||||||
|
] as const;
|
Reference in New Issue
Block a user