39 Commits

Author SHA1 Message Date
dependabot[bot]
0d2bedf529 build(deps): bump axios from 1.7.5 to 1.8.2 (#456) 2025-03-09 19:11:40 +08:00
dependabot[bot]
b525f84fa0 build(deps): bump vue-i18n from 9.2.2 to 9.14.3 (#455) 2025-03-09 19:04:12 +08:00
Fine0830
1fe58f5f6c build: remove lodash library (#454) 2025-03-06 17:10:54 +08:00
周小杰
012ae1db6c fix: time range when generate link (#453) 2025-03-06 15:47:55 +08:00
Zixin Zhou
79ec865ee7 Add skywalking go agent self observability menu (#452) 2025-03-02 18:48:14 +08:00
Fine0830
9ab8ac44bc add pagination for endpoint list (#451) 2025-02-28 17:15:55 +08:00
Fine0830
7a690e6704 feat: add topN selector for endpoint list (#450) 2025-02-28 16:19:27 +08:00
Fine0830
65607a5540 fix echarts tooltips (#449) 2025-02-24 16:38:17 +08:00
Fine0830
5bb4218bfe feat: Implement the legend selector in metrics charts (#447) 2025-02-20 16:19:22 +08:00
Fine0830
2b6f3ecaa8 build: bump up dependencies (#448) 2025-02-20 13:49:58 +08:00
Fine0830
2246a9a045 fix name in routes (#446) 2025-02-13 18:06:56 +08:00
dependabot[bot]
9318d32b0b build(deps-dev): bump vite from 4.5.3 to 4.5.9 (#445) 2025-01-22 14:19:08 +08:00
Fine0830
8ea50c8680 feat: visualize Snapshot on Alerting page (#444) 2025-01-13 17:24:32 +08:00
Fine0830
55b3867bea fix: set up async profiling tasks (#443) 2024-12-30 15:51:26 +08:00
Super-Lu
c33d6c4180 Fix the issue where when initiating a query on the endpoint topology page, if the query parameters contain the ID of the virtual endpoint User, all link information will be retrieved. (#442) 2024-12-21 20:25:44 +08:00
dependabot[bot]
70ea9fd06f build(deps): bump nanoid from 3.3.7 to 3.3.8 (#441) 2024-12-15 14:55:06 +08:00
Fine0830
2fca7a79a2 fix unit (#440) 2024-12-09 17:12:57 +08:00
Fine0830
f5cfb030a3 fix style (#439) 2024-12-04 10:29:30 +08:00
Fine0830
fbeeca8d9a Fix Async Profiling widget (#438) 2024-12-03 19:16:51 +08:00
Fine0830
ea0f5e5f62 Feat: Polish Async profiling widget (#437) 2024-12-03 12:41:51 +08:00
Fine0830
8771ce4a19 refactor: polish Trace Profiling widget (#436) 2024-12-02 15:10:24 +08:00
Fine0830
99a2461734 update style (#435) 2024-11-28 21:35:44 +08:00
Fine0830
fb0817eed8 feat: implement Async Profiling widget (#434) 2024-11-28 17:50:43 +08:00
Fine0830
e164d87209 Fix: releases an existing object URL (#433) 2024-11-25 11:26:42 +08:00
Fine0830
5c92a46569 Refactor copy util with Web API (#432) 2024-11-24 17:25:13 +08:00
Zixin Zhou
aff69c057f feat: Add support collapse span (#431) 2024-11-23 19:55:54 +08:00
Fine0830
7338cec6b4 fix metrics query (#430) 2024-11-22 15:53:37 +08:00
Fine0830
536df8c052 feat: Split topology metric query to avoid exceeding the maximum query complexity (#429) 2024-11-14 11:38:13 +08:00
Fine0830
64d4a2b59b hide entrances to unrelated dashboards in topn list (#428) 2024-11-08 10:03:49 +08:00
Fine0830
6e1a6cf19b fix (#427) 2024-11-07 12:41:19 +08:00
Fine0830
aeddb39637 feat: implement owner option for TopList widgets in related trace options (#426) 2024-11-07 10:31:06 +08:00
Fine0830
14fa5d65b6 feat: View data value related dashboards in TopList widgets (#425) 2024-11-05 11:21:08 +08:00
Fine0830
224e761d70 Fix initialization dashboards (#424) 2024-10-24 15:49:02 +08:00
Fine0830
4c60f69aef Fix Value Mappings (#423) 2024-10-18 14:48:55 +08:00
Zixin Zhou
0007e3e3ae Update mappingTip (#422) 2024-10-18 14:36:25 +08:00
Fine0830
b6522f4555 feat: support ranges for Value Mappings (#421) 2024-10-18 10:31:17 +08:00
Fine0830
bddbe40974 add duration (#420) 2024-10-16 10:26:45 +08:00
Fine0830
61449f4b17 feat: add content decorations to Table and Card widgets (#419) 2024-10-15 16:25:21 +08:00
Zixin Zhou
a92365efcf feat: add support for case-insensitive search in the dashboard list (#418) 2024-10-07 23:14:22 +08:00
115 changed files with 5518 additions and 4931 deletions

View File

@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [16.x, 18.x, 20.x] node-version: [18.x, 20.x, 22.x]
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}

7319
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "skywalking-booster-ui", "name": "skywalking-booster-ui",
"version": "9.4.0", "version": "10.2.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -18,19 +18,18 @@
"check-components-types": "if (! git diff --quiet -U0 ./src/types); then echo 'type files are not updated correctly'; git diff -U0 ./src/types; exit 1; fi" "check-components-types": "if (! git diff --quiet -U0 ./src/types); then echo 'type files are not updated correctly'; git diff -U0 ./src/types; exit 1; fi"
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.5", "axios": "^1.8.2",
"d3": "^7.3.0", "d3": "^7.3.0",
"d3-flame-graph": "^4.1.3", "d3-flame-graph": "^4.1.3",
"d3-tip": "^0.9.1", "d3-tip": "^0.9.1",
"echarts": "^5.2.2", "echarts": "^5.2.2",
"element-plus": "^2.2.5", "element-plus": "^2.9.4",
"lodash": "^4.17.21",
"monaco-editor": "^0.34.1", "monaco-editor": "^0.34.1",
"pinia": "^2.0.28", "pinia": "^2.0.28",
"vis-timeline": "^7.5.1", "vis-timeline": "^7.5.1",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-grid-layout": "^3.0.0-beta1", "vue-grid-layout": "^3.0.0-beta1",
"vue-i18n": "^9.1.9", "vue-i18n": "^9.14.3",
"vue-router": "^4.1.6", "vue-router": "^4.1.6",
"vue-types": "^4.1.1" "vue-types": "^4.1.1"
}, },
@@ -42,11 +41,10 @@
"@types/d3-tip": "^3.5.5", "@types/d3-tip": "^3.5.5",
"@types/echarts": "^4.9.12", "@types/echarts": "^4.9.12",
"@types/jsdom": "^20.0.1", "@types/jsdom": "^20.0.1",
"@types/lodash": "^4.14.179",
"@types/node": "^18.11.12", "@types/node": "^18.11.12",
"@types/three": "^0.131.0", "@types/three": "^0.131.0",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^3.0.0", "@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0", "@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.2.6", "@vue/test-utils": "^2.2.6",
@@ -64,21 +62,21 @@
"postcss-html": "^1.3.0", "postcss-html": "^1.3.0",
"postcss-scss": "^4.0.2", "postcss-scss": "^4.0.2",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"sass": "^1.56.1", "sass": "^1.85.0",
"start-server-and-test": "^2.0.5", "start-server-and-test": "^2.0.5",
"stylelint": "15.9.0", "stylelint": "15.9.0",
"stylelint-config-html": "^1.0.0", "stylelint-config-html": "^1.0.0",
"stylelint-config-prettier": "9.0.4", "stylelint-config-prettier": "9.0.4",
"stylelint-config-standard": "^33.0.0", "stylelint-config-standard": "^33.0.0",
"stylelint-order": "^6.0.3", "stylelint-order": "^6.0.3",
"typescript": "~4.7.4", "typescript": "^5.7.3",
"unplugin-auto-import": "^0.18.2", "unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.3", "unplugin-vue-components": "^0.27.3",
"vite": "^4.5.3", "vite": "^6.1.0",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "^2.0.1",
"vitest": "^0.25.6", "vitest": "^3.0.5",
"vue-tsc": "^1.8.27" "vue-tsc": "^2.2.2"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",

View File

@@ -0,0 +1,18 @@
<!-- 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 class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M512 992c-83.2 0-166.4-19.2-243.2-64-89.6-51.2-160-134.4-204.8-230.4C25.6 601.6 19.2 486.4 51.2 390.4c25.6-102.4 89.6-192 172.8-256C307.2 70.4 409.6 38.4 518.4 38.4c19.2 0 32 12.8 32 32s-19.2 25.6-38.4 25.6c-89.6 0-179.2 32-256 83.2S128 313.6 108.8 403.2s-12.8 185.6 19.2 268.8c32 83.2 96 153.6 179.2 198.4 76.8 44.8 172.8 64 262.4 51.2 89.6-12.8 172.8-51.2 236.8-115.2s108.8-147.2 115.2-236.8c12.8-89.6-6.4-185.6-51.2-262.4-6.4-12.8-6.4-32 12.8-44.8 12.8-12.8 38.4-6.4 44.8 12.8 51.2 89.6 76.8 198.4 57.6 300.8-12.8 102.4-64 204.8-134.4 275.2-76.8 76.8-172.8 121.6-275.2 134.4-19.2 6.4-44.8 6.4-64 6.4z" p-id="8538"></path><path d="M512 480c-19.2 0-32-12.8-32-32V64c0-19.2 12.8-32 32-32s32 12.8 32 32v384c0 19.2-12.8 32-32 32z" p-id="8539"></path><path d="M512 608c-12.8 0-25.6 0-38.4-6.4-12.8-6.4-19.2-12.8-32-19.2-6.4-12.8-12.8-19.2-19.2-32-6.4-12.8-6.4-25.6-6.4-38.4 0-25.6 12.8-51.2 25.6-70.4 38.4-38.4 102.4-38.4 134.4 0 19.2 19.2 32 44.8 32 70.4 0 25.6-12.8 51.2-25.6 70.4-19.2 12.8-44.8 25.6-70.4 25.6z m0-128c-6.4 0-19.2 6.4-25.6 6.4 0 6.4-6.4 19.2-6.4 25.6v12.8c0 6.4 6.4 6.4 6.4 12.8 0 0 6.4 6.4 12.8 6.4 12.8 6.4 25.6 0 32-6.4 6.4-6.4 12.8-19.2 12.8-25.6 0-6.4-6.4-19.2-6.4-25.6-6.4 0-19.2-6.4-25.6-6.4z" p-id="8540"></path><path d="M512 800c-51.2 0-102.4-12.8-147.2-38.4-57.6-32-96-83.2-121.6-140.8-19.2-57.6-25.6-121.6-6.4-185.6 19.2-64 51.2-115.2 102.4-153.6C384 243.2 448 224 512 224c19.2 0 32 12.8 32 32s-12.8 32-32 32c-51.2 0-96 19.2-134.4 44.8-38.4 32-70.4 76.8-83.2 121.6s-6.4 96 12.8 140.8c19.2 44.8 51.2 83.2 96 108.8 44.8 25.6 89.6 32 140.8 25.6 51.2-6.4 96-32 128-64s57.6-83.2 64-128c6.4-51.2-6.4-96-25.6-140.8-12.8-12.8-6.4-32 6.4-38.4 12.8-6.4 32-6.4 44.8 12.8 32 57.6 44.8 121.6 38.4 179.2-6.4 64-38.4 121.6-83.2 166.4-44.8 44.8-102.4 76.8-166.4 83.2H512z"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -13,6 +13,13 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<SelectorLegend
:data="option.legend.data"
:show="legendSelector.isSelector"
:isConfigPage="legendSelector.isConfigPage"
:colors="option.color"
@change="changeLegend"
/>
<div class="chart" ref="chartRef" :style="`height:${height};width:${width};`"> <div class="chart" ref="chartRef" :style="`height:${height};width:${width};`">
<div v-if="!available" class="no-data">No Data</div> <div v-if="!available" class="no-data">No Data</div>
<div <div
@@ -54,6 +61,7 @@ limitations under the License. -->
import Trace from "@/views/dashboard/related/trace/Index.vue"; import Trace from "@/views/dashboard/related/trace/Index.vue";
import associateProcessor from "@/hooks/useAssociateProcessor"; import associateProcessor from "@/hooks/useAssociateProcessor";
import { WidgetType } from "@/views/dashboard/data"; import { WidgetType } from "@/views/dashboard/data";
import SelectorLegend from "./Legend.vue";
/*global Nullable, defineProps, defineEmits, Indexable*/ /*global Nullable, defineProps, defineEmits, Indexable*/
const emits = defineEmits(["select"]); const emits = defineEmits(["select"]);
@@ -84,6 +92,10 @@ limitations under the License. -->
type: Array as PropType<{ widgetId: string }[]>, type: Array as PropType<{ widgetId: string }[]>,
default: () => [], default: () => [],
}, },
legendSelector: {
type: Object as PropType<Indexable>,
default: () => ({ isConfigPage: false, isSelector: false }),
},
}); });
const available = computed( const available = computed(
() => () =>
@@ -103,6 +115,7 @@ limitations under the License. -->
if (!instance) { if (!instance) {
return; return;
} }
instance.on("click", (params: EventParams) => { instance.on("click", (params: EventParams) => {
currentParams.value = params; currentParams.value = params;
if (props.option.series.type === "sankey") { if (props.option.series.type === "sankey") {
@@ -203,6 +216,23 @@ limitations under the License. -->
}); });
} }
function changeLegend(names: string[]) {
const instance = getInstance();
for (const item of props.option.legend.data) {
if (names.includes(item.name)) {
instance.dispatchAction({
type: "legendSelect",
name: item.name,
});
} else {
instance.dispatchAction({
type: "legendUnSelect",
name: item.name,
});
}
}
}
watch( watch(
() => props.option, () => props.option,
(newVal, oldVal) => { (newVal, oldVal) => {

View File

@@ -0,0 +1,74 @@
<!-- 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>
<Selector
class="mb-10"
multiple
:value="legend"
size="small"
:options="Options"
@change="changeLegend"
filterable
collapseTags
collapseTagsTooltip
v-if="show"
/>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from "vue";
import type { PropType } from "vue";
import type { Option } from "@/types/app";
import Selector from "./Selector.vue";
const props = defineProps({
data: {
type: Array as PropType<{ name: string }[]>,
default: () => [],
},
colors: {
type: Array as PropType<string[]>,
default: () => [],
},
show: {
type: Boolean,
default: false,
},
isConfigPage: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(["change"]);
const legend = ref<string[]>([]);
const Options = computed(() =>
props.data.map((d: { name: string }, index: number) => ({
label: d.name,
value: d.name,
color: props.colors[index % props.colors.length],
})),
);
function changeLegend(opt: Option[]) {
legend.value = opt.map((d: Option) => d.value);
emits("change", legend.value);
}
watch(
() => props.data,
() => {
legend.value = props.data.map((d) => d.name);
},
);
</script>

View File

@@ -0,0 +1,107 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<el-select
:size="size"
v-model="selected"
:placeholder="placeholder"
@change="changeSelected"
:multiple="multiple"
:disabled="disabled"
:style="{ borderRadius }"
:clearable="clearable"
:remote="isRemote"
:reserve-keyword="isRemote"
:remote-method="remoteMethod"
:filterable="filterable"
:collapse-tags="collapseTags"
:collapse-tags-tooltip="collapseTagsTooltip"
>
<el-option
v-for="(item, index) in options"
:key="`${item.value}${index}`"
:label="item.label || ''"
:value="item.value || ''"
:disabled="item.disabled || false"
>
<div class="flex items-center">
<el-tag :color="item.color" class="mr-5" size="small" />
<span :style="{ color: item.color }">{{ item.label }}</span>
</div>
</el-option>
</el-select>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import type { PropType } from "vue";
/*global defineProps, defineEmits, Indexable*/
const emit = defineEmits(["change", "query"]);
const props = defineProps({
options: {
type: Array as PropType<
({
label: string | number;
value: string | number;
color: string;
} & { disabled?: boolean })[]
>,
default: () => [],
},
value: {
type: [Array, String, Number, undefined] as PropType<any>,
default: () => [],
},
size: { type: null, default: "default" },
placeholder: {
type: [String, undefined] as PropType<string>,
default: "Select a option",
},
borderRadius: { type: Number, default: 3 },
multiple: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
clearable: { type: Boolean, default: false },
isRemote: { type: Boolean, default: false },
filterable: { type: Boolean, default: true },
collapseTags: { type: Boolean, default: false },
collapseTagsTooltip: { type: Boolean, default: false },
});
const selected = ref<string[] | string>(props.value);
function changeSelected() {
const options = props.options.filter((d: Indexable) =>
props.multiple ? selected.value.includes(d.value) : selected.value === d.value,
);
emit("change", options);
}
function remoteMethod(query: string) {
if (props.isRemote) {
emit("query", query);
}
}
watch(
() => props.value,
(data) => {
selected.value = data;
},
);
</script>
<style lang="scss" scoped>
.el-input__inner {
border-radius: unset !important;
}
</style>

View File

@@ -26,6 +26,8 @@ limitations under the License. -->
:reserve-keyword="isRemote" :reserve-keyword="isRemote"
:remote-method="remoteMethod" :remote-method="remoteMethod"
:filterable="filterable" :filterable="filterable"
:collapse-tags="collapseTags"
:collapse-tags-tooltip="collapseTagsTooltip"
> >
<el-option <el-option
v-for="(item, index) in options" v-for="(item, index) in options"
@@ -41,11 +43,6 @@ limitations under the License. -->
import { ref, watch } from "vue"; import { ref, watch } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
// interface Option {
// label: string | number;
// value: string | number;
// }
/*global defineProps, defineEmits, Indexable*/ /*global defineProps, defineEmits, Indexable*/
const emit = defineEmits(["change", "query"]); const emit = defineEmits(["change", "query"]);
const props = defineProps({ const props = defineProps({
@@ -73,6 +70,8 @@ limitations under the License. -->
clearable: { type: Boolean, default: false }, clearable: { type: Boolean, default: false },
isRemote: { type: Boolean, default: false }, isRemote: { type: Boolean, default: false },
filterable: { type: Boolean, default: true }, filterable: { type: Boolean, default: true },
collapseTags: { type: Boolean, default: false },
collapseTagsTooltip: { type: Boolean, default: false },
}); });
const selected = ref<string[] | string>(props.value); const selected = ref<string[] | string>(props.value);

View File

@@ -18,7 +18,7 @@ import type { App } from "vue";
import Icon from "./Icon.vue"; 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/Graph.vue";
import Radio from "./Radio.vue"; import Radio from "./Radio.vue";
import SelectSingle from "./SelectSingle.vue"; import SelectSingle from "./SelectSingle.vue";
import Tags from "./Tags.vue"; import Tags from "./Tags.vue";

View File

@@ -24,6 +24,7 @@ export const Alarm = {
message message
startTime startTime
scope scope
name
tags { tags {
key key
value value
@@ -43,6 +44,35 @@ export const Alarm = {
startTime startTime
endTime endTime
} }
snapshot {
expression
metrics {
name
results {
metric {
labels {
key
value
}
}
values {
id
owner {
scope
serviceID
serviceName
normal
serviceInstanceID
serviceInstanceName
endpointID
endpointName
}
value
traceID
}
}
}
}
} }
}`, }`,
}; };

View File

@@ -0,0 +1,80 @@
/**
* 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 GetAsyncTaskList = {
variable: "$request: AsyncProfilerTaskListRequest!",
query: `
asyncTaskList: queryAsyncProfilerTaskList(request: $request) {
errorReason
tasks {
id
serviceId
serviceInstanceIds
createTime
events
duration
execArgs
}
}
`,
};
export const GetAsyncProfileTaskProcess = {
variable: "$taskId: String!",
query: `
taskProgress: queryAsyncProfilerTaskProgress(taskId: $taskId) {
logs {
id
instanceId
instanceName
operationType
operationTime
}
errorInstanceIds
successInstanceIds
}
`,
};
export const CreateAsyncProfileTask = {
variable: "$asyncProfilerTaskCreationRequest: AsyncProfilerTaskCreationRequest!",
query: `
task: createAsyncProfilerTask(asyncProfilerTaskCreationRequest: $asyncProfilerTaskCreationRequest) {
id
errorReason
code
}
`,
};
export const GetAsyncProfileAnalyze = {
variable: "$request: AsyncProfilerAnalyzationRequest!",
query: `
analysisResult: queryAsyncProfilerAnalyze(request: $request) {
tree {
type
elements {
id
parentId
symbol: codeSignature
dumpCount: total
self
}
}
}
`,
};

View File

@@ -73,9 +73,9 @@ export const Processes = {
}; };
export const Endpoints = { export const Endpoints = {
variable: "$serviceId: ID!, $keyword: String!", variable: "$serviceId: ID!, $keyword: String!, $duration: Duration, $limit: Int!",
query: ` query: `
pods: findEndpoint(serviceId: $serviceId, keyword: $keyword, limit: 20) { pods: findEndpoint(serviceId: $serviceId, keyword: $keyword, limit: $limit, duration: $duration) {
id id
value: name value: name
label: name label: name

View File

@@ -28,6 +28,7 @@ import * as alarm from "./query/alarm";
import * as event from "./query/event"; import * as event from "./query/event";
import * as ebpf from "./query/ebpf"; import * as ebpf from "./query/ebpf";
import * as demandLog from "./query/demand-log"; import * as demandLog from "./query/demand-log";
import * as asyncProfile from "./query/async-profile";
const query: { [key: string]: string } = { const query: { [key: string]: string } = {
...app, ...app,
@@ -41,6 +42,7 @@ const query: { [key: string]: string } = {
...event, ...event,
...ebpf, ...ebpf,
...demandLog, ...demandLog,
...asyncProfile,
}; };
class Graphql { class Graphql {
private queryData = ""; private queryData = "";

View File

@@ -0,0 +1,31 @@
/**
* 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 {
GetAsyncTaskList,
GetAsyncProfileTaskProcess,
CreateAsyncProfileTask,
GetAsyncProfileAnalyze,
} from "../fragments/async-profile";
export const getAsyncTaskList = `query getAsyncTaskList(${GetAsyncTaskList.variable}) {${GetAsyncTaskList.query}}`;
export const getAsyncProfileTaskProcess = `query getAsyncProfileTaskProcess(${GetAsyncProfileTaskProcess.variable}) {${GetAsyncProfileTaskProcess.query}}`;
export const saveAsyncProfileTask = `mutation createAsyncProfileTask(${CreateAsyncProfileTask.variable}) {${CreateAsyncProfileTask.query}}`;
export const getAsyncProfileAnalyze = `query getAsyncProfileAnalyze(${GetAsyncProfileAnalyze.variable}) {${GetAsyncProfileAnalyze.query}}`;

View File

@@ -57,6 +57,16 @@ export const RespFields: Indexable = {
name: id name: id
value value
refId: traceID refId: traceID
owner {
scope
serviceID
serviceName
normal
serviceInstanceID
serviceInstanceName
endpointID
endpointName
}
} }
} }
error error
@@ -102,3 +112,5 @@ export const LightChartColors = [
"#546570", "#546570",
"#c4ccd3", "#c4ccd3",
]; ];
export const MaxQueryLength = 120;

View File

@@ -47,11 +47,11 @@ export function createBreakpointListen(fn?: (opt: CreateCallbackParams) => void)
function getWindowWidth() { function getWindowWidth() {
const width = document.body.clientWidth; const width = document.body.clientWidth;
const xs = screenMap.get(sizeEnum.XS) || ""; const xs = screenMap.get(sizeEnum.XS) || 0;
const sm = screenMap.get(sizeEnum.SM) || ""; const sm = screenMap.get(sizeEnum.SM) || 0;
const md = screenMap.get(sizeEnum.MD) || ""; const md = screenMap.get(sizeEnum.MD) || 0;
const lg = screenMap.get(sizeEnum.LG) || ""; const lg = screenMap.get(sizeEnum.LG) || 0;
const xl = screenMap.get(sizeEnum.XL) || ""; const xl = screenMap.get(sizeEnum.XL) || 0;
if (width < xs) { if (width < xs) {
screenRef.value = sizeEnum.XS; screenRef.value = sizeEnum.XS;
} else if (width < sm) { } else if (width < sm) {

View File

@@ -14,9 +14,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { RespFields, MaximumEntities } from "./data"; import { RespFields, MaximumEntities, MaxQueryLength } from "./data";
import { EntityType, ExpressionResultType } from "@/views/dashboard/data"; import { EntityType, ExpressionResultType } from "@/views/dashboard/data";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useTopologyStore } from "@/store/modules/topology";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
@@ -24,6 +25,14 @@ import type { MetricConfigOpt } from "@/types/dashboard";
import type { Instance, Endpoint, Service } from "@/types/selector"; import type { Instance, Endpoint, Service } from "@/types/selector";
import type { Node, Call } from "@/types/topology"; import type { Node, Call } from "@/types/topology";
function chunkArray(array: any[], chunkSize: number) {
const result = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize));
}
return result;
}
export async function useDashboardQueryProcessor(configList: Indexable[]) { export async function useDashboardQueryProcessor(configList: Indexable[]) {
function expressionsGraphql(config: Indexable, idx: number) { function expressionsGraphql(config: Indexable, idx: number) {
if (!(config.metrics && config.metrics[0])) { if (!(config.metrics && config.metrics[0])) {
@@ -181,13 +190,6 @@ export async function useDashboardQueryProcessor(configList: Indexable[]) {
return { 0: { source: {}, tips: [], typesOfMQE: [] } }; return { 0: { source: {}, tips: [], typesOfMQE: [] } };
} }
} }
function chunkArray(array: any[], chunkSize: number) {
const result = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize));
}
return result;
}
const partArr = chunkArray(configList, 6); const partArr = chunkArray(configList, 6);
const promiseArr = partArr.map((d: Array<Indexable>) => fetchMetrics(d)); const promiseArr = partArr.map((d: Array<Indexable>) => fetchMetrics(d));
@@ -394,7 +396,7 @@ export function useQueryTopologyExpressionsProcessor(metrics: string[], instance
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
function getExpressionQuery() { function getExpressionQuery(partMetrics?: string[]) {
const conditions: { [key: string]: unknown } = { const conditions: { [key: string]: unknown } = {
duration: appStore.durationTime, duration: appStore.durationTime,
}; };
@@ -448,7 +450,7 @@ export function useQueryTopologyExpressionsProcessor(metrics: string[], instance
}; };
variables.push(`$entity${index}: Entity!`); variables.push(`$entity${index}: Entity!`);
conditions[`entity${index}`] = entity; conditions[`entity${index}`] = entity;
const f = metrics.map((name: string, idx: number) => { const f = (partMetrics || metrics).map((name: string, idx: number) => {
if (index === 0) { if (index === 0) {
variables.push(`$expression${idx}: String!`); variables.push(`$expression${idx}: String!`);
conditions[`expression${idx}`] = name; conditions[`expression${idx}`] = name;
@@ -462,19 +464,19 @@ export function useQueryTopologyExpressionsProcessor(metrics: string[], instance
return { queryStr, conditions }; return { queryStr, conditions };
} }
function handleExpressionValues(resp: { [key: string]: any }) { function handleExpressionValues(partMetrics: string[], resp: { [key: string]: any }) {
const obj: any = {}; const obj: any = {};
for (let idx = 0; idx < instances.length; idx++) { for (let idx = 0; idx < instances.length; idx++) {
for (let index = 0; index < metrics.length; index++) { for (let index = 0; index < partMetrics.length; index++) {
const k = "expression" + idx + index; const k = "expression" + idx + index;
if (metrics[index]) { if (partMetrics[index]) {
if (!obj[metrics[index]]) { if (!obj[partMetrics[index]]) {
obj[metrics[index]] = { obj[partMetrics[index]] = {
values: [], values: [],
}; };
} }
obj[metrics[index]].values.push({ obj[partMetrics[index]].values.push({
value: resp[k].results[0] && resp[k].results[0].values[0].value, value: resp[k] && resp[k].results[0] && resp[k].results[0].values[0].value,
id: instances[idx].id, id: instances[idx].id,
}); });
} }
@@ -482,6 +484,31 @@ export function useQueryTopologyExpressionsProcessor(metrics: string[], instance
} }
return obj; return obj;
} }
async function fetchMetrics(partMetrics: string[]) {
const topologyStore = useTopologyStore();
const param = getExpressionQuery(partMetrics);
const res = await topologyStore.getTopologyExpressionValue(param);
if (res.errors) {
ElMessage.error(res.errors);
return;
}
return handleExpressionValues(partMetrics, res.data);
}
return { getExpressionQuery, handleExpressionValues }; async function getMetrics() {
const count = Math.floor(MaxQueryLength / instances.length);
const metricsArr = chunkArray(metrics, count);
const promiseArr = metricsArr.map((d: string[]) => fetchMetrics(d));
const responseList = await Promise.all(promiseArr);
let resp = {};
for (const item of responseList) {
resp = {
...resp,
...item,
};
}
return resp;
}
return { getMetrics, getExpressionQuery };
} }

View File

@@ -35,7 +35,7 @@ export default function useLegendProcess(legend?: LegendOptions) {
if (keys.length === 1) { if (keys.length === 1) {
return false; return false;
} }
if (legend && legend.asTable) { if (legend && (legend.asTable || legend.asSelector)) {
return false; return false;
} }
return true; return true;

47
src/hooks/useSnapshot.ts Normal file
View File

@@ -0,0 +1,47 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { MetricsResults } from "@/types/dashboard";
export function useSnapshot(metrics: { name: string; results: MetricsResults[] }[]) {
function processResults() {
const sources = metrics.map((metric: { name: string; results: MetricsResults[] }) => {
const values = metric.results.map(
(r: { values: { value: string }[]; metric: { labels: { key: string; value: string }[] } }) => {
const arr = r.values.map((v: { value: string }) => Number(v.value));
if (!r.metric.labels.length) {
return { values: arr };
}
const name = r.metric.labels
.map(
(label: { key: string; value: string }) =>
`${metric.name}${label ? "{" : ""}${label.key}=${label.value}${label ? "}" : ""}`,
)
.join(",");
return { name, values: arr };
},
);
return { name: metric.name, values };
});
return sources;
}
return {
processResults,
};
}

View File

@@ -61,7 +61,7 @@ limitations under the License. -->
<Icon iconName="retry" :loading="appStore.autoRefresh" class="middle" /> <Icon iconName="retry" :loading="appStore.autoRefresh" class="middle" />
</span> </span>
<span class="version ml-5 cp"> <span class="version ml-5 cp">
<el-popover trigger="hover" width="250" placement="bottom" :content="appStore.version"> <el-popover trigger="hover" :width="250" placement="bottom" :content="appStore.version">
<template #reference> <template #reference>
<span> <span>
<Icon iconName="info_outline" size="middle" /> <Icon iconName="info_outline" size="middle" />

View File

@@ -50,7 +50,7 @@ limitations under the License. -->
</el-menu-item> </el-menu-item>
</el-menu-item-group> </el-menu-item-group>
</el-sub-menu> </el-sub-menu>
<el-menu-item :index="String(menu.name)" @click="changePage(menu)" v-else> <el-menu-item :index="String(menu.name)" v-else>
<el-icon class="menu-icons" :style="{ marginRight: '12px' }" @mouseover="setCollapse"> <el-icon class="menu-icons" :style="{ marginRight: '12px' }" @mouseover="setCollapse">
<router-link class="items menu-title" :to="menu.children[0].path"> <router-link class="items menu-title" :to="menu.children[0].path">
<Icon size="lg" :iconName="menu.meta.icon" /> <Icon size="lg" :iconName="menu.meta.icon" />
@@ -83,7 +83,6 @@ limitations under the License. -->
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const router = useRouter(); const router = useRouter();
const name = ref<string>(String(router.currentRoute.value.name)); const name = ref<string>(String(router.currentRoute.value.name));
const theme = ["VirtualMachine", "Kubernetes"].includes(name.value || "") ? ref("light") : ref("black");
const routes = ref<RouteRecordRaw[] | any>( const routes = ref<RouteRecordRaw[] | any>(
(router.options.routes || []).filter((d: any) => d.meta && d.meta.activate), (router.options.routes || []).filter((d: any) => d.meta && d.meta.activate),
); );
@@ -100,9 +99,7 @@ limitations under the License. -->
if (route.name === "ViewWidget") { if (route.name === "ViewWidget") {
showMenu.value = false; showMenu.value = false;
} }
const changePage = (menu: RouteRecordRaw) => {
theme.value = ["VirtualMachine", "Kubernetes"].includes(String(menu.name)) ? "light" : "black";
};
const filterMenus = (menus: Recordable[]) => { const filterMenus = (menus: Recordable[]) => {
return menus.filter((d) => d.meta && !d.meta.notShow && d.meta.activate); return menus.filter((d) => d.meta && !d.meta.notShow && d.meta.activate);
}; };

View File

@@ -139,7 +139,6 @@ const msg = {
enableAssociate: "Enable association", enableAssociate: "Enable association",
text: "Text", text: "Text",
query: "Query", query: "Query",
endpointTips: "The table shows up to 20 pieces of endpoints.",
viewTrace: "View Related Traces", viewTrace: "View Related Traces",
relatedTraceOptions: "Related Trace Options", relatedTraceOptions: "Related Trace Options",
setLatencyDuration: "Latency Related Metrics", setLatencyDuration: "Latency Related Metrics",
@@ -154,6 +153,7 @@ const msg = {
legendOptions: "Legend Options", legendOptions: "Legend Options",
showLegend: "Show Legend", showLegend: "Show Legend",
asTable: "As Table", asTable: "As Table",
asSelector: "As Selector",
toTheRight: "To The Right", toTheRight: "To The Right",
legendValues: "Legend Values", legendValues: "Legend Values",
minDuration: "Minimal Request Duration", minDuration: "Minimal Request Duration",
@@ -386,5 +386,16 @@ const msg = {
tabExpressions: "Tab Expressions", tabExpressions: "Tab Expressions",
hierarchyNodeMetrics: "Metrics for Hierarchy Graph Node", hierarchyNodeMetrics: "Metrics for Hierarchy Graph Node",
hierarchyNodeDashboard: "As dashboard for Hierarchy Graph Node", hierarchyNodeDashboard: "As dashboard for Hierarchy Graph Node",
valueMappings: "Value Mappings",
mappingTip: "Notice: The mapping key is a Regex string, e.g. ^([0-9])$",
valueDashboard: "Data Value Related Dashboard",
viewValueDashboard: "View Dashboard",
errorInstances: "Error Instances",
successInstances: "Success Instances",
profilingEvents: "Async Profiling Events",
execArgs: "Exec Args",
instances: "Instances",
snapshot: "Snapshot",
expression: "Expression",
}; };
export default msg; export default msg;

View File

@@ -138,7 +138,6 @@ const msg = {
"El nombre sólo admite chino e inglés, líneas horizontales y subrayado, y la longitud del nombre no excederá de 300 caracteres", "El nombre sólo admite chino e inglés, líneas horizontales y subrayado, y la longitud del nombre no excederá de 300 caracteres",
enableAssociate: "Activar asociación", enableAssociate: "Activar asociación",
query: "Consulta", query: "Consulta",
endpointTips: "Aquí, la tabla muestra hasta 20 punto final.",
queryOrder: "Consulta por duración", queryOrder: "Consulta por duración",
setOrder: "Orden de consulta", setOrder: "Orden de consulta",
latency: "Retraso", latency: "Retraso",
@@ -386,5 +385,17 @@ const msg = {
tabExpressions: "Tab Expressions", tabExpressions: "Tab Expressions",
hierarchyNodeMetrics: "Metrics for Hierarchy Graph Node", hierarchyNodeMetrics: "Metrics for Hierarchy Graph Node",
hierarchyNodeDashboard: "As dashboard for Hierarchy Graph Node", hierarchyNodeDashboard: "As dashboard for Hierarchy Graph Node",
valueMappings: "Value Mappings",
mappingTip: "Aviso: La clave de mapeo es una cadena Regex, p. ej. ^([0-9])$",
valueDashboard: "Data Value Related Dashboard",
viewValueDashboard: "View Dashboard",
errorInstances: "Error Instances",
successInstances: "Success Instances",
profilingEvents: "Async Profiling Events",
execArgs: "Exec Args",
instances: "Instances",
snapshot: "Snapshot",
expression: "Expression",
asSelector: "As Selector",
}; };
export default msg; export default msg;

View File

@@ -128,6 +128,9 @@ const titles = {
self_observability_java_agent: "SkyWalking Java Agent", self_observability_java_agent: "SkyWalking Java Agent",
self_observability_java_agent_desc: self_observability_java_agent_desc:
"The self observability of SkyWalking Java Agent, which provides the abilities to measure the tracing performance and error statistics of plugins.", "The self observability of SkyWalking Java Agent, which provides the abilities to measure the tracing performance and error statistics of plugins.",
self_observability_go_agent: "SkyWalking Go Agent",
self_observability_go_agent_desc:
"The self observability of SkyWalking Go Agent, which provides the abilities to measure the tracing performance and error statistics of plugins.",
cilium: "Cilium", cilium: "Cilium",
cilium_desc: cilium_desc:
"Cilium is a CNI plugin for Kubernetes that provides eBPF-based networking, security, and load balancing.", "Cilium is a CNI plugin for Kubernetes that provides eBPF-based networking, security, and load balancing.",

View File

@@ -128,7 +128,10 @@ const titles = {
"Satellite: an open-source agent designed for the cloud-native infrastructures, which provides a low-cost, high-efficient, and more secure way to collect telemetry data. It is the recommended load balancer for telemetry collecting.", "Satellite: an open-source agent designed for the cloud-native infrastructures, which provides a low-cost, high-efficient, and more secure way to collect telemetry data. It is the recommended load balancer for telemetry collecting.",
self_observability_java_agent: "SkyWalking Java Agent", self_observability_java_agent: "SkyWalking Java Agent",
self_observability_java_agent_desc: self_observability_java_agent_desc:
"The self observability of SkyWalking Java Agent, which provides the abilities to measure the tracing performance and error statistics of plugins.", "La auto-observabilidad de SkyWalking Java Agent, que proporciona la capacidad de medir el rendimiento del trazado y las estasticas de errores de los plugins.",
self_observability_go_agent: "SkyWalking Go Agent",
self_observability_go_agent_desc:
"La auto-observabilidad de SkyWalking Go Agent, que proporciona la capacidad de medir el rendimiento del trazado y las estadísticas de errores de los plugins.",
cilium: "Cilium", cilium: "Cilium",
cilium_desc: cilium_desc:
"Cilium es un complemento CNI para Kubernetes que proporciona redes, seguridad y equilibrio de carga basados en eBPF.", "Cilium es un complemento CNI para Kubernetes que proporciona redes, seguridad y equilibrio de carga basados en eBPF.",

View File

@@ -113,6 +113,8 @@ const titles = {
"Satellite为云原生基础设施设计的开源代理提供了一种低成本、高效、更安全的遥测数据收集方式。它是遥测采集的推荐负载均衡器。", "Satellite为云原生基础设施设计的开源代理提供了一种低成本、高效、更安全的遥测数据收集方式。它是遥测采集的推荐负载均衡器。",
self_observability_java_agent: "SkyWalking Java Agent", self_observability_java_agent: "SkyWalking Java Agent",
self_observability_java_agent_desc: "SkyWalking Java Agent 自监控提供了对 agent 插件的性能追踪和错误统计。", self_observability_java_agent_desc: "SkyWalking Java Agent 自监控提供了对 agent 插件的性能追踪和错误统计。",
self_observability_go_agent: "SkyWalking Go Agent",
self_observability_go_agent_desc: "SkyWalking Go Agent 自监控提供了对 agent 插件的性能追踪和错误统计。",
cilium: "Cilium", cilium: "Cilium",
cilium_desc: "Cilium是Kubernetes上的CNI插件提供基于eBPF的网络、安全和负载均衡。", cilium_desc: "Cilium是Kubernetes上的CNI插件提供基于eBPF的网络、安全和负载均衡。",
cilium_service: "Cilium服务", cilium_service: "Cilium服务",

View File

@@ -137,7 +137,6 @@ const msg = {
duplicateName: "重复的名称", duplicateName: "重复的名称",
text: "文本", text: "文本",
query: "查询", query: "查询",
endpointTips: "这里最多展示20条endpoints。",
viewTrace: "查看相关Trace", viewTrace: "查看相关Trace",
relatedTraceOptions: "相关的Trace选项", relatedTraceOptions: "相关的Trace选项",
setLatencyDuration: "延迟相关指标", setLatencyDuration: "延迟相关指标",
@@ -152,6 +151,7 @@ const msg = {
legendOptions: "图例选项", legendOptions: "图例选项",
showLegend: "显示图例", showLegend: "显示图例",
asTable: "作为表格", asTable: "作为表格",
asSelector: "作为选择器",
toTheRight: "在右边", toTheRight: "在右边",
legendValues: "图例值", legendValues: "图例值",
minDuration: "最小请求持续时间", minDuration: "最小请求持续时间",
@@ -384,5 +384,16 @@ const msg = {
tabExpressions: "Tab表达式", tabExpressions: "Tab表达式",
hierarchyNodeMetrics: "层次图节点的指标", hierarchyNodeMetrics: "层次图节点的指标",
hierarchyNodeDashboard: "作为层次图节点的dashboard", hierarchyNodeDashboard: "作为层次图节点的dashboard",
valueMappings: "值映射",
mappingTip: "注意: 映射键是一个正则表达式字符串,比如 ^([0-9])$",
valueDashboard: "数据值相关的仪表板",
viewValueDashboard: "查看仪表板",
errorInstances: "错误的实例",
successInstances: "成功的实例",
profilingEvents: "异步分析事件",
execArgs: "String任务扩展",
instances: "实例",
snapshot: "快照",
expression: "表达式",
}; };
export default msg; export default msg;

View File

@@ -33,7 +33,7 @@ export const routesAlarm: Array<RouteRecordRaw> = [
children: [ children: [
{ {
path: "/alerting", path: "/alerting",
name: "Alarm", name: "ViewAlarm",
component: Alarm, component: Alarm,
}, },
], ],

View File

@@ -100,7 +100,7 @@ export const routesDashboard: Array<RouteRecordRaw> = [
path: "", path: "",
redirect: "/dashboard/related/:layerId/:entity/:serviceId/:destServiceId/:name", redirect: "/dashboard/related/:layerId/:entity/:serviceId/:destServiceId/:name",
component: Edit, component: Edit,
name: "ViewServiceRelation", name: "ServiceRelations",
meta: { meta: {
notShow: true, notShow: true,
}, },
@@ -121,7 +121,7 @@ export const routesDashboard: Array<RouteRecordRaw> = [
path: "", path: "",
redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:name", redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:name",
component: Edit, component: Edit,
name: "ViewPod", name: "Pods",
meta: { meta: {
notShow: true, notShow: true,
}, },
@@ -142,7 +142,7 @@ export const routesDashboard: Array<RouteRecordRaw> = [
path: "", path: "",
redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name", redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name",
component: Edit, component: Edit,
name: "ViewProcess", name: "Processes",
meta: { meta: {
notShow: true, notShow: true,
}, },
@@ -163,7 +163,7 @@ export const routesDashboard: Array<RouteRecordRaw> = [
path: "", path: "",
redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:destServiceId/:destPodId/:name", redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:destServiceId/:destPodId/:name",
component: Edit, component: Edit,
name: "PodRelation", name: "PodRelations",
meta: { meta: {
notShow: true, notShow: true,
}, },
@@ -185,7 +185,7 @@ export const routesDashboard: Array<RouteRecordRaw> = [
redirect: redirect:
"/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name", "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name",
component: Edit, component: Edit,
name: "ProcessRelation", name: "ProcessRelations",
meta: { meta: {
notShow: true, notShow: true,
}, },

View File

@@ -33,7 +33,7 @@ export const routesSettings: Array<RouteRecordRaw> = [
children: [ children: [
{ {
path: "/settings", path: "/settings",
name: "Settings", name: "ViewSettings",
component: Settings, component: Settings,
}, },
], ],

View File

@@ -47,6 +47,7 @@ export const ControlsTypes = [
WidgetType.DemandLog, WidgetType.DemandLog,
WidgetType.Ebpf, WidgetType.Ebpf,
WidgetType.NetworkProfiling, WidgetType.NetworkProfiling,
WidgetType.AsyncProfiling,
WidgetType.ThirdPartyApp, WidgetType.ThirdPartyApp,
WidgetType.ContinuousProfiling, WidgetType.ContinuousProfiling,
WidgetType.TaskTimeline, WidgetType.TaskTimeline,
@@ -55,3 +56,5 @@ export enum EBPFProfilingTriggerType {
FIXED_TIME = "FIXED_TIME", FIXED_TIME = "FIXED_TIME",
CONTINUOUS_PROFILING = "CONTINUOUS_PROFILING", CONTINUOUS_PROFILING = "CONTINUOUS_PROFILING",
} }
export const EndpointsTopNDefault = 20;

View File

@@ -0,0 +1,141 @@
/**
* 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 type {
AsyncProfilingTask,
AsyncProfileTaskCreationRequest,
AsyncProfilerStackElement,
AsyncProfilerTaskProgress,
} from "@/types/async-profiling";
import { store } from "@/store";
import graphql from "@/graphql";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import type { AxiosResponse } from "axios";
import type { Instance } from "@/types/selector";
interface AsyncProfilingState {
taskList: Array<Recordable<AsyncProfilingTask>>;
selectedTask: Recordable<AsyncProfilingTask>;
taskProgress: Recordable<AsyncProfilerTaskProgress>;
instances: Instance[];
analyzeTrees: AsyncProfilerStackElement[];
loadingTree: boolean;
loadingTasks: boolean;
}
export const asyncProfilingStore = defineStore({
id: "asyncProfiling",
state: (): AsyncProfilingState => ({
taskList: [],
selectedTask: {},
taskProgress: {},
instances: [],
analyzeTrees: [],
loadingTree: false,
loadingTasks: false,
}),
actions: {
setSelectedTask(task: Recordable<AsyncProfilingTask>) {
this.selectedTask = task || {};
},
setAnalyzeTrees(tree: AsyncProfilerStackElement[]) {
this.analyzeTrees = tree;
},
async getTaskList() {
const selectorStore = useSelectorStore();
this.loadingTasks = true;
const res: AxiosResponse = await graphql.query("getAsyncTaskList").params({
request: {
serviceId: selectorStore.currentService.id,
limit: 10000,
},
});
this.loadingTasks = false;
if (res.data.errors) {
return res.data;
}
this.taskList = res.data.data.asyncTaskList.tasks || [];
this.selectedTask = this.taskList[0] || {};
this.setAnalyzeTrees([]);
this.setSelectedTask(this.selectedTask);
if (!this.taskList.length) {
return res.data;
}
return res.data;
},
async getTaskLogs(param: { taskID: string }) {
const res: AxiosResponse = await graphql.query("getAsyncProfileTaskProcess").params(param);
if (res.data.errors) {
return res.data;
}
this.taskProgress = res.data.data.taskProgress;
return res.data;
},
async getServiceInstances(param: { serviceId: string; isRelation: boolean }): Promise<Nullable<AxiosResponse>> {
if (!param.serviceId) {
return null;
}
const res: AxiosResponse = await graphql.query("queryInstances").params({
serviceId: param.serviceId,
duration: useAppStoreWithOut().durationTime,
});
if (!res.data.errors) {
this.instances = (res.data.data.pods || []).map((d: Instance) => {
d.value = d.id || "";
return d;
});
}
return res.data;
},
async createTask(param: AsyncProfileTaskCreationRequest) {
const res: AxiosResponse = await graphql
.query("saveAsyncProfileTask")
.params({ asyncProfilerTaskCreationRequest: param });
if (res.data.errors) {
return res.data;
}
this.getTaskList();
return res.data;
},
async getAsyncProfilingAnalyze(params: { taskId: string; instanceIds: Array<string>; eventType: string }) {
if (!params.instanceIds.length) {
return new Promise((resolve) => resolve({}));
}
this.loadingTree = true;
const res: AxiosResponse = await graphql.query("getAsyncProfileAnalyze").params({ request: params });
this.loadingTree = false;
if (res.data.errors) {
this.analyzeTrees = [];
return res.data;
}
const { analysisResult } = res.data.data;
if (!analysisResult) {
this.analyzeTrees = [];
return res.data;
}
this.analyzeTrees = [analysisResult.tree];
return res.data;
},
},
});
export function useAsyncProfilingStore(): Recordable {
return asyncProfilingStore(store);
}

View File

@@ -22,6 +22,7 @@ import type { Event, QueryEventCondition } from "@/types/events";
import type { Instance, Endpoint } from "@/types/selector"; import type { Instance, Endpoint } from "@/types/selector";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { EndpointsTopNDefault } from "../data";
interface eventState { interface eventState {
loading: boolean; loading: boolean;
@@ -54,7 +55,7 @@ export const eventStore = defineStore({
if (res.data.errors) { if (res.data.errors) {
return res.data; return res.data;
} }
this.instances = [{ value: "", label: "All" }, ...res.data.data.pods] || [{ value: "", label: "All" }]; this.instances = [{ value: "", label: "All" }, ...res.data.data.pods];
return res.data; return res.data;
}, },
async getEndpoints(keyword: string) { async getEndpoints(keyword: string) {
@@ -66,11 +67,12 @@ export const eventStore = defineStore({
serviceId, serviceId,
duration: useAppStoreWithOut().durationTime, duration: useAppStoreWithOut().durationTime,
keyword: keyword || "", keyword: keyword || "",
limit: EndpointsTopNDefault,
}); });
if (res.data.errors) { if (res.data.errors) {
return res.data; return res.data;
} }
this.endpoints = [{ value: "", label: "All" }, ...res.data.data.pods] || [{ value: "", label: "All" }]; this.endpoints = [{ value: "", label: "All" }, ...res.data.data.pods];
return res.data; return res.data;
}, },
async getEvents() { async getEvents() {

View File

@@ -22,6 +22,7 @@ import type { AxiosResponse } from "axios";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { EndpointsTopNDefault } from "../data";
interface LogState { interface LogState {
services: Service[]; services: Service[];
@@ -80,7 +81,7 @@ export const logStore = defineStore({
if (res.data.errors) { if (res.data.errors) {
return res.data; return res.data;
} }
this.instances = [{ value: "0", label: "All" }, ...res.data.data.pods] || [{ value: " 0", label: "All" }]; this.instances = [{ value: "0", label: "All" }, ...res.data.data.pods];
return res.data; return res.data;
}, },
async getEndpoints(id: string, keyword?: string) { async getEndpoints(id: string, keyword?: string) {
@@ -89,11 +90,12 @@ export const logStore = defineStore({
serviceId, serviceId,
duration: useAppStoreWithOut().durationTime, duration: useAppStoreWithOut().durationTime,
keyword: keyword || "", keyword: keyword || "",
limit: EndpointsTopNDefault,
}); });
if (res.data.errors) { if (res.data.errors) {
return res.data; return res.data;
} }
this.endpoints = [{ value: "0", label: "All" }, ...res.data.data.pods] || [{ value: "0", label: "All" }]; this.endpoints = [{ value: "0", label: "All" }, ...res.data.data.pods];
return res.data; return res.data;
}, },
async getLogsByKeywords() { async getLogsByKeywords() {

View File

@@ -28,6 +28,7 @@ import { store } from "@/store";
import graphql from "@/graphql"; import graphql from "@/graphql";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { EndpointsTopNDefault } from "../data";
interface ProfileState { interface ProfileState {
endpoints: Endpoint[]; endpoints: Endpoint[];
@@ -80,7 +81,7 @@ export const profileStore = defineStore({
this.analyzeTrees = []; this.analyzeTrees = [];
}, },
setCurrentSegment(segment: Trace) { setCurrentSegment(segment: Trace) {
this.currentSegment = segment; this.currentSegment = segment || {};
this.segmentSpans = segment.spans || []; this.segmentSpans = segment.spans || [];
if (segment.spans) { if (segment.spans) {
this.currentSpan = segment.spans[0] || {}; this.currentSpan = segment.spans[0] || {};
@@ -97,6 +98,7 @@ export const profileStore = defineStore({
serviceId, serviceId,
duration: useAppStoreWithOut().durationTime, duration: useAppStoreWithOut().durationTime,
keyword: keyword || "", keyword: keyword || "",
limit: EndpointsTopNDefault,
}); });
if (res.data.errors) { if (res.data.errors) {
return res.data; return res.data;
@@ -109,6 +111,7 @@ export const profileStore = defineStore({
serviceId, serviceId,
duration: useAppStoreWithOut().durationTime, duration: useAppStoreWithOut().durationTime,
keyword: keyword || "", keyword: keyword || "",
limit: EndpointsTopNDefault,
}); });
if (res.data.errors) { if (res.data.errors) {
return res.data; return res.data;
@@ -155,10 +158,10 @@ export const profileStore = defineStore({
return res.data; return res.data;
} }
if (segmentList[0]) { if (segmentList[0]) {
this.currentSegment = segmentList[0]; this.setCurrentSegment(segmentList[0]);
this.getSegmentSpans(segmentList[0].segmentId); this.getSegmentSpans(segmentList[0].segmentId);
} else { } else {
this.currentSegment = {}; this.setCurrentSegment({});
} }
return res.data; return res.data;
}, },

View File

@@ -20,6 +20,7 @@ import { store } from "@/store";
import graphql from "@/graphql"; import graphql from "@/graphql";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { EndpointsTopNDefault } from "../data";
interface SelectorState { interface SelectorState {
services: Service[]; services: Service[];
destServices: Service[]; destServices: Service[];
@@ -143,7 +144,7 @@ export const selectorStore = defineStore({
serviceId, serviceId,
duration: useAppStoreWithOut().durationTime, duration: useAppStoreWithOut().durationTime,
keyword: params.keyword || "", keyword: params.keyword || "",
limit: params.limit, limit: params.limit || EndpointsTopNDefault,
}); });
if (!res.data.errors) { if (!res.data.errors) {
if (params.isRelation) { if (params.isRelation) {

View File

@@ -24,7 +24,6 @@ import { useAppStoreWithOut } from "@/store/modules/app";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import query from "@/graphql/fetch"; import query from "@/graphql/fetch";
import { useQueryTopologyExpressionsProcessor } from "@/hooks/useExpressionsProcessor"; import { useQueryTopologyExpressionsProcessor } from "@/hooks/useExpressionsProcessor";
import { ElMessage } from "element-plus";
interface MetricVal { interface MetricVal {
[key: string]: { values: { id: string; value: unknown }[] }; [key: string]: { values: { id: string; value: unknown }[] };
@@ -339,7 +338,10 @@ export const topologyStore = defineStore({
} }
const res = await this.getEndpointTopology(endpointIds); const res = await this.getEndpointTopology(endpointIds);
if (depth > 1) { if (depth > 1) {
const ids = res.nodes.map((item: Node) => item.id).filter((d: string) => !endpointIds.includes(d)); const userNodeName = "User";
const ids = res.nodes
.filter((d: Node) => !endpointIds.includes(d.id) && d.name !== userNodeName)
.map((item: Node) => item.id);
if (!ids.length) { if (!ids.length) {
this.setTopology(res); this.setTopology(res);
return; return;
@@ -347,8 +349,8 @@ export const topologyStore = defineStore({
const json = await this.getEndpointTopology(ids); const json = await this.getEndpointTopology(ids);
if (depth > 2) { if (depth > 2) {
const pods = json.nodes const pods = json.nodes
.map((item: Node) => item.id) .filter((d: Node) => ![...ids, ...endpointIds].includes(d.id) && d.name !== userNodeName)
.filter((d: string) => ![...ids, ...endpointIds].includes(d)); .map((item: Node) => item.id);
if (!pods.length) { if (!pods.length) {
const nodes = [...res.nodes, ...json.nodes]; const nodes = [...res.nodes, ...json.nodes];
const calls = [...res.calls, ...json.calls]; const calls = [...res.calls, ...json.calls];
@@ -358,8 +360,8 @@ export const topologyStore = defineStore({
const topo = await this.getEndpointTopology(pods); const topo = await this.getEndpointTopology(pods);
if (depth > 3) { if (depth > 3) {
const endpoints = topo.nodes const endpoints = topo.nodes
.map((item: Node) => item.id) .filter((d: Node) => ![...ids, ...pods, ...endpointIds].includes(d.id) && d.name !== userNodeName)
.filter((d: string) => ![...ids, ...pods, ...endpointIds].includes(d)); .map((item: Node) => item.id);
if (!endpoints.length) { if (!endpoints.length) {
const nodes = [...res.nodes, ...json.nodes, ...topo.nodes]; const nodes = [...res.nodes, ...json.nodes, ...topo.nodes];
const calls = [...res.calls, ...json.calls, ...topo.calls]; const calls = [...res.calls, ...json.calls, ...topo.calls];
@@ -369,8 +371,11 @@ export const topologyStore = defineStore({
const data = await this.getEndpointTopology(endpoints); const data = await this.getEndpointTopology(endpoints);
if (depth > 4) { if (depth > 4) {
const nodeIds = data.nodes const nodeIds = data.nodes
.map((item: Node) => item.id) .filter(
.filter((d: string) => ![...endpoints, ...ids, ...pods, ...endpointIds].includes(d)); (d: Node) =>
![...endpoints, ...ids, ...pods, ...endpointIds].includes(d.id) && d.name !== userNodeName,
)
.map((item: Node) => item.id);
if (!nodeIds.length) { if (!nodeIds.length) {
const nodes = [...res.nodes, ...json.nodes, ...topo.nodes, ...data.nodes]; const nodes = [...res.nodes, ...json.nodes, ...topo.nodes, ...data.nodes];
const calls = [...res.calls, ...json.calls, ...topo.calls, ...data.calls]; const calls = [...res.calls, ...json.calls, ...topo.calls, ...data.calls];
@@ -443,7 +448,7 @@ export const topologyStore = defineStore({
return { calls, nodes }; return { calls, nodes };
}, },
async getNodeExpressionValue(param: { queryStr: string; conditions: { [key: string]: unknown } }) { async getTopologyExpressionValue(param: { queryStr: string; conditions: { [key: string]: unknown } }) {
const res: AxiosResponse = await query(param); const res: AxiosResponse = await query(param);
if (res.data.errors) { if (res.data.errors) {
@@ -461,14 +466,8 @@ export const topologyStore = defineStore({
if (!calls.length) { if (!calls.length) {
return; return;
} }
const { getExpressionQuery, handleExpressionValues } = useQueryTopologyExpressionsProcessor(expressions, calls); const { getMetrics } = useQueryTopologyExpressionsProcessor(expressions, calls);
const param = getExpressionQuery(); const metrics = await getMetrics();
const res = await this.getNodeExpressionValue(param);
if (res.errors) {
ElMessage.error(res.errors);
return;
}
const metrics = handleExpressionValues(res.data);
if (type === "SERVER") { if (type === "SERVER") {
this.setLinkServerMetrics(metrics); this.setLinkServerMetrics(metrics);
} else { } else {
@@ -484,17 +483,11 @@ export const topologyStore = defineStore({
this.setNodeMetricValue({}); this.setNodeMetricValue({});
return; return;
} }
const { getExpressionQuery, handleExpressionValues } = useQueryTopologyExpressionsProcessor( const { getMetrics } = useQueryTopologyExpressionsProcessor(
expressions, expressions,
this.nodes.filter((d: Node) => d.isReal), this.nodes.filter((d: Node) => d.isReal),
); );
const param = getExpressionQuery(); const metrics = await getMetrics();
const res = await this.getNodeExpressionValue(param);
if (res.errors) {
ElMessage.error(res.errors);
return;
}
const metrics = handleExpressionValues(res.data);
this.setNodeMetricValue(metrics); this.setNodeMetricValue(metrics);
}, },
async getHierarchyServiceTopology() { async getHierarchyServiceTopology() {
@@ -550,17 +543,6 @@ export const topologyStore = defineStore({
this.setHierarchyInstanceTopology(res.data.data.hierarchyInstanceTopology || {}, levels); this.setHierarchyInstanceTopology(res.data.data.hierarchyInstanceTopology || {}, levels);
return res.data; return res.data;
}, },
async queryHierarchyExpressions(expressions: string[], nodes: Node[]) {
const { getExpressionQuery, handleExpressionValues } = useQueryTopologyExpressionsProcessor(expressions, nodes);
const param = getExpressionQuery();
const res = await this.getNodeExpressionValue(param);
if (res.errors) {
ElMessage.error(res.errors);
return;
}
const metrics = handleExpressionValues(res.data);
return metrics;
},
async queryHierarchyNodeExpressions(expressions: string[], layer: string) { async queryHierarchyNodeExpressions(expressions: string[], layer: string) {
const nodes = this.hierarchyServiceNodes.filter((n: HierarchyNode) => n.layer === layer); const nodes = this.hierarchyServiceNodes.filter((n: HierarchyNode) => n.layer === layer);
if (!nodes.length) { if (!nodes.length) {
@@ -571,7 +553,8 @@ export const topologyStore = defineStore({
this.setHierarchyNodeMetricValue({}, layer); this.setHierarchyNodeMetricValue({}, layer);
return; return;
} }
const metrics = await this.queryHierarchyExpressions(expressions, nodes); const { getMetrics } = useQueryTopologyExpressionsProcessor(expressions, nodes);
const metrics = await getMetrics();
this.setHierarchyNodeMetricValue(metrics, layer); this.setHierarchyNodeMetricValue(metrics, layer);
}, },
async queryHierarchyInstanceNodeExpressions(expressions: string[], layer: string) { async queryHierarchyInstanceNodeExpressions(expressions: string[], layer: string) {
@@ -585,7 +568,8 @@ export const topologyStore = defineStore({
this.setHierarchyInstanceNodeMetricValue({}, layer); this.setHierarchyInstanceNodeMetricValue({}, layer);
return; return;
} }
const metrics = await this.queryHierarchyExpressions(expressions, nodes); const { getMetrics } = useQueryTopologyExpressionsProcessor(expressions, nodes);
const metrics = await getMetrics();
this.setHierarchyInstanceNodeMetricValue(metrics, layer); this.setHierarchyInstanceNodeMetricValue(metrics, layer);
}, },
}, },

View File

@@ -23,6 +23,7 @@ import type { AxiosResponse } from "axios";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { QueryOrders } from "@/views/dashboard/data"; import { QueryOrders } from "@/views/dashboard/data";
import { EndpointsTopNDefault } from "../data";
interface TraceState { interface TraceState {
services: Service[]; services: Service[];
instances: Instance[]; instances: Instance[];
@@ -133,6 +134,7 @@ export const traceStore = defineStore({
serviceId, serviceId,
duration: useAppStoreWithOut().durationTime, duration: useAppStoreWithOut().durationTime,
keyword: keyword || "", keyword: keyword || "",
limit: EndpointsTopNDefault,
}); });
if (res.data.errors) { if (res.data.errors) {
return res.data; return res.data;

View File

@@ -139,6 +139,10 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
.mb-20 {
margin-bottom: 20px;
}
.mr-5 { .mr-5 {
margin-right: 5px; margin-right: 5px;
} }

View File

@@ -32,6 +32,7 @@
html { html {
--el-color-primary: #409eff; --el-color-primary: #409eff;
--el-color-info-light-9: #666;
--theme-background: #fff; --theme-background: #fff;
--font-color: #3d444f; --font-color: #3d444f;
--disabled-color: #ccc; --disabled-color: #ccc;
@@ -69,10 +70,12 @@ html {
--sw-drawer-header: #72767b; --sw-drawer-header: #72767b;
--sw-marketplace-border: #dedfe0; --sw-marketplace-border: #dedfe0;
--sw-grid-item-active: #d4d7de; --sw-grid-item-active: #d4d7de;
--sw-trace-line: #999;
} }
html.dark { html.dark {
--el-color-primary: #409eff; --el-color-primary: #409eff;
--el-color-info-light-9: #333;
--theme-background: #212224; --theme-background: #212224;
--font-color: #fafbfc; --font-color: #fafbfc;
--disabled-color: #999; --disabled-color: #999;
@@ -110,14 +113,16 @@ html.dark {
--sw-drawer-header: #e9e9eb; --sw-drawer-header: #e9e9eb;
--sw-marketplace-border: #606266; --sw-marketplace-border: #606266;
--sw-grid-item-active: #73767a; --sw-grid-item-active: #73767a;
--sw-trace-line: #e8e8e8;
} }
.el-drawer__header { .el-drawer__header {
color: var(--sw-drawer-header); color: var(--sw-drawer-header);
} }
.el-table tr { .el-table {
background-color: var(--el-table-tr-bg-color); --el-table-tr-bg-color: var(--theme-background);
--el-table-header-bg-color: var(--theme-background);
} }
.el-popper.is-light { .el-popper.is-light {
@@ -129,27 +134,6 @@ html.dark {
--el-switch-off-color: #aaa; --el-switch-off-color: #aaa;
} }
.el-table__body-wrapper tr td.el-table-fixed-column--left,
.el-table__body-wrapper tr td.el-table-fixed-column--right,
.el-table__body-wrapper tr th.el-table-fixed-column--left,
.el-table__body-wrapper tr th.el-table-fixed-column--right,
.el-table__footer-wrapper tr td.el-table-fixed-column--left,
.el-table__footer-wrapper tr td.el-table-fixed-column--right,
.el-table__footer-wrapper tr th.el-table-fixed-column--left,
.el-table__footer-wrapper tr th.el-table-fixed-column--right,
.el-table__header-wrapper tr td.el-table-fixed-column--left,
.el-table__header-wrapper tr td.el-table-fixed-column--right,
.el-table__header-wrapper tr th.el-table-fixed-column--left,
.el-table__header-wrapper tr th.el-table-fixed-column--right {
background-color: var(--sw-table-col);
}
.el-table.is-scrolling-none th.el-table-fixed-column--left,
.el-table.is-scrolling-none th.el-table-fixed-column--right,
.el-table th.el-table__cell {
background-color: var(--sw-table-col);
}
$tool-icon-btn-bg: var(--sw-icon-btn-bg); $tool-icon-btn-bg: var(--sw-icon-btn-bg);
$tool-icon-btn-color: var(--sw-icon-btn-color); $tool-icon-btn-color: var(--sw-icon-btn-color);
$popper-hover-bg-color: var(--popper-hover-bg); $popper-hover-bg-color: var(--popper-hover-bg);
@@ -261,6 +245,10 @@ div:has(> a.menu-title) {
text-align: left !important; text-align: left !important;
} }
.el-input--small .el-input__inner {
--el-input-inner-height: calc(var(--el-input-height, 24px));
}
html { html {
&::view-transition-old(root), &::view-transition-old(root),
&::view-transition-new(root) { &::view-transition-new(root) {

View File

@@ -27,6 +27,7 @@ export interface Alarm {
scope: string; scope: string;
tags: Array<{ key: string; value: string }>; tags: Array<{ key: string; value: string }>;
events: Event[]; events: Event[];
snapshot: Indexable;
} }
export interface Event { export interface Event {

View File

@@ -0,0 +1,68 @@
/**
* 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 type AsyncProfilingTask = {
id: string;
serviceId: string;
serviceInstanceIds: string[];
createTime: number;
events: string;
duration: number;
execArgs: string;
};
export type AsyncProfileTaskCreationRequest = {
serviceId: string;
serviceInstanceIds: string[];
duration: number;
events: string[];
execArgs: string;
};
export type AsyncProfilerStackElement = {
id: string;
parentId: string;
codeSignature: string;
total: number;
self: number;
};
export type AsyncProfilerTaskProgress = {
errorInstanceIds: string[];
successInstanceIds: string[];
logs: AsyncProfilerTaskLog[];
};
type AsyncProfilerTaskLog = {
id: string;
instanceId: string;
instanceName: string;
operationType: string;
operationTime: number;
};
export type StackElement = {
id: string;
originId: string;
name: string;
parentId: string;
codeSignature: string;
total: number;
self: number;
value: number;
children?: StackElement[];
};

View File

@@ -12,6 +12,8 @@ declare module 'vue' {
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCollapse: typeof import('element-plus/es')['ElCollapse'] ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
@@ -42,8 +44,9 @@ declare module 'vue' {
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag'] ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
Graph: typeof import('./../components/Graph.vue')['default'] Graph: typeof import('./../components/Graph/Graph.vue')['default']
Icon: typeof import('./../components/Icon.vue')['default'] Icon: typeof import('./../components/Icon.vue')['default']
Legend: typeof import('./../components/Graph/Legend.vue')['default']
Radio: typeof import('./../components/Radio.vue')['default'] Radio: typeof import('./../components/Radio.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']

View File

@@ -46,6 +46,7 @@ export interface LayoutConfig {
relatedTrace?: RelatedTrace; relatedTrace?: RelatedTrace;
subExpressions?: string[]; subExpressions?: string[];
subTypesOfMQE?: string[]; subTypesOfMQE?: string[];
valueRelatedDashboard?: string;
} }
export type RelatedTrace = { export type RelatedTrace = {
duration: DurationTime; duration: DurationTime;
@@ -110,6 +111,7 @@ export interface LineConfig extends AreaConfig {
showYAxis?: boolean; showYAxis?: boolean;
smallTips?: boolean; smallTips?: boolean;
showlabels?: boolean; showlabels?: boolean;
noTooltips?: boolean;
} }
export interface AreaConfig { export interface AreaConfig {
@@ -123,6 +125,7 @@ export interface CardConfig {
fontSize?: number; fontSize?: number;
showUnit?: boolean; showUnit?: boolean;
textAlign?: "center" | "right" | "left"; textAlign?: "center" | "right" | "left";
valueMappings?: { [key: string]: string };
} }
export interface TextConfig { export interface TextConfig {
@@ -194,4 +197,19 @@ export type LegendOptions = {
asTable: boolean; asTable: boolean;
toTheRight: boolean; toTheRight: boolean;
width: number; width: number;
asSelector: boolean;
};
export type MetricsResults = {
metric: { labels: MetricLabel[] };
values: MetricValue[];
};
type MetricLabel = {
key: string;
value: string;
};
type MetricValue = {
name: string;
value: string;
owner: null | string;
refId: null | string;
}; };

View File

@@ -41,6 +41,9 @@ export interface TaskListItem {
dumpPeriod: number; dumpPeriod: number;
maxSamplingCount: number; maxSamplingCount: number;
logs: TaskLog[]; logs: TaskLog[];
errorInstanceIds: string[];
successInstanceIds: string[];
serviceInstanceIds: string[];
} }
export interface SegmentSpan { export interface SegmentSpan {
spanId: string; spanId: string;

View File

@@ -16,18 +16,22 @@
*/ */
import { ElNotification } from "element-plus"; import { ElNotification } from "element-plus";
export default (value: string): void => {
const input = document.createElement("input"); export default (text: string): void => {
input.value = value; navigator.clipboard
document.body.appendChild(input); .writeText(text)
input.select(); .then(() => {
if (document.execCommand("Copy")) {
document.execCommand("Copy");
}
input.remove();
ElNotification({ ElNotification({
title: "Success", title: "Success",
message: "Copied", message: "Copied",
type: "success", type: "success",
}); });
})
.catch((err) => {
ElNotification({
title: "Error",
message: err,
type: "warning",
});
});
}; };

View File

@@ -37,8 +37,10 @@ export const saveFile = (data: any, name: string) => {
tagA.download = name; tagA.download = name;
tagA.style.display = "none"; tagA.style.display = "none";
const blob = new Blob([newData]); const blob = new Blob([newData]);
tagA.href = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
tagA.href = url;
document.body.appendChild(tagA); document.body.appendChild(tagA);
tagA.click(); tagA.click();
document.body.removeChild(tagA); document.body.removeChild(tagA);
URL.revokeObjectURL(url);
}; };

View File

@@ -17,7 +17,7 @@
class Vec2 extends Float32Array { class Vec2 extends Float32Array {
constructor(v?: unknown, y?: unknown) { constructor(v?: unknown, y?: unknown) {
super(2); super(2);
if (v instanceof Vec2 || v instanceof Float32Array || (v instanceof Array && v.length == 2)) { if (v instanceof Vec2 || v instanceof Float32Array || (v instanceof Array && v.length === 2)) {
this[0] = v[0]; this[0] = v[0];
this[1] = v[1]; this[1] = v[1];
} else if (typeof v === "number" && typeof y === "number") { } else if (typeof v === "number" && typeof y === "number") {
@@ -104,7 +104,7 @@ class Vec2 extends Float32Array {
} }
norm(out?: number[] | Vec2): number[] | Vec2 | undefined { norm(out?: number[] | Vec2): number[] | Vec2 | undefined {
const mag = Math.sqrt(this[0] * this[0] + this[1] * this[1]); const mag = Math.sqrt(this[0] * this[0] + this[1] * this[1]);
if (mag == 0) return this; if (mag === 0) return this;
out = out || this; out = out || this;
out[0] = this[0] / mag; out[0] = this[0] / mag;
out[1] = this[1] / mag; out[1] = this[1] / mag;

View File

@@ -17,7 +17,7 @@
class Vec3 extends Float32Array { class Vec3 extends Float32Array {
constructor(v?: unknown, y?: unknown, z?: unknown) { constructor(v?: unknown, y?: unknown, z?: unknown) {
super(3); super(3);
if (v instanceof Vec3 || v instanceof Float32Array || (v instanceof Array && v.length == 3)) { if (v instanceof Vec3 || v instanceof Float32Array || (v instanceof Array && v.length === 3)) {
this[0] = v[0]; this[0] = v[0];
this[1] = v[1]; this[1] = v[1];
this[2] = v[2]; this[2] = v[2];
@@ -150,7 +150,7 @@ class Vec3 extends Float32Array {
} }
static norm(x: unknown, y: unknown, z: unknown): Vec3 { static norm(x: unknown, y: unknown, z: unknown): Vec3 {
const rtn = new Vec3(); const rtn = new Vec3();
if (x instanceof Vec3 || x instanceof Float32Array || (x instanceof Array && x.length == 3)) { if (x instanceof Vec3 || x instanceof Float32Array || (x instanceof Array && x.length === 3)) {
rtn.copy(x); rtn.copy(x);
} else if (typeof x === "number" && typeof y === "number" && typeof z === "number") { } else if (typeof x === "number" && typeof y === "number" && typeof z === "number") {
rtn.xyz(x, y, z); rtn.xyz(x, y, z);

View File

@@ -22,7 +22,7 @@ limitations under the License. -->
placeholder="Select a language" placeholder="Select a language"
@change="setLang" @change="setLang"
size="small" size="small"
style="font-size: 14px" style="font-size: 14px; width: 180px"
/> />
</div> </div>
<div class="flex-h item"> <div class="flex-h item">

View File

@@ -22,8 +22,9 @@ limitations under the License. -->
<div class="message mb-5 b"> <div class="message mb-5 b">
{{ i.message }} {{ i.message }}
</div> </div>
<div class="flex-h">
<div <div
class="timeline-table-i-scope mr-10 l sm" class="timeline-table-i-scope"
:class="{ :class="{
blue: i.scope === 'Service', blue: i.scope === 'Service',
green: i.scope === 'Endpoint', green: i.scope === 'Endpoint',
@@ -32,6 +33,7 @@ limitations under the License. -->
> >
{{ t(i.scope.toLowerCase()) }} {{ t(i.scope.toLowerCase()) }}
</div> </div>
</div>
<div class="grey sm show-xs"> <div class="grey sm show-xs">
{{ dateFormat(parseInt(i.startTime)) }} {{ dateFormat(parseInt(i.startTime)) }}
</div> </div>
@@ -46,7 +48,7 @@ limitations under the License. -->
:destroy-on-close="true" :destroy-on-close="true"
@closed="isShowDetails = false" @closed="isShowDetails = false"
> >
<div class="mb-10 clear alarm-detail" v-for="(item, index) in AlarmDetailCol" :key="index"> <div class="mb-20 clear alarm-detail" v-for="(item, index) in AlarmDetailCol" :key="index">
<span class="g-sm-2 grey">{{ t(item.value) }}:</span> <span class="g-sm-2 grey">{{ t(item.value) }}:</span>
<span v-if="item.label === 'startTime'"> <span v-if="item.label === 'startTime'">
{{ dateFormat(currentDetail[item.label]) }} {{ dateFormat(currentDetail[item.label]) }}
@@ -54,7 +56,7 @@ limitations under the License. -->
<span v-else-if="item.label === 'tags'"> <span v-else-if="item.label === 'tags'">
<div v-for="(d, index) in alarmTags" :key="index">{{ d }}</div> <div v-for="(d, index) in alarmTags" :key="index">{{ d }}</div>
</span> </span>
<span v-else-if="item.label === 'events'" class="event-detail"> <span v-else-if="item.label === 'events'">
<div> <div>
<ul> <ul>
<li> <li>
@@ -75,6 +77,12 @@ limitations under the License. -->
</ul> </ul>
</div> </div>
</span> </span>
<span v-else-if="item.label === 'expression'">
{{ currentDetail.snapshot.expression }}
</span>
<span v-else-if="item.label === 'snapshot'">
<Snapshot :snapshot="currentDetail.snapshot" />
</span>
<span v-else>{{ currentDetail[item.label] }}</span> <span v-else>{{ currentDetail[item.label] }}</span>
</div> </div>
</el-dialog> </el-dialog>
@@ -85,7 +93,7 @@ limitations under the License. -->
:destroy-on-close="true" :destroy-on-close="true"
@closed="showEventDetails = false" @closed="showEventDetails = false"
> >
<div class="event-detail"> <div>
<div class="mb-10" v-for="(eventKey, index) in EventsDetailKeys" :key="index"> <div class="mb-10" v-for="(eventKey, index) in EventsDetailKeys" :key="index">
<span class="keys">{{ t(eventKey.text) }}</span> <span class="keys">{{ t(eventKey.text) }}</span>
<span v-if="eventKey.class === 'parameters'"> <span v-if="eventKey.class === 'parameters'">
@@ -117,6 +125,7 @@ limitations under the License. -->
import { useAlarmStore } from "@/store/modules/alarm"; import { useAlarmStore } from "@/store/modules/alarm";
import { EventsDetailHeaders, AlarmDetailCol, EventsDetailKeys } from "./data"; import { EventsDetailHeaders, AlarmDetailCol, EventsDetailKeys } from "./data";
import { dateFormat } from "@/utils/dateFormat"; import { dateFormat } from "@/utils/dateFormat";
import Snapshot from "./components/Snapshot.vue";
const { t } = useI18n(); const { t } = useI18n();
const alarmStore = useAlarmStore(); const alarmStore = useAlarmStore();
@@ -186,11 +195,10 @@ limitations under the License. -->
} }
.timeline-table-i-scope { .timeline-table-i-scope {
display: inline-block; padding: 0 5px;
padding: 0 8px;
border: 1px solid; border: 1px solid;
margin-top: -1px; border-radius: 3px;
border-radius: 4px; display: inline-block;
} }
.timeline-item { .timeline-item {
@@ -224,9 +232,6 @@ limitations under the License. -->
} }
.alarm-detail { .alarm-detail {
max-height: 600px;
overflow: auto;
ul { ul {
min-height: 100px; min-height: 100px;
overflow: auto; overflow: auto;
@@ -247,4 +252,9 @@ limitations under the License. -->
} }
} }
} }
.mini-chart {
height: 20px;
width: 400px;
}
</style> </style>

View File

@@ -38,7 +38,7 @@ limitations under the License. -->
:total="total" :total="total"
@current-change="changePage" @current-change="changePage"
:pager-count="5" :pager-count="5"
small size="small"
:style=" :style="
appStore.theme === Themes.Light appStore.theme === Themes.Light
? `--el-pagination-bg-color: #f0f2f5; --el-pagination-button-disabled-bg-color: #f0f2f5;` ? `--el-pagination-bg-color: #f0f2f5; --el-pagination-button-disabled-bg-color: #f0f2f5;`

View File

@@ -0,0 +1,141 @@
<!-- 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>
<Graph :option="option" @select="clickEvent" />
</template>
<script lang="ts" setup>
import { computed } from "vue";
import type { PropType } from "vue";
import type { EventParams } from "@/types/dashboard";
import useLegendProcess from "@/hooks/useLegendProcessor";
import { useAppStoreWithOut } from "@/store/modules/app";
import { Themes } from "@/constants/data";
/*global defineProps, defineEmits */
const emits = defineEmits(["click"]);
const props = defineProps({
data: {
type: Array as PropType<any>,
default: () => [],
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
const appStore = useAppStoreWithOut();
const option = computed(() => getOption());
function getOption() {
const { chartColors } = useLegendProcess();
const color: string[] = chartColors();
const series = [];
const grid = [];
const xAxis = [];
const yAxis = [];
for (const [index, metric] of props.data.entries()) {
grid.push({
top: 300 * index + 30,
left: 0,
right: 10,
bottom: 5,
containLabel: true,
height: 260,
});
xAxis.push({
type: "category",
show: true,
axisTick: {
lineStyle: { color: "#c1c5ca41" },
alignWithLabel: true,
},
splitLine: { show: false },
axisLine: { lineStyle: { color: "rgba(0,0,0,0)" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
gridIndex: index,
});
yAxis.push({
type: "value",
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: "#c1c5ca41", type: "dashed" } },
axisLabel: {
color: "#9da5b2",
fontSize: "11",
show: true,
},
gridIndex: index,
});
for (const item of metric.values) {
series.push({
data: item.values.map((item: number, itemIndex: number) => [props.intervalTime[itemIndex], item]),
name: item.name || metric.name,
type: "line",
symbol: "circle",
symbolSize: 4,
xAxisIndex: index,
yAxisIndex: index,
lineStyle: {
width: 2,
type: "solid",
},
});
}
}
const tooltip = {
trigger: "axis",
backgroundColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
borderColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
textStyle: {
fontSize: 12,
color: appStore.theme === Themes.Dark ? "#eee" : "#333",
},
enterable: true,
confine: true,
extraCssText: "max-height:85%; overflow: auto;",
axisPointer: {
animation: false,
},
};
return {
color,
tooltip,
axisPointer: {
link: { xAxisIndex: "all" },
},
legend: {
type: "scroll",
icon: "circle",
top: -5,
left: 0,
itemWidth: 12,
textStyle: {
color: appStore.theme === Themes.Dark ? "#fff" : "#333",
},
},
grid,
xAxis,
yAxis,
series,
};
}
function clickEvent(params: EventParams) {
emits("click", params);
}
</script>
<style lang="scss" scoped>
.snapshot-charts {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,35 @@
<!-- 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>
<LineChart
:intervalTime="appStore.intervalTime"
:data="metrics"
:style="{ width: `800px`, height: `${metrics.length * 300}px` }"
/>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useSnapshot } from "@/hooks/useSnapshot";
import { useAppStoreWithOut } from "@/store/modules/app";
import LineChart from "./Line.vue";
/*global defineProps */
const props = defineProps({
snapshot: { type: Object, default: () => {} },
});
const { processResults } = useSnapshot(props.snapshot.metrics);
const metrics = computed(() => processResults());
const appStore = useAppStoreWithOut();
</script>

View File

@@ -52,6 +52,14 @@ export const AlarmDetailCol = [
label: "events", label: "events",
value: "eventDetail", value: "eventDetail",
}, },
{
label: "expression",
value: "expression",
},
{
label: "snapshot",
value: "snapshot",
},
]; ];
export const EventsDetailKeys = [ export const EventsDetailKeys = [

View File

@@ -612,7 +612,7 @@ limitations under the License. -->
} }
function searchDashboards(pageIndex: number) { function searchDashboards(pageIndex: number) {
const list = JSON.parse(sessionStorage.getItem("dashboards") || "[]"); const list = JSON.parse(sessionStorage.getItem("dashboards") || "[]");
const arr = list.filter((d: { name: string }) => d.name.includes(searchText.value)); const arr = list.filter((d: { name: string }) => d.name.toLowerCase().includes(searchText.value.toLowerCase()));
total.value = arr.length; total.value = arr.length;
dashboards.value = arr.filter( dashboards.value = arr.filter(

View File

@@ -32,11 +32,13 @@ limitations under the License. -->
:config="{ :config="{
i: 0, i: 0,
...graph, ...graph,
valueMappings: graph?.valueMappings,
metricConfig: config.metricConfig, metricConfig: config.metricConfig,
expressions: config.expressions || [], expressions: config.expressions || [],
typesOfMQE: typesOfMQE || [], typesOfMQE: typesOfMQE || [],
subExpressions: config.subExpressions || [], subExpressions: config.subExpressions || [],
subTypesOfMQE: config.subTypesOfMQE || [], subTypesOfMQE: config.subTypesOfMQE || [],
valueRelatedDashboard: config.valueRelatedDashboard,
}" }"
:needQuery="true" :needQuery="true"
/> />

View File

@@ -126,7 +126,7 @@ limitations under the License. -->
opt.auto = Number(f.value) * 60 * 60 * 1000; opt.auto = Number(f.value) * 60 * 60 * 1000;
} }
if (f.step === TimeType.DAY_TIME) { if (f.step === TimeType.DAY_TIME) {
opt.auto = Number(f.value) * 60 * 60 * 60 * 1000; opt.auto = Number(f.value) * 24 * 60 * 60 * 1000;
} }
} }
const config = JSON.stringify(opt); const config = JSON.stringify(opt);

View File

@@ -32,7 +32,8 @@ limitations under the License. -->
:data="states.source" :data="states.source"
:config="{ :config="{
...graph, ...graph,
legend: (dashboardStore.selectedGrid.graph || {}).legend, decorations: dashboardStore.selectedGrid.graph?.decorations,
legend: dashboardStore.selectedGrid.graph?.legend,
i: dashboardStore.selectedGrid.i, i: dashboardStore.selectedGrid.i,
metricConfig: dashboardStore.selectedGrid.metricConfig, metricConfig: dashboardStore.selectedGrid.metricConfig,
relatedTrace: dashboardStore.selectedGrid.relatedTrace, relatedTrace: dashboardStore.selectedGrid.relatedTrace,
@@ -40,6 +41,7 @@ limitations under the License. -->
typesOfMQE: dashboardStore.selectedGrid.typesOfMQE || [], typesOfMQE: dashboardStore.selectedGrid.typesOfMQE || [],
subExpressions: dashboardStore.selectedGrid.subExpressions || [], subExpressions: dashboardStore.selectedGrid.subExpressions || [],
subTypesOfMQE: dashboardStore.selectedGrid.subTypesOfMQE || [], subTypesOfMQE: dashboardStore.selectedGrid.subTypesOfMQE || [],
valueRelatedDashboard: dashboardStore.selectedGrid.valueRelatedDashboard,
}" }"
:needQuery="true" :needQuery="true"
@expressionTips="getErrors" @expressionTips="getErrors"

View File

@@ -81,7 +81,7 @@ limitations under the License. -->
const latency = ref<boolean>(traceOpt.latency || false); const latency = ref<boolean>(traceOpt.latency || false);
const enableRelate = ref<boolean>(traceOpt.enableRelate || false); const enableRelate = ref<boolean>(traceOpt.enableRelate || false);
const type = ref<string>((graph && graph.type) || ""); const type = ref<string>((graph && graph.type) || "");
const refIdType = ref<string>(traceOpt.refIdType || "traceId"); const refIdType = ref<string>(traceOpt.refIdType || "");
function updateConfig(param: { [key: string]: unknown }) { function updateConfig(param: { [key: string]: unknown }) {
const relatedTrace = { const relatedTrace = {

View File

@@ -13,6 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div>
<value-mappings />
</div>
<div> <div>
<span class="label">{{ t("fontSize") }}</span> <span class="label">{{ t("fontSize") }}</span>
<el-slider <el-slider
@@ -26,7 +29,7 @@ limitations under the License. -->
@change="updateConfig({ fontSize })" @change="updateConfig({ fontSize })"
/> />
</div> </div>
<div class="item"> <div>
<span class="label">{{ t("showUnit") }}</span> <span class="label">{{ t("showUnit") }}</span>
<el-switch v-model="showUnit" active-text="Yes" inactive-text="No" @change="updateConfig({ showUnit })" /> <el-switch v-model="showUnit" active-text="Yes" inactive-text="No" @change="updateConfig({ showUnit })" />
</div> </div>
@@ -35,6 +38,7 @@ limitations under the License. -->
import { ref } from "vue"; import { ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import ValueMappings from "./components/ValueMappings.vue";
const { t } = useI18n(); const { t } = useI18n();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();

View File

@@ -14,6 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div> <div>
<value-mappings />
</div>
<div class="item">
<span class="label">{{ t("showValues") }}</span> <span class="label">{{ t("showValues") }}</span>
<el-switch <el-switch
v-model="showTableValues" v-model="showTableValues"
@@ -37,6 +40,7 @@ limitations under the License. -->
import { ref } from "vue"; import { ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import ValueMappings from "./components/ValueMappings.vue";
const { t } = useI18n(); const { t } = useI18n();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();

View File

@@ -22,6 +22,15 @@ limitations under the License. -->
@change="updateLegendConfig({ show: legend.show })" @change="updateLegendConfig({ show: legend.show })"
/> />
</div> </div>
<div>
<span class="label mr-5">{{ t("asSelector") }}</span>
<el-switch
v-model="legend.asSelector"
active-text="Yes"
inactive-text="No"
@change="updateLegendConfig({ asSelector: legend.asSelector })"
/>
</div>
<div> <div>
<span class="label">{{ t("asTable") }}</span> <span class="label">{{ t("asTable") }}</span>
<el-switch <el-switch
@@ -97,6 +106,7 @@ limitations under the License. -->
max: false, max: false,
mean: false, mean: false,
asTable: false, asTable: false,
asSelector: false,
toTheRight: false, toTheRight: false,
width: 130, width: 130,
...graph.value.legend, ...graph.value.legend,

View File

@@ -0,0 +1,112 @@
<!-- 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>
<span class="label">{{ t("valueMappings") }}</span>
<span class="label red">{{ t("mappingTip") }}</span>
</div>
<div v-for="(key, index) in keys" :key="index" class="mb-10 flex-h">
<div class="content-decoration" contenteditable="true" @blur="changeKeys($event, index)">
{{ key }}
</div>
<div class="ml-5 mr-10">:</div>
<div class="content-decoration" contenteditable="true" @blur="changeValues($event, key)">
{{ valueMappings[key] }}
</div>
<div v-if="index === keys.length - 1">
<Icon class="cp mr-5" iconName="add_circle_outlinecontrol_point" size="middle" @click="addDecoration" />
<Icon
v-if="index !== 0"
class="cp"
iconName="remove_circle_outline"
size="middle"
@click="deleteDecoration(index)"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const graph = dashboardStore.selectedGrid.graph;
const valueMappings = ref<{ [key: string]: string }>(graph?.valueMappings || {});
const keys = ref<string[]>(graph.valueMappings ? Object.keys(valueMappings.value) : [""]);
function changeKeys(event: any, index: number) {
const params = event.target.textContent || "";
const list = Object.keys(valueMappings.value);
if (params) {
valueMappings.value[params] = valueMappings.value[list[index]];
}
delete valueMappings.value[list[index]];
keys.value = Object.keys(valueMappings.value);
if (!keys.value.length) {
keys.value = [""];
}
updateConfig();
}
function changeValues(event: any, key: string) {
valueMappings.value[key] = event.target.textContent || "";
updateConfig();
}
function addDecoration() {
keys.value.push("");
}
function deleteDecoration(index: number) {
if (!keys.value.length) {
return;
}
delete valueMappings.value[keys.value[index]];
keys.value.splice(index, 1);
updateConfig();
}
function updateConfig() {
const graph = {
...dashboardStore.selectedGrid.graph,
valueMappings: valueMappings.value,
};
dashboardStore.selectWidget({ ...dashboardStore.selectedGrid, graph });
}
</script>
<style lang="scss" scoped>
.content-decoration {
width: 350px;
border: 1px solid $border-color;
cursor: text;
padding: 0 5px;
border-radius: 3px;
outline: none;
margin-right: 5px;
min-height: 26px;
&:focus {
border-color: $active-color;
}
}
.label {
font-size: 13px;
font-weight: 500;
padding-right: 10px;
}
</style>

View File

@@ -95,6 +95,20 @@ limitations under the License. -->
{{ type.label }} {{ type.label }}
</span> </span>
</div> </div>
<div v-if="states.isTopList" class="mt-10">
<div>{{ t("valueDashboard") }}</div>
<div>
<Selector
:value="states.valueRelatedDashboard || ''"
:options="states.dashboardList"
size="small"
placeholder="Please select a dashboard name"
@change="changeValueDashboard"
class="selectors"
:clearable="true"
/>
</div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref, computed } from "vue"; import { reactive, ref, computed } from "vue";
@@ -140,21 +154,25 @@ limitations under the License. -->
metricTypes: string[]; metricTypes: string[];
metricTypeList: Option[][]; metricTypeList: Option[][];
isList: boolean; isList: boolean;
isTopList: boolean;
dashboardName: string; dashboardName: string;
dashboardList: ((DashboardItem & { label: string; value: string }) | any)[]; dashboardList: ((DashboardItem & { label: string; value: string }) | any)[];
tips: string[]; tips: string[];
subTips: string[]; subTips: string[];
valueRelatedDashboard: string;
}>({ }>({
metrics: metrics.value.length ? metrics.value : [""], metrics: metrics.value.length ? metrics.value : [""],
metricTypes: typesOfMQE.value.length ? typesOfMQE.value : [""], metricTypes: typesOfMQE.value.length ? typesOfMQE.value : [""],
metricTypeList: [], metricTypeList: [],
isList: false, isList: false,
isTopList: false,
dashboardName: graph.value.dashboardName, dashboardName: graph.value.dashboardName,
dashboardList: [{ label: "", value: "" }], dashboardList: [{ label: "", value: "" }],
tips: [], tips: [],
subTips: [], subTips: [],
subMetrics: subMetrics.value.length ? subMetrics.value : [""], subMetrics: subMetrics.value.length ? subMetrics.value : [""],
subMetricTypes: subMetricTypes.value.length ? subMetricTypes.value : [""], subMetricTypes: subMetricTypes.value.length ? subMetricTypes.value : [""],
valueRelatedDashboard: dashboardStore.selectedGrid.valueRelatedDashboard,
}); });
const currentMetricConfig = ref<MetricConfigOpt>({ const currentMetricConfig = ref<MetricConfigOpt>({
unit: "", unit: "",
@@ -163,6 +181,7 @@ limitations under the License. -->
sortOrder: "DES", sortOrder: "DES",
}); });
states.isTopList = graph.value.type === ChartTypes[4].value;
states.isList = ListChartTypes.includes(graph.value.type); states.isList = ListChartTypes.includes(graph.value.type);
const defaultLen = ref<number>(states.isList ? 5 : 20); const defaultLen = ref<number>(states.isList ? 5 : 20);
@@ -187,9 +206,10 @@ limitations under the License. -->
const arr = list.reduce((prev: (DashboardItem & { label: string; value: string })[], d: DashboardItem) => { const arr = list.reduce((prev: (DashboardItem & { label: string; value: string })[], d: DashboardItem) => {
if (d.layer === dashboardStore.layerId) { if (d.layer === dashboardStore.layerId) {
if ( if (
(d.entity === EntityType[0].value && chart === "ServiceList") || (d.entity === EntityType[0].value && chart === ChartTypes[8].value) ||
(d.entity === EntityType[2].value && chart === "EndpointList") || (d.entity === EntityType[2].value && chart === ChartTypes[9].value) ||
(d.entity === EntityType[3].value && chart === "InstanceList") (d.entity === EntityType[3].value && chart === ChartTypes[10].value) ||
states.isTopList
) { ) {
prev.push({ prev.push({
...d, ...d,
@@ -254,9 +274,28 @@ limitations under the License. -->
typesOfMQE: states.metricTypes, typesOfMQE: states.metricTypes,
}); });
emit("update", params.source || {}); emit("update", params.source || {});
if (states.isTopList) {
const values: any = Object.values(params.source)[0];
if (!values) {
return;
}
states.dashboardList = states.dashboardList.filter((d) => d.entity === values[0].owner.scope);
}
} }
function changeDashboard(opt: any) { function changeValueDashboard(opt: { value: string }[]) {
if (!opt[0]) {
states.valueRelatedDashboard = "";
} else {
states.valueRelatedDashboard = opt[0].value;
}
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
valueRelatedDashboard: states.valueRelatedDashboard,
});
}
function changeDashboard(opt: { value: string }[]) {
if (!opt[0]) { if (!opt[0]) {
states.dashboardName = ""; states.dashboardName = "";
} else { } else {

View File

@@ -0,0 +1,85 @@
<!-- 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" v-if="dashboardStore.editMode">
<template #reference>
<span class="operation cp">
<Icon iconName="ellipsis_v" size="middle" />
</span>
</template>
<div class="tools" @click="removeWidget">
<span>{{ t("delete") }}</span>
</div>
</el-popover>
<Header />
<Content :config="props.data" />
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import Content from "../related/async-profiling/Content.vue";
import Header from "../related/async-profiling/Header.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: $font-size-smaller;
position: relative;
}
.operation {
position: absolute;
top: 8px;
right: 3px;
}
.header {
padding: 10px;
font-size: $font-size-smaller;
border-bottom: 1px solid $border-color;
}
.tools {
padding: 5px 0;
color: #999;
cursor: pointer;
position: relative;
text-align: center;
&:hover {
color: $active-color;
background-color: $popper-hover-bg-color;
}
}
</style>

View File

@@ -184,7 +184,7 @@ limitations under the License. -->
position: relative; position: relative;
width: 2px; width: 2px;
height: 100%; height: 100%;
background-color: #e8e8e8; background-color: var(--sw-trace-line);
cursor: ew-resize; cursor: ew-resize;
&:hover { &:hover {

View File

@@ -61,6 +61,7 @@ limitations under the License. -->
typesOfMQE: typesOfMQE || [], typesOfMQE: typesOfMQE || [],
subExpressions: data.subExpressions || [], subExpressions: data.subExpressions || [],
subTypesOfMQE: data.subTypesOfMQE || [], subTypesOfMQE: data.subTypesOfMQE || [],
valueRelatedDashboard: data.valueRelatedDashboard,
}" }"
:needQuery="needQuery" :needQuery="needQuery"
@click="clickHandle" @click="clickHandle"

View File

@@ -26,6 +26,7 @@ import DemandLog from "./DemandLog.vue";
import Event from "./Event.vue"; import Event from "./Event.vue";
import NetworkProfiling from "./NetworkProfiling.vue"; import NetworkProfiling from "./NetworkProfiling.vue";
import ContinuousProfiling from "./ContinuousProfiling.vue"; import ContinuousProfiling from "./ContinuousProfiling.vue";
import AsyncProfiling from "./AsyncProfiling.vue";
import TimeRange from "./TimeRange.vue"; import TimeRange from "./TimeRange.vue";
import ThirdPartyApp from "./ThirdPartyApp.vue"; import ThirdPartyApp from "./ThirdPartyApp.vue";
import TaskTimeline from "./TaskTimeline.vue"; import TaskTimeline from "./TaskTimeline.vue";
@@ -43,6 +44,7 @@ export default {
Event, Event,
NetworkProfiling, NetworkProfiling,
ContinuousProfiling, ContinuousProfiling,
AsyncProfiling,
TimeRange, TimeRange,
ThirdPartyApp, ThirdPartyApp,
TaskTimeline, TaskTimeline,

View File

@@ -25,6 +25,7 @@ import DemandLog from "./DemandLog.vue";
import Event from "./Event.vue"; import Event from "./Event.vue";
import NetworkProfiling from "./NetworkProfiling.vue"; import NetworkProfiling from "./NetworkProfiling.vue";
import ContinuousProfiling from "./ContinuousProfiling.vue"; import ContinuousProfiling from "./ContinuousProfiling.vue";
import AsyncProfiling from "./AsyncProfiling.vue";
import TimeRange from "./TimeRange.vue"; import TimeRange from "./TimeRange.vue";
import ThirdPartyApp from "./ThirdPartyApp.vue"; import ThirdPartyApp from "./ThirdPartyApp.vue";
import TaskTimeline from "./TaskTimeline.vue"; import TaskTimeline from "./TaskTimeline.vue";
@@ -43,5 +44,6 @@ export default {
TimeRange, TimeRange,
ThirdPartyApp, ThirdPartyApp,
ContinuousProfiling, ContinuousProfiling,
AsyncProfiling,
TaskTimeline, TaskTimeline,
}; };

View File

@@ -150,6 +150,7 @@ export enum WidgetType {
Event = "Event", Event = "Event",
NetworkProfiling = "NetworkProfiling", NetworkProfiling = "NetworkProfiling",
ContinuousProfiling = "ContinuousProfiling", ContinuousProfiling = "ContinuousProfiling",
AsyncProfiling = "AsyncProfiling",
ThirdPartyApp = "ThirdPartyApp", ThirdPartyApp = "ThirdPartyApp",
TaskTimeline = "TaskTimeline", TaskTimeline = "TaskTimeline",
} }
@@ -171,6 +172,7 @@ export const ServiceTools = [
{ name: "timeline", content: "Add Trace Profiling", id: WidgetType.Profile }, { name: "timeline", content: "Add Trace Profiling", id: WidgetType.Profile },
{ name: "insert_chart", content: "Add eBPF Profiling", id: WidgetType.Ebpf }, { name: "insert_chart", content: "Add eBPF Profiling", id: WidgetType.Ebpf },
{ name: "continuous_profiling", content: "Add Continuous Profiling", id: WidgetType.ContinuousProfiling }, { name: "continuous_profiling", content: "Add Continuous Profiling", id: WidgetType.ContinuousProfiling },
{ name: "async_profiling", content: "Add Async Profiling", id: WidgetType.AsyncProfiling },
{ name: "assignment", content: "Add Log", id: WidgetType.Log }, { name: "assignment", content: "Add Log", id: WidgetType.Log },
{ name: "demand", content: "Add On Demand Log", id: WidgetType.DemandLog }, { name: "demand", content: "Add On Demand Log", id: WidgetType.DemandLog },
{ name: "event", content: "Add Event", id: WidgetType.Event }, { name: "event", content: "Add Event", id: WidgetType.Event },
@@ -266,8 +268,9 @@ export const TextColors: { [key: string]: string } = {
}; };
export const RefIdTypes = [ export const RefIdTypes = [
{ label: "Trace ID", value: "traceId" },
{ label: "None", value: "none" }, { label: "None", value: "none" },
{ label: "Trace ID", value: "traceId" },
{ label: "Owner", value: "owner" },
]; ];
export const RefreshOptions = [ export const RefreshOptions = [
{ label: "Last 30 minutes", value: "30", step: "MINUTE" }, { label: "Last 30 minutes", value: "30", step: "MINUTE" },

View File

@@ -75,6 +75,34 @@ limitations under the License. -->
}; };
}); });
const color: string[] = chartColors(); const color: string[] = chartColors();
const legend =
appStore.theme === Themes.Dark
? {
pageIconColor: "#ccc",
pageIconInactiveColor: "#444",
backgroundColor: "#333",
borderColor: "#fff",
textStyle: {
fontSize: 12,
color: "#eee",
},
pageTextStyle: {
color: "#eee",
},
}
: {
pageIconColor: "#666",
pageIconInactiveColor: "#ccc",
backgroundColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
borderColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
textStyle: {
fontSize: 12,
color: "#333",
},
pageTextStyle: {
color: "#333",
},
};
return { return {
color, color,
tooltip: { tooltip: {
@@ -85,7 +113,7 @@ limitations under the License. -->
}, },
enterable: true, enterable: true,
confine: true, confine: true,
extraCssText: "max-height:85%; overflow: auto;", extraCssText: "max-width: 100%; max-height: 75%; white-space: normal; overflow: auto;",
}, },
legend: { legend: {
type: "scroll", type: "scroll",
@@ -94,15 +122,10 @@ limitations under the License. -->
top: 0, top: 0,
left: 0, left: 0,
itemWidth: 12, itemWidth: 12,
backgroundColor: appStore.theme === Themes.Dark ? "#333" : "#fff", ...legend,
borderColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
textStyle: {
fontSize: 12,
color: appStore.theme === Themes.Dark ? "#eee" : "#333",
},
}, },
grid: { grid: {
top: keys.length === 1 ? 15 : 40, top: showEchartsLegend(keys) ? 35 : 10,
left: 0, left: 0,
right: 10, right: 10,
bottom: 5, bottom: 5,

View File

@@ -22,7 +22,7 @@ limitations under the License. -->
justifyContent: config.textAlign || 'center', justifyContent: config.textAlign || 'center',
}" }"
> >
{{ singleVal }} {{ getValue() }}
<span class="unit" v-show="config.showUnit && unit"> <span class="unit" v-show="config.showUnit && unit">
{{ decodeURIComponent(unit) }} {{ decodeURIComponent(unit) }}
</span> </span>
@@ -34,6 +34,7 @@ limitations under the License. -->
import type { PropType } from "vue"; import type { PropType } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import type { CardConfig, MetricConfigOpt } from "@/types/dashboard"; import type { CardConfig, MetricConfigOpt } from "@/types/dashboard";
import { ElMessage } from "element-plus";
/*global defineProps */ /*global defineProps */
const props = defineProps({ const props = defineProps({
@@ -48,16 +49,36 @@ limitations under the License. -->
showUnit: true, showUnit: true,
textAlign: "center", textAlign: "center",
metricConfig: [], metricConfig: [],
valueMappings: {},
}), }),
}, },
}); });
const { t } = useI18n(); const { t } = useI18n();
const metricConfig = computed(() => props.config.metricConfig || []); const metricConfig = computed(() => props.config.metricConfig || []);
const valueMappings = computed(() => props.config.valueMappings || {});
const key = computed(() => Object.keys(props.data)[0]); const key = computed(() => Object.keys(props.data)[0]);
const singleVal = computed(() => const singleVal = computed(() =>
Array.isArray(props.data[key.value]) ? props.data[key.value][0] : props.data[key.value], Array.isArray(props.data[key.value]) ? props.data[key.value][0] : props.data[key.value],
); );
const unit = computed(() => metricConfig.value[0] && encodeURIComponent(metricConfig.value[0].unit || "")); const unit = computed(() => metricConfig.value[0] && encodeURIComponent(metricConfig.value[0].unit || ""));
function getValue() {
if (valueMappings.value[singleVal.value]) {
return valueMappings.value[singleVal.value];
}
const list = Object.keys(valueMappings.value);
for (const i of list) {
try {
if (new RegExp(i).test(String(singleVal.value))) {
return valueMappings.value[i] || singleVal.value;
}
} catch (e) {
ElMessage.error("invalid regex string");
return singleVal.value;
}
}
return singleVal.value;
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chart-card { .chart-card {

View File

@@ -16,16 +16,18 @@ limitations under the License. -->
<div class="table"> <div class="table">
<div class="search"> <div class="search">
<el-input v-model="searchText" placeholder="Search for more endpoints" @change="searchList" class="inputs"> <el-input v-model="searchText" placeholder="Search for more endpoints" @change="searchList" class="inputs">
<template #prepend>
<Selector style="width: 120px" v-model="topN" :options="topNList" placeholder="Select" />
</template>
<template #append> <template #append>
<el-button @click="searchList"> <el-button @click="searchList">
<Icon size="middle" iconName="search" /> <Icon size="middle" iconName="search" />
</el-button> </el-button>
</template> </template>
</el-input> </el-input>
<span class="ml-5 tips">{{ t("endpointTips") }}</span>
</div> </div>
<div class="list"> <div class="list">
<el-table v-loading="chartLoading" :data="endpoints" style="width: 100%"> <el-table v-loading="chartLoading" :data="currentEndpoints" style="width: 100%">
<el-table-column label="Endpoints" fixed min-width="220"> <el-table-column label="Endpoints" fixed min-width="220">
<template #default="scope"> <template #default="scope">
<span class="link" @click="clickEndpoint(scope)" :style="{ fontSize: `${config.fontSize}px` }"> <span class="link" @click="clickEndpoint(scope)" :style="{ fontSize: `${config.fontSize}px` }">
@@ -46,13 +48,22 @@ limitations under the License. -->
/> />
</el-table> </el-table>
</div> </div>
<el-pagination
class="pagination flex-h"
layout="prev, pager, next"
:current-page="currentPage"
:page-size="pageSize"
:total="endpoints.length"
@current-change="handleCurrentChange"
@prev-click="changePage"
@next-click="changePage"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from "vue"; import { ref, watch } from "vue";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
import type { PropType } from "vue"; import type { PropType } from "vue";
import type { EndpointListConfig } from "@/types/dashboard"; import type { EndpointListConfig } from "@/types/dashboard";
import type { Endpoint } from "@/types/selector"; import type { Endpoint } from "@/types/selector";
@@ -90,16 +101,26 @@ limitations under the License. -->
}); });
const emit = defineEmits(["expressionTips"]); const emit = defineEmits(["expressionTips"]);
const { t } = useI18n();
const selectorStore = useSelectorStore(); const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const chartLoading = ref<boolean>(false); const chartLoading = ref<boolean>(false);
const endpoints = ref<Endpoint[]>([]); const endpoints = ref<Endpoint[]>([]); // all of endpoints
const currentEndpoints = ref<Endpoint[]>([]); // current page of endpoints
const searchText = ref<string>(""); const searchText = ref<string>("");
const colMetrics = ref<string[]>([]); const colMetrics = ref<string[]>([]);
const colSubMetrics = ref<string[]>([]); const colSubMetrics = ref<string[]>([]);
const metricConfig = ref<MetricConfigOpt[]>(props.config.metricConfig || []); const metricConfig = ref<MetricConfigOpt[]>(props.config.metricConfig || []);
const typesOfMQE = ref<string[]>(props.config.typesOfMQE || []); const typesOfMQE = ref<string[]>(props.config.typesOfMQE || []);
const topN = ref<number>(20);
const currentPage = ref<number>(1);
const pageSize = 10;
const topNList = [
{ label: "TopN20", value: 20 },
{ label: "TopN50", value: 50 },
{ label: "TopN100", value: 100 },
{ label: "TopN150", value: 150 },
{ label: "TopN200", value: 200 },
];
if (props.needQuery) { if (props.needQuery) {
queryEndpoints(); queryEndpoints();
@@ -108,6 +129,7 @@ limitations under the License. -->
chartLoading.value = true; chartLoading.value = true;
const resp = await selectorStore.getEndpoints({ const resp = await selectorStore.getEndpoints({
keyword: searchText.value, keyword: searchText.value,
limit: topN.value,
}); });
chartLoading.value = false; chartLoading.value = false;
@@ -116,7 +138,8 @@ limitations under the License. -->
return; return;
} }
endpoints.value = resp.data.pods || []; endpoints.value = resp.data.pods || [];
queryEndpointMetrics(endpoints.value); currentEndpoints.value = endpoints.value.filter((d: unknown, index: number) => index < pageSize);
queryEndpointMetrics(currentEndpoints.value);
} }
async function queryEndpointMetrics(arr: Endpoint[]) { async function queryEndpointMetrics(arr: Endpoint[]) {
if (!arr.length) { if (!arr.length) {
@@ -142,7 +165,7 @@ limitations under the License. -->
{ metricConfig: metricConfig.value || [], expressions, subExpressions }, { metricConfig: metricConfig.value || [], expressions, subExpressions },
EntityType[2].value, EntityType[2].value,
); );
endpoints.value = params.data; currentEndpoints.value = params.data;
colMetrics.value = params.names; colMetrics.value = params.names;
colSubMetrics.value = params.subNames; colSubMetrics.value = params.subNames;
metricConfig.value = params.metricConfigArr; metricConfig.value = params.metricConfigArr;
@@ -151,7 +174,7 @@ limitations under the License. -->
return; return;
} }
endpoints.value = currentPods; currentEndpoints.value = currentPods;
colMetrics.value = []; colMetrics.value = [];
colSubMetrics.value = []; colSubMetrics.value = [];
metricConfig.value = []; metricConfig.value = [];
@@ -175,6 +198,16 @@ limitations under the License. -->
async function searchList() { async function searchList() {
await queryEndpoints(); await queryEndpoints();
} }
function changePage() {
currentEndpoints.value = endpoints.value.filter(
(_, index: number) => index >= (currentPage.value - 1) * pageSize && index < currentPage.value * pageSize,
);
queryEndpointMetrics(currentEndpoints.value);
}
function handleCurrentChange(val: number) {
currentPage.value = val;
changePage();
}
watch( watch(
() => [ () => [
...(props.config.metricConfig || []), ...(props.config.metricConfig || []),

View File

@@ -16,10 +16,11 @@ limitations under the License. -->
<div class="graph flex-v" :class="setRight ? 'flex-h' : 'flex-v'"> <div class="graph flex-v" :class="setRight ? 'flex-h' : 'flex-v'">
<Graph <Graph
:option="option" :option="option"
@select="clickEvent"
:filters="config.filters" :filters="config.filters"
:relatedTrace="config.relatedTrace" :relatedTrace="config.relatedTrace"
:associate="config.associate || []" :associate="config.associate || []"
:legendSelector="{ isSelector: legendSelector, sConfigPage: dashboardStore.showConfig }"
@select="clickEvent"
/> />
<Legend :config="config.legend" :data="data" :intervalTime="intervalTime" /> <Legend :config="config.legend" :data="data" :intervalTime="intervalTime" />
</div> </div>
@@ -32,6 +33,7 @@ limitations under the License. -->
import useLegendProcess from "@/hooks/useLegendProcessor"; import useLegendProcess from "@/hooks/useLegendProcessor";
import { isDef } from "@/utils/is"; import { isDef } from "@/utils/is";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { useDashboardStore } from "@/store/modules/dashboard";
import { Themes } from "@/constants/data"; import { Themes } from "@/constants/data";
/*global defineProps, defineEmits */ /*global defineProps, defineEmits */
@@ -60,19 +62,24 @@ limitations under the License. -->
showYAxis: true, showYAxis: true,
smallTips: false, smallTips: false,
showlabels: true, showlabels: true,
noTooltips: false,
}), }),
}, },
}); });
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
const setRight = ref<boolean>(false); const setRight = ref<boolean>(false);
const legendSelector = computed(() => props.config.legend?.asSelector);
const option = computed(() => getOption()); const option = computed(() => getOption());
function getOption() { function getOption() {
const { showEchartsLegend, isRight, chartColors } = useLegendProcess(props.config.legend); const { showEchartsLegend, isRight, chartColors } = useLegendProcess(props.config.legend);
setRight.value = isRight; setRight.value = isRight;
const keys = Object.keys(props.data || {}).filter((i: any) => Array.isArray(props.data[i]) && props.data[i].length); const keys = Object.keys(props.data || {}).filter(
const temp = keys.map((i: any) => { (i: string) => Array.isArray(props.data[i]) && props.data[i].length,
);
const temp = keys.map((i: string) => {
const serie: any = { const serie: any = {
data: props.data[i].map((item: any, itemIndex: number) => [props.intervalTime[itemIndex], item]), data: props.data[i].map((item: number, itemIndex: number) => [props.intervalTime[itemIndex], item]),
name: i, name: i,
type: "line", type: "line",
symbol: "circle", symbol: "circle",
@@ -95,6 +102,7 @@ limitations under the License. -->
const color: string[] = chartColors(); const color: string[] = chartColors();
const tooltip = { const tooltip = {
trigger: "axis", trigger: "axis",
show: !props.config.noTooltips,
backgroundColor: appStore.theme === Themes.Dark ? "#333" : "#fff", backgroundColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
borderColor: appStore.theme === Themes.Dark ? "#333" : "#fff", borderColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
textStyle: { textStyle: {
@@ -103,13 +111,13 @@ limitations under the License. -->
}, },
enterable: true, enterable: true,
confine: true, confine: true,
extraCssText: "max-height:85%; overflow: auto;", extraCssText: "max-width: 100%; max-height: 75%; white-space: normal; overflow: auto;",
}; };
const tips = { const tips = {
show: !props.config.noTooltips,
formatter(params: any) { formatter(params: any) {
return `${params[0].value[1]}`; return `${params[0].value[1]}`;
}, },
confine: true,
extraCssText: `height: 20px; padding:0 2px;`, extraCssText: `height: 20px; padding:0 2px;`,
trigger: "axis", trigger: "axis",
backgroundColor: appStore.theme === Themes.Dark ? "#666" : "#eee", backgroundColor: appStore.theme === Themes.Dark ? "#666" : "#eee",
@@ -119,7 +127,30 @@ limitations under the License. -->
color: appStore.theme === Themes.Dark ? "#eee" : "#333", color: appStore.theme === Themes.Dark ? "#eee" : "#333",
}, },
}; };
const legend =
appStore.theme === Themes.Dark
? {
pageIconColor: "#ccc",
pageIconInactiveColor: "#444",
textStyle: {
fontSize: 12,
color: "#eee",
},
pageTextStyle: {
color: "#eee",
},
}
: {
pageIconColor: "#666",
pageIconInactiveColor: "#ccc",
textStyle: {
fontSize: 12,
color: "#333",
},
pageTextStyle: {
color: "#333",
},
};
return { return {
color, color,
tooltip: props.config.smallTips ? tips : tooltip, tooltip: props.config.smallTips ? tips : tooltip,
@@ -130,16 +161,15 @@ limitations under the License. -->
top: 0, top: 0,
left: 0, left: 0,
itemWidth: 12, itemWidth: 12,
textStyle: { data: keys.map((d: string) => ({ name: d })),
color: appStore.theme === Themes.Dark ? "#fff" : "#333", ...legend,
},
}, },
grid: { grid: {
top: showEchartsLegend(keys) ? 35 : 10, top: showEchartsLegend(keys) ? 35 : 10,
left: 0, left: 0,
right: 10, right: 10,
bottom: 5, bottom: 5,
containLabel: props.config.showlabels === undefined ? true : props.config.showlabels, containLabel: isDef(props.config.showlabels) ? props.config.showlabels : true,
}, },
xAxis: { xAxis: {
type: "category", type: "category",

View File

@@ -37,7 +37,7 @@ limitations under the License. -->
>{{ k.split("=")[1] }}</div >{{ k.split("=")[1] }}</div
> >
<div class="value-col" v-if="config.showTableValues"> <div class="value-col" v-if="config.showTableValues">
{{ data[(keys as string[]).join(",")][data[(keys as string[]).join(",")].length - 1 || 0] }} {{ getColValue(keys) }}
</div> </div>
</div> </div>
</div> </div>
@@ -47,6 +47,8 @@ limitations under the License. -->
import { computed } from "vue"; import { computed } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { ElMessage } from "element-plus";
/*global defineProps */ /*global defineProps */
const props = defineProps({ const props = defineProps({
data: { data: {
@@ -58,12 +60,14 @@ limitations under the License. -->
showTableValues: boolean; showTableValues: boolean;
tableHeaderCol2: string; tableHeaderCol2: string;
typesOfMQE: string[]; typesOfMQE: string[];
valueMappings: {};
}>, }>,
default: () => ({ showTableValues: true }), default: () => ({ showTableValues: true }),
}, },
}); });
const { t } = useI18n(); const { t } = useI18n();
const valueMappings = computed<{ [key: string]: string }>(() => props.config.valueMappings || {});
const nameWidth = computed(() => (props.config.showTableValues ? 80 : 100)); const nameWidth = computed(() => (props.config.showTableValues ? 80 : 100));
const dataKeys = computed(() => { const dataKeys = computed(() => {
const keys = Object.keys(props.data || {}).filter( const keys = Object.keys(props.data || {}).filter(
@@ -73,6 +77,25 @@ limitations under the License. -->
return list; return list;
}); });
function getColValue(keys: string[]) {
const source = props.data[(keys as string[]).join(",")][props.data[(keys as string[]).join(",")].length - 1 || 0];
if (valueMappings.value[source]) {
return valueMappings.value[source];
}
const list = Object.keys(valueMappings.value);
for (const i of list) {
try {
if (new RegExp(i).test(String(source))) {
return valueMappings.value[i] || source;
}
} catch (e) {
ElMessage.error("invalid regex string");
return source;
}
}
return source;
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chart-table { .chart-table {

View File

@@ -32,9 +32,12 @@ limitations under the License. -->
<div class="operation" @click="handleClick(i.name)"> <div class="operation" @click="handleClick(i.name)">
<span>{{ t("copy") }}</span> <span>{{ t("copy") }}</span>
</div> </div>
<div class="operation" @click="viewTrace(i)" v-show="refIdType === RefIdTypes[0].value"> <div class="operation" @click="viewTrace(i)" v-show="![RefIdTypes[0].value].includes(refIdType)">
<span>{{ t("viewTrace") }}</span> <span>{{ t("viewTrace") }}</span>
</div> </div>
<div class="operation" @click="viewDashboard(i)" v-show="props.config.valueRelatedDashboard">
<span>{{ t("viewValueDashboard") }}</span>
</div>
</el-popover> </el-popover>
</div> </div>
<el-progress <el-progress
@@ -61,16 +64,18 @@ limitations under the License. -->
import type { PropType } from "vue"; import type { PropType } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import router from "@/router";
import { useDashboardStore } from "@/store/modules/dashboard";
import copy from "@/utils/copy"; import copy from "@/utils/copy";
import { TextColors } from "@/views/dashboard/data"; import { TextColors, MetricCatalog } from "@/views/dashboard/data";
import Trace from "@/views/dashboard/related/trace/Index.vue"; import Trace from "@/views/dashboard/related/trace/Index.vue";
import { WidgetType, QueryOrders, Status, RefIdTypes, ExpressionResultType } from "@/views/dashboard/data"; import { WidgetType, QueryOrders, Status, RefIdTypes, ExpressionResultType } from "@/views/dashboard/data";
/*global defineProps */ /*global defineProps, Recordable*/
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object as PropType<{ type: Object as PropType<{
[key: string]: { name: string; value: number; refId: string }[]; [key: string]: { name: string; value: number; refId: string; owner: object }[];
}>, }>,
default: () => ({}), default: () => ({}),
}, },
@@ -80,12 +85,14 @@ limitations under the License. -->
expressions: string[]; expressions: string[];
typesOfMQE: string[]; typesOfMQE: string[];
relatedTrace: any; relatedTrace: any;
valueRelatedDashboard: string;
}>, }>,
default: () => ({ color: "purple" }), default: () => ({ color: "purple" }),
}, },
intervalTime: { type: Array as PropType<string[]>, default: () => [] }, intervalTime: { type: Array as PropType<string[]>, default: () => [] },
}); });
const { t } = useI18n(); const { t } = useI18n();
const dashboardStore = useDashboardStore();
const showTrace = ref<boolean>(false); const showTrace = ref<boolean>(false);
const traceOptions = ref<{ type: string; filters?: unknown }>({ const traceOptions = ref<{ type: string; filters?: unknown }>({
type: WidgetType.Trace, type: WidgetType.Trace,
@@ -107,14 +114,15 @@ limitations under the License. -->
function handleClick(i: string) { function handleClick(i: string) {
copy(i); copy(i);
} }
function viewTrace(item: { name: string; refId: string; value: unknown }) { function viewTrace(item: { name: string; refId: string; value: unknown; owner: object }) {
const filters = { const filters = {
...item, ...item,
queryOrder: QueryOrders[1].value, queryOrder: QueryOrders[1].value,
status: Status[2].value, status: Status[2].value,
id: item.refId, id: refIdType.value === RefIdTypes[1].value ? item.refId : undefined,
metricValue: [{ label: props.config.expressions[0], data: item.value, value: item.name }], metricValue: [{ label: props.config.expressions[0], data: item.value, value: item.name }],
isReadRecords: props.config.typesOfMQE.includes(ExpressionResultType.RECORD_LIST) || undefined, isReadRecords: props.config.typesOfMQE.includes(ExpressionResultType.RECORD_LIST) || undefined,
owner: refIdType.value === RefIdTypes[2].value ? item.owner : null,
}; };
traceOptions.value = { traceOptions.value = {
...traceOptions.value, ...traceOptions.value,
@@ -122,6 +130,23 @@ limitations under the License. -->
}; };
showTrace.value = true; showTrace.value = true;
} }
function viewDashboard(item: Recordable) {
const { owner } = item;
let path;
if (owner.scope === MetricCatalog.SERVICE) {
path = `/dashboard/${dashboardStore.layerId}/${owner.scope}/${owner.serviceID}/${props.config.valueRelatedDashboard}`;
}
if (owner.scope === MetricCatalog.SERVICE_INSTANCE) {
path = `/dashboard/${dashboardStore.layerId}/${owner.scope}/${owner.serviceID}/${owner.serviceInstanceID}/${props.config.valueRelatedDashboard}`;
}
if (owner.scope === MetricCatalog.ENDPOINT) {
path = `/dashboard/${dashboardStore.layerId}/${owner.scope}/${owner.serviceID}/${owner.endpointID}/${props.config.valueRelatedDashboard}`;
}
if (!path) {
return;
}
router.push(path);
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.top-list { .top-list {
@@ -187,11 +212,10 @@ limitations under the License. -->
} }
.operation { .operation {
padding: 5px 0; padding: 5px;
color: $font-color; color: $font-color;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
text-align: center;
font-size: $font-size-smaller; font-size: $font-size-smaller;
&:hover { &:hover {

View File

@@ -53,5 +53,5 @@
} }
.inputs { .inputs {
width: 300px; width: 400px;
} }

View File

@@ -77,16 +77,20 @@ limitations under the License. -->
} }
} }
async function queryMetrics() { async function queryMetrics() {
const widgets = []; const widgets: LayoutConfig[] = [];
for (const item of dashboardStore.layout) { for (const item of dashboardStore.layout) {
const isList = ListChartTypes.includes(item.type || ""); if (item.type === WidgetType.Widget) {
if (item.type === WidgetType.Widget && !isList) { if (!ListChartTypes.includes(item.graph?.type || "")) {
widgets.push(item); widgets.push(item);
} }
}
if (item.type === WidgetType.Tab) { if (item.type === WidgetType.Tab) {
const index = isNaN(item.activedTabIndex) ? 0 : item.activedTabIndex; const index = isNaN(item.activedTabIndex) ? 0 : item.activedTabIndex;
widgets.push(...item.children[index].children); widgets.push(
...item.children[index].children.filter(
(d: LayoutConfig) => d.type === WidgetType.Widget && !ListChartTypes.includes(d.graph?.type || ""),
),
);
} }
} }
const configList = widgets.map((d: LayoutConfig) => ({ const configList = widgets.map((d: LayoutConfig) => ({
@@ -103,7 +107,9 @@ limitations under the License. -->
} }
async function queryTabsMetrics() { async function queryTabsMetrics() {
const configList = dashboardStore.currentTabItems const configList = dashboardStore.currentTabItems
.filter((item: LayoutConfig) => item.type === WidgetType.Widget && !ListChartTypes.includes(item.type || "")) .filter(
(item: LayoutConfig) => item.type === WidgetType.Widget && !ListChartTypes.includes(item.graph?.type || ""),
)
.map((d: LayoutConfig) => ({ .map((d: LayoutConfig) => ({
metrics: d.expressions || [], metrics: d.expressions || [],
metricConfig: d.metricConfig || [], metricConfig: d.metricConfig || [],
@@ -151,7 +157,7 @@ limitations under the License. -->
}, },
); );
watch( watch(
() => appStore.durationTime, () => [appStore.durationTime, dashboardStore.layout],
() => { () => {
if (dashboardStore.entity === EntityType[1].value) { if (dashboardStore.entity === EntityType[1].value) {
queryMetrics(); queryMetrics();

View File

@@ -16,7 +16,7 @@ limitations under the License. -->
<div class="dashboard-tool flex-h"> <div class="dashboard-tool flex-h">
<div :class="isRelation ? 'flex-v' : 'flex-h'" class="tool-selectors"> <div :class="isRelation ? 'flex-v' : 'flex-h'" class="tool-selectors">
<div class="flex-h"> <div class="flex-h">
<div class="selectors-item" v-if="key !== 10"> <div class="flex-h selectors-item" v-if="key !== 10">
<span class="label">$Service</span> <span class="label">$Service</span>
<Selector <Selector
v-model="states.currentService" v-model="states.currentService"
@@ -30,7 +30,7 @@ limitations under the License. -->
<Icon size="small" iconName="hierarchy_topology" /> <Icon size="small" iconName="hierarchy_topology" />
</span> </span>
</div> </div>
<div class="selectors-item" v-if="key === 3 || key === 4 || key === 5 || key === 6"> <div class="flex-h selectors-item" v-if="key === 3 || key === 4 || key === 5 || key === 6">
<span class="label"> <span class="label">
{{ ["EndpointRelation", "Endpoint"].includes(dashboardStore.entity) ? "$Endpoint" : "$ServiceInstance" }} {{ ["EndpointRelation", "Endpoint"].includes(dashboardStore.entity) ? "$Endpoint" : "$ServiceInstance" }}
</span> </span>
@@ -52,7 +52,7 @@ limitations under the License. -->
<Icon size="small" iconName="hierarchy_topology" /> <Icon size="small" iconName="hierarchy_topology" />
</span> </span>
</div> </div>
<div class="selectors-item" v-if="key === 5 || key === 6"> <div class="flex-h selectors-item" v-if="key === 5 || key === 6">
<span class="label"> $Process </span> <span class="label"> $Process </span>
<Selector <Selector
v-model="states.currentProcess" v-model="states.currentProcess"
@@ -65,7 +65,7 @@ limitations under the License. -->
</div> </div>
</div> </div>
<div class="flex-h" :class="isRelation ? 'relation' : ''"> <div class="flex-h" :class="isRelation ? 'relation' : ''">
<div class="selectors-item" v-if="key === 2 || key === 4 || key === 5"> <div class="flex-h selectors-item" v-if="key === 2 || key === 4 || key === 5">
<span class="label">$DestinationService</span> <span class="label">$DestinationService</span>
<Selector <Selector
v-model="states.currentDestService" v-model="states.currentDestService"
@@ -76,7 +76,7 @@ limitations under the License. -->
class="selectors" class="selectors"
/> />
</div> </div>
<div class="selectors-item" v-if="key === 4 || key === 5"> <div class="flex-h selectors-item" v-if="key === 4 || key === 5">
<span class="label"> <span class="label">
{{ dashboardStore.entity === "EndpointRelation" ? "$DestinationEndpoint" : "$DestinationServiceInstance" }} {{ dashboardStore.entity === "EndpointRelation" ? "$DestinationEndpoint" : "$DestinationServiceInstance" }}
</span> </span>
@@ -91,7 +91,7 @@ limitations under the License. -->
:isRemote="dashboardStore.entity === 'EndpointRelation'" :isRemote="dashboardStore.entity === 'EndpointRelation'"
/> />
</div> </div>
<div class="selectors-item" v-if="key === 5"> <div class="flex-h selectors-item" v-if="key === 5">
<span class="label"> $DestinationProcess </span> <span class="label"> $DestinationProcess </span>
<Selector <Selector
v-model="states.currentDestProcess" v-model="states.currentDestProcess"
@@ -105,11 +105,11 @@ limitations under the License. -->
</div> </div>
</div> </div>
<div class="flex-h tools" v-loading="loading" v-if="!appStore.isMobile"> <div class="flex-h tools" v-loading="loading" v-if="!appStore.isMobile">
<div class="tool-icons" v-if="dashboardStore.editMode"> <div class="flex-h" v-if="dashboardStore.editMode">
<el-dropdown content="Controls" placement="bottom" :persistent="false"> <el-dropdown content="Controls" placement="bottom" :persistent="false">
<i> <div class="icon-btn">
<Icon class="icon-btn" size="sm" iconName="control" /> <Icon size="sm" iconName="control" />
</i> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="clickIcons(t)" v-for="(t, index) in toolIcons" :key="index" :title="t.content"> <el-dropdown-item @click="clickIcons(t)" v-for="(t, index) in toolIcons" :key="index" :title="t.content">
@@ -120,9 +120,9 @@ limitations under the License. -->
</template> </template>
</el-dropdown> </el-dropdown>
<el-tooltip content="Apply" placement="bottom"> <el-tooltip content="Apply" placement="bottom">
<i @click="applyDashboard"> <div class="icon-btn" @click="applyDashboard">
<Icon class="icon-btn" size="sm" iconName="save" /> <Icon size="sm" iconName="save" />
</i> </div>
</el-tooltip> </el-tooltip>
</div> </div>
<div class="ml-5"> <div class="ml-5">
@@ -693,10 +693,6 @@ limitations under the License. -->
padding: 4px 2px; padding: 4px 2px;
} }
.tool-icons {
margin-top: 2px;
}
.tools { .tools {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -704,9 +700,10 @@ limitations under the License. -->
} }
.icon-btn { .icon-btn {
display: inline-block; width: 22px;
padding: 3px; height: 22px;
text-align: center; text-align: center;
line-height: 18px;
border: 1px solid var(--sw-icon-btn-border); border: 1px solid var(--sw-icon-btn-border);
border-radius: 3px; border-radius: 3px;
margin-left: 6px; margin-left: 6px;
@@ -734,7 +731,7 @@ limitations under the License. -->
.hierarchy-btn { .hierarchy-btn {
display: inline-block; display: inline-block;
padding: 0 2px 2px; padding: 0 3px;
border: 1px solid #666; border: 1px solid #666;
border-radius: 4px; border-radius: 4px;
text-align: center; text-align: center;

View File

@@ -0,0 +1,69 @@
<!-- 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">
<TaskList />
<div class="vis-graph ml-5">
<div class="mb-20">
<Filter />
</div>
<div class="stack" v-loading="asyncProfilingStore.loadingTree">
<EBPFStack :type="ComponentType" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { ElMessage } from "element-plus";
import { useAsyncProfilingStore } from "@/store/modules/async-profiling";
import { useSelectorStore } from "@/store/modules/selectors";
import TaskList from "./components/TaskList.vue";
import Filter from "./components/Filter.vue";
import EBPFStack from "@/views/dashboard/related/ebpf/components/EBPFStack.vue";
import { ComponentType } from "./components/data";
const asyncProfilingStore = useAsyncProfilingStore();
const selectorStore = useSelectorStore();
onMounted(async () => {
const resp = await asyncProfilingStore.getServiceInstances({ serviceId: selectorStore.currentService.id });
if (resp && resp.errors) {
ElMessage.error(resp.errors);
}
});
</script>
<style lang="scss" scoped>
.content {
height: calc(100% - 30px);
width: 100%;
}
.vis-graph {
height: 100%;
flex-grow: 2;
min-width: 700px;
overflow: hidden;
position: relative;
width: calc(100% - 330px);
}
.stack {
width: 100%;
overflow: auto;
height: calc(100% - 100px);
padding-bottom: 10px;
}
</style>

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>
<div class="flex-h header">
<div class="title">Async Profiling</div>
<el-button class="mr-20" size="small" type="primary" @click="() => (newTask = true)">
{{ 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 } from "vue";
import { useI18n } from "vue-i18n";
import NewTask from "./components/NewTask.vue";
const { t } = useI18n();
const newTask = ref<boolean>(false);
</script>
<style lang="scss" scoped>
.header {
padding: 10px;
font-size: $font-size-smaller;
border-bottom: 1px solid $border-color;
justify-content: space-between;
}
.name {
width: 270px;
}
.title {
font-weight: bold;
line-height: 24px;
}
</style>

View File

@@ -0,0 +1,115 @@
<!-- 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">
<Selector
class="filter-selector"
:multiple="true"
:value="serviceInstanceIds"
size="small"
:options="instances"
placeholder="Select instances"
@change="changeInstances"
/>
<Selector
class="filter-events"
:value="selectedEventType"
size="small"
:options="eventTypes"
placeholder="Select a event"
@change="changeEventType"
/>
<el-button type="primary" size="small" @click="analyzeProfiling">
{{ t("analyze") }}
</el-button>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { ElMessage } from "element-plus";
import { useAsyncProfilingStore } from "@/store/modules/async-profiling";
import type { Instance } from "@/types/selector";
import type { Option } from "@/types/app";
import { EventsMap, ProfilingEvents, JFREventType } from "./data";
const { t } = useI18n();
const asyncProfilingStore = useAsyncProfilingStore();
const serviceInstanceIds = ref<string[]>([]);
const selectedEventType = ref<string>("");
const eventTypes = computed(() =>
(asyncProfilingStore.selectedTask.events ?? [])
.map((d: string) => {
if (d === ProfilingEvents[1]) {
return [
{ label: JFREventType.OBJECT_ALLOCATION_IN_NEW_TLAB, value: JFREventType.OBJECT_ALLOCATION_IN_NEW_TLAB },
{ label: JFREventType.OBJECT_ALLOCATION_OUTSIDE_TLAB, value: JFREventType.OBJECT_ALLOCATION_OUTSIDE_TLAB },
];
}
return { label: d, value: d };
})
.flat(),
);
const instances = computed(() =>
asyncProfilingStore.instances.filter((d: Instance) =>
(asyncProfilingStore.selectedTask.successInstanceIds ?? []).includes(d.id),
),
);
function changeInstances(options: Option[]) {
serviceInstanceIds.value = options.map((d: Option) => d.value);
asyncProfilingStore.setAnalyzeTrees([]);
}
function changeEventType(options: Option[]) {
selectedEventType.value = options[0].value;
asyncProfilingStore.setAnalyzeTrees([]);
}
async function analyzeProfiling() {
const instanceIds = asyncProfilingStore.instances
.filter((d: Instance) => (serviceInstanceIds.value ?? []).includes(d.value))
.map((d: Instance) => d.id);
const res = await asyncProfilingStore.getAsyncProfilingAnalyze({
instanceIds,
taskId: asyncProfilingStore.selectedTask.id,
eventType: (EventsMap as any)[selectedEventType.value],
});
if (res.data && res.data.errors) {
ElMessage.error(res.data.errors);
return;
}
}
watch(
() => asyncProfilingStore.selectedTask.successInstanceIds,
() => {
serviceInstanceIds.value = [];
selectedEventType.value = "";
},
);
</script>
<style>
.filter-selector {
width: 400px;
margin-right: 10px;
}
.filter-events {
width: 300px;
margin-right: 10px;
}
</style>

View File

@@ -0,0 +1,187 @@
<!-- 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="async-profile-task">
<div>
<div class="label">{{ t("instance") }}</div>
<Selector
class="profile-input"
:multiple="true"
:value="serviceInstanceIds"
size="small"
:options="asyncProfilingStore.instances"
placeholder="Select instances"
@change="changeInstances"
:filterable="false"
/>
</div>
<div>
<div class="label">{{ t("duration") }}</div>
<Radio class="mb-5" :value="duration" :options="DurationOptions" @change="changeDuration" />
</div>
<div>
<div class="label">{{ t("profilingEvents") }}</div>
<el-checkbox-group v-model="asyncEvents" class="profile-input mb-5">
<el-checkbox
v-for="event in ProfilingEvents"
:label="event"
:value="event"
:key="event"
:disabled="disableEvents(event)"
/>
</el-checkbox-group>
</div>
<div>
<div class="label">
<span class="mr-5 cp">{{ t("execArgs") }}</span>
<el-popover placement="right" :width="480" trigger="hover" title="Async profiler extension parameters">
<template #reference>
<span>
<Icon iconName="help" />
</span>
</template>
<div>
<p>
<span class="mr-10">live </span>
<span>- build allocation profile from live objects only</span>
</p>
<p>
<span class="mr-10">lock[=DURATION] </span>
<span>- profile contended locks overflowing the DURATION ns bucket (default: 10us)</span>
</p>
<p>
<span class="mr-10">alloc[=BYTES] </span>
<span>- profile allocations with BYTES interval</span>
</p>
<p>
<span class="mr-10">interval=N </span>
<span>- sampling interval in ns (default: 10'000'000, i.e. 10 ms)</span>
</p>
<p>
<span class="mr-10">jstackdepth=N </span>
<span>- maximum Java stack depth (default: 2048)</span>
</p>
<p>
<span class="mr-10">chunksize=N </span>
<span>- approximate size of JFR chunk in bytes (default: 100 MB)</span>
</p>
<p>
<span class="mr-10">chunktime=N </span>
<span>- duration of JFR chunk in seconds (default: 1 hour)</span>
</p>
</div>
</el-popover>
</div>
<el-input size="small" class="profile-input" v-model="execArgs" />
</div>
<div>
<el-button @click="createTask" type="primary" class="create-task-btn" :loading="loading">
{{ t("createTask") }}
</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useAsyncProfilingStore } from "@/store/modules/async-profiling";
import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus";
import { DurationOptions, ProfilingEvents } from "./data";
/* global defineEmits */
const emits = defineEmits(["close"]);
const asyncProfilingStore = useAsyncProfilingStore();
const selectorStore = useSelectorStore();
const { t } = useI18n();
const serviceInstanceIds = ref<string[]>([]);
const asyncEvents = ref<string[]>([]);
const duration = ref<string>(DurationOptions[0].value);
const execArgs = ref<string>("");
const loading = ref<boolean>(false);
const PartofEvents = [ProfilingEvents[3], ProfilingEvents[4], ProfilingEvents[5]];
function changeDuration(val: string) {
duration.value = val;
}
function changeInstances(options: { id: string }[]) {
serviceInstanceIds.value = options.map((d: { id: string }) => d.id);
}
function disableEvents(item: string) {
if (asyncEvents.value.includes(ProfilingEvents[0]) && PartofEvents.includes(item)) {
return true;
}
if (item === ProfilingEvents[0]) {
for (const event of PartofEvents) {
if (asyncEvents.value.includes(event)) {
return true;
}
}
}
return false;
}
async function createTask() {
const params = {
serviceId: selectorStore.currentService.id,
serviceInstanceIds: serviceInstanceIds.value,
duration: Number(duration.value) * 60,
events: asyncEvents.value,
execArgs: execArgs.value,
};
loading.value = true;
const res = await asyncProfilingStore.createTask(params);
loading.value = false;
if (res.errors) {
ElMessage.error(res.errors);
return;
}
const { errorReason } = res.data;
if (errorReason) {
ElMessage.error(errorReason);
return;
}
emits("close");
ElMessage.success("Task created successfully");
}
</script>
<style lang="scss" scoped>
.async-profile-task {
margin: 0 auto;
width: 600px;
}
.date {
font-size: $font-size-smaller;
}
.label {
margin-top: 10px;
font-size: $font-size-normal;
}
.profile-input {
width: 600px;
}
.create-task-btn {
width: 600px;
margin-top: 50px;
}
</style>

View File

@@ -0,0 +1,259 @@
<!-- 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" v-loading="asyncProfilingStore.loadingTasks">
<div class="profile-t-tool flex-h">{{ t("taskList") }}</div>
<div class="profile-t-wrapper">
<div class="no-data" v-show="!asyncProfilingStore.taskList.length">
{{ t("noData") }}
</div>
<table class="profile-t">
<tr
class="profile-tr cp"
v-for="(i, index) in asyncProfilingStore.taskList"
@click="changeTask(i)"
:key="index"
:class="{
selected: asyncProfilingStore.selectedTask.id === i.id,
}"
>
<td class="profile-td">
<div class="ell">
<span>{{ i.id }}</span>
<a class="profile-btn r" @click="() => (showDetail = true)">
<Icon iconName="view" size="middle" />
</a>
</div>
<div class="grey ell sm">
<span class="mr-10 sm">
{{ dateFormat(i.createTime) }}
</span>
<span class="mr-10 sm">
{{ dateFormat(i.createTime + i.duration * 1000) }}
</span>
</div>
</td>
</tr>
</table>
</div>
</div>
<el-dialog v-model="showDetail" :destroy-on-close="true" fullscreen @closed="showDetail = false">
<div class="profile-detail flex-v" v-if="asyncProfilingStore.selectedTask.id">
<div>
<h5 class="mb-10">{{ t("task") }}.</h5>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">ID:</span>
<span class="g-sm-8 wba">{{ asyncProfilingStore.selectedTask.id }}</span>
</div>
<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("execArgs") }}:</span>
<span class="g-sm-8 wba">{{ asyncProfilingStore.selectedTask.execArgs }}</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("duration") }}:</span>
<span class="g-sm-8 wba">{{ asyncProfilingStore.selectedTask.duration / 60 }}min</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("events") }}:</span>
<span class="g-sm-8 wba"> {{ asyncProfilingStore.selectedTask.events.join(", ") }} </span>
</div>
</div>
<div>
<h5
class="mb-5 mt-10"
v-show="asyncProfilingStore.selectedTask.logs && asyncProfilingStore.selectedTask.logs.length"
>
{{ t("logs") }}.
</h5>
<div v-for="(i, index) in Object.keys(instanceLogs)" :key="index">
<div class="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>
<h5 class="mb-10 mt-10" v-show="errorInstances.length"> {{ t("errorInstances") }}</h5>
<div v-for="(instance, index) in errorInstances" :key="instance.value || index">
<div class="mb-10 sm">
<span class="mr-10 grey">{{ t("instance") }}:</span>
<span>{{ instance.label }}</span>
</div>
<div v-for="(d, index) in instance.attributes" :key="d.value + index">
<span class="mr-10 grey">{{ d.name }}:</span>
<span class="mr-20">{{ d.value }}</span>
</div>
</div>
</div>
<div>
<h5 class="mb-10 mt-10" v-show="successInstances.length"> {{ t("successInstances") }}</h5>
<div v-for="(instance, index) in successInstances" :key="instance.value || index">
<div class="mb-10 sm">
<span class="mr-10 grey">{{ t("instance") }}:</span>
<span>{{ instance.label }}</span>
</div>
<div v-for="(d, index) in instance.attributes" :key="d.value + index">
<span class="mr-10 grey">{{ d.name }}:</span>
<span class="mr-20">{{ d.value }}</span>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import { useSelectorStore } from "@/store/modules/selectors";
import { useAsyncProfilingStore } from "@/store/modules/async-profiling";
import type { TaskLog, TaskListItem } from "@/types/profile";
import { ElMessage } from "element-plus";
import { dateFormat } from "@/utils/dateFormat";
import type { Instance, Service } from "@/types/selector";
const { t } = useI18n();
const asyncProfilingStore = useAsyncProfilingStore();
const selectorStore = useSelectorStore();
const showDetail = ref<boolean>(false);
const service = ref<string>("");
const instanceLogs = ref<TaskLog | any>({});
const errorInstances = ref<Instance[]>([]);
const successInstances = ref<Instance[]>([]);
onMounted(() => {
fetchTasks();
});
async function fetchTasks() {
const res = await asyncProfilingStore.getTaskList();
if (res.errors) {
return ElMessage.error(res.errors);
}
if (res.data.errorReason) {
ElMessage.error(res.errors);
}
}
async function changeTask(item: TaskListItem) {
if (item.id !== asyncProfilingStore.selectedTask.id) {
asyncProfilingStore.setAnalyzeTrees([]);
asyncProfilingStore.setSelectedTask(item);
}
service.value = (selectorStore.services.filter((s: Service) => s.id === item.serviceId)[0] ?? {}).label;
const res = await asyncProfilingStore.getTaskLogs({ taskId: item.id });
if (res.errors) {
ElMessage.error(res.errors);
return;
}
item = {
...item,
...asyncProfilingStore.taskProgress,
};
asyncProfilingStore.setSelectedTask(item);
errorInstances.value = asyncProfilingStore.instances.filter(
(d: Instance) => d.id && item.errorInstanceIds.includes(d.id),
);
successInstances.value = asyncProfilingStore.instances.filter(
(d: Instance) => d.id && item.successInstanceIds.includes(d.id),
);
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 }];
}
}
}
</script>
<style lang="scss" scoped>
.profile-task-list {
width: 300px;
height: calc(100% - 20px);
overflow: auto;
border-right: 1px solid var(--sw-trace-list-border);
}
.item span {
height: 21px;
}
.profile-td {
padding: 5px 10px;
border-bottom: 1px solid var(--sw-trace-list-border);
}
.selected {
background-color: var(--sw-list-selected);
}
.no-data {
text-align: center;
margin-top: 10px;
}
.profile-t-wrapper {
overflow: auto;
flex-grow: 1;
}
.profile-t {
width: 100%;
border-spacing: 0;
table-layout: fixed;
flex-grow: 1;
position: relative;
}
.profile-tr {
&:hover {
background-color: var(--sw-list-selected);
}
}
.profile-segment {
border-top: 1px solid var(--sw-trace-list-border);
}
.profile-t-tool {
padding: 5px 10px;
font-weight: bold;
border-right: 1px solid var(--sw-trace-list-border);
border-bottom: 1px solid var(--sw-trace-list-border);
background-color: var(--sw-table-header);
}
.profile-btn {
color: $font-color;
padding: 1px 3px;
border-radius: 2px;
font-size: $font-size-smaller;
float: right;
}
</style>

View File

@@ -0,0 +1,44 @@
/**
* 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 DurationOptions = [
{ value: "5", label: "5 min" },
{ value: "10", label: "10 min" },
{ value: "15", label: "15 min" },
];
export const ProfilingEvents = ["CPU", "ALLOC", "LOCK", "WALL", "CTIMER", "ITIMER"];
export enum EventsMap {
CPU = "EXECUTION_SAMPLE",
WALL = "EXECUTION_SAMPLE",
CTIMER = "EXECUTION_SAMPLE",
ITIMER = "EXECUTION_SAMPLE",
LOCK = "LOCK",
OBJECT_ALLOCATION_IN_NEW_TLAB = "OBJECT_ALLOCATION_IN_NEW_TLAB",
OBJECT_ALLOCATION_OUTSIDE_TLAB = "OBJECT_ALLOCATION_OUTSIDE_TLAB",
}
export enum JFREventType {
EXECUTION_SAMPLE = "EXECUTION_SAMPLE",
LOCK = "LOCK",
OBJECT_ALLOCATION_IN_NEW_TLAB = "OBJECT_ALLOCATION_IN_NEW_TLAB",
OBJECT_ALLOCATION_OUTSIDE_TLAB = "OBJECT_ALLOCATION_OUTSIDE_TLAB",
PROFILER_LIVE_OBJECT = "PROFILER_LIVE_OBJECT",
}
export const ComponentType = "ASYNC_PROFILING";

View File

@@ -64,7 +64,7 @@ limitations under the License. -->
</el-table> </el-table>
<el-pagination <el-pagination
class="mt-10" class="mt-10"
small size="small"
layout="prev, pager, next" layout="prev, pager, next"
:page-size="pageSize" :page-size="pageSize"
:total="continousProfilingStore.instances.length" :total="continousProfilingStore.instances.length"

View File

@@ -190,7 +190,7 @@ limitations under the License. -->
.profile-t-tool { .profile-t-tool {
padding: 10px 5px 10px 10px; padding: 10px 5px 10px 10px;
border-bottom: 1px solid rgb(0 0 0 / 7%); border-right: 1px solid rgb(0 0 0 / 7%);
background-color: var(--sw-table-header); background-color: var(--sw-table-header);
width: 100%; width: 100%;
font-weight: bold; font-weight: bold;

View File

@@ -29,8 +29,6 @@ export const TargetTypes = [
{ label: "NETWORK", value: "NETWORK" }, { label: "NETWORK", value: "NETWORK" },
]; ];
export const ComponentType = "CONTINOUS_PROFILING";
export const HeaderLabels = [ export const HeaderLabels = [
{ value: "triggeredCount", label: "Triggered Count", width: 150 }, { value: "triggeredCount", label: "Triggered Count", width: 150 },
{ value: "lastTriggerTime", label: "Last Trigger Time", width: 170 }, { value: "lastTriggerTime", label: "Last Trigger Time", width: 170 },

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div class="flex-h row"> <div class="flex-h row">
<div class="mr-5 mb-5" v-if="dashboardStore.entity !== EntityType[3].value"> <div class="mr-10 mb-5" v-if="dashboardStore.entity !== EntityType[3].value">
<span class="grey mr-5"> {{ t("instance") }}: </span> <span class="grey mr-5"> {{ t("instance") }}: </span>
<Selector <Selector
size="small" size="small"
@@ -25,7 +25,7 @@ limitations under the License. -->
class="selectors" class="selectors"
/> />
</div> </div>
<div class="mr-5 mb-5" v-if="state.container"> <div class="mr-10 mb-5" v-if="state.container">
<span class="grey mr-5">{{ t("container") }}:</span> <span class="grey mr-5">{{ t("container") }}:</span>
<Selector <Selector
size="small" size="small"
@@ -36,18 +36,7 @@ limitations under the License. -->
class="selectors" class="selectors"
/> />
</div> </div>
<!-- <div class="mr-5"> <div class="mr-10">
<span class="grey mr-5">{{ t("limit") }}:</span>
<el-input-number
v-model="limit"
:min="1"
:max="1000"
size="small"
controls-position="right"
@change="changeField('limit', $event)"
/>
</div> -->
<div class="mr-5">
<span class="grey mr-5">{{ t("duration") }}:</span> <span class="grey mr-5">{{ t("duration") }}:</span>
<Selector <Selector
size="small" size="small"
@@ -58,18 +47,19 @@ limitations under the License. -->
class="duration-range" class="duration-range"
/> />
</div> </div>
<div class="mr-5"> <div class="mr-10">
<span class="grey mr-5">{{ t("interval") }}:</span> <span class="grey mr-5">{{ t("interval") }}:</span>
<Selector <Selector
size="small" size="small"
:value="state.interval.value" :value="state.interval.value"
:options="IntervalOpts" :options="IntervalOpts"
@change="changeField('interval', $event)" @change="changeField('interval', $event)"
class="selectors"
/> />
</div> </div>
</div> </div>
<div class="flex-h row"> <div class="flex-h row">
<div class="mr-5"> <div class="mr-10">
<span class="mr-5 grey">{{ t("keywordsOfContent") }}:</span> <span class="mr-5 grey">{{ t("keywordsOfContent") }}:</span>
<span class="log-tags"> <span class="log-tags">
<span class="selected" v-for="(item, index) in keywordsOfContent" :key="`keywordsOfContent${index}`"> <span class="selected" v-for="(item, index) in keywordsOfContent" :key="`keywordsOfContent${index}`">
@@ -85,7 +75,7 @@ limitations under the License. -->
@change="addLabels('keywordsOfContent')" @change="addLabels('keywordsOfContent')"
/> />
</div> </div>
<div class="mr-5"> <div class="mr-10">
<span class="grey mr-5"> {{ t("excludingKeywordsOfContent") }}: </span> <span class="grey mr-5"> {{ t("excludingKeywordsOfContent") }}: </span>
<span class="log-tags"> <span class="log-tags">
<span <span

View File

@@ -15,7 +15,7 @@ limitations under the License. -->
<template> <template>
<div class="flex-h header"> <div class="flex-h header">
<div class="title">eBPF Profiling</div> <div class="title">eBPF Profiling</div>
<el-button type="primary" size="small" @click="createTask"> <el-button class="mr-10 mt-10" type="primary" size="small" @click="createTask">
{{ t("newTask") }} {{ t("newTask") }}
</el-button> </el-button>
</div> </div>

View File

@@ -76,7 +76,7 @@ limitations under the License. -->
</el-table> </el-table>
<el-pagination <el-pagination
class="pagination" class="pagination"
small size="small"
layout="prev, pager, next" layout="prev, pager, next"
:page-size="pageSize" :page-size="pageSize"
:total="processes.length" :total="processes.length"
@@ -97,21 +97,12 @@ limitations under the License. -->
import type { Option } from "@/types/app"; import type { Option } from "@/types/app";
import { TableHeader, AggregateTypes } from "./data"; import { TableHeader, AggregateTypes } from "./data";
import { useEbpfStore } from "@/store/modules/ebpf"; import { useEbpfStore } from "@/store/modules/ebpf";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import type { EBPFProfilingSchedule, Process } from "@/types/ebpf"; import type { EBPFProfilingSchedule, Process } from "@/types/ebpf";
import { ElMessage, ElTable } from "element-plus"; import { ElMessage, ElTable } from "element-plus";
import { dateFormat } from "@/utils/dateFormat"; import { dateFormat } from "@/utils/dateFormat";
import { ComponentType } from "@/views/dashboard/related/continuous-profiling/data";
const { t } = useI18n(); const { t } = useI18n();
/*global defineProps*/ const ebpfStore = useEbpfStore();
const props = defineProps({
type: {
type: String,
default: "",
},
});
const ebpfStore = props.type === ComponentType ? useContinousProfilingStore() : useEbpfStore();
const pageSize = 5; const pageSize = 5;
const multipleTableRef = ref<InstanceType<typeof ElTable>>(); const multipleTableRef = ref<InstanceType<typeof ElTable>>();
const selectedProcesses = ref<string[]>([]); const selectedProcesses = ref<string[]>([]);
@@ -126,12 +117,12 @@ limitations under the License. -->
return attr.map((d: { name: string; value: string }) => `${d.name}=${d.value}`).join("; "); return attr.map((d: { name: string; value: string }) => `${d.name}=${d.value}`).join("; ");
}; };
function changeLabels(opt: any[]) { function changeLabels(opt: Option[]) {
const arr = opt.map((d) => d.value); const arr = opt.map((d) => d.value);
selectedLabels.value = arr; selectedLabels.value = arr;
} }
function changeAggregateType(opt: any[]) { function changeAggregateType(opt: Option[]) {
aggregateType.value = opt[0].value; aggregateType.value = opt[0].value;
ebpfStore.setAnalyzeTrees([]); ebpfStore.setAnalyzeTrees([]);
} }

View File

@@ -23,10 +23,10 @@ limitations under the License. -->
import d3tip from "d3-tip"; import d3tip from "d3-tip";
import { flamegraph } from "d3-flame-graph"; import { flamegraph } from "d3-flame-graph";
import { useEbpfStore } from "@/store/modules/ebpf"; import { useEbpfStore } from "@/store/modules/ebpf";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling"; import { useAsyncProfilingStore } from "@/store/modules/async-profiling";
import { ComponentType } from "@/views/dashboard/related/continuous-profiling/data";
import type { StackElement } from "@/types/ebpf"; import type { StackElement } from "@/types/ebpf";
import { AggregateTypes } from "./data"; import { AggregateTypes } from "./data";
import { JFREventType, ComponentType } from "@/views/dashboard/related/async-profiling/components/data";
import "d3-flame-graph/dist/d3-flamegraph.css"; import "d3-flame-graph/dist/d3-flamegraph.css";
import { treeForeach } from "@/utils/flameGraph"; import { treeForeach } from "@/utils/flameGraph";
@@ -37,7 +37,7 @@ limitations under the License. -->
default: "", default: "",
}, },
}); });
const ebpfStore = props.type === ComponentType ? useContinousProfilingStore() : useEbpfStore(); const ebpfStore = props.type === ComponentType ? useAsyncProfilingStore() : useEbpfStore();
const stackTree = ref<Nullable<StackElement>>(null); const stackTree = ref<Nullable<StackElement>>(null);
const selectStack = ref<Nullable<StackElement>>(null); const selectStack = ref<Nullable<StackElement>>(null);
const graph = ref<Nullable<HTMLDivElement>>(null); const graph = ref<Nullable<HTMLDivElement>>(null);
@@ -52,7 +52,7 @@ limitations under the License. -->
if (!ebpfStore.analyzeTrees.length) { if (!ebpfStore.analyzeTrees.length) {
return (stackTree.value = null); return (stackTree.value = null);
} }
const root: StackElement = { let root: StackElement = {
parentId: "0", parentId: "0",
originId: "1", originId: "1",
name: "Virtual Root", name: "Virtual Root",
@@ -66,6 +66,11 @@ limitations under the License. -->
rateOfParent: "", rateOfParent: "",
}; };
countRange(); countRange();
if (props.type === ComponentType) {
const elements = processTree(ebpfStore.analyzeTrees[0].elements);
stackTree.value = elements;
root = { ...root, ...elements };
} else {
for (const tree of ebpfStore.analyzeTrees) { for (const tree of ebpfStore.analyzeTrees) {
const ele = processTree(tree.elements); const ele = processTree(tree.elements);
root.children && root.children.push(ele); root.children && root.children.push(ele);
@@ -81,6 +86,8 @@ limitations under the License. -->
root.value = param[0]; root.value = param[0];
root.dumpCount = param[1]; root.dumpCount = param[1];
stackTree.value = root; stackTree.value = root;
}
const width = (graph.value && graph.value.getBoundingClientRect().width) || 0; const width = (graph.value && graph.value.getBoundingClientRect().width) || 0;
const w = width < 800 ? 802 : width; const w = width < 800 ? 802 : width;
flameChart.value = flamegraph() flameChart.value = flamegraph()
@@ -102,10 +109,7 @@ limitations under the License. -->
.direction("s") .direction("s")
.html((d: { data: StackElement } & { parent: { data: StackElement } }) => { .html((d: { data: StackElement } & { parent: { data: StackElement } }) => {
const name = d.data.name.replace("<", "&lt;").replace(">", "&gt;"); const name = d.data.name.replace("<", "&lt;").replace(">", "&gt;");
const valStr = const valStr = tooltipContent(d.data);
ebpfStore.aggregateType === AggregateTypes[0].value
? `<div class="mb-5">Dump Count: ${d.data.dumpCount}</div>`
: `<div class="mb-5">Duration: ${d.data.dumpCount} ns</div>`;
const rateOfParent = const rateOfParent =
(d.parent && (d.parent &&
`<div class="mb-5">Percentage Of Selected: ${ `<div class="mb-5">Percentage Of Selected: ${
@@ -125,6 +129,26 @@ limitations under the License. -->
d3.select("#graph-stack").datum(stackTree.value).call(flameChart.value); d3.select("#graph-stack").datum(stackTree.value).call(flameChart.value);
} }
function tooltipContent(item: StackElement) {
if (props.type === ComponentType) {
if (!ebpfStore.analyzeTrees.length) {
return;
}
const { type } = ebpfStore.analyzeTrees[0];
if ([JFREventType.LOCK].includes(type)) {
return `<div class="mb-5">Duration: ${item.dumpCount} ns</div>`;
}
if (type === JFREventType.EXECUTION_SAMPLE) {
return `<div class="mb-5">Count: ${item.dumpCount}</div>`;
}
return `<div class="mb-5">Memory: ${item.dumpCount} byte</div>`;
}
ebpfStore.aggregateType === AggregateTypes[0].value
? `<div class="mb-5">Dump Count: ${item.dumpCount}</div>`
: `<div class="mb-5">Duration: ${item.dumpCount} ns</div>`;
}
function countRange() { function countRange() {
const list = []; const list = [];
for (const tree of ebpfStore.analyzeTrees) { for (const tree of ebpfStore.analyzeTrees) {
@@ -148,9 +172,9 @@ limitations under the License. -->
obj[item.originId] = item; obj[item.originId] = item;
} }
const scale = d3.scaleLinear().domain([min.value, max.value]).range([1, 200]); const scale = d3.scaleLinear().domain([min.value, max.value]).range([1, 200]);
const condition = props.type === ComponentType ? "0" : "1";
for (const item of copyArr) { for (const item of copyArr) {
if (item.parentId === "1") { if (item.parentId === condition) {
const val = Number(scale(item.dumpCount).toFixed(4)); const val = Number(scale(item.dumpCount).toFixed(4));
res = item; res = item;
res.value = val; res.value = val;

View File

@@ -13,9 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div class="flex-h row"> <div class="flex-h">
<div class="mr-5" v-if="dashboardStore.entity !== EntityType[3].value"> <div class="mr-5 flex-h" v-if="dashboardStore.entity !== EntityType[3].value">
<span class="grey mr-5"> {{ t("instance") }}: </span> <span class="grey mr-5 label"> {{ t("instance") }}: </span>
<Selector <Selector
size="small" size="small"
:value="state.instance.value" :value="state.instance.value"
@@ -24,8 +24,8 @@ limitations under the License. -->
@change="changeField('instance', $event)" @change="changeField('instance', $event)"
/> />
</div> </div>
<div class="mr-5" v-if="dashboardStore.entity !== EntityType[2].value"> <div class="mr-5 flex-h" v-if="dashboardStore.entity !== EntityType[2].value">
<span class="grey mr-5"> {{ t("endpoint") }}: </span> <span class="grey mr-5 label"> {{ t("endpoint") }}: </span>
<Selector <Selector
size="small" size="small"
:value="state.endpoint.value" :value="state.endpoint.value"
@@ -36,8 +36,8 @@ limitations under the License. -->
@query="searchEndpoints" @query="searchEndpoints"
/> />
</div> </div>
<div class="mr-5"> <div class="mr-5 flex-h">
<span class="grey">{{ t("eventsType") }}: </span> <span class="grey label" style="width: 95px">{{ t("eventsType") }}: </span>
<Selector <Selector
v-model="state.eventType" v-model="state.eventType"
:options="EventTypes" :options="EventTypes"
@@ -47,6 +47,7 @@ limitations under the License. -->
size="small" size="small"
/> />
</div> </div>
<div>
<el-pagination <el-pagination
v-model="pageNum" v-model="pageNum"
:page-size="pageSize" :page-size="pageSize"
@@ -54,8 +55,9 @@ limitations under the License. -->
:total="total" :total="total"
@current-change="updatePage" @current-change="updatePage"
:pager-count="5" :pager-count="5"
small size="small"
/> />
</div>
<el-button class="search-btn" size="small" type="primary" @click="queryEvents"> <el-button class="search-btn" size="small" type="primary" @click="queryEvents">
{{ t("search") }} {{ t("search") }}
</el-button> </el-button>
@@ -231,4 +233,8 @@ limitations under the License. -->
font-size: $font-size-smaller; font-size: $font-size-smaller;
margin: 0 2px; margin: 0 2px;
} }
.label {
line-height: 24px;
}
</style> </style>

View File

@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div class="flex-h row"> <div class="flex-h row">
<div class="mr-5" v-if="dashboardStore.entity === EntityType[1].value"> <div class="mr-10 flex-h" v-if="dashboardStore.entity === EntityType[1].value">
<span class="grey mr-5">{{ t("service") }}:</span> <span class="grey mr-5 label">{{ t("service") }}:</span>
<Selector <Selector
size="small" size="small"
:value="state.service.value" :value="state.service.value"
@@ -24,8 +24,8 @@ limitations under the License. -->
@change="changeField('service', $event)" @change="changeField('service', $event)"
/> />
</div> </div>
<div class="mr-5" v-if="dashboardStore.entity !== EntityType[3].value"> <div class="mr-10 flex-h" v-if="dashboardStore.entity !== EntityType[3].value">
<span class="grey mr-5"> {{ isBrowser ? t("version") : t("instance") }}: </span> <span class="grey mr-5 label"> {{ isBrowser ? t("version") : t("instance") }}: </span>
<Selector <Selector
size="small" size="small"
:value="state.instance.value" :value="state.instance.value"
@@ -34,8 +34,8 @@ limitations under the License. -->
@change="changeField('instance', $event)" @change="changeField('instance', $event)"
/> />
</div> </div>
<div class="mr-5" v-if="dashboardStore.entity !== EntityType[2].value"> <div class="mr-10 flex-h" v-if="dashboardStore.entity !== EntityType[2].value">
<span class="grey mr-5"> {{ isBrowser ? t("page") : t("endpoint") }}: </span> <span class="grey mr-5 label"> {{ isBrowser ? t("page") : t("endpoint") }}: </span>
<Selector <Selector
size="small" size="small"
:value="state.endpoint.value" :value="state.endpoint.value"
@@ -46,8 +46,8 @@ limitations under the License. -->
@query="searchEndpoints" @query="searchEndpoints"
/> />
</div> </div>
<div class="mr-5" v-if="isBrowser"> <div class="mr-10 flex-h" v-if="isBrowser">
<span class="grey mr-5"> {{ t("category") }}: </span> <span class="grey mr-5 label"> {{ t("category") }}: </span>
<Selector <Selector
size="small" size="small"
:value="state.category.value" :value="state.category.value"
@@ -61,8 +61,8 @@ limitations under the License. -->
</el-button> </el-button>
</div> </div>
<div class="flex-h row" v-show="!isBrowser"> <div class="flex-h row" v-show="!isBrowser">
<div class="mr-5 traceId"> <div class="mr-10 traceId">
<span class="grey mr-5">{{ t("traceID") }}:</span> <span class="grey mr-5 label">{{ t("traceID") }}:</span>
<el-input v-model="traceId" class="inputs-max" size="small" /> <el-input v-model="traceId" class="inputs-max" size="small" />
</div> </div>
<ConditionTags :type="'LOG'" @update="updateTags" /> <ConditionTags :type="'LOG'" @update="updateTags" />
@@ -78,8 +78,8 @@ limitations under the License. -->
</div> </div>
<div v-show="logStore.supportQueryLogsByKeywords"> <div v-show="logStore.supportQueryLogsByKeywords">
<div class="flex-h"> <div class="flex-h">
<div class="mr-5"> <div class="mr-10">
<span class="mr-5 grey">{{ t("keywordsOfContent") }}:</span> <span class="mr-5 grey label">{{ t("keywordsOfContent") }}:</span>
<span class="log-tags"> <span class="log-tags">
<span class="selected" v-for="(item, index) in keywordsOfContent" :key="`keywordsOfContent${index}`"> <span class="selected" v-for="(item, index) in keywordsOfContent" :key="`keywordsOfContent${index}`">
<span>{{ item }}</span> <span>{{ item }}</span>
@@ -94,8 +94,8 @@ limitations under the License. -->
@change="addLabels('keywordsOfContent')" @change="addLabels('keywordsOfContent')"
/> />
</div> </div>
<div class="mr-5"> <div class="mr-10">
<span class="grey mr-5"> {{ t("excludingKeywordsOfContent") }}: </span> <span class="grey mr-5 label"> {{ t("excludingKeywordsOfContent") }}: </span>
<span class="log-tags"> <span class="log-tags">
<span <span
class="selected" class="selected"
@@ -437,4 +437,8 @@ limitations under the License. -->
margin-left: 3px; margin-left: 3px;
cursor: pointer; cursor: pointer;
} }
.label {
line-height: 24px;
}
</style> </style>

View File

@@ -21,7 +21,7 @@ limitations under the License. -->
<el-pagination <el-pagination
v-model="logStore.conditions.paging.pageNum" v-model="logStore.conditions.paging.pageNum"
:page-size="pageSize" :page-size="pageSize"
:small="true" size="small"
layout="prev, pager, next" layout="prev, pager, next"
:pager-count="5" :pager-count="5"
:total="total" :total="total"

View File

@@ -91,7 +91,7 @@ class Hex extends Int16Array {
} }
xyz(x: number, y: number, z: number | null = null): Hex { xyz(x: number, y: number, z: number | null = null): Hex {
if (z == null) z = -x - y; if (z === null) z = -x - y;
if (x + y + z != 0) { if (x + y + z != 0) {
console.log("Bad Axial Coordinate : : q %d r %d s %d", x, y, z); console.log("Bad Axial Coordinate : : q %d r %d s %d", x, y, z);
} }

View File

@@ -21,9 +21,9 @@ limitations under the License. -->
<div class="item"> <div class="item">
<SpanTree @loading="loadTrees" @displayMode="setDisplayMode" /> <SpanTree @loading="loadTrees" @displayMode="setDisplayMode" />
<div class="thread-stack"> <div class="thread-stack">
<div id="graph-stack" ref="graph" v-show="displayMode == 'flame'" /> <div id="graph-stack" ref="graph" v-show="displayMode === 'flame'" />
<StackTable <StackTable
v-show="displayMode == 'tree'" v-show="displayMode === 'tree'"
v-if="profileStore.analyzeTrees.length" v-if="profileStore.analyzeTrees.length"
:data="profileStore.analyzeTrees" :data="profileStore.analyzeTrees"
:highlightTop="profileStore.highlightTop" :highlightTop="profileStore.highlightTop"

View File

@@ -77,7 +77,7 @@ limitations under the License. -->
ElMessage.error(res.errors); ElMessage.error(res.errors);
return; return;
} }
endpointName.value = profileStore.endpoints[0].value; endpointName.value = profileStore.endpoints[0]?.value;
} }
function changeEndpoint(opt: any[]) { function changeEndpoint(opt: any[]) {

View File

@@ -56,12 +56,7 @@ limitations under the License. -->
const { t } = useI18n(); const { t } = useI18n();
const profileStore = useProfileStore(); const profileStore = useProfileStore();
const key = computed( const key = computed(
() => () => (profileStore.currentSegment.spans?.length && profileStore.currentSegment.spans[0].segmentId) || "",
(profileStore.currentSegment &&
profileStore.currentSegment.spans &&
profileStore.currentSegment.spans.length &&
profileStore.currentSegment.spans[0].segmentId) ||
"",
); );
async function selectSegment(item: Trace) { async function selectSegment(item: Trace) {

Some files were not shown because too many files have changed in this diff Show More