feat: implement Profile in the dashboard (#19)

This commit is contained in:
Fine0830 2022-03-02 16:41:48 +08:00 committed by GitHub
parent 977ffbaf74
commit ca6d08827f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 2165 additions and 117 deletions

View 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 version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M18.984 6.984v-1.969h-9.984v1.969h9.984zM15 15v-2.016h-6v2.016h6zM18.984 11.016v-2.016h-9.984v2.016h9.984zM20.016 2.016q0.797 0 1.383 0.586t0.586 1.383v12q0 0.797-0.586 1.406t-1.383 0.609h-12q-0.797 0-1.406-0.609t-0.609-1.406v-12q0-0.797 0.609-1.383t1.406-0.586h12zM3.984 6v14.016h14.016v1.969h-14.016q-0.797 0-1.383-0.586t-0.586-1.383v-14.016h1.969z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -553,6 +553,9 @@ const ok = (info: any) => {
if (props.right && _time.getTime() / 100000 > start.value) { if (props.right && _time.getTime() / 100000 > start.value) {
emit("setDates", _time, "right"); emit("setDates", _time, "right");
} }
if (!(props.left && props.right)) {
emit("setDates", _time);
}
emit("ok", info === "h"); emit("ok", info === "h");
}; };
onMounted(() => { onMounted(() => {

50
src/components/Radio.vue Normal file
View File

@ -0,0 +1,50 @@
<!-- 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>
<el-radio-group v-model="selected" @change="checked">
<el-radio v-for="item in options" :key="item.value" :label="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import type { PropType } from "vue";
interface Option {
label: string;
value: string;
}
/*global defineProps, defineEmits */
const emit = defineEmits(["change"]);
const props = defineProps({
options: {
type: Array as PropType<(Option & { disabled: boolean })[]>,
default: () => [],
},
value: {
type: String as PropType<string>,
default: "",
},
size: { type: null, default: "default" },
});
const selected = ref<string>(props.value);
function checked(opt: string) {
emit("change", opt);
}
</script>

View File

@ -76,42 +76,6 @@ watch(
); );
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.icon {
width: 16px;
height: 16px;
vertical-align: middle;
fill: currentColor;
&.sm {
width: 14px;
height: 14px;
}
&.middle {
width: 18px;
height: 18px;
}
&.lg {
width: 24px;
height: 24px;
}
&.loading {
animation: loading 1.5s linear infinite;
}
&.logo {
height: 30px;
width: 110px;
}
&.xl {
height: 30px;
width: 30px;
}
}
.el-input__inner { .el-input__inner {
border-radius: unset !important; border-radius: unset !important;
} }

View File

@ -276,11 +276,11 @@ const ok = (leaveOpened: boolean) => {
}, 1); }, 1);
}; };
const setDates = (d: Date, pos: string) => { const setDates = (d: Date, pos: string) => {
if (pos === "left") { if (pos === "right") {
dates.value[0] = d; dates.value[1] = d;
return; return;
} }
dates.value[1] = d; dates.value[0] = d;
}; };
const dc = (e: any) => { const dc = (e: any) => {
show.value = (datepicker.value as any).contains(e.target) && !props.disabled; show.value = (datepicker.value as any).contains(e.target) && !props.disabled;

View File

@ -18,6 +18,7 @@ import Icon from "./Icon.vue";
import TimePicker from "./TimePicker.vue"; import TimePicker from "./TimePicker.vue";
import Selector from "./Selector.vue"; import Selector from "./Selector.vue";
import Graph from "./Graph.vue"; import Graph from "./Graph.vue";
import Radio from "./Radio.vue";
import type { App } from "vue"; import type { App } from "vue";
import VueGridLayout from "vue-grid-layout"; import VueGridLayout from "vue-grid-layout";
@ -27,6 +28,7 @@ const components: { [key: string]: any } = {
VueGridLayout, VueGridLayout,
Selector, Selector,
Graph, Graph,
Radio,
}; };
const componentsName: string[] = Object.keys(components); const componentsName: string[] = Object.keys(components);

View File

@ -0,0 +1,124 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const ProfileSegment = {
variable: "$segmentId: String",
query: `
segment: getProfiledSegment(segmentId: $segmentId) {
spans {
spanId
parentSpanId
serviceCode
startTime
endTime
endpointName
type
peer
component
isError
layer
tags {
key value
}
logs {
time
data {
key
value
}
}
}
}
`,
};
export const CreateProfileTask = {
variable: "$creationRequest: ProfileTaskCreationRequest",
query: `
task: createProfileTask(creationRequest: $creationRequest) {
id
errorReason
}
`,
};
export const GetProfileTaskList = {
variable: "$endpointName: String, $serviceId: ID",
query: `
taskList: getProfileTaskList(endpointName: $endpointName, serviceId: $serviceId) {
serviceId
endpointName
startTime
duration
minDurationThreshold
dumpPeriod
maxSamplingCount
id
logs {
id
instanceId
instanceName
operationType
operationTime
}
}
`,
};
export const GetProfileTaskSegmentList = {
variable: "$taskID: String",
query: `
segmentList: getProfileTaskSegmentList(taskID: $taskID) {
segmentId
endpointNames
start
duration
traceIds
isError
}
`,
};
export const GetProfileAnalyze = {
variable: "$segmentId: String!, $timeRanges: [ProfileAnalyzeTimeRange!]!",
query: `
analyze: getProfileAnalyze(segmentId: $segmentId, timeRanges: $timeRanges) {
tip
trees {
elements {
id
parentId
codeSignature
duration
durationChildExcluded
count
}
}
}
`,
};
export const GetProfileTaskLogs = {
variable: "$taskID: String",
query: `
taskLogs: getProfileTaskLogs(taskID: $taskID) {
id
instanceId
instanceName
operationTime
operationType
}
`,
};

View File

@ -22,6 +22,7 @@ import * as dashboard from "./query/dashboard";
import * as topology from "./query/topology"; import * as topology from "./query/topology";
import * as trace from "./query/trace"; import * as trace from "./query/trace";
import * as log from "./query/log"; import * as log from "./query/log";
import * as profile from "./query/profile";
const query: { [key: string]: string } = { const query: { [key: string]: string } = {
...app, ...app,
@ -30,6 +31,7 @@ const query: { [key: string]: string } = {
...topology, ...topology,
...trace, ...trace,
...log, ...log,
...profile,
}; };
class Graphql { class Graphql {
private queryData = ""; private queryData = "";

View File

@ -0,0 +1,39 @@
/**
* 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 {
ProfileSegment,
CreateProfileTask,
GetProfileTaskList,
GetProfileTaskSegmentList,
GetProfileAnalyze,
GetProfileTaskLogs,
} from "../fragments/profile";
export const queryProfileSegment = `query queryProfileSegment(${ProfileSegment.variable}) {${ProfileSegment.query}}`;
export const saveProfileTask = `mutation createProfileTask(${CreateProfileTask.variable}) {${CreateProfileTask.query}}`;
export const getProfileTaskList = `query getProfileTaskList(${GetProfileTaskList.variable}) {
${GetProfileTaskList.query}}`;
export const getProfileTaskSegmentList = `query getProfileTaskSegmentList(${GetProfileTaskSegmentList.variable}) {
${GetProfileTaskSegmentList.query}}`;
export const getProfileAnalyze = `query getProfileAnalyze(${GetProfileAnalyze.variable}) {${GetProfileAnalyze.query}}`;
export const getProfileTaskLogs = `query profileTaskLogs(${GetProfileTaskLogs.variable}) {${GetProfileTaskLogs.query}}`;

View File

@ -93,6 +93,8 @@ const msg = {
default: "Default", default: "Default",
topSlow: "Top 5 of slow", topSlow: "Top 5 of slow",
topChildren: "Top 5 of children", topChildren: "Top 5 of children",
taskList: "Task List",
sampledTraces: "Sampled Traces",
hourTip: "Select Hour", hourTip: "Select Hour",
minuteTip: "Select Minute", minuteTip: "Select Minute",
secondTip: "Select Second", secondTip: "Select Second",

View File

@ -93,6 +93,8 @@ const msg = {
topSlow: "迟缓的前5名", topSlow: "迟缓的前5名",
topChildren: "小孩数量的前5名", topChildren: "小孩数量的前5名",
showDepth: "展示深度选择器", showDepth: "展示深度选择器",
taskList: "任务列表",
sampledTraces: "采样的追踪",
hourTip: "选择小时", hourTip: "选择小时",
minuteTip: "选择分钟", minuteTip: "选择分钟",
secondTip: "选择秒数", secondTip: "选择秒数",

View File

@ -95,8 +95,8 @@ export const dashboardStore = defineStore({
showDepth: true, showDepth: true,
}; };
} }
if (type === "Trace") { if (type === "Trace" || type === "Profile") {
newItem.h = 24; newItem.h = 36;
} }
this.layout = this.layout.map((d: LayoutConfig) => { this.layout = this.layout.map((d: LayoutConfig) => {
d.y = d.y + newItem.h; d.y = d.y + newItem.h;

View File

@ -0,0 +1,212 @@
/**
* 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 { defineStore } from "pinia";
import { Duration } from "@/types/app";
import { Service } from "@/types/selector";
import {
TaskListItem,
SegmentSpan,
ProfileAnalyzationTrees,
TaskLog,
ProfileTaskCreationRequest,
} from "@/types/profile";
import { Trace, Span } from "@/types/trace";
import { store } from "@/store";
import graphql from "@/graphql";
import { AxiosResponse } from "axios";
import { useAppStoreWithOut } from "@/store/modules/app";
interface ProfileState {
services: Service[];
durationTime: Duration;
condition: { serviceId: string; endpointName: string };
taskList: TaskListItem[];
segmentList: Trace[];
currentSegment: Trace | Record<string, never>;
segmentSpans: SegmentSpan[];
currentSpan: SegmentSpan | Record<string, never>;
analyzeTrees: ProfileAnalyzationTrees;
taskLogs: TaskLog[];
highlightTop: boolean;
}
export const traceStore = defineStore({
id: "profile",
state: (): ProfileState => ({
services: [{ value: "0", label: "All" }],
durationTime: useAppStoreWithOut().durationTime,
condition: { serviceId: "", endpointName: "" },
taskList: [],
segmentList: [],
currentSegment: {},
segmentSpans: [],
currentSpan: {},
analyzeTrees: [],
taskLogs: [],
highlightTop: true,
}),
actions: {
setConditions(data: { serviceId?: string; endpointName?: string }) {
this.condition = {
...this.condition,
...data,
};
},
setCurrentSpan(span: Span) {
this.currentSpan = span;
},
setCurrentSegment(s: Trace) {
this.currentSegment = s;
},
setHighlightTop() {
this.highlightTop = !this.highlightTop;
},
async getServices(layer: string) {
const res: AxiosResponse = await graphql.query("queryServices").params({
layer,
});
if (res.data.errors) {
return res.data;
}
this.services = [
{ value: "0", label: "All" },
...res.data.data.services,
] || [{ value: "0", label: "All" }];
return res.data;
},
async getTaskList() {
const res: AxiosResponse = await graphql
.query("getProfileTaskList")
.params(this.condition);
if (res.data.errors) {
return res.data;
}
const list = res.data.data.taskList;
this.taskList = list;
if (!list.length) {
this.segmentList = [];
this.segmentSpans = [];
this.analyzeTrees = [];
return res.data;
}
this.getSegmentList({ taskID: list[0].id });
return res.data;
},
async getSegmentList(params: { taskID: string }) {
const res: AxiosResponse = await graphql
.query("getProfileTaskSegmentList")
.params(params);
if (res.data.errors) {
this.segmentList = [];
return res.data;
}
const { segmentList } = res.data.data;
this.segmentList = segmentList;
if (!segmentList.length) {
this.segmentSpans = [];
this.analyzeTrees = [];
return res.data;
}
if (segmentList[0]) {
this.currentSegment = segmentList[0];
this.getSegmentSpans({ segmentId: segmentList[0].segmentId });
} else {
this.currentSegment = null;
}
return res.data;
},
async getSegmentSpans(params: { segmentId: string }) {
const res: AxiosResponse = await graphql
.query("queryProfileSegment")
.params(params);
if (res.data.errors) {
this.segmentSpans = [];
return res.data;
}
const { segment } = res.data.data;
if (!segment) {
this.segmentSpans = [];
this.analyzeTrees = [];
return res.data;
}
this.segmentSpans = segment.spans;
if (!(segment.spans && segment.spans.length)) {
this.analyzeTrees = [];
return res.data;
}
const index = segment.spans.length - 1 || 0;
this.currentSpan = segment.spans[index];
return res.data;
},
async getProfileAnalyze(params: {
segmentId: string;
timeRanges: Array<{ start: number; end: number }>;
}) {
const res: AxiosResponse = await graphql
.query("getProfileAnalyze")
.params(params);
if (res.data.errors) {
this.analyzeTrees = [];
return res.data;
}
const { analyze, tip } = res.data.data;
if (tip) {
this.analyzeTrees = [];
return res.data;
}
if (!analyze) {
this.analyzeTrees = [];
return res.data;
}
this.analyzeTrees = analyze.trees;
return res.data;
},
async createTask(param: ProfileTaskCreationRequest) {
const res: AxiosResponse = await graphql
.query("saveProfileTask")
.params({ creationRequest: param });
if (res.data.errors) {
return res.data;
}
this.getTaskList();
return res.data;
},
async getTaskLogs(param: { taskID: string }) {
const res: AxiosResponse = await graphql
.query("getProfileTaskLogs")
.params(param);
if (res.data.errors) {
return res.data;
}
this.taskLogs = res.data.data.taskLogs;
return res.data;
},
},
});
export function useProfileStore(): any {
return traceStore(store);
}

View File

@ -30,6 +30,10 @@
overflow: hidden; overflow: hidden;
} }
.wba {
word-break: break-all;
}
.cp { .cp {
cursor: pointer; cursor: pointer;
} }

70
src/types/profile.d.ts vendored Normal file
View 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.
*/
type ProfileStackElement = {
id: string;
parentId: string;
codeSignature: string;
duration: number;
durationChildExcluded: number;
count: number;
};
export type ProfileAnalyzationTrees = { elements: ProfileStackElement[] }[];
export interface TaskLog {
id: string;
instanceId: string;
instanceName: string;
operationTime: number;
operationType: string;
}
export interface TaskListItem {
id: string;
serviceId: string;
serviceName: string;
endpointName: string;
startTime: number;
duration: number;
minDurationThreshold: number;
dumpPeriod: number;
maxSamplingCount: number;
logs: TaskLog[];
}
export interface SegmentSpan {
spanId: string;
parentSpanId: string;
serviceCode: string;
serviceInstanceName: string;
startTime: number;
endTime: number;
endpointName: string;
type: string;
peer: string;
component: string;
isError: boolean;
layer: string;
tags: any[];
logs: any[];
}
export interface ProfileTaskCreationRequest {
serviceId: string;
endpointName: string;
startTime: number;
duration: number;
minDurationThreshold: number;
dumpPeriod: number;
maxSamplingCount: number;
}

View File

@ -22,6 +22,7 @@ export interface Trace {
operationNames: string[]; operationNames: string[];
start: string; start: string;
traceIds: Array<string | any>; traceIds: Array<string | any>;
segmentId: string;
} }
export interface Span { export interface Span {

View File

@ -0,0 +1,89 @@
<!-- 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="profile-wrapper flex-v">
<el-popover placement="bottom" trigger="click" :width="100">
<template #reference>
<span class="delete cp">
<Icon iconName="ellipsis_v" size="middle" class="operation" />
</span>
</template>
<div class="tools" @click="removeWidget">
<span>{{ t("delete") }}</span>
</div>
</el-popover>
<Header />
<Content />
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import Header from "../related/profile/Header.vue";
import Content from "../related/profile/Content.vue";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<any>,
default: () => ({ graph: {} }),
},
activeIndex: { type: String, default: "" },
});
const { t } = useI18n();
const dashboardStore = useDashboardStore();
function removeWidget() {
dashboardStore.removeControls(props.data);
}
</script>
<style lang="scss" scoped>
.profile-wrapper {
width: 100%;
height: 100%;
font-size: 12px;
position: relative;
}
.delete {
position: absolute;
top: 5px;
right: 3px;
}
.header {
padding: 10px;
font-size: 12px;
border-bottom: 1px solid #dcdfe6;
}
.tools {
padding: 5px 0;
color: #999;
cursor: pointer;
position: relative;
text-align: center;
&:hover {
color: #409eff;
background-color: #eee;
}
}
.trace {
width: 100%;
overflow: auto;
}
</style>

View File

@ -20,8 +20,8 @@ limitations under the License. -->
<Icon iconName="ellipsis_v" size="middle" class="operation" /> <Icon iconName="ellipsis_v" size="middle" class="operation" />
</span> </span>
</template> </template>
<div class="tools"> <div class="tools" @click="removeWidget">
<span @click="removeWidget">{{ t("delete") }}</span> <span>{{ t("delete") }}</span>
</div> </div>
</el-popover> </el-popover>
<div class="header"> <div class="header">

View File

@ -18,5 +18,6 @@ import Topology from "./Topology.vue";
import Tab from "./Tab.vue"; import Tab from "./Tab.vue";
import Widget from "./Widget.vue"; import Widget from "./Widget.vue";
import Trace from "./Trace.vue"; import Trace from "./Trace.vue";
import Profile from "./Profile.vue";
export default { Tab, Widget, Trace, Topology }; export default { Tab, Widget, Trace, Topology, Profile };

View File

@ -167,6 +167,7 @@ export const ToolIcons = [
{ name: "all_inbox", content: "Add Tab", id: "addTab" }, { name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "device_hub", content: "Add Topology", id: "topology" }, { name: "device_hub", content: "Add Topology", id: "topology" },
{ name: "merge", content: "Add Trace", id: "trace" }, { name: "merge", content: "Add Trace", id: "trace" },
{ name: "timeline", content: "Add Profile", id: "profile" },
// { name: "save_alt", content: "Export", id: "export" }, // { name: "save_alt", content: "Export", id: "export" },
// { name: "folder_open", content: "Import", id: "import" }, // { name: "folder_open", content: "Import", id: "import" },
// { name: "settings", content: "Settings", id: "settings" }, // { name: "settings", content: "Settings", id: "settings" },
@ -209,7 +210,7 @@ export const Status = [
{ label: "Error", value: "ERROR" }, { label: "Error", value: "ERROR" },
]; ];
export const QueryOrders = [ export const QueryOrders = [
{ label: "startTime", value: "BY_START_TIME" }, { label: "Start Time", value: "BY_START_TIME" },
{ label: "duration", value: "BY_DURATION" }, { label: "Duration", value: "BY_DURATION" },
]; ];
export const TraceEntitys = ["All", "Service", "ServiceInstance", "Endpoint"]; export const TraceEntitys = ["All", "Service", "ServiceInstance", "Endpoint"];

View File

@ -31,7 +31,7 @@ limitations under the License. -->
:key="item.i" :key="item.i"
@click="clickGrid(item)" @click="clickGrid(item)"
:class="{ active: dashboardStore.activedGridItem === item.i }" :class="{ active: dashboardStore.activedGridItem === item.i }"
drag-ignore-from="svg.d3-trace-tree" drag-ignore-from="svg.d3-trace-tree, .dragger"
> >
<component :is="item.type" :data="item" /> <component :is="item.type" :data="item" />
</grid-item> </grid-item>

View File

@ -84,10 +84,13 @@ limitations under the License. -->
size="sm" size="sm"
:iconName="t.name" :iconName="t.name"
v-if=" v-if="
!['topology', 'trace'].includes(t.id) || !['topology', 'trace', 'profile'].includes(t.id) ||
(t.id === 'topology' && (t.id === 'topology' &&
hasTopology.includes(dashboardStore.entity)) || hasTopology.includes(dashboardStore.entity)) ||
(t.id === 'trace' && TraceEntitys.includes(dashboardStore.entity)) (t.id === 'trace' &&
TraceEntitys.includes(dashboardStore.entity)) ||
(t.id === 'profile' &&
dashboardStore.entity === EntityType[0].value)
" "
/> />
</span> </span>
@ -295,6 +298,9 @@ function clickIcons(t: { id: string; content: string; name: string }) {
case "trace": case "trace":
dashboardStore.addControl("Trace"); dashboardStore.addControl("Trace");
break; break;
case "profile":
dashboardStore.addControl("Profile");
break;
case "topology": case "topology":
dashboardStore.addControl("Topology"); dashboardStore.addControl("Topology");
break; break;

View File

@ -0,0 +1,83 @@
<!-- 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 content">
<div class="list">
<TaskList />
<SegmentList />
</div>
<div class="item">
<SpanTree @loading="loadTrees" />
<div class="thread-stack">
<StackTable
v-if="profileStore.analyzeTrees.length"
:data="profileStore.analyzeTrees"
:highlightTop="profileStore.highlightTop"
/>
<div class="t-loading" v-show="loading">
<Icon :loading="true" iconName="spinner" size="middle" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import TaskList from "./components/TaskList.vue";
import SegmentList from "./components/SegmentList.vue";
import SpanTree from "./components/SpanTree.vue";
import StackTable from "./components/Stack/Index.vue";
import { useProfileStore } from "@/store/modules/profile";
const loading = ref<boolean>(false);
const profileStore = useProfileStore();
function loadTrees(l: boolean) {
loading.value = l;
}
</script>
<style lang="scss" scoped>
.content {
height: calc(100% - 30px);
width: 100%;
}
.item {
height: 100%;
width: calc(100% - 300px);
}
.list {
width: 300px;
height: 100%;
}
.thread-stack {
padding: 5px 12px;
height: calc(50% - 50px);
overflow: auto;
width: 100%;
}
.t-loading {
text-align: center;
position: absolute;
width: 100%;
height: 70px;
margin-top: 40px;
line-height: 88px;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,117 @@
<!-- 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 header">
<!-- <div class="mr-10" v-if="dashboardStore.entity==='All'">
<span class="grey mr-5">{{ t("service") }}:</span>
<Selector
size="small"
:value="service.value"
:options="profileStore.services"
placeholder="Select a service"
@change="changeService"
/>
</div> -->
<div class="mr-10">
<span class="grey mr-5">{{ t("endpointName") }}:</span>
<el-input v-model="endpointName" class="name" />
</div>
<el-button
class="search-btn"
size="small"
type="primary"
@click="searchTasks"
>
{{ t("search") }}
</el-button>
<el-button class="search-btn" size="small" @click="createTask">
{{ t("newTask") }}
</el-button>
</div>
<el-dialog
v-model="newTask"
:destroy-on-close="true"
fullscreen
@closed="newTask = false"
>
<NewTask @close="newTask = false" />
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useProfileStore } from "@/store/modules/profile";
import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus";
import NewTask from "./components/NewTask.vue";
const profileStore = useProfileStore();
const selectorStore = useSelectorStore();
const { t } = useI18n();
// const service = ref<any>({});
const endpointName = ref<string>("");
const newTask = ref<boolean>(false);
searchTasks();
// getServices();
// async function getServices() {
// const res = await profileStore.getServices(dashboardStore.layerId);
// if (res.errors) {
// ElMessage.error(res.errors);
// return;
// }
// service.value = profileStore.services[0];
// searchTasks();
// }
// function changeService(opt: any[]) {
// service.value = opt[0];
// }
async function searchTasks() {
profileStore.setConditions({
serviceId: selectorStore.currentService.id,
endpointName: endpointName.value,
});
const res = await profileStore.getTaskList();
if (res.errors) {
ElMessage.error(res.errors);
}
}
function createTask() {
newTask.value = true;
}
watch(
() => selectorStore.currentService,
() => {
searchTasks();
}
);
</script>
<style lang="scss" scoped>
.header {
padding: 10px;
font-size: 12px;
border-bottom: 1px solid #dcdfe6;
}
.name {
width: 270px;
}
</style>

View File

@ -0,0 +1,171 @@
<!-- 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="profile-task">
<div>
<div class="label">{{ t("endpointName") }}</div>
<el-input v-model="endpointName" class="profile-input" />
</div>
<div>
<div class="label">{{ t("monitorTime") }}</div>
<div>
<Radio
class="mb-5"
:value="monitorTime"
:options="InitTaskField.monitorTimeEn"
@change="changeMonitorTime"
/>
<span class="date">
<TimePicker
:value="time"
position="bottom"
format="YYYY-MM-DD HH:mm:ss"
@input="changeTimeRange"
/>
</span>
</div>
</div>
<div>
<div class="label">{{ t("monitorDuration") }}</div>
<Radio
class="mb-5"
:value="monitorDuration"
:options="InitTaskField.monitorDuration"
@change="changeMonitorDuration"
/>
</div>
<div>
<div class="label">{{ t("minThreshold") }} (ms)</div>
<el-input-number class="profile-input" :min="0" v-model="minThreshold" />
</div>
<div>
<div class="label">{{ t("dumpPeriod") }}</div>
<Radio
class="mb-5"
:value="dumpPeriod"
:options="InitTaskField.dumpPeriod"
@change="changeDumpPeriod"
/>
</div>
<div>
<div class="label">{{ t("maxSamplingCount") }}</div>
<Selector
size="small"
:value="maxSamplingCount"
:options="InitTaskField.maxSamplingCount"
placeholder="Select a data"
@change="changeMaxSamplingCount"
class="profile-input"
/>
</div>
<div>
<el-button @click="createTask" type="primary" class="create-task-btn">
{{ t("createTask") }}
</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useProfileStore } from "@/store/modules/profile";
import { useSelectorStore } from "@/store/modules/selectors";
import { useAppStoreWithOut } from "@/store/modules/app";
import { ElMessage } from "element-plus";
import { InitTaskField } from "./data";
/* global defineEmits */
const emits = defineEmits(["close"]);
const profileStore = useProfileStore();
const selectorStore = useSelectorStore();
const appStore = useAppStoreWithOut();
const { t } = useI18n();
const endpointName = ref<string>("");
const monitorTime = ref<string>(InitTaskField.monitorTimeEn[0].value);
const monitorDuration = ref<string>(InitTaskField.monitorDuration[0].value);
const time = ref<Date>(appStore.durationRow.start);
const minThreshold = ref<number>(0);
const dumpPeriod = ref<string>(InitTaskField.dumpPeriod[0].value);
const maxSamplingCount = ref<string>(InitTaskField.maxSamplingCount[0].value);
function changeMonitorTime(opt: string) {
monitorTime.value = opt;
}
function changeMonitorDuration(val: string) {
monitorDuration.value = val;
}
function changeDumpPeriod(val: string) {
dumpPeriod.value = val;
}
function changeMaxSamplingCount(opt: any[]) {
maxSamplingCount.value = opt[0].value;
}
async function createTask() {
emits("close");
const date =
monitorTime.value === "0" ? appStore.durationRow.start : time.value;
const params = {
serviceId: selectorStore.currentService.id,
endpointName: endpointName.value,
startTime: date.getTime(),
duration: Number(monitorDuration.value),
minDurationThreshold: Number(minThreshold.value),
dumpPeriod: Number(dumpPeriod.value),
maxSamplingCount: Number(maxSamplingCount.value),
};
const res = await profileStore.createTask(params);
if (res.errors) {
ElMessage.error(res.errors);
return;
}
const { tip } = res.data;
if (tip) {
ElMessage.error(tip);
return;
}
ElMessage.success("Task created successfully");
}
function changeTimeRange(val: Date) {
time.value = val;
}
</script>
<style lang="scss" scoped>
.profile-task {
margin: 0 auto;
width: 350px;
}
.date {
font-size: 12px;
}
.label {
margin-top: 10px;
font-size: 14px;
}
.profile-input {
width: 300px;
}
.create-task-btn {
width: 300px;
margin-top: 50px;
}
</style>

View File

@ -0,0 +1,156 @@
<!-- 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="profile-trace-wrapper profile-segment flex-v">
<div class="profile-t-tool flex-h">{{ t("sampledTraces") }}</div>
<div class="profile-t-wrapper">
<div class="no-data" v-show="!profileStore.segmentList.length">
{{ t("noData") }}
</div>
<table class="profile-t">
<tr
class="profile-tr cp"
v-for="(i, index) in profileStore.segmentList"
@click="selectTrace(i)"
:key="index"
>
<td
class="profile-td"
:class="{
selected: selectedKey == i.segmentId,
}"
>
<div
class="ell mb-5"
:class="{
blue: !i.isError,
red: i.isError,
}"
>
<span class="b">{{ i.endpointNames[0] }}</span>
</div>
<div class="grey ell sm">
<span class="tag mr-10 sm"> {{ i.duration }} ms </span>
{{ dateFormat(parseInt(i.start)) }}
</div>
</td>
</tr>
</table>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
import { useProfileStore } from "@/store/modules/profile";
import { Trace } from "@/types/trace";
import { ElMessage } from "element-plus";
const { t } = useI18n();
const profileStore = useProfileStore();
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
const selectedKey = ref<string>("");
async function selectTrace(item: Trace) {
profileStore.setCurrentSegment(item);
selectedKey.value = item.segmentId;
const res = await profileStore.getSegmentSpans({ segmentId: item.segmentId });
if (res.errors) {
ElMessage.error(res.errors);
}
}
</script>
<style lang="scss" scoped>
.profile-trace-wrapper {
width: 300px;
height: 50%;
overflow: auto;
.no-data {
text-align: center;
margin-top: 10px;
}
.profile-t-wrapper {
overflow: auto;
flex-grow: 1;
border-right: 1px solid rgba(0, 0, 0, 0.1);
}
.profile-t-loading {
text-align: center;
position: absolute;
width: 100%;
height: 70px;
margin-top: 40px;
line-height: 88px;
overflow: hidden;
.icon {
width: 30px;
height: 30px;
}
}
.profile-t {
width: 100%;
border-spacing: 0;
table-layout: fixed;
flex-grow: 1;
position: relative;
}
.profile-tr {
&:hover {
background-color: rgba(0, 0, 0, 0.04);
}
}
.profile-td {
padding: 5px 10px;
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
&.selected {
background-color: #ededed;
}
}
.profile-t-tool {
padding: 5px 10px;
font-weight: bold;
border-right: 1px solid rgba(0, 0, 0, 0.07);
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
background: #f3f4f9;
}
.log-item {
margin-top: 20px;
}
.profile-btn {
color: #3d444f;
padding: 1px 3px;
border-radius: 2px;
font-size: 12px;
}
}
.profile-segment {
border-top: 1px solid rgba(0, 0, 0, 0.07);
}
</style>

