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
strategy:
matrix:
node-version: [16.x, 18.x, 20.x]
node-version: [18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}

7325
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "skywalking-booster-ui",
"version": "9.4.0",
"version": "10.2.0",
"private": true,
"scripts": {
"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"
},
"dependencies": {
"axios": "^1.7.5",
"axios": "^1.8.2",
"d3": "^7.3.0",
"d3-flame-graph": "^4.1.3",
"d3-tip": "^0.9.1",
"echarts": "^5.2.2",
"element-plus": "^2.2.5",
"lodash": "^4.17.21",
"element-plus": "^2.9.4",
"monaco-editor": "^0.34.1",
"pinia": "^2.0.28",
"vis-timeline": "^7.5.1",
"vue": "^3.2.45",
"vue-grid-layout": "^3.0.0-beta1",
"vue-i18n": "^9.1.9",
"vue-i18n": "^9.14.3",
"vue-router": "^4.1.6",
"vue-types": "^4.1.1"
},
@@ -42,11 +41,10 @@
"@types/d3-tip": "^3.5.5",
"@types/echarts": "^4.9.12",
"@types/jsdom": "^20.0.1",
"@types/lodash": "^4.14.179",
"@types/node": "^18.11.12",
"@types/three": "^0.131.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.2.6",
@@ -64,21 +62,21 @@
"postcss-html": "^1.3.0",
"postcss-scss": "^4.0.2",
"prettier": "^2.7.1",
"sass": "^1.56.1",
"sass": "^1.85.0",
"start-server-and-test": "^2.0.5",
"stylelint": "15.9.0",
"stylelint-config-html": "^1.0.0",
"stylelint-config-prettier": "9.0.4",
"stylelint-config-standard": "^33.0.0",
"stylelint-order": "^6.0.3",
"typescript": "~4.7.4",
"typescript": "^5.7.3",
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.3",
"vite": "^4.5.3",
"vite": "^6.1.0",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svg-icons": "^2.0.1",
"vitest": "^0.25.6",
"vue-tsc": "^1.8.27"
"vitest": "^3.0.5",
"vue-tsc": "^2.2.2"
},
"browserslist": [
"> 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
limitations under the License. -->
<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 v-if="!available" class="no-data">No Data</div>
<div
@@ -54,6 +61,7 @@ limitations under the License. -->
import Trace from "@/views/dashboard/related/trace/Index.vue";
import associateProcessor from "@/hooks/useAssociateProcessor";
import { WidgetType } from "@/views/dashboard/data";
import SelectorLegend from "./Legend.vue";
/*global Nullable, defineProps, defineEmits, Indexable*/
const emits = defineEmits(["select"]);
@@ -84,6 +92,10 @@ limitations under the License. -->
type: Array as PropType<{ widgetId: string }[]>,
default: () => [],
},
legendSelector: {
type: Object as PropType<Indexable>,
default: () => ({ isConfigPage: false, isSelector: false }),
},
});
const available = computed(
() =>
@@ -103,6 +115,7 @@ limitations under the License. -->
if (!instance) {
return;
}
instance.on("click", (params: EventParams) => {
currentParams.value = params;
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(
() => props.option,
(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"
:remote-method="remoteMethod"
:filterable="filterable"
:collapse-tags="collapseTags"
:collapse-tags-tooltip="collapseTagsTooltip"
>
<el-option
v-for="(item, index) in options"
@@ -41,11 +43,6 @@ limitations under the License. -->
import { ref, watch } from "vue";
import type { PropType } from "vue";
// interface Option {
// label: string | number;
// value: string | number;
// }
/*global defineProps, defineEmits, Indexable*/
const emit = defineEmits(["change", "query"]);
const props = defineProps({
@@ -73,6 +70,8 @@ limitations under the License. -->
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);

View File

@@ -18,7 +18,7 @@ import type { App } from "vue";
import Icon from "./Icon.vue";
import TimePicker from "./TimePicker.vue";
import Selector from "./Selector.vue";
import Graph from "./Graph.vue";
import Graph from "./Graph/Graph.vue";
import Radio from "./Radio.vue";
import SelectSingle from "./SelectSingle.vue";
import Tags from "./Tags.vue";

View File

@@ -24,6 +24,7 @@ export const Alarm = {
message
startTime
scope
name
tags {
key
value
@@ -43,6 +44,35 @@ export const Alarm = {
startTime
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 = {
variable: "$serviceId: ID!, $keyword: String!",
variable: "$serviceId: ID!, $keyword: String!, $duration: Duration, $limit: Int!",
query: `
pods: findEndpoint(serviceId: $serviceId, keyword: $keyword, limit: 20) {
pods: findEndpoint(serviceId: $serviceId, keyword: $keyword, limit: $limit, duration: $duration) {
id
value: name
label: name

View File

@@ -28,6 +28,7 @@ import * as alarm from "./query/alarm";
import * as event from "./query/event";
import * as ebpf from "./query/ebpf";
import * as demandLog from "./query/demand-log";
import * as asyncProfile from "./query/async-profile";
const query: { [key: string]: string } = {
...app,
@@ -41,6 +42,7 @@ const query: { [key: string]: string } = {
...event,
...ebpf,
...demandLog,
...asyncProfile,
};
class Graphql {
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
value
refId: traceID
owner {
scope
serviceID
serviceName
normal
serviceInstanceID
serviceInstanceName
endpointID
endpointName
}
}
}
error
@@ -102,3 +112,5 @@ export const LightChartColors = [
"#546570",
"#c4ccd3",
];
export const MaxQueryLength = 120;

View File

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

View File

@@ -14,9 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RespFields, MaximumEntities } from "./data";
import { RespFields, MaximumEntities, MaxQueryLength } from "./data";
import { EntityType, ExpressionResultType } from "@/views/dashboard/data";
import { ElMessage } from "element-plus";
import { useTopologyStore } from "@/store/modules/topology";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
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 { 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[]) {
function expressionsGraphql(config: Indexable, idx: number) {
if (!(config.metrics && config.metrics[0])) {
@@ -181,13 +190,6 @@ export async function useDashboardQueryProcessor(configList: Indexable[]) {
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 promiseArr = partArr.map((d: Array<Indexable>) => fetchMetrics(d));
@@ -394,7 +396,7 @@ export function useQueryTopologyExpressionsProcessor(metrics: string[], instance
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
function getExpressionQuery() {
function getExpressionQuery(partMetrics?: string[]) {
const conditions: { [key: string]: unknown } = {
duration: appStore.durationTime,
};
@@ -448,7 +450,7 @@ export function useQueryTopologyExpressionsProcessor(metrics: string[], instance
};
variables.push(`$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) {
variables.push(`$expression${idx}: String!`);
conditions[`expression${idx}`] = name;
@@ -462,19 +464,19 @@ export function useQueryTopologyExpressionsProcessor(metrics: string[], instance
return { queryStr, conditions };
}
function handleExpressionValues(resp: { [key: string]: any }) {
function handleExpressionValues(partMetrics: string[], resp: { [key: string]: any }) {
const obj: any = {};
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;
if (metrics[index]) {
if (!obj[metrics[index]]) {
obj[metrics[index]] = {
if (partMetrics[index]) {
if (!obj[partMetrics[index]]) {
obj[partMetrics[index]] = {
values: [],
};
}
obj[metrics[index]].values.push({
value: resp[k].results[0] && resp[k].results[0].values[0].value,
obj[partMetrics[index]].values.push({
value: resp[k] && resp[k].results[0] && resp[k].results[0].values[0].value,
id: instances[idx].id,
});
}
@@ -482,6 +484,31 @@ export function useQueryTopologyExpressionsProcessor(metrics: string[], instance
}
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) {
return false;
}
if (legend && legend.asTable) {
if (legend && (legend.asTable || legend.asSelector)) {
return false;
}
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" />
</span>
<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>
<span>
<Icon iconName="info_outline" size="middle" />

View File

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

View File

@@ -139,7 +139,6 @@ const msg = {
enableAssociate: "Enable association",
text: "Text",
query: "Query",
endpointTips: "The table shows up to 20 pieces of endpoints.",
viewTrace: "View Related Traces",
relatedTraceOptions: "Related Trace Options",
setLatencyDuration: "Latency Related Metrics",
@@ -154,6 +153,7 @@ const msg = {
legendOptions: "Legend Options",
showLegend: "Show Legend",
asTable: "As Table",
asSelector: "As Selector",
toTheRight: "To The Right",
legendValues: "Legend Values",
minDuration: "Minimal Request Duration",
@@ -386,5 +386,16 @@ const msg = {
tabExpressions: "Tab Expressions",
hierarchyNodeMetrics: "Metrics 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;

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",
enableAssociate: "Activar asociación",
query: "Consulta",
endpointTips: "Aquí, la tabla muestra hasta 20 punto final.",
queryOrder: "Consulta por duración",
setOrder: "Orden de consulta",
latency: "Retraso",
@@ -386,5 +385,17 @@ const msg = {
tabExpressions: "Tab Expressions",
hierarchyNodeMetrics: "Metrics 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;

View File

@@ -128,6 +128,9 @@ const titles = {
self_observability_java_agent: "SkyWalking Java Agent",
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.",
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_desc:
"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.",
self_observability_java_agent: "SkyWalking Java Agent",
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_desc:
"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为云原生基础设施设计的开源代理提供了一种低成本、高效、更安全的遥测数据收集方式。它是遥测采集的推荐负载均衡器。",
self_observability_java_agent: "SkyWalking Java 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_desc: "Cilium是Kubernetes上的CNI插件提供基于eBPF的网络、安全和负载均衡。",
cilium_service: "Cilium服务",

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,6 +47,7 @@ export const ControlsTypes = [
WidgetType.DemandLog,
WidgetType.Ebpf,
WidgetType.NetworkProfiling,
WidgetType.AsyncProfiling,
WidgetType.ThirdPartyApp,
WidgetType.ContinuousProfiling,
WidgetType.TaskTimeline,
@@ -55,3 +56,5 @@ export enum EBPFProfilingTriggerType {
FIXED_TIME = "FIXED_TIME",
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 { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import { EndpointsTopNDefault } from "../data";
interface eventState {
loading: boolean;
@@ -54,7 +55,7 @@ export const eventStore = defineStore({
if (res.data.errors) {
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;
},
async getEndpoints(keyword: string) {
@@ -66,11 +67,12 @@ export const eventStore = defineStore({
serviceId,
duration: useAppStoreWithOut().durationTime,
keyword: keyword || "",
limit: EndpointsTopNDefault,
});
if (res.data.errors) {
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;
},
async getEvents() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,6 +27,7 @@ export interface Alarm {
scope: string;
tags: Array<{ key: string; value: string }>;
events: Event[];
snapshot: Indexable;
}
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']
ElButton: typeof import('element-plus/es')['ElButton']
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']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
@@ -42,8 +44,9 @@ declare module 'vue' {
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
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']
Legend: typeof import('./../components/Graph/Legend.vue')['default']
Radio: typeof import('./../components/Radio.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

@@ -46,6 +46,7 @@ export interface LayoutConfig {
relatedTrace?: RelatedTrace;
subExpressions?: string[];
subTypesOfMQE?: string[];
valueRelatedDashboard?: string;
}
export type RelatedTrace = {
duration: DurationTime;
@@ -110,6 +111,7 @@ export interface LineConfig extends AreaConfig {
showYAxis?: boolean;
smallTips?: boolean;
showlabels?: boolean;
noTooltips?: boolean;
}
export interface AreaConfig {
@@ -123,6 +125,7 @@ export interface CardConfig {
fontSize?: number;
showUnit?: boolean;
textAlign?: "center" | "right" | "left";
valueMappings?: { [key: string]: string };
}
export interface TextConfig {
@@ -194,4 +197,19 @@ export type LegendOptions = {
asTable: boolean;
toTheRight: boolean;
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;
maxSamplingCount: number;
logs: TaskLog[];
errorInstanceIds: string[];
successInstanceIds: string[];
serviceInstanceIds: string[];
}
export interface SegmentSpan {
spanId: string;

View File

@@ -16,18 +16,22 @@
*/
import { ElNotification } from "element-plus";
export default (value: string): void => {
const input = document.createElement("input");
input.value = value;
document.body.appendChild(input);
input.select();
if (document.execCommand("Copy")) {
document.execCommand("Copy");
}
input.remove();
ElNotification({
title: "Success",
message: "Copied",
type: "success",
});
export default (text: string): void => {
navigator.clipboard
.writeText(text)
.then(() => {
ElNotification({
title: "Success",
message: "Copied",
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.style.display = "none";
const blob = new Blob([newData]);
tagA.href = URL.createObjectURL(blob);
const url = URL.createObjectURL(blob);
tagA.href = url;
document.body.appendChild(tagA);
tagA.click();
document.body.removeChild(tagA);
URL.revokeObjectURL(url);
};

View File

@@ -17,7 +17,7 @@
class Vec2 extends Float32Array {
constructor(v?: unknown, y?: unknown) {
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[1] = v[1];
} else if (typeof v === "number" && typeof y === "number") {
@@ -104,7 +104,7 @@ class Vec2 extends Float32Array {
}
norm(out?: number[] | Vec2): number[] | Vec2 | undefined {
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[0] = this[0] / mag;
out[1] = this[1] / mag;

View File

@@ -17,7 +17,7 @@
class Vec3 extends Float32Array {
constructor(v?: unknown, y?: unknown, z?: unknown) {
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[1] = v[1];
this[2] = v[2];
@@ -150,7 +150,7 @@ class Vec3 extends Float32Array {
}
static norm(x: unknown, y: unknown, z: unknown): 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);
} else if (typeof x === "number" && typeof y === "number" && typeof z === "number") {
rtn.xyz(x, y, z);

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ limitations under the License. -->
:total="total"
@current-change="changePage"
:pager-count="5"
small
size="small"
:style="
appStore.theme === Themes.Light
? `--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",
value: "eventDetail",
},
{
label: "expression",
value: "expression",
},
{
label: "snapshot",
value: "snapshot",
},
];
export const EventsDetailKeys = [

View File

@@ -612,7 +612,7 @@ limitations under the License. -->
}
function searchDashboards(pageIndex: number) {
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;
dashboards.value = arr.filter(

View File

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

View File

@@ -126,7 +126,7 @@ limitations under the License. -->
opt.auto = Number(f.value) * 60 * 60 * 1000;
}
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);

View File

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

View File

@@ -81,7 +81,7 @@ limitations under the License. -->
const latency = ref<boolean>(traceOpt.latency || false);
const enableRelate = ref<boolean>(traceOpt.enableRelate || false);
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 }) {
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
limitations under the License. -->
<template>
<div>
<value-mappings />
</div>
<div>
<span class="label">{{ t("fontSize") }}</span>
<el-slider
@@ -26,7 +29,7 @@ limitations under the License. -->
@change="updateConfig({ fontSize })"
/>
</div>
<div class="item">
<div>
<span class="label">{{ t("showUnit") }}</span>
<el-switch v-model="showUnit" active-text="Yes" inactive-text="No" @change="updateConfig({ showUnit })" />
</div>
@@ -35,6 +38,7 @@ limitations under the License. -->
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import ValueMappings from "./components/ValueMappings.vue";
const { t } = useI18n();
const dashboardStore = useDashboardStore();

View File

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

View File

@@ -22,6 +22,15 @@ limitations under the License. -->
@change="updateLegendConfig({ show: legend.show })"
/>
</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>
<span class="label">{{ t("asTable") }}</span>
<el-switch
@@ -97,6 +106,7 @@ limitations under the License. -->
max: false,
mean: false,
asTable: false,
asSelector: false,
toTheRight: false,
width: 130,
...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 }}
</span>
</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>
<script lang="ts" setup>
import { reactive, ref, computed } from "vue";
@@ -140,21 +154,25 @@ limitations under the License. -->
metricTypes: string[];
metricTypeList: Option[][];
isList: boolean;
isTopList: boolean;
dashboardName: string;
dashboardList: ((DashboardItem & { label: string; value: string }) | any)[];
tips: string[];
subTips: string[];
valueRelatedDashboard: string;
}>({
metrics: metrics.value.length ? metrics.value : [""],
metricTypes: typesOfMQE.value.length ? typesOfMQE.value : [""],
metricTypeList: [],
isList: false,
isTopList: false,
dashboardName: graph.value.dashboardName,
dashboardList: [{ label: "", value: "" }],
tips: [],
subTips: [],
subMetrics: subMetrics.value.length ? subMetrics.value : [""],
subMetricTypes: subMetricTypes.value.length ? subMetricTypes.value : [""],
valueRelatedDashboard: dashboardStore.selectedGrid.valueRelatedDashboard,
});
const currentMetricConfig = ref<MetricConfigOpt>({
unit: "",
@@ -163,6 +181,7 @@ limitations under the License. -->
sortOrder: "DES",
});
states.isTopList = graph.value.type === ChartTypes[4].value;
states.isList = ListChartTypes.includes(graph.value.type);
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) => {
if (d.layer === dashboardStore.layerId) {
if (
(d.entity === EntityType[0].value && chart === "ServiceList") ||
(d.entity === EntityType[2].value && chart === "EndpointList") ||
(d.entity === EntityType[3].value && chart === "InstanceList")
(d.entity === EntityType[0].value && chart === ChartTypes[8].value) ||
(d.entity === EntityType[2].value && chart === ChartTypes[9].value) ||
(d.entity === EntityType[3].value && chart === ChartTypes[10].value) ||
states.isTopList
) {
prev.push({
...d,
@@ -254,9 +274,28 @@ limitations under the License. -->
typesOfMQE: states.metricTypes,
});
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]) {
states.dashboardName = "";
} 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;
width: 2px;
height: 100%;
background-color: #e8e8e8;
background-color: var(--sw-trace-line);
cursor: ew-resize;
&:hover {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ limitations under the License. -->
justifyContent: config.textAlign || 'center',
}"
>
{{ singleVal }}
{{ getValue() }}
<span class="unit" v-show="config.showUnit && unit">
{{ decodeURIComponent(unit) }}
</span>
@@ -34,6 +34,7 @@ limitations under the License. -->
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import type { CardConfig, MetricConfigOpt } from "@/types/dashboard";
import { ElMessage } from "element-plus";
/*global defineProps */
const props = defineProps({
@@ -48,16 +49,36 @@ limitations under the License. -->
showUnit: true,
textAlign: "center",
metricConfig: [],
valueMappings: {},
}),
},
});
const { t } = useI18n();
const metricConfig = computed(() => props.config.metricConfig || []);
const valueMappings = computed(() => props.config.valueMappings || {});
const key = computed(() => Object.keys(props.data)[0]);
const singleVal = computed(() =>
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 || ""));
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>
<style lang="scss" scoped>
.chart-card {

View File

@@ -16,16 +16,18 @@ limitations under the License. -->
<div class="table">
<div class="search">
<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>
<el-button @click="searchList">
<Icon size="middle" iconName="search" />
</el-button>
</template>
</el-input>
<span class="ml-5 tips">{{ t("endpointTips") }}</span>
</div>
<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">
<template #default="scope">
<span class="link" @click="clickEndpoint(scope)" :style="{ fontSize: `${config.fontSize}px` }">
@@ -46,13 +48,22 @@ limitations under the License. -->
/>
</el-table>
</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>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
import type { PropType } from "vue";
import type { EndpointListConfig } from "@/types/dashboard";
import type { Endpoint } from "@/types/selector";
@@ -90,16 +101,26 @@ limitations under the License. -->
});
const emit = defineEmits(["expressionTips"]);
const { t } = useI18n();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
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 colMetrics = ref<string[]>([]);
const colSubMetrics = ref<string[]>([]);
const metricConfig = ref<MetricConfigOpt[]>(props.config.metricConfig || []);
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) {
queryEndpoints();
@@ -108,6 +129,7 @@ limitations under the License. -->
chartLoading.value = true;
const resp = await selectorStore.getEndpoints({
keyword: searchText.value,
limit: topN.value,
});
chartLoading.value = false;
@@ -116,7 +138,8 @@ limitations under the License. -->
return;
}
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[]) {
if (!arr.length) {
@@ -142,7 +165,7 @@ limitations under the License. -->
{ metricConfig: metricConfig.value || [], expressions, subExpressions },
EntityType[2].value,
);
endpoints.value = params.data;
currentEndpoints.value = params.data;
colMetrics.value = params.names;
colSubMetrics.value = params.subNames;
metricConfig.value = params.metricConfigArr;
@@ -151,7 +174,7 @@ limitations under the License. -->
return;
}
endpoints.value = currentPods;
currentEndpoints.value = currentPods;
colMetrics.value = [];
colSubMetrics.value = [];
metricConfig.value = [];
@@ -175,6 +198,16 @@ limitations under the License. -->
async function searchList() {
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(
() => [
...(props.config.metricConfig || []),

View File

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

View File

@@ -37,7 +37,7 @@ limitations under the License. -->
>{{ k.split("=")[1] }}</div
>
<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>
@@ -47,6 +47,8 @@ limitations under the License. -->
import { computed } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { ElMessage } from "element-plus";
/*global defineProps */
const props = defineProps({
data: {
@@ -58,12 +60,14 @@ limitations under the License. -->
showTableValues: boolean;
tableHeaderCol2: string;
typesOfMQE: string[];
valueMappings: {};
}>,
default: () => ({ showTableValues: true }),
},
});
const { t } = useI18n();
const valueMappings = computed<{ [key: string]: string }>(() => props.config.valueMappings || {});
const nameWidth = computed(() => (props.config.showTableValues ? 80 : 100));
const dataKeys = computed(() => {
const keys = Object.keys(props.data || {}).filter(
@@ -73,6 +77,25 @@ limitations under the License. -->
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>
<style lang="scss" scoped>
.chart-table {

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ limitations under the License. -->
<div class="dashboard-tool flex-h">
<div :class="isRelation ? 'flex-v' : 'flex-h'" class="tool-selectors">
<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>
<Selector
v-model="states.currentService"
@@ -30,7 +30,7 @@ limitations under the License. -->
<Icon size="small" iconName="hierarchy_topology" />
</span>
</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">
{{ ["EndpointRelation", "Endpoint"].includes(dashboardStore.entity) ? "$Endpoint" : "$ServiceInstance" }}
</span>
@@ -52,7 +52,7 @@ limitations under the License. -->
<Icon size="small" iconName="hierarchy_topology" />
</span>
</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>
<Selector
v-model="states.currentProcess"
@@ -65,7 +65,7 @@ limitations under the License. -->
</div>
</div>
<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>
<Selector
v-model="states.currentDestService"
@@ -76,7 +76,7 @@ limitations under the License. -->
class="selectors"
/>
</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">
{{ dashboardStore.entity === "EndpointRelation" ? "$DestinationEndpoint" : "$DestinationServiceInstance" }}
</span>
@@ -91,7 +91,7 @@ limitations under the License. -->
:isRemote="dashboardStore.entity === 'EndpointRelation'"
/>
</div>
<div class="selectors-item" v-if="key === 5">
<div class="flex-h selectors-item" v-if="key === 5">
<span class="label"> $DestinationProcess </span>
<Selector
v-model="states.currentDestProcess"
@@ -105,11 +105,11 @@ limitations under the License. -->
</div>
</div>
<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">
<i>
<Icon class="icon-btn" size="sm" iconName="control" />
</i>
<div class="icon-btn">
<Icon size="sm" iconName="control" />
</div>
<template #dropdown>
<el-dropdown-menu>
<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>
</el-dropdown>
<el-tooltip content="Apply" placement="bottom">
<i @click="applyDashboard">
<Icon class="icon-btn" size="sm" iconName="save" />
</i>
<div class="icon-btn" @click="applyDashboard">
<Icon size="sm" iconName="save" />
</div>
</el-tooltip>
</div>
<div class="ml-5">
@@ -693,10 +693,6 @@ limitations under the License. -->
padding: 4px 2px;
}
.tool-icons {
margin-top: 2px;
}
.tools {
justify-content: space-between;
align-items: center;
@@ -704,9 +700,10 @@ limitations under the License. -->
}
.icon-btn {
display: inline-block;
padding: 3px;
width: 22px;
height: 22px;
text-align: center;
line-height: 18px;
border: 1px solid var(--sw-icon-btn-border);
border-radius: 3px;
margin-left: 6px;
@@ -734,7 +731,7 @@ limitations under the License. -->
.hierarchy-btn {
display: inline-block;
padding: 0 2px 2px;
padding: 0 3px;
border: 1px solid #666;
border-radius: 4px;
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-pagination
class="mt-10"
small
size="small"
layout="prev, pager, next"
:page-size="pageSize"
:total="continousProfilingStore.instances.length"

View File

@@ -190,7 +190,7 @@ limitations under the License. -->
.profile-t-tool {
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);
width: 100%;
font-weight: bold;

View File

@@ -29,8 +29,6 @@ export const TargetTypes = [
{ label: "NETWORK", value: "NETWORK" },
];
export const ComponentType = "CONTINOUS_PROFILING";
export const HeaderLabels = [
{ value: "triggeredCount", label: "Triggered Count", width: 150 },
{ 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. -->
<template>
<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>
<Selector
size="small"
@@ -25,7 +25,7 @@ limitations under the License. -->
class="selectors"
/>
</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>
<Selector
size="small"
@@ -36,18 +36,7 @@ limitations under the License. -->
class="selectors"
/>
</div>
<!-- <div class="mr-5">
<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">
<div class="mr-10">
<span class="grey mr-5">{{ t("duration") }}:</span>
<Selector
size="small"
@@ -58,18 +47,19 @@ limitations under the License. -->
class="duration-range"
/>
</div>
<div class="mr-5">
<div class="mr-10">
<span class="grey mr-5">{{ t("interval") }}:</span>
<Selector
size="small"
:value="state.interval.value"
:options="IntervalOpts"
@change="changeField('interval', $event)"
class="selectors"
/>
</div>
</div>
<div class="flex-h row">
<div class="mr-5">
<div class="mr-10">
<span class="mr-5 grey">{{ t("keywordsOfContent") }}:</span>
<span class="log-tags">
<span class="selected" v-for="(item, index) in keywordsOfContent" :key="`keywordsOfContent${index}`">
@@ -85,7 +75,7 @@ limitations under the License. -->
@change="addLabels('keywordsOfContent')"
/>
</div>
<div class="mr-5">
<div class="mr-10">
<span class="grey mr-5"> {{ t("excludingKeywordsOfContent") }}: </span>
<span class="log-tags">
<span

View File

@@ -15,7 +15,7 @@ limitations under the License. -->
<template>
<div class="flex-h header">
<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") }}
</el-button>
</div>

View File

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

View File

@@ -23,10 +23,10 @@ limitations under the License. -->
import d3tip from "d3-tip";
import { flamegraph } from "d3-flame-graph";
import { useEbpfStore } from "@/store/modules/ebpf";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import { ComponentType } from "@/views/dashboard/related/continuous-profiling/data";
import { useAsyncProfilingStore } from "@/store/modules/async-profiling";
import type { StackElement } from "@/types/ebpf";
import { AggregateTypes } from "./data";
import { JFREventType, ComponentType } from "@/views/dashboard/related/async-profiling/components/data";
import "d3-flame-graph/dist/d3-flamegraph.css";
import { treeForeach } from "@/utils/flameGraph";
@@ -37,7 +37,7 @@ limitations under the License. -->
default: "",
},
});
const ebpfStore = props.type === ComponentType ? useContinousProfilingStore() : useEbpfStore();
const ebpfStore = props.type === ComponentType ? useAsyncProfilingStore() : useEbpfStore();
const stackTree = ref<Nullable<StackElement>>(null);
const selectStack = ref<Nullable<StackElement>>(null);
const graph = ref<Nullable<HTMLDivElement>>(null);
@@ -52,7 +52,7 @@ limitations under the License. -->
if (!ebpfStore.analyzeTrees.length) {
return (stackTree.value = null);
}
const root: StackElement = {
let root: StackElement = {
parentId: "0",
originId: "1",
name: "Virtual Root",
@@ -66,21 +66,28 @@ limitations under the License. -->
rateOfParent: "",
};
countRange();
for (const tree of ebpfStore.analyzeTrees) {
const ele = processTree(tree.elements);
root.children && root.children.push(ele);
if (props.type === ComponentType) {
const elements = processTree(ebpfStore.analyzeTrees[0].elements);
stackTree.value = elements;
root = { ...root, ...elements };
} else {
for (const tree of ebpfStore.analyzeTrees) {
const ele = processTree(tree.elements);
root.children && root.children.push(ele);
}
const param = (root.children || []).reduce(
(prev: number[], curr: StackElement) => {
prev[0] += curr.value;
prev[1] += curr.dumpCount;
return prev;
},
[0, 0],
);
root.value = param[0];
root.dumpCount = param[1];
stackTree.value = root;
}
const param = (root.children || []).reduce(
(prev: number[], curr: StackElement) => {
prev[0] += curr.value;
prev[1] += curr.dumpCount;
return prev;
},
[0, 0],
);
root.value = param[0];
root.dumpCount = param[1];
stackTree.value = root;
const width = (graph.value && graph.value.getBoundingClientRect().width) || 0;
const w = width < 800 ? 802 : width;
flameChart.value = flamegraph()
@@ -102,10 +109,7 @@ limitations under the License. -->
.direction("s")
.html((d: { data: StackElement } & { parent: { data: StackElement } }) => {
const name = d.data.name.replace("<", "&lt;").replace(">", "&gt;");
const valStr =
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 valStr = tooltipContent(d.data);
const rateOfParent =
(d.parent &&
`<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);
}
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() {
const list = [];
for (const tree of ebpfStore.analyzeTrees) {
@@ -148,9 +172,9 @@ limitations under the License. -->
obj[item.originId] = item;
}
const scale = d3.scaleLinear().domain([min.value, max.value]).range([1, 200]);
const condition = props.type === ComponentType ? "0" : "1";
for (const item of copyArr) {
if (item.parentId === "1") {
if (item.parentId === condition) {
const val = Number(scale(item.dumpCount).toFixed(4));
res = item;
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
limitations under the License. -->
<template>
<div class="flex-h row">
<div class="mr-5" v-if="dashboardStore.entity !== EntityType[3].value">
<span class="grey mr-5"> {{ t("instance") }}: </span>
<div class="flex-h">
<div class="mr-5 flex-h" v-if="dashboardStore.entity !== EntityType[3].value">
<span class="grey mr-5 label"> {{ t("instance") }}: </span>
<Selector
size="small"
:value="state.instance.value"
@@ -24,8 +24,8 @@ limitations under the License. -->
@change="changeField('instance', $event)"
/>
</div>
<div class="mr-5" v-if="dashboardStore.entity !== EntityType[2].value">
<span class="grey mr-5"> {{ t("endpoint") }}: </span>
<div class="mr-5 flex-h" v-if="dashboardStore.entity !== EntityType[2].value">
<span class="grey mr-5 label"> {{ t("endpoint") }}: </span>
<Selector
size="small"
:value="state.endpoint.value"
@@ -36,8 +36,8 @@ limitations under the License. -->
@query="searchEndpoints"
/>
</div>
<div class="mr-5">
<span class="grey">{{ t("eventsType") }}: </span>
<div class="mr-5 flex-h">
<span class="grey label" style="width: 95px">{{ t("eventsType") }}: </span>
<Selector
v-model="state.eventType"
:options="EventTypes"
@@ -47,15 +47,17 @@ limitations under the License. -->
size="small"
/>
</div>
<el-pagination
v-model="pageNum"
:page-size="pageSize"
layout="prev, pager, next"
:total="total"
@current-change="updatePage"
:pager-count="5"
small
/>
<div>
<el-pagination
v-model="pageNum"
:page-size="pageSize"
layout="prev, pager, next"
:total="total"
@current-change="updatePage"
:pager-count="5"
size="small"
/>
</div>
<el-button class="search-btn" size="small" type="primary" @click="queryEvents">
{{ t("search") }}
</el-button>
@@ -231,4 +233,8 @@ limitations under the License. -->
font-size: $font-size-smaller;
margin: 0 2px;
}
.label {
line-height: 24px;
}
</style>

View File

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

View File

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

View File

@@ -91,7 +91,7 @@ class Hex extends Int16Array {
}
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) {
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">
<SpanTree @loading="loadTrees" @displayMode="setDisplayMode" />
<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
v-show="displayMode == 'tree'"
v-show="displayMode === 'tree'"
v-if="profileStore.analyzeTrees.length"
:data="profileStore.analyzeTrees"
:highlightTop="profileStore.highlightTop"

View File

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

View File

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

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