mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-07-18 08:25:25 +00:00
feat: add trace list
This commit is contained in:
parent
c23e24293b
commit
ceceabec66
1
package-lock.json
generated
1
package-lock.json
generated
@ -13,6 +13,7 @@
|
||||
"d3-tip": "^0.9.1",
|
||||
"echarts": "^5.2.2",
|
||||
"element-plus": "^2.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.0.5",
|
||||
"three": "^0.131.3",
|
||||
"three-orbit-controls": "^82.1.0",
|
||||
|
@ -15,6 +15,7 @@
|
||||
"d3-tip": "^0.9.1",
|
||||
"echarts": "^5.2.2",
|
||||
"element-plus": "^2.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.0.5",
|
||||
"three": "^0.131.3",
|
||||
"three-orbit-controls": "^82.1.0",
|
||||
|
12
src/views/dashboard/related/trace/Content.vue → src/assets/icons/spinner.svg
Normal file → Executable file
12
src/views/dashboard/related/trace/Content.vue → src/assets/icons/spinner.svg
Normal file → Executable file
@ -4,15 +4,13 @@ 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
|
||||
|
||||
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>
|
||||
<Detail />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Detail from "./components/Detail.vue";
|
||||
</script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></svg>
|
Before Width: | Height: | Size: 877 B After Width: | Height: | Size: 1.0 KiB |
@ -19,7 +19,7 @@ limitations under the License. -->
|
||||
</div>
|
||||
<div class="trace flex-h">
|
||||
<TraceList />
|
||||
<Content />
|
||||
<Detail />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -27,7 +27,7 @@ limitations under the License. -->
|
||||
import type { PropType } from "vue";
|
||||
import Filter from "../related/trace/Filter.vue";
|
||||
import TraceList from "../related/trace/TraceList.vue";
|
||||
import Content from "../related/trace/Content.vue";
|
||||
import Detail from "../related/trace/Detail.vue";
|
||||
|
||||
/*global defineProps */
|
||||
defineProps({
|
||||
|
@ -92,14 +92,20 @@ limitations under the License. -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<List
|
||||
v-if="displayMode == 'list' && traceStore.currentTrace.endpointNames"
|
||||
:data="traceStore.traceSpans"
|
||||
:traceId="traceStore.currentTrace.traceIds[0].value"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import dayjs from "dayjs";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import { Option } from "@/types/app";
|
||||
import copy from "@/utils/copy";
|
||||
import List from "./components/List.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
const traceStore = useTraceStore();
|
||||
@ -107,11 +113,6 @@ const traceId = ref<string>("");
|
||||
const displayMode = ref<string>("list");
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
onMounted(() => {
|
||||
if (traceStore.currentTrace.traceIds && traceStore.currentTrace.traceIds[0]) {
|
||||
traceId.value = traceStore.currentTrace.traceIds[0].value;
|
||||
}
|
||||
});
|
||||
function handleClick(ids: string[]) {
|
||||
let copyValue = null;
|
||||
if (ids.length === 1) {
|
@ -32,7 +32,7 @@ limitations under the License. -->
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trace-t-loading" v-loading="loading">
|
||||
<div class="trace-t-loading" v-show="loading">
|
||||
<Icon iconName="spinner" size="sm" />
|
||||
</div>
|
||||
<div class="trace-t-wrapper">
|
||||
|
390
src/views/dashboard/related/trace/components/List.vue
Normal file
390
src/views/dashboard/related/trace/components/List.vue
Normal file
@ -0,0 +1,390 @@
|
||||
<!-- 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="time-charts scroll_hide">
|
||||
<div class="trace-t-loading" v-show="loading">
|
||||
<Icon iconName="spinner" size="sm" />
|
||||
</div>
|
||||
<transition-group name="fade" tag="a" class="mb-5">
|
||||
<span
|
||||
class="time-charts-item mr-10"
|
||||
v-for="(i, index) in data"
|
||||
:key="index"
|
||||
:style="`color:${computedScale(index)}`"
|
||||
>
|
||||
<svg class="icon vm mr-5 sm">
|
||||
<use xlink:href="#issue-open-m"></use>
|
||||
</svg>
|
||||
<span>{{ i }}</span>
|
||||
</span>
|
||||
</transition-group>
|
||||
<a class="rk-btn r vm tc" @click="downloadTrace">{{ t("exportImage") }}</a>
|
||||
<div class="trace-list">
|
||||
<div ref="traceList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onBeforeUnmount, onMounted } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import _ from "lodash";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import * as d3 from "d3";
|
||||
import ListGraph from "./utils/d3-trace";
|
||||
import copy from "@/utils/copy";
|
||||
import { Span, Trace } from "@/types/trace";
|
||||
import { Option } from "@/types/app";
|
||||
/* global defineProps, Nullable*/
|
||||
const props = defineProps({
|
||||
data: { type: Array as PropType<Span[]>, default: () => [] },
|
||||
traceId: { type: String, default: "" },
|
||||
});
|
||||
const { t } = useI18n();
|
||||
const loading = ref<boolean>(false);
|
||||
const showDetail = ref<boolean>(false);
|
||||
const fixSpansSize = ref<number>(0);
|
||||
const segmentId = ref<any>([]);
|
||||
const currentSpan = ref<Array<Span>>([]);
|
||||
const list = ref<any>([]);
|
||||
const tree = ref<any>(null);
|
||||
const traceList = ref<Nullable<HTMLDivElement>>(null);
|
||||
|
||||
onMounted(() => {
|
||||
loading.value = true;
|
||||
changeTree();
|
||||
if (!traceList.value) {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
tree.value = new ListGraph(traceList.value, handleSelectSpan);
|
||||
tree.value.init(
|
||||
{ label: "TRACE_ROOT", children: segmentId.value },
|
||||
props.data,
|
||||
fixSpansSize.value
|
||||
);
|
||||
tree.value.draw();
|
||||
loading.value = false;
|
||||
});
|
||||
function handleSelectSpan(i: any) {
|
||||
currentSpan.value = i.data;
|
||||
showDetail.value = true;
|
||||
}
|
||||
function traverseTree(node: any, spanId: string, segmentId: string, data: any) {
|
||||
if (!node || node.isBroken) {
|
||||
return;
|
||||
}
|
||||
if (node.spanId === spanId && node.segmentId === segmentId) {
|
||||
node.children.push(data);
|
||||
return;
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach((nodeItem: any) => {
|
||||
traverseTree(nodeItem, spanId, segmentId, data);
|
||||
});
|
||||
}
|
||||
}
|
||||
function computedScale(i: any) {
|
||||
// Rainbow map
|
||||
const sequentialScale = d3
|
||||
.scaleSequential()
|
||||
.domain([0, list.value.length + 1])
|
||||
.interpolator(d3.interpolateCool);
|
||||
return sequentialScale(i);
|
||||
}
|
||||
function collapse(d: Span) {
|
||||
if (d.children) {
|
||||
let dur = d.endTime - d.startTime;
|
||||
d.children.forEach((i: Span) => {
|
||||
dur -= i.endTime - i.startTime;
|
||||
});
|
||||
d.dur = dur < 0 ? 0 : dur;
|
||||
d.children.forEach((i: Span) => collapse(i));
|
||||
}
|
||||
}
|
||||
function downloadTrace() {
|
||||
const serializer = new XMLSerializer();
|
||||
const svgNode: any = d3.select(".trace-list-dowanload").node();
|
||||
const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(
|
||||
svgNode
|
||||
)}`;
|
||||
const canvas = document.createElement("canvas");
|
||||
const context: any = canvas.getContext("2d");
|
||||
canvas.width = (
|
||||
d3.select(".trace-list-dowanload") as any
|
||||
)._groups[0][0].clientWidth;
|
||||
canvas.height = (
|
||||
d3.select(".trace-list-dowanload") as any
|
||||
)._groups[0][0].clientHeight;
|
||||
context.fillStyle = "#fff";
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
const image = new Image();
|
||||
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`;
|
||||
image.onload = () => {
|
||||
context.drawImage(image, 0, 0);
|
||||
const tagA = document.createElement("a");
|
||||
tagA.download = "trace-list.png";
|
||||
tagA.href = canvas.toDataURL("image/png");
|
||||
tagA.click();
|
||||
};
|
||||
}
|
||||
function changeTree() {
|
||||
if (props.data.length === 0) {
|
||||
return [];
|
||||
}
|
||||
list.value = Array.from(new Set(props.data.map((i: any) => i.serviceCode)));
|
||||
segmentId.value = [];
|
||||
const segmentGroup: any = {};
|
||||
const segmentIdGroup: any = [];
|
||||
const fixSpans: any = [];
|
||||
const segmentHeaders: any = [];
|
||||
for (const span of props.data) {
|
||||
if (span.parentSpanId === -1) {
|
||||
segmentHeaders.push(span);
|
||||
} else {
|
||||
const index = props.data.findIndex(
|
||||
(i: any) =>
|
||||
i.segmentId === span.segmentId && i.spanId === span.spanId - 1
|
||||
);
|
||||
const fixSpanKeyContent = {
|
||||
traceId: span.traceId,
|
||||
segmentId: span.segmentId,
|
||||
spanId: span.spanId - 1,
|
||||
parentSpanId: span.spanId - 2,
|
||||
};
|
||||
if (index === -1 && !_.find(fixSpans, fixSpanKeyContent)) {
|
||||
fixSpans.push({
|
||||
...fixSpanKeyContent,
|
||||
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: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
segmentHeaders.forEach((span: Span) => {
|
||||
if (span.refs.length) {
|
||||
span.refs.forEach((ref) => {
|
||||
const index = props.data.findIndex(
|
||||
(i: any) =>
|
||||
ref.parentSegmentId === i.segmentId && ref.parentSpanId === i.spanId
|
||||
);
|
||||
if (index === -1) {
|
||||
// create a known broken node.
|
||||
const i = ref.parentSpanId;
|
||||
const fixSpanKeyContent = {
|
||||
traceId: ref.traceId,
|
||||
segmentId: ref.parentSegmentId,
|
||||
spanId: i,
|
||||
parentSpanId: i > -1 ? 0 : -1,
|
||||
};
|
||||
if (!_.find(fixSpans, fixSpanKeyContent)) {
|
||||
fixSpans.push({
|
||||
...fixSpanKeyContent,
|
||||
refs: [],
|
||||
endpointName: `VNode: ${ref.parentSegmentId}`,
|
||||
serviceCode: "VirtualNode",
|
||||
type: `[Broken] ${ref.type}`,
|
||||
peer: "",
|
||||
component: `VirtualNode: #${i}`,
|
||||
isError: true,
|
||||
isBroken: true,
|
||||
layer: "Broken",
|
||||
tags: [],
|
||||
logs: [],
|
||||
});
|
||||
}
|
||||
// if root broken node is not exist, create a root broken node.
|
||||
if (fixSpanKeyContent.parentSpanId > -1) {
|
||||
const fixRootSpanKeyContent = {
|
||||
traceId: ref.traceId,
|
||||
segmentId: ref.parentSegmentId,
|
||||
spanId: 0,
|
||||
parentSpanId: -1,
|
||||
};
|
||||
if (!_.find(fixSpans, fixRootSpanKeyContent)) {
|
||||
fixSpans.push({
|
||||
...fixRootSpanKeyContent,
|
||||
refs: [],
|
||||
endpointName: `VNode: ${ref.parentSegmentId}`,
|
||||
serviceCode: "VirtualNode",
|
||||
type: `[Broken] ${ref.type}`,
|
||||
peer: "",
|
||||
component: `VirtualNode: #0`,
|
||||
isError: true,
|
||||
isBroken: true,
|
||||
layer: "Broken",
|
||||
tags: [],
|
||||
logs: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
[...fixSpans, ...props.data].forEach((i) => {
|
||||
i.label = i.endpointName || "no operation name";
|
||||
i.children = [];
|
||||
if (segmentGroup[i.segmentId] === undefined) {
|
||||
segmentIdGroup.push(i.segmentId);
|
||||
segmentGroup[i.segmentId] = [];
|
||||
segmentGroup[i.segmentId].push(i);
|
||||
} else {
|
||||
segmentGroup[i.segmentId].push(i);
|
||||
}
|
||||
});
|
||||
fixSpansSize.value = fixSpans.length;
|
||||
segmentIdGroup.forEach((id: string) => {
|
||||
const currentSegment = segmentGroup[id].sort(
|
||||
(a: Span, b: Span) => b.parentSpanId - a.parentSpanId
|
||||
);
|
||||
currentSegment.forEach((s: any) => {
|
||||
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 = _.filter(props.data, (span: Span) => {
|
||||
return _.find(span.refs, {
|
||||
traceId: s.traceId,
|
||||
parentSegmentId: s.segmentId,
|
||||
parentSpanId: s.spanId,
|
||||
});
|
||||
});
|
||||
if (children.length > 0) {
|
||||
s.children.push(...children);
|
||||
}
|
||||
}
|
||||
});
|
||||
segmentGroup[id] = currentSegment[currentSegment.length - 1];
|
||||
});
|
||||
segmentIdGroup.forEach((id: string) => {
|
||||
segmentGroup[id].refs.forEach((ref: any) => {
|
||||
if (ref.traceId === props.traceId) {
|
||||
traverseTree(
|
||||
segmentGroup[ref.parentSegmentId],
|
||||
ref.parentSpanId,
|
||||
ref.parentSegmentId,
|
||||
segmentGroup[id]
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
for (const i in segmentGroup) {
|
||||
if (segmentGroup[i].refs.length === 0) {
|
||||
segmentId.value.push(segmentGroup[i]);
|
||||
}
|
||||
}
|
||||
segmentId.value.forEach((i: any) => {
|
||||
collapse(i);
|
||||
});
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
d3.selectAll(".d3-tip").remove();
|
||||
});
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
if (!props.data.length) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
changeTree();
|
||||
tree.value.init(
|
||||
{ label: "TRACE_ROOT", children: segmentId.value },
|
||||
props.data,
|
||||
fixSpansSize.value
|
||||
);
|
||||
tree.value.draw(() => {
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.time-charts {
|
||||
overflow: auto;
|
||||
padding: 10px 30px;
|
||||
position: relative;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.trace-node .group {
|
||||
cursor: pointer;
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
.trace-node-container {
|
||||
fill: rgba(0, 0, 0, 0);
|
||||
stroke-width: 5px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
fill: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.trace-node .node-text {
|
||||
font: 12.5px sans-serif;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.domain {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.time-charts-item {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid;
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.trace-list {
|
||||
fill: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.trace-list .trace-node rect {
|
||||
&:hover {
|
||||
fill: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-c-text {
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
313
src/views/dashboard/related/trace/components/utils/d3-trace.ts
Normal file
313
src/views/dashboard/related/trace/components/utils/d3-trace.ts
Normal file
@ -0,0 +1,313 @@
|
||||
/**</template>
|
||||
* 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 * as d3 from "d3";
|
||||
import d3tip from "d3-tip";
|
||||
import { Span, Trace } from "@/types/trace";
|
||||
|
||||
const type = {
|
||||
MQ: "#bf99f8",
|
||||
Http: "#72a5fd",
|
||||
Database: "#ff6732",
|
||||
Unknown: "#ffc107",
|
||||
Cache: "#00bcd4",
|
||||
RPCFramework: "#ee4395",
|
||||
};
|
||||
export default class ListGraph {
|
||||
private barHeight = 48;
|
||||
private handleSelectSpan: Nullable<(i: Trace) => void> = null;
|
||||
private el: Nullable<HTMLDivElement> = null;
|
||||
private i = 0;
|
||||
private width = 0;
|
||||
private height = 0;
|
||||
private svg: any = null;
|
||||
private treemap: any = null;
|
||||
private tip: any = null;
|
||||
private row: any[] = [];
|
||||
private data: any = [];
|
||||
private min = 0;
|
||||
private max = 0;
|
||||
private list: any[] = [];
|
||||
private xScale: any = null;
|
||||
private xAxis: any = null;
|
||||
private sequentialScale: any = null;
|
||||
private root: any = null;
|
||||
constructor(el: HTMLDivElement, handleSelectSpan: (i: Trace) => void) {
|
||||
this.handleSelectSpan = handleSelectSpan;
|
||||
this.el = el;
|
||||
this.width = el.clientWidth;
|
||||
this.height = el.clientHeight;
|
||||
this.svg = d3
|
||||
.select(this.el)
|
||||
.append("svg")
|
||||
.attr("class", "trace-list-dowanload")
|
||||
.attr("width", this.width)
|
||||
.attr("height", this.height);
|
||||
this.treemap = d3.tree().size([this.height * 0.7, this.width]);
|
||||
this.tip = (d3tip as any)()
|
||||
.attr("class", "d3-tip")
|
||||
.offset([-8, 0])
|
||||
.html(
|
||||
(d: any) => `
|
||||
<div class="mb-5">${d.data.label}</div>
|
||||
${
|
||||
d.data.dur
|
||||
? '<div class="sm">SelfDuration: ' + d.data.dur + "ms</div>"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
d.data.endTime - d.data.startTime
|
||||
? '<div class="sm">TotalDuration: ' +
|
||||
(d.data.endTime - d.data.startTime) +
|
||||
"ms</div>"
|
||||
: ""
|
||||
}
|
||||
`
|
||||
);
|
||||
this.svg.call(this.tip);
|
||||
}
|
||||
diagonal(d: any) {
|
||||
return `M ${d.source.y} ${d.source.x + 5}
|
||||
L ${d.source.y} ${d.target.x - 30}
|
||||
L${d.target.y} ${d.target.x - 20}
|
||||
L${d.target.y} ${d.target.x - 5}`;
|
||||
}
|
||||
init(data: any, row: any[], fixSpansSize: number) {
|
||||
d3.select(".trace-xaxis").remove();
|
||||
this.row = row;
|
||||
this.data = data;
|
||||
this.min = d3.min(this.row.map((i) => i.startTime));
|
||||
this.max = d3.max(this.row.map((i) => i.endTime - this.min)) || 0;
|
||||
this.list = Array.from(new Set(this.row.map((i) => i.serviceCode)));
|
||||
this.xScale = d3
|
||||
.scaleLinear()
|
||||
.range([0, this.width * 0.387])
|
||||
.domain([0, this.max]);
|
||||
this.xAxis = d3.axisTop(this.xScale).tickFormat((d: any) => {
|
||||
if (d === 0) return 0;
|
||||
if (d >= 1000) return d / 1000 + "s";
|
||||
return d;
|
||||
});
|
||||
this.svg.attr(
|
||||
"height",
|
||||
(this.row.length + fixSpansSize + 1) * this.barHeight
|
||||
);
|
||||
this.svg
|
||||
.append("g")
|
||||
.attr("class", "trace-xaxis")
|
||||
|
||||
.attr("transform", `translate(${this.width * 0.618 - 20},${30})`)
|
||||
.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.x0 = 0;
|
||||
this.root.y0 = 0;
|
||||
}
|
||||
draw(callback: any) {
|
||||
this.update(this.root, callback);
|
||||
}
|
||||
click(d: any, scope: any) {
|
||||
if (!d.data.type) return;
|
||||
if (d.children) {
|
||||
d._children = d.children;
|
||||
d.children = null;
|
||||
} else {
|
||||
d.children = d._children;
|
||||
d._children = null;
|
||||
}
|
||||
scope.update(d);
|
||||
}
|
||||
update(source: any, callback: any) {
|
||||
const nodes = this.root.descendants();
|
||||
let index = -1;
|
||||
this.root.eachBefore((n: any) => {
|
||||
n.x = ++index * this.barHeight + 24;
|
||||
n.y = n.depth * 12;
|
||||
});
|
||||
const node = this.svg
|
||||
.selectAll(".trace-node")
|
||||
.data(nodes, (d: any) => d.id || (d.id = ++this.i));
|
||||
const nodeEnter = node
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("transform", `translate(${source.y0},${source.x0})`)
|
||||
.attr("class", "trace-node")
|
||||
.style("opacity", 0)
|
||||
.on("mouseover", (d: Trace) => {
|
||||
this.tip.show(d, this);
|
||||
})
|
||||
.on("mouseout", (d: Trace) => {
|
||||
this.tip.hide(d, this);
|
||||
})
|
||||
.on("click", (d: Trace) => {
|
||||
if (this.handleSelectSpan) {
|
||||
this.handleSelectSpan(d);
|
||||
}
|
||||
});
|
||||
nodeEnter
|
||||
.append("rect")
|
||||
.attr("height", 42)
|
||||
.attr("ry", 2)
|
||||
.attr("rx", 2)
|
||||
.attr("y", -22)
|
||||
.attr("x", 20)
|
||||
.attr("width", "100%")
|
||||
.attr("fill", "rgba(0,0,0,0)");
|
||||
nodeEnter
|
||||
.append("text")
|
||||
.attr("x", 13)
|
||||
.attr("y", 5)
|
||||
.attr("fill", "#E54C17")
|
||||
.html((d: any) => (d.data.isError ? "◉" : ""));
|
||||
nodeEnter
|
||||
.append("text")
|
||||
.attr("class", "node-text")
|
||||
.attr("x", 35)
|
||||
.attr("y", -6)
|
||||
.attr("fill", "#333")
|
||||
.text((d: any) => {
|
||||
if (d.data.label === "TRACE_ROOT") {
|
||||
return "";
|
||||
}
|
||||
return d.data.label.length > 40
|
||||
? `${d.data.label.slice(0, 40)}...`
|
||||
: `${d.data.label}`;
|
||||
});
|
||||
nodeEnter
|
||||
.append("text")
|
||||
.attr("class", "node-text")
|
||||
.attr("x", 35)
|
||||
.attr("y", 12)
|
||||
.attr("fill", "#ccc")
|
||||
.style("font-size", "11px")
|
||||
.text(
|
||||
(d: any) =>
|
||||
`${d.data.layer || ""} ${
|
||||
d.data.component ? "- " + d.data.component : d.data.component || ""
|
||||
}`
|
||||
);
|
||||
nodeEnter
|
||||
.append("rect")
|
||||
.attr("rx", 2)
|
||||
.attr("ry", 2)
|
||||
.attr("height", 4)
|
||||
.attr("width", (d: any) => {
|
||||
if (!d.data.endTime || !d.data.startTime) return 0;
|
||||
return this.xScale(d.data.endTime - d.data.startTime) + 1 || 0;
|
||||
})
|
||||
.attr("x", (d: any) =>
|
||||
!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)
|
||||
.style(
|
||||
"fill",
|
||||
(d: any) =>
|
||||
`${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`
|
||||
);
|
||||
nodeEnter
|
||||
.transition()
|
||||
.duration(400)
|
||||
.attr("transform", (d: any) => `translate(${d.y},${d.x})`)
|
||||
.style("opacity", 1);
|
||||
nodeEnter
|
||||
.append("circle")
|
||||
.attr("r", 3)
|
||||
.style("cursor", "pointer")
|
||||
.attr("stroke-width", 2.5)
|
||||
.attr("fill", (d: any) =>
|
||||
d._children
|
||||
? `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`
|
||||
: "rbga(0,0,0,0)"
|
||||
)
|
||||
.style("stroke", (d: any) =>
|
||||
d.data.label === "TRACE_ROOT"
|
||||
? ""
|
||||
: `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`
|
||||
)
|
||||
.on("click", (d: any) => {
|
||||
this.click(d, this);
|
||||
(d3 as any).event.stopPropagation();
|
||||
});
|
||||
node
|
||||
.transition()
|
||||
.duration(400)
|
||||
.attr("transform", (d: any) => `translate(${d.y},${d.x})`)
|
||||
.style("opacity", 1)
|
||||
.select("circle")
|
||||
.attr("fill", (d: any) =>
|
||||
d._children
|
||||
? `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`
|
||||
: ""
|
||||
);
|
||||
|
||||
// Transition exiting nodes to the parent's new position.
|
||||
node
|
||||
.exit()
|
||||
.transition()
|
||||
.duration(400)
|
||||
.attr("transform", `translate(${source.y},${source.x})`)
|
||||
.style("opacity", 0)
|
||||
.remove();
|
||||
const link = this.svg
|
||||
.selectAll(".trace-link")
|
||||
.data(this.root.links(), function (d: any) {
|
||||
return d.target.id;
|
||||
});
|
||||
|
||||
link
|
||||
.enter()
|
||||
.insert("path", "g")
|
||||
.attr("class", "trace-link")
|
||||
.attr("fill", "rgba(0,0,0,0)")
|
||||
.attr("stroke", "rgba(0, 0, 0, 0.1)")
|
||||
.attr("stroke-width", 2)
|
||||
.attr("d", () => {
|
||||
const o = { x: source.x0 + 35, y: source.y0 };
|
||||
return this.diagonal({ source: o, target: o });
|
||||
})
|
||||
.transition()
|
||||
.duration(400)
|
||||
.attr("d", this.diagonal);
|
||||
|
||||
link.transition().duration(400).attr("d", this.diagonal);
|
||||
|
||||
link
|
||||
.exit()
|
||||
.transition()
|
||||
.duration(400)
|
||||
.attr("d", () => {
|
||||
const o = { x: source.x + 35, y: source.y };
|
||||
return this.diagonal({ source: o, target: o });
|
||||
})
|
||||
.remove();
|
||||
this.root.each(function (d: any) {
|
||||
d.x0 = d.x;
|
||||
d.y0 = d.y;
|
||||
});
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user