View File

@ -0,0 +1,173 @@
<!-- 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="profile-trace-dashboard" v-if="profileStore.currentSegment">
<div class="profile-trace-detail-wrapper">
<Selector
size="small"
:value="traceId || (traceIds[0] && traceIds[0].value) || ''"
:options="traceIds"
placeholder="Select a trace id"
@change="changeTraceId"
class="profile-trace-detail-ids mr-10"
/>
<Selector
size="small"
:value="mode"
:options="ProfileMode"
placeholder="Select a mode"
@change="spanModeChange"
class="mr-10"
/>
<el-button type="primary" size="small" @click="analyzeProfile()">
{{ t("analyze") }}
</el-button>
</div>
<Table
:data="profileStore.segmentSpans"
:traceId="
profileStore.currentSegment.traceIds &&
profileStore.currentSegment.traceIds[0]
"
:showBtnDetail="true"
headerType="profile"
@select="selectSpan"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import Table from "../../trace/components/Table/Index.vue";
import { useProfileStore } from "@/store/modules/profile";
import Selector from "@/components/Selector.vue";
import { Span } from "@/types/trace";
import { Option } from "@/types/app";
import { ElMessage } from "element-plus";
import { ProfileMode } from "./data";
/* global defineEmits*/
const emits = defineEmits(["loading"]);
const { t } = useI18n();
const profileStore = useProfileStore();
const mode = ref<string>("include");
const message = ref<string>("");
const timeRange = ref<Array<{ start: number; end: number }>>([]);
const traceId = ref<string>("");
const traceIds = computed(() =>
(profileStore.currentSegment.traceIds || []).map((id: string) => ({
label: id,
value: id,
}))
);
function selectSpan(span: Span) {
profileStore.setCurrentSpan(span);
}
function spanModeChange(item: Option[]) {
mode.value = item[0].value;
updateTimeRange();
}
function changeTraceId(opt: Option[]) {
traceId.value = opt[0].value;
}
async function analyzeProfile() {
emits("loading", true);
updateTimeRange();
const res = await profileStore.getProfileAnalyze({
segmentId: profileStore.currentSegment.segmentId,
timeRanges: timeRange.value,
});
emits("loading", false);
if (res.errors) {
ElMessage.error(res.errors);
}
if (res.tip) {
message.value = res.tip;
}
}
function updateTimeRange() {
if (mode.value === "include") {
timeRange.value = [
{
start: profileStore.currentSpan.startTime,
end: profileStore.currentSpan.endTime,
},
];
} else {
const { children, startTime, endTime } = profileStore.currentSpan;
let dateRange = [];
if (!children || !children.length) {
timeRange.value = [
{
start: this.currentSpan.startTime,
end: this.currentSpan.endTime,
},
];
return;
}
for (const item of children) {
dateRange.push(
{
start: startTime,
end: item.startTime,
},
{
start: item.endTime,
end: endTime,
}
);
}
dateRange = dateRange.reduce((prev: any[], cur) => {
let isUpdate = false;
for (const item of prev) {
if (cur.start <= item.end && item.start <= cur.start) {
isUpdate = true;
item.start = item.start < cur.start ? cur.start : item.start;
item.end = item.end < cur.end ? item.end : cur.end;
}
}
if (!isUpdate) {
prev.push(cur);
}
return prev;
}, []);
timeRange.value = dateRange.filter((item: any) => item.start !== item.end);
}
}
</script>
<style lang="scss" scoped>
.profile-trace-dashboard {
padding: 5px;
flex-shrink: 0;
height: 50%;
overflow: auto;
width: 100%;
}
.profile-trace-detail-wrapper {
padding: 5px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.profile-trace-detail-ids {
width: 300px;
}
</style>

View File

@ -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. -->
<template>
<div class="profile">
<div class="profile-header">
<div class="thread" :style="`width: ${thread}px`">
Thread Stack
<span class="r cp dragger" ref="dragger">
<Icon iconName="settings_ethernet" />
</span>
</div>
<div class="self">Duration (ms)</div>
<div class="exec-ms">
Self Duration (ms)
<a
class="profile-set-btn"
@click="updateHighlightTop"
title="Highlight top 10 slow methods"
:style="{ color: highlightTop ? '#448dfe' : '#484b55' }"
>
top slow
</a>
</div>
<div class="dump-count">Dump Count</div>
</div>
<TableItem
:thread="thread"
v-for="(item, index) in tableData"
:data="item"
:key="'key' + index"
/>
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import { useProfileStore } from "@/store/modules/profile";
import { ref, onMounted } from "vue";
import type { PropType } from "vue";
import TableItem from "./Item.vue";
/* global defineProps */
defineProps({
tableData: { type: Array as PropType<any>, default: () => [] },
highlightTop: { type: Boolean, default: false },
});
const dragger = ref<any>(null);
const thread = ref<number>(500);
const profileStore = useProfileStore();
onMounted(() => {
dragger.value.onmousedown = (event: any) => {
const diffX = event.clientX;
const copy = thread.value;
document.onmousemove = (documentEvent) => {
const moveX = documentEvent.clientX - diffX;
thread.value = copy + moveX;
};
document.onmouseup = () => {
document.onmousemove = null;
document.onmouseup = null;
};
};
});
function updateHighlightTop() {
profileStore.setHighlightTop();
}
</script>
<style lang="scss" scoped>
@import "./profile.scss";
.dragger {
float: right;
}
.profile {
font-size: 12px;
height: 100%;
.profile-set-btn {
font-size: 12px;
border: 1px solid #ccc;
border-radius: 3px;
text-align: center;
width: 57px;
overflow: hidden;
display: inline-block;
height: 20px;
line-height: 20px;
position: absolute;
top: 4px;
right: 3px;
padding: 0 3px;
}
}
.profile-header {
white-space: nowrap;
user-select: none;
border-left: 0;
border-right: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.profile-header div {
display: inline-block;
padding: 0 4px;
border-right: 1px dotted silver;
line-height: 30px;
background-color: #f3f4f9;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -0,0 +1,109 @@
<!-- 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="profile-detail-chart-table">
<Container :tableData="tableData" :highlightTop="highlightTop">
<div class="profile-tips" v-if="!tableData.length">{{ t("noData") }}</div>
</Container>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { ref, onMounted, watch } from "vue";
import type { PropType } from "vue";
import Container from "./Container.vue";
const { t } = useI18n();
/* global defineProps */
const props = defineProps({
data: { type: Array as PropType<any>, default: () => [] },
highlightTop: { type: Boolean, default: false },
});
const tableData = ref<any>([]);
onMounted(() => {
tableData.value = processTree();
});
function processTree() {
if (!props.data.length) {
return [];
}
const durationChildExcluded = props.data
.map((d: any) => {
return d.elements.map((item: any) => item.durationChildExcluded);
})
.flat(1);
function compare(val: number, val1: number) {
return val1 - val;
}
const topDur = durationChildExcluded
.sort(compare)
.filter((item: any, index: number) => index < 10 && item !== 0);
const trees = [];
for (const item of props.data) {
const newArr = sortArr(item.elements, topDur);
trees.push(...newArr);
}
return trees;
}
function sortArr(arr: any[], topDur: any) {
const copyArr = JSON.parse(JSON.stringify(arr));
const obj: any = {};
const res = [];
for (const item of copyArr) {
obj[item.id] = item;
}
for (const item of copyArr) {
item.topDur =
topDur.includes(item.durationChildExcluded) && props.highlightTop;
if (item.parentId === "0") {
res.push(item);
}
for (const key in obj) {
if (item.id === obj[key].parentId) {
if (item.children) {
item.children.push(obj[key]);
} else {
item.children = [obj[key]];
}
}
}
}
return res;
}
watch(
() => [props.data, props.highlightTop],
() => {
if (!props.data.length) {
tableData.value = [];
return;
}
tableData.value = processTree();
}
);
</script>
<style lang="scss" scoped>
.profile-detail-chart-table {
height: 100%;
overflow: auto;
}
</style>

View File

@ -0,0 +1,153 @@
<!-- 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>
<div
:class="['profile-item', 'level' + data.parentId]"
:style="{ color: data.topDur ? '#448dfe' : '#3d444f' }"
>
<div
:class="['thread', 'level' + data.parentId]"
:style="{
'text-indent': data.parentId * 4 + 'px',
width: `${thread}px`,
}"
>
<Icon
class="icon vm cp trans"
:style="!displayChildren ? 'transform: rotate(-90deg);' : ''"
@click.stop="toggle"
v-if="data.children && data.children.length"
iconName="arrow-down"
/>
<el-tooltip :content="data.codeSignature" placement="bottom">
<span>
{{ data.codeSignature }}
</span>
</el-tooltip>
</div>
<div class="self">{{ data.duration }}</div>
<div class="exec-ms">{{ data.durationChildExcluded }}</div>
<div class="dump-count">{{ data.count }}</div>
</div>
<div
v-show="data.children && data.children.length && displayChildren"
class="children-trace"
>
<table-item
:thread="thread"
v-for="(item, index) in data.children"
:key="index"
:data="item"
/>
</div>
</div>
</template>
<script lang="ts">
import { ref, defineComponent, toRefs } from "vue";
import type { PropType } from "vue";
const props = {
data: { type: Object as PropType<any>, default: () => ({}) },
thread: { type: Number, default: 0 },
};
export default defineComponent({
name: "TableItem",
props,
setup(props) {
const displayChildren = ref<boolean>(true);
function toggle() {
displayChildren.value = !displayChildren.value;
}
return { toggle, displayChildren, ...toRefs(props) };
},
});
</script>
<style lang="scss" scoped>
@import "./profile.scss";
.profile-item.level0 {
background: rgba(0, 0, 0, 0.04);
color: #448dfe;
&:hover {
background: rgba(0, 0, 0, 0.04);
color: #448dfe;
}
&::before {
position: absolute;
content: "";
width: 5px;
height: 100%;
background: #448dfe;
left: 0;
}
}
.profile-item {
position: relative;
white-space: nowrap;
}
.profile-item.selected {
background: rgba(0, 0, 0, 0.04);
}
.profile-item:not(.level0):hover {
background: rgba(0, 0, 0, 0.04);
}
.profile-item > div {
display: inline-block;
padding: 0 5px;
border: 1px solid transparent;
border-right: 1px dotted silver;
overflow: hidden;
line-height: 30px;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-item > div.method {
padding-left: 10px;
}
.profile-item div.exec-percent {
width: 10%;
height: 30px;
padding: 0 8px;
.outer-progress_bar {
width: 100%;
height: 6px;
border-radius: 3px;
background: rgb(63, 177, 227);
position: relative;
margin-top: 11px;
border: none;
}
.inner-progress_bar {
position: absolute;
background: rgb(110, 64, 170);
height: 4px;
border-radius: 2px;
left: 0;
border: none;
top: 1px;
}
}
</style>

View File

@ -0,0 +1,28 @@
/**
* 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.
*/
.dump-count {
width: 100px;
}
.exec-ms {
width: 200px;
position: relative;
}
.self {
width: 100px;
}

View File

@ -0,0 +1,246 @@
<!-- 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="profile-task-list flex-v">
<div class="profile-task-wrapper flex-v">
<div class="profile-t-tool flex-h">{{ t("taskList") }}</div>
<div class="profile-t-wrapper">
<div class="no-data" v-show="!profileStore.taskList.length">
{{ t("noData") }}
</div>
<table class="profile-t">
<tr
class="profile-tr cp"
v-for="(i, index) in profileStore.taskList"
@click="changeTask(i)"
:key="index"
>
<td
class="profile-td"
:class="{
selected: selectedTask.id === i.id,
}"
>
<div class="ell">
<span>{{ i.endpointName }}</span>
<a class="profile-btn r" @click="viewTask($event, i)">
<Icon iconName="library_books" size="middle" />
</a>
</div>
<div class="grey ell sm">
<span class="mr-10 sm">{{ dateFormat(i.startTime) }}</span>
<span class="mr-10 sm">
{{ dateFormat(i.startTime + i.duration * 60 * 1000) }}
</span>
</div>
</td>
</tr>
</table>
</div>
</div>
</div>
<el-dialog
v-model="viewDetail"
:destroy-on-close="true"
fullscreen
@closed="viewDetail = false"
>
<div class="profile-detail flex-v">
<div>
<h5 class="mb-10">{{ t("task") }}.</h5>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("service") }}:</span>
<span class="g-sm-8 wba">{{ service }}</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("endpoint") }}:</span>
<span class="g-sm-8 wba">{{ selectedTask.endpointName }}</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("monitorTime") }}:</span>
<span class="g-sm-8 wba">
{{ dateFormat(selectedTask.startTime) }}
</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("monitorDuration") }}:</span
><span class="g-sm-8 wba">{{ selectedTask.duration }} min</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("minThreshold") }}:</span>
<span class="g-sm-8 wba">
{{ selectedTask.minDurationThreshold }} ms
</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("dumpPeriod") }}:</span>
<span class="g-sm-8 wba">{{ selectedTask.dumpPeriod }}</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("maxSamplingCount") }}:</span>
<span class="g-sm-8 wba">{{ selectedTask.maxSamplingCount }}</span>
</div>
</div>
<div>
<h5
class="mb-10 mt-10"
v-show="selectedTask.logs && selectedTask.logs.length"
>
{{ t("logs") }}.
</h5>
<div
class="log-item"
v-for="(i, index) in Object.keys(instanceLogs)"
:key="index"
>
<div class="mb-10 sm">
<span class="mr-10 grey">{{ t("instance") }}:</span>
<span>{{ i }}</span>
</div>
<div v-for="(d, index) in instanceLogs[i]" :key="index">
<span class="mr-10 grey">{{ t("operationType") }}:</span>
<span class="mr-20">{{ d.operationType }}</span>
<span class="mr-10 grey">{{ t("time") }}:</span>
<span>{{ dateFormat(d.operationTime) }}</span>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
import { useProfileStore } from "@/store/modules/profile";
import { TaskLog, TaskListItem } from "@/types/profile";
import { ElMessage } from "element-plus";
const { t } = useI18n();
const profileStore = useProfileStore();
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
const viewDetail = ref<boolean>(false);
const service = ref<string>("");
const selectedTask = ref<TaskListItem | Record<string, never>>({});
const instanceLogs = ref<TaskLog | any>({});
async function changeTask(item: TaskListItem) {
selectedTask.value = item;
const res = await profileStore.getSegmentList({ taskID: item.id });
if (res.errors) {
ElMessage.error(res.errors);
}
}
async function viewTask(e: Event, item: TaskListItem) {
window.event ? (window.event.cancelBubble = true) : e.stopPropagation();
viewDetail.value = true;
selectedTask.value = item;
service.value = (
profileStore.services.filter((s: any) => s.id === item.serviceId)[0] || {}
).label;
const res = await profileStore.getTaskLogs({ taskID: item.id });
if (res.errors) {
ElMessage.error(res.errors);
return;
}
item.logs = profileStore.taskLogs;
instanceLogs.value = {};
for (const d of item.logs) {
if (instanceLogs.value[d.instanceName]) {
instanceLogs.value[d.instanceName].push({
operationType: d.operationType,
operationTime: d.operationTime,
});
} else {
instanceLogs.value[d.instanceName] = [
{ operationType: d.operationType, operationTime: d.operationTime },
];
}
}
selectedTask.value = item;
}
</script>
<style lang="scss" scoped>
.profile-task-list {
width: 300px;
height: calc((100% - 60px) / 2);
overflow: auto;
}
.item span {
height: 21px;
}
.profile-td {
padding: 5px 10px;
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
&.selected {
background-color: #ededed;
}
}
.no-data {
text-align: center;
margin-top: 10px;
}
.profile-t-wrapper {
overflow: auto;
flex-grow: 1;
border-right: 1px solid rgba(0, 0, 0, 0.1);
}
.profile-t {
width: 100%;
border-spacing: 0;
table-layout: fixed;
flex-grow: 1;
position: relative;
}
.profile-tr {
&:hover {
background-color: rgba(0, 0, 0, 0.04);
}
}
.profile-segment {
border-top: 1px solid rgba(0, 0, 0, 0.07);
}
.profile-t-tool {
padding: 5px 10px;
font-weight: bold;
border-right: 1px solid rgba(0, 0, 0, 0.07);
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
background: #f3f4f9;
}
.log-item {
margin-top: 20px;
}
.profile-btn {
color: #3d444f;
padding: 1px 3px;
border-radius: 2px;
font-size: 12px;
float: right;
}
</style>

View File

@ -0,0 +1,63 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const ProfileMode: any[] = [
{ label: "Include Children", value: "include" },
{ label: "Exclude Children", value: "exclude" },
];
export const NewTaskField = {
service: { key: "", label: "None" },
monitorTime: { key: "0", label: "monitor now" },
monitorDuration: { key: 5, label: "5 min" },
minThreshold: 0,
dumpPeriod: { key: 10, label: "10ms" },
endpointName: "",
maxSamplingCount: { key: 5, label: "5" },
};
export const InitTaskField = {
serviceSource: [{ key: "", label: "None" }],
monitorTimeEn: [
{ value: "0", label: "monitor now" },
{ value: "1", label: "set start time" },
],
monitorTimeCn: [
{ value: "0", label: "此刻" },
{ value: "1", label: "设置时间" },
],
monitorDuration: [
{ value: "5", label: "5 min" },
{ value: "10", label: "10 min" },
{ value: "15", label: "15 min" },
],
dumpPeriod: [
{ value: "10", label: "10 ms" },
{ value: "20", label: "20 ms" },
{ value: "50", label: "50 ms" },
{ value: "100", label: "100 ms" },
],
maxSamplingCount: [
{ value: "1", label: "1" },
{ value: "2", label: "2" },
{ value: "3", label: "3" },
{ value: "4", label: "4" },
{ value: "5", label: "5" },
{ value: "6", label: "6" },
{ value: "7", label: "7" },
{ value: "8", label: "8" },
{ value: "9", label: "9" },
],
};

View File

@ -123,14 +123,16 @@ limitations under the License. -->
</div> </div>
</div> </div>
<div class="no-data" v-else>{{ t("noData") }}</div> <div class="no-data" v-else>{{ t("noData") }}</div>
<component <div class="trace-chart">
v-if="traceStore.currentTrace.endpointNames" <component
:is="displayMode" v-if="traceStore.currentTrace.endpointNames"
:data="traceStore.traceSpans" :is="displayMode"
:traceId="traceStore.currentTrace.traceIds[0].value" :data="traceStore.traceSpans"
:showBtnDetail="false" :traceId="traceStore.currentTrace.traceIds[0].value"
HeaderType="trace" :showBtnDetail="false"
/> HeaderType="trace"
/>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -227,6 +229,10 @@ export default defineComponent({
overflow: hidden; overflow: hidden;
} }
.trace-chart {
height: 100%;
}
.trace-detail-wrapper { .trace-detail-wrapper {
font-size: 12px; font-size: 12px;
padding: 5px 10px; padding: 5px 10px;

View File

@ -129,7 +129,7 @@ async function queryTraces() {
} }
} }
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.trace-t-tool { .trace-t-tool {
background-color: rgba(196, 200, 225, 0.2); background-color: rgba(196, 200, 225, 0.2);
justify-content: space-between; justify-content: space-between;

View File

@ -15,45 +15,45 @@ limitations under the License. -->
<template> <template>
<div> <div>
<h5 class="mb-15">{{ t("tags") }}.</h5> <h5 class="mb-15">{{ t("tags") }}.</h5>
<div class="mb-10 clear"> <div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("service") }}:</span> <span class="g-sm-4 grey">{{ t("service") }}:</span>
<span class="g-sm-8 wba">{{ currentSpan.serviceCode }}</span> <span class="g-sm-8 wba">{{ currentSpan.serviceCode }}</span>
</div> </div>
<div class="mb-10 clear"> <div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("instance") }}:</span> <span class="g-sm-4 grey">{{ t("instance") }}:</span>
<span class="g-sm-8 wba">{{ currentSpan.serviceInstanceName }}</span> <span class="g-sm-8 wba">{{ currentSpan.serviceInstanceName }}</span>
</div> </div>
<div class="mb-10 clear"> <div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("endpoint") }}:</span> <span class="g-sm-4 grey">{{ t("endpoint") }}:</span>
<span class="g-sm-8 wba">{{ currentSpan.label }}</span> <span class="g-sm-8 wba">{{ currentSpan.label }}</span>
</div> </div>
<div class="mb-10 clear"> <div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("spanType") }}:</span> <span class="g-sm-4 grey">{{ t("spanType") }}:</span>
<span class="g-sm-8 wba">{{ currentSpan.type }}</span> <span class="g-sm-8 wba">{{ currentSpan.type }}</span>
</div> </div>
<div class="mb-10 clear"> <div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("component") }}:</span> <span class="g-sm-4 grey">{{ t("component") }}:</span>
<span class="g-sm-8 wba">{{ currentSpan.component }}</span> <span class="g-sm-8 wba">{{ currentSpan.component }}</span>
</div> </div>
<div class="mb-10 clear"> <div class="mb-10 clear item">
<span class="g-sm-4 grey">Peer:</span> <span class="g-sm-4 grey">Peer:</span>
<span class="g-sm-8 wba">{{ currentSpan.peer || "No Peer" }}</span> <span class="g-sm-8 wba">{{ currentSpan.peer || "No Peer" }}</span>
</div> </div>
<div class="mb-10 clear"> <div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("error") }}:</span> <span class="g-sm-4 grey">{{ t("error") }}:</span>
<span class="g-sm-8 wba">{{ currentSpan.isError }}</span> <span class="g-sm-8 wba">{{ currentSpan.isError }}</span>
</div> </div>
<div class="mb-10 clear" v-for="i in currentSpan.tags" :key="i.key"> <div class="mb-10 clear item" v-for="i in currentSpan.tags" :key="i.key">
<span class="g-sm-4 grey">{{ i.key }}:</span> <span class="g-sm-4 grey">{{ i.key }}:</span>
<span class="g-sm-8 wba"> <span class="g-sm-8 wba">
{{ i.value }} {{ i.value }}
<svg <span
v-if="i.key === 'db.statement'" v-if="i.key === 'db.statement'"
class="icon vm grey link-hover cp ml-5" class="grey link-hover cp ml-5"
@click="copy(i.value)" @click="copy(i.value)"
> >
<use xlink:href="#review-list"></use> <Icon iconName="review-list" />
</svg> </span>
</span> </span>
</div> </div>
<h5 class="mb-10" v-if="currentSpan.logs" v-show="currentSpan.logs.length"> <h5 class="mb-10" v-if="currentSpan.logs" v-show="currentSpan.logs.length">
@ -71,19 +71,15 @@ limitations under the License. -->
class="r rk-sidebox-magnify" class="r rk-sidebox-magnify"
@click="showCurrentSpanDetail(_i.value)" @click="showCurrentSpanDetail(_i.value)"
> >
<svg class="icon"> <Icon iconName="magnify" />
<use xlink:href="#magnify"></use>
</svg>
</span> </span>
</div> </div>
<pre class="pl-15 mt-0 mb-0 sm oa">{{ _i.value }}</pre> <pre class="pl-15 mt-0 mb-0 sm oa">{{ _i.value }}</pre>
</div> </div>
</div> </div>
<div @click="getTaceLogs()"> <el-button class="popup-btn" type="primary" @click="getTaceLogs">
<el-button class="popup-btn" type="primary"> {{ t("relatedTraceLogs") }}
{{ t("relatedTraceLogs") }} </el-button>
</el-button>
</div>
</div> </div>
<el-dialog <el-dialog
v-model="showRelatedLogs" v-model="showRelatedLogs"
@ -161,4 +157,16 @@ function showCurrentSpanDetail(text: string) {
width: 100%; width: 100%;
text-align: center; text-align: center;
} }
.item span {
height: 21px;
}
.link-hover {
cursor: pointer;
}
.link-hover:hover {
color: #448dfe;
}
</style> </style>

View File

@ -88,7 +88,7 @@ function downloadTrace() {
.charts { .charts {
overflow: auto; overflow: auto;
padding: 10px; padding: 10px;
height: calc(100% - 95px); height: 100%;
width: 100%; width: 100%;
} }

View File

@ -20,7 +20,8 @@ limitations under the License. -->
<TableContainer <TableContainer
:tableData="tableData" :tableData="tableData"
type="table" type="table"
:HeaderType="HeaderType" :headerType="headerType"
@select="handleSelectSpan"
> >
<div class="trace-tips" v-if="!tableData.length">{{ $t("noData") }}</div> <div class="trace-tips" v-if="!tableData.length">{{ $t("noData") }}</div>
</TableContainer> </TableContainer>
@ -31,27 +32,26 @@ import { ref, watch, onMounted } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import TableContainer from "./TableContainer.vue"; import TableContainer from "./TableContainer.vue";
import traceTable from "../../utils/trace-table"; import traceTable from "../../utils/trace-table";
import { Span } from "@/types/trace";
/* global defineProps, defineEmits */ /* global defineProps, defineEmits */
const props = defineProps({ const props = defineProps({
data: { type: Array as PropType<any>, default: () => [] }, data: { type: Array as PropType<any>, default: () => [] },
traceId: { type: String, default: "" }, traceId: { type: String, default: "" },
showBtnDetail: { type: Boolean, default: false }, showBtnDetail: { type: Boolean, default: false },
HeaderType: { type: String, default: "" }, headerType: { type: String, default: "" },
}); });
const emit = defineEmits(["select", "view", "load"]); const emit = defineEmits(["select", "view", "load"]);
const loading = ref<boolean>(true); const loading = ref<boolean>(true);
const tableData = ref<any>([]); const tableData = ref<any>([]);
const showDetail = ref<boolean>(false); const showDetail = ref<boolean>(false);
const currentSpan = ref<any[]>([]); const currentSpan = ref<Span | any>({});
onMounted(() => { onMounted(() => {
tableData.value = formatData( tableData.value = formatData(
traceTable.changeTree(props.data, props.traceId) traceTable.changeTree(props.data, props.traceId)
); );
loading.value = false; loading.value = false;
emit("select", handleSelectSpan);
emit("view", handleViewSpan);
emit("load", () => { emit("load", () => {
loading.value = true; loading.value = true;
}); });
@ -69,7 +69,7 @@ function formatData(arr: any[], level = 1, totalExec?: number) {
return arr; return arr;
} }
function handleSelectSpan(data: any[]) { function handleSelectSpan(data: Span) {
currentSpan.value = data; currentSpan.value = data;
if (!props.showBtnDetail) { if (!props.showBtnDetail) {
showDetail.value = true; showDetail.value = true;
@ -77,10 +77,6 @@ function handleSelectSpan(data: any[]) {
emit("select", data); emit("select", data);
} }
function handleViewSpan() {
showDetail.value = true;
}
watch( watch(
() => props.data, () => props.data,
() => { () => {
@ -109,8 +105,7 @@ watch(
} }
.trace-table { .trace-table {
padding: 10px; height: 100%;
height: calc(100% - 95px);
width: 100%; width: 100%;
} }
</style> </style>

View File

@ -30,7 +30,7 @@ limitations under the License. -->
</div> </div>
<div class="trace-header" v-else> <div class="trace-header" v-else>
<div class="method" :style="`width: ${method}px`"> <div class="method" :style="`width: ${method}px`">
<span class="r cp" ref="dragger"> <span class="cp dragger" ref="dragger">
<Icon iconName="settings_ethernet" size="sm" /> <Icon iconName="settings_ethernet" size="sm" />
</span> </span>
{{ headerData[0].value }} {{ headerData[0].value }}
@ -43,12 +43,14 @@ limitations under the License. -->
{{ item.value }} {{ item.value }}
</div> </div>
</div> </div>
<table-item <TableItem
:method="method" :method="method"
v-for="(item, index) in tableData" v-for="(item, index) in tableData"
:data="item" :data="item"
:key="'key' + index" :key="'key' + index"
:type="type" :type="type"
:headerType="headerType"
@select="selectItem"
/> />
<slot></slot> <slot></slot>
</div> </div>
@ -56,21 +58,24 @@ limitations under the License. -->
<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 { PropType } from "vue";
import { Span } from "@/types/trace";
import TableItem from "./TableItem.vue"; import TableItem from "./TableItem.vue";
import { ProfileConstant, TraceConstant, StatisticsConstant } from "./data"; import { ProfileConstant, TraceConstant, StatisticsConstant } from "./data";
/* global defineProps, Nullable */ /* global defineProps, Nullable, defineEmits */
const props = defineProps({ const props = defineProps({
tableData: { type: Array as PropType<any>, default: () => [] }, tableData: { type: Array as PropType<any>, default: () => [] },
type: { type: String, default: "" }, type: { type: String, default: "" },
HeaderType: { type: String, default: "" }, headerType: { type: String, default: "" },
}); });
const emits = defineEmits(["select"]);
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: any[] = TraceConstant; let headerData: any[] = TraceConstant;
if (props.HeaderType === "profile") {
if (props.headerType === "profile") {
headerData = ProfileConstant; headerData = ProfileConstant;
} }
if (props.type === "statistics") { if (props.type === "statistics") {
@ -94,6 +99,9 @@ onMounted(() => {
}; };
}; };
}); });
function selectItem(span: Span) {
emits("select", span);
}
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++) {
@ -150,6 +158,10 @@ function sortStatistics(key: string) {
width: 100%; width: 100%;
} }
.dragger {
float: right;
}
.trace-header { .trace-header {
white-space: nowrap; white-space: nowrap;
user-select: none; user-select: none;

View File

@ -15,7 +15,7 @@ limitations under the License. -->
<template> <template>
<div v-if="type === 'statistics'"> <div v-if="type === 'statistics'">
<div :class="['trace-item']" ref="traceItem"> <div class="trace-item">
<div :class="['method']"> <div :class="['method']">
<el-tooltip :content="data.groupRef.endpointName" placement="bottom"> <el-tooltip :content="data.groupRef.endpointName" placement="bottom">
<span> <span>
@ -49,13 +49,12 @@ limitations under the License. -->
</div> </div>
<div v-else> <div v-else>
<div <div
@click="viewSpanDetail" @click="selectSpan"
:class="[ :class="[
'trace-item', 'trace-item',
'level' + (data.level - 1), 'level' + (data.level - 1),
{ 'trace-item-error': data.isError }, { 'trace-item-error': data.isError },
]" ]"
ref="traceItem"
> >
<div <div
:class="['method', 'level' + (data.level - 1)]" :class="['method', 'level' + (data.level - 1)]"
@ -106,7 +105,7 @@ limitations under the License. -->
<span>{{ data.serviceCode }}</span> <span>{{ data.serviceCode }}</span>
</el-tooltip> </el-tooltip>
</div> </div>
<div class="application" v-show="type === 'profile'"> <div class="application" v-show="headerType === 'profile'">
<span @click="viewSpanDetail">{{ t("view") }}</span> <span @click="viewSpanDetail">{{ t("view") }}</span>
</div> </div>
</div> </div>
@ -120,6 +119,8 @@ limitations under the License. -->
:key="index" :key="index"
:data="child" :data="child"
:type="type" :type="type"
:headerType="headerType"
@select="selectedItem(child)"
/> />
</div> </div>
<el-dialog <el-dialog
@ -143,6 +144,7 @@ const props = {
data: { type: Object as PropType<any>, default: () => ({}) }, data: { type: Object as PropType<any>, default: () => ({}) },
method: { type: Number, default: 0 }, method: { type: Number, default: 0 },
type: { type: String, default: "" }, type: { type: String, default: "" },
headerType: { type: String, default: "" },
}; };
export default defineComponent({ export default defineComponent({
name: "TableItem", name: "TableItem",
@ -154,7 +156,6 @@ export default defineComponent({
const displayChildren = ref<boolean>(true); const displayChildren = ref<boolean>(true);
const showDetail = ref<boolean>(false); const showDetail = ref<boolean>(false);
const { t } = useI18n(); const { t } = useI18n();
const traceItem = ref<Nullable<HTMLDivElement>>(null);
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") => const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern); dayjs(date).format(pattern);
const selfTime = computed(() => (props.data.dur ? props.data.dur : 0)); const selfTime = computed(() => (props.data.dur ? props.data.dur : 0));
@ -183,30 +184,38 @@ export default defineComponent({
}); });
function toggle() { function toggle() {
displayChildren.value = !this.displayChildren.value; displayChildren.value = !displayChildren.value;
} }
function showSelectSpan() { function showSelectSpan(dom: any) {
if (!dom) {
return;
}
const items: any = document.querySelectorAll(".trace-item"); const items: any = document.querySelectorAll(".trace-item");
for (const item of items) { for (const item of items) {
item.style.background = "#fff"; item.style.background = "#fff";
} }
if (!traceItem.value) { dom.style.background = "rgba(0, 0, 0, 0.1)";
}
function selectSpan(event: any) {
const dom = event.path.find((d: any) =>
d.className.includes("trace-item")
);
emit("select", props.data);
if (props.headerType === "profile") {
showSelectSpan(dom);
return; return;
} }
traceItem.value.style.background = "rgba(0, 0, 0, 0.1)"; viewSpanDetail(dom);
}
function viewSpanDetail() {
showDetail.value = true;
showSelectSpan();
emit("select", props.data);
} }
watch( function selectedItem(data: any) {
() => props.data, emit("select", data);
() => { }
showSelectSpan(); function viewSpanDetail(dom: any) {
} showSelectSpan(dom);
); showDetail.value = true;
}
return { return {
displayChildren, displayChildren,
outterPercent, outterPercent,
@ -216,6 +225,8 @@ export default defineComponent({
dateFormat, dateFormat,
showSelectSpan, showSelectSpan,
showDetail, showDetail,
selectSpan,
selectedItem,
t, t,
}; };
}, },

View File

@ -86,7 +86,7 @@ function computedScale(i: number) {
overflow: auto; overflow: auto;
padding: 10px; padding: 10px;
position: relative; position: relative;
height: calc(100% - 95px); height: 100%;
width: 100%; width: 100%;
} }