mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-05-01 18:53:40 +00:00
feat: Implement the network profiling widget (#132)
This commit is contained in:
parent
ffabc7c7a7
commit
a4fc5192ac
30
package-lock.json
generated
30
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "skywalking-booster-ui",
|
||||
"version": "9.1.0",
|
||||
"version": "9.3.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "skywalking-booster-ui",
|
||||
"version": "9.1.0",
|
||||
"version": "9.3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.24.0",
|
||||
"d3": "^7.3.0",
|
||||
@ -7259,14 +7259,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001299",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
|
||||
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==",
|
||||
"version": "1.0.30001379",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001379.tgz",
|
||||
"integrity": "sha512-zXf+qxuN8OJrK5Bl5HbJg8cc5/Zm01WNW4ooVWUh92YlKqQZW3fwN5lXLB+kI8wkP5vTWkIIN+rutZuJhf4ykw==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
}
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/capture-exit": {
|
||||
"version": "2.0.0",
|
||||
@ -34806,9 +34812,9 @@
|
||||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001299",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
|
||||
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==",
|
||||
"version": "1.0.30001379",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001379.tgz",
|
||||
"integrity": "sha512-zXf+qxuN8OJrK5Bl5HbJg8cc5/Zm01WNW4ooVWUh92YlKqQZW3fwN5lXLB+kI8wkP5vTWkIIN+rutZuJhf4ykw==",
|
||||
"dev": true
|
||||
},
|
||||
"capture-exit": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "skywalking-booster-ui",
|
||||
"version": "9.1.0",
|
||||
"version": "9.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
|
16
src/assets/icons/setting_empty.svg
Normal file
16
src/assets/icons/setting_empty.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.7 KiB |
@ -12,4 +12,8 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<svg t="1660294515307" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1020" width="48" height="48"><path d="M240 512h64c9.6 0 16-6.4 16-16v-32c0-9.6-6.4-16-16-16h-64c-9.6 0-16 6.4-16 16v32c0 9.6 6.4 16 16 16z m160 0h384c9.6 0 16-6.4 16-16v-32c0-9.6-6.4-16-16-16H400c-9.6 0-16 6.4-16 16v32c0 9.6 6.4 16 16 16z m384 256h-64c-9.6 0-16 6.4-16 16v32c0 9.6 6.4 16 16 16h64c9.6 0 16-6.4 16-16v-32c0-9.6-6.4-16-16-16z" p-id="1021"></path><path d="M896 128H768V96c0-16-12.8-32-32-32s-32 12.8-32 32v32H320V96c0-16-12.8-32-32-32s-32 12.8-32 32v32H128c-35.2 0-64 28.8-64 64v704c0 35.2 28.8 64 64 64h768c35.2 0 64-28.8 64-64V192c0-35.2-28.8-64-64-64z m0 736c0 19.2-12.8 32-32 32H160c-19.2 0-32-12.8-32-32V384h768v480z m0-544H128v-96c0-19.2 12.8-32 32-32h96v32c0 16 12.8 32 32 32s32-12.8 32-32v-32h384v32c0 16 12.8 32 32 32s32-12.8 32-32v-32h96c19.2 0 32 12.8 32 32v96z" p-id="1022"></path><path d="M240 832h384c9.6 0 16-6.4 16-16v-32c0-9.6-6.4-16-16-16H240c-9.6 0-16 6.4-16 16v32c0 9.6 6.4 16 16 16z m0-160h544c9.6 0 16-6.4 16-16v-32c0-9.6-6.4-16-16-16H240c-9.6 0-16 6.4-16 16v32c0 9.6 6.4 16 16 16z" p-id="1023"></path></svg>
|
||||
<svg t="1660976558460" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="813" width="48" height="48">
|
||||
<path d="M498.596 482.29H345.42v57.308h210.478V274.197h-57.301V482.29z m0 0M577.685 644.985h379.88v57.302h-379.88v-57.302z m0 0M577.685 773.765h379.88v57.307h-379.88v-57.307z m0 0M577.685 902.55h379.88v57.307h-379.88V902.55z m0 0" p-id="814"></path>
|
||||
<path d="M102.523 382.29a28.668 28.668 0 0 0 23.367 2.56l190.81-61.886c15.053-4.883 23.298-21.04 18.415-36.09-4.882-15.052-21.04-23.297-36.093-18.415l-123.346 40c15.994-26.117 35.17-50.538 57.37-72.745 73.768-73.767 171.847-114.388 276.169-114.388 104.32 0 202.395 40.622 276.161 114.388S899.77 407.56 899.77 511.882c0 26.428-2.616 52.45-7.71 77.78h58.303c4.465-25.499 6.709-51.47 6.709-77.78 0-60.45-11.846-119.102-35.205-174.336-22.56-53.335-54.85-101.227-95.969-142.35-41.122-41.122-89.017-73.408-142.348-95.968-55.233-23.361-113.89-35.207-174.334-35.207-60.45 0-119.107 11.846-174.337 35.208-53.335 22.56-101.23 54.846-142.35 95.969-23.98 23.98-44.933 50.278-62.727 78.6l-20.738-105.654c-3.043-15.528-18.105-25.642-33.632-22.6-15.528 3.048-25.643 18.105-22.6 33.637l36.103 183.932a28.666 28.666 0 0 0 13.588 19.178z m0 0M126.02 587.942H67.768c5.76 33.679 15.368 66.544 28.79 98.278 22.56 53.334 54.85 101.225 95.972 142.348 41.123 41.123 89.014 73.409 142.349 95.969 54.112 22.888 111.518 34.711 170.668 35.182v-57.324c-102.95-0.941-199.595-41.446-272.5-114.349-55.501-55.502-92.237-124.77-107.027-200.104z m0 0" p-id="815">
|
||||
</path>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.2 KiB |
@ -13,7 +13,7 @@ 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="nav-bar flex-h" :class="{ dark: theme === 'dark' }">
|
||||
<div class="nav-bar flex-h">
|
||||
<div class="title">{{ appStore.pageTitle || t(pageName) }}</div>
|
||||
<div class="app-config">
|
||||
<span class="red" v-show="timeRange">{{ t("timeTips") }}</span>
|
||||
@ -49,34 +49,38 @@ limitations under the License. -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import timeFormat from "@/utils/timeFormat";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import { ElMessage } from "element-plus";
|
||||
import getLocalTime from "@/utils/localtime";
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStoreWithOut();
|
||||
const route = useRoute();
|
||||
const pageName = ref<string>("");
|
||||
const timeRange = ref<number>(0);
|
||||
const theme = ref<string>("light");
|
||||
|
||||
getVersion();
|
||||
const setConfig = (value: string) => {
|
||||
pageName.value = value || "";
|
||||
// theme.value = route.path.includes("/infrastructure/") ? "dark" : "light";
|
||||
};
|
||||
const time = computed(() => [
|
||||
const time = ref<Date[]>([
|
||||
appStore.durationRow.start,
|
||||
appStore.durationRow.end,
|
||||
]);
|
||||
|
||||
resetDuration();
|
||||
getVersion();
|
||||
const setConfig = (value: string) => {
|
||||
pageName.value = value || "";
|
||||
};
|
||||
|
||||
const handleReload = () => {
|
||||
const gap =
|
||||
appStore.duration.end.getTime() - appStore.duration.start.getTime();
|
||||
const time: Date[] = [new Date(new Date().getTime() - gap), new Date()];
|
||||
appStore.setDuration(timeFormat(time));
|
||||
const dates: Date[] = [
|
||||
getLocalTime(appStore.utc, new Date(new Date().getTime() - gap)),
|
||||
getLocalTime(appStore.utc, new Date()),
|
||||
];
|
||||
appStore.setDuration(timeFormat(dates));
|
||||
};
|
||||
function changeTimeRange(val: Date[] | any) {
|
||||
timeRange.value =
|
||||
@ -99,6 +103,20 @@ async function getVersion() {
|
||||
ElMessage.error(res.errors);
|
||||
}
|
||||
}
|
||||
function resetDuration() {
|
||||
const { duration }: any = route.params;
|
||||
if (duration) {
|
||||
const d = JSON.parse(duration);
|
||||
|
||||
appStore.updateDurationRow({
|
||||
start: new Date(d.start),
|
||||
end: new Date(d.end),
|
||||
step: d.step,
|
||||
});
|
||||
appStore.updateUTC(d.utc);
|
||||
time.value = [new Date(d.start), new Date(d.end)];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.nav-bar {
|
||||
|
@ -150,6 +150,7 @@ const msg = {
|
||||
duplicateName: "Duplicate name",
|
||||
enableAssociate: "Enable association",
|
||||
text: "Text",
|
||||
query: "Query",
|
||||
postgreSQL: "PostgreSQL",
|
||||
seconds: "Seconds",
|
||||
hourTip: "Select Hour",
|
||||
|
@ -150,6 +150,7 @@ const msg = {
|
||||
nameTip:
|
||||
"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",
|
||||
postgreSQL: "PostgreSQL",
|
||||
seconds: "Segundos",
|
||||
hourTip: "Seleccione Hora",
|
||||
|
@ -147,6 +147,7 @@ const msg = {
|
||||
nameTip: "该名称仅支持中文和英文、横线和下划线, 并且限制长度为300个字符",
|
||||
duplicateName: "重复的名称",
|
||||
text: "文本",
|
||||
query: "查询",
|
||||
postgreSQL: "PostgreSQL",
|
||||
seconds: "秒",
|
||||
hourTip: "选择小时",
|
||||
|
@ -234,6 +234,14 @@ export const routesDashboard: Array<RouteRecordRaw> = [
|
||||
),
|
||||
name: "ViewProcessRelationActiveTabIndex",
|
||||
},
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name/duration/:duration",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dashboards" */ "@/views/dashboard/Edit.vue"
|
||||
),
|
||||
name: "ViewProcessRelationDuration",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -119,12 +119,18 @@ export const appStore = defineStore({
|
||||
}
|
||||
this.runEventStack();
|
||||
},
|
||||
updateDurationRow(data: Duration) {
|
||||
this.durationRow = data;
|
||||
},
|
||||
setUTC(utcHour: number, utcMin: number): void {
|
||||
this.runEventStack();
|
||||
this.utcMin = utcMin;
|
||||
this.utcHour = utcHour;
|
||||
this.utc = `${utcHour}:${utcMin}`;
|
||||
},
|
||||
updateUTC(data: string) {
|
||||
this.utc = data;
|
||||
},
|
||||
setIsMobile(mode: boolean) {
|
||||
this.isMobile = mode;
|
||||
},
|
||||
@ -155,10 +161,9 @@ export const appStore = defineStore({
|
||||
.params({});
|
||||
if (res.data.errors) {
|
||||
this.utc = -(new Date().getTimezoneOffset() / 60) + ":0";
|
||||
return res.data;
|
||||
} else {
|
||||
this.utc = res.data.data.getTimeInfo.timezone / 100 + ":0";
|
||||
}
|
||||
this.utc = res.data.data.getTimeInfo.timezone / 100 + ":0";
|
||||
|
||||
const utcArr = this.utc.split(":");
|
||||
this.utcHour = isNaN(Number(utcArr[0])) ? 0 : Number(utcArr[0]);
|
||||
this.utcMin = isNaN(Number(utcArr[1])) ? 0 : Number(utcArr[1]);
|
||||
|
183
src/store/modules/network-profiling.ts
Normal file
183
src/store/modules/network-profiling.ts
Normal file
@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 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 { EBPFTaskList, ProcessNode } from "@/types/ebpf";
|
||||
import { store } from "@/store";
|
||||
import graphql from "@/graphql";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Call } from "@/types/topology";
|
||||
import { LayoutConfig } from "@/types/dashboard";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
interface NetworkProfilingState {
|
||||
networkTasks: EBPFTaskList[];
|
||||
networkTip: string;
|
||||
selectedNetworkTask: Recordable<EBPFTaskList>;
|
||||
nodes: ProcessNode[];
|
||||
calls: Call[];
|
||||
node: Nullable<ProcessNode>;
|
||||
call: Nullable<Call>;
|
||||
metricsLayout: LayoutConfig[];
|
||||
selectedMetric: Nullable<LayoutConfig>;
|
||||
activeMetricIndex: string;
|
||||
aliveNetwork: boolean;
|
||||
loadNodes: boolean;
|
||||
}
|
||||
|
||||
export const networkProfilingStore = defineStore({
|
||||
id: "networkProfiling",
|
||||
state: (): NetworkProfilingState => ({
|
||||
networkTasks: [],
|
||||
networkTip: "",
|
||||
selectedNetworkTask: {},
|
||||
nodes: [],
|
||||
calls: [],
|
||||
node: null,
|
||||
call: null,
|
||||
metricsLayout: [],
|
||||
selectedMetric: null,
|
||||
activeMetricIndex: "",
|
||||
aliveNetwork: false,
|
||||
loadNodes: false,
|
||||
}),
|
||||
actions: {
|
||||
setSelectedNetworkTask(task: EBPFTaskList) {
|
||||
this.selectedNetworkTask = task || {};
|
||||
},
|
||||
setNode(node: Node) {
|
||||
this.node = node;
|
||||
},
|
||||
setLink(link: Call) {
|
||||
this.call = link;
|
||||
},
|
||||
setMetricsLayout(layout: LayoutConfig[]) {
|
||||
this.metricsLayout = layout;
|
||||
},
|
||||
setSelectedMetric(item: LayoutConfig) {
|
||||
this.selectedMetric = item;
|
||||
},
|
||||
setActiveItem(index: string) {
|
||||
this.activeMetricIndex = index;
|
||||
},
|
||||
setTopology(data: { nodes: ProcessNode[]; calls: Call[] }) {
|
||||
const obj = {} as any;
|
||||
let calls = (data.calls || []).reduce((prev: Call[], next: Call) => {
|
||||
if (!obj[next.id]) {
|
||||
obj[next.id] = true;
|
||||
next.value = next.value || 1;
|
||||
for (const node of data.nodes) {
|
||||
if (next.source === node.id) {
|
||||
next.sourceObj = node;
|
||||
}
|
||||
if (next.target === node.id) {
|
||||
next.targetObj = node;
|
||||
}
|
||||
}
|
||||
next.value = next.value || 1;
|
||||
prev.push(next);
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
calls = calls.map((d: any) => {
|
||||
d.sourceId = d.source;
|
||||
d.targetId = d.target;
|
||||
d.source = d.sourceObj;
|
||||
d.target = d.targetObj;
|
||||
delete d.sourceObj;
|
||||
delete d.targetObj;
|
||||
return d;
|
||||
});
|
||||
this.calls = calls;
|
||||
this.nodes = data.nodes;
|
||||
},
|
||||
async createNetworkTask(param: {
|
||||
serviceId: string;
|
||||
serviceInstanceId: string;
|
||||
}) {
|
||||
const res: AxiosResponse = await graphql
|
||||
.query("newNetworkProfiling")
|
||||
.params({ request: { instanceId: param.serviceInstanceId } });
|
||||
|
||||
if (res.data.errors) {
|
||||
return res.data;
|
||||
}
|
||||
return res.data;
|
||||
},
|
||||
async getTaskList(params: {
|
||||
serviceId: string;
|
||||
serviceInstanceId: string;
|
||||
targets: string[];
|
||||
}) {
|
||||
if (!params.serviceId) {
|
||||
return new Promise((resolve) => resolve({}));
|
||||
}
|
||||
const res: AxiosResponse = await graphql
|
||||
.query("getEBPFTasks")
|
||||
.params(params);
|
||||
|
||||
this.networkTip = "";
|
||||
if (res.data.errors) {
|
||||
return res.data;
|
||||
}
|
||||
this.networkTasks = res.data.data.queryEBPFTasks || [];
|
||||
this.selectedNetworkTask = this.networkTasks[0] || {};
|
||||
this.setSelectedNetworkTask(this.selectedNetworkTask);
|
||||
return res.data;
|
||||
},
|
||||
async keepNetworkProfiling(taskId: string) {
|
||||
if (!taskId) {
|
||||
return new Promise((resolve) => resolve({}));
|
||||
}
|
||||
const res: AxiosResponse = await graphql
|
||||
.query("aliveNetworkProfiling")
|
||||
.params({ taskId });
|
||||
|
||||
this.aliveMessage = "";
|
||||
if (res.data.errors) {
|
||||
return res.data;
|
||||
}
|
||||
this.aliveNetwork = res.data.data.keepEBPFNetworkProfiling.status;
|
||||
if (!this.aliveNetwork) {
|
||||
ElMessage.warning(res.data.data.keepEBPFNetworkProfiling.errorReason);
|
||||
}
|
||||
return res.data;
|
||||
},
|
||||
async getProcessTopology(params: {
|
||||
duration: any;
|
||||
serviceInstanceId: string;
|
||||
}) {
|
||||
this.loadNodes = true;
|
||||
const res: AxiosResponse = await graphql
|
||||
.query("getProcessTopology")
|
||||
.params(params);
|
||||
this.loadNodes = false;
|
||||
if (res.data.errors) {
|
||||
this.nodes = [];
|
||||
this.calls = [];
|
||||
return res.data;
|
||||
}
|
||||
const { topology } = res.data.data;
|
||||
|
||||
this.setTopology(topology);
|
||||
return res.data;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function useNetworkProfilingStore(): any {
|
||||
return networkProfilingStore(store);
|
||||
}
|
@ -14,6 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import dayjs from "dayjs";
|
||||
export default function dateFormatStep(
|
||||
date: Date,
|
||||
step: string,
|
||||
@ -99,3 +100,6 @@ export const dateFormatTime = (date: Date, step: string): string => {
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(new Date(date)).format(pattern);
|
||||
|
@ -75,6 +75,7 @@ import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import timeFormat from "@/utils/timeFormat";
|
||||
import { Languages } from "@/constants/data";
|
||||
import Selector from "@/components/Selector.vue";
|
||||
import getLocalTime from "@/utils/localtime";
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const appStore = useAppStoreWithOut();
|
||||
@ -88,8 +89,11 @@ appStore.setPageTitle("Setting");
|
||||
const handleReload = () => {
|
||||
const gap =
|
||||
appStore.duration.end.getTime() - appStore.duration.start.getTime();
|
||||
const time: Date[] = [new Date(new Date().getTime() - gap), new Date()];
|
||||
appStore.setDuration(timeFormat(time));
|
||||
const dates: Date[] = [
|
||||
getLocalTime(appStore.utc, new Date(new Date().getTime() - gap)),
|
||||
getLocalTime(appStore.utc, new Date()),
|
||||
];
|
||||
appStore.setDuration(timeFormat(dates));
|
||||
};
|
||||
const handleAuto = () => {
|
||||
if (autoTime.value < 1) {
|
||||
|
@ -145,16 +145,14 @@ limitations under the License. -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Alarm, Event } from "@/types/alarm";
|
||||
import { useAlarmStore } from "@/store/modules/alarm";
|
||||
import { EventsDetailHeaders, AlarmDetailCol, EventsDetailKeys } from "./data";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
|
||||
const { t } = useI18n();
|
||||
const alarmStore = useAlarmStore();
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
const isShowDetails = ref<boolean>(false);
|
||||
const showEventDetails = ref<boolean>(false);
|
||||
const currentDetail = ref<Alarm | any>({});
|
||||
|
97
src/views/dashboard/controls/NetworkProfiling.vue
Normal file
97
src/views/dashboard/controls/NetworkProfiling.vue
Normal file
@ -0,0 +1,97 @@
|
||||
<!-- 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">
|
||||
<div class="title">Network Profiling</div>
|
||||
<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>
|
||||
<Content />
|
||||
</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/network-profiling/Content.vue";
|
||||
|
||||
/*global defineProps */
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => ({ graph: {} }),
|
||||
},
|
||||
activeIndex: { type: String, default: "" },
|
||||
needQuery: { type: Boolean, default: true },
|
||||
});
|
||||
const { t } = useI18n();
|
||||
const dashboardStore = useDashboardStore();
|
||||
|
||||
function removeWidget() {
|
||||
dashboardStore.removeControls(props.data);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.profile-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.operation {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 3px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.tools {
|
||||
padding: 5px 0;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
line-height: 40px;
|
||||
padding: 0 10px;
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
}
|
||||
</style>
|
@ -24,6 +24,7 @@ import Text from "./Text.vue";
|
||||
import Ebpf from "./Ebpf.vue";
|
||||
import DemandLog from "./DemandLog.vue";
|
||||
import Event from "./Event.vue";
|
||||
import NetworkProfiling from "./NetworkProfiling.vue";
|
||||
import TimeRange from "./TimeRange.vue";
|
||||
|
||||
export default {
|
||||
@ -37,5 +38,6 @@ export default {
|
||||
Ebpf,
|
||||
DemandLog,
|
||||
Event,
|
||||
NetworkProfiling,
|
||||
TimeRange,
|
||||
};
|
||||
|
@ -23,6 +23,7 @@ import Text from "./Text.vue";
|
||||
import Ebpf from "./Ebpf.vue";
|
||||
import DemandLog from "./DemandLog.vue";
|
||||
import Event from "./Event.vue";
|
||||
import NetworkProfiling from "./NetworkProfiling.vue";
|
||||
import TimeRange from "./TimeRange.vue";
|
||||
|
||||
export default {
|
||||
@ -35,5 +36,6 @@ export default {
|
||||
Ebpf,
|
||||
DemandLog,
|
||||
Event,
|
||||
NetworkProfiling,
|
||||
TimeRange,
|
||||
};
|
||||
|
@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export const dragIgnoreFrom =
|
||||
"svg.d3-trace-tree, .dragger, .micro-topo-chart, .schedules, .vis-item, .vis-timeline";
|
||||
"svg.d3-trace-tree, .dragger, .micro-topo-chart, .schedules, .vis-item, .vis-timeline, .process-svg";
|
||||
|
||||
export const PodsChartTypes = ["EndpointList", "InstanceList"];
|
||||
|
||||
@ -198,6 +198,11 @@ export const InstanceTools = [
|
||||
{ name: "assignment", content: "Add Log", id: "addLog" },
|
||||
{ name: "demand", content: "Add On Demand Log", id: "addDemandLog" },
|
||||
{ name: "event", content: "Add Event", id: "addEvent" },
|
||||
{
|
||||
name: "timeline",
|
||||
content: "Add Network Profiling",
|
||||
id: "addNetworkProfiling",
|
||||
},
|
||||
];
|
||||
export const EndpointTools = [
|
||||
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
||||
|
@ -287,7 +287,7 @@ async function setSourceSelector() {
|
||||
await selectorStore.getService(String(params.serviceId));
|
||||
states.currentService = selectorStore.currentService.value;
|
||||
const e = String(params.entity).split("Relation")[0];
|
||||
await fetchPods(e, selectorStore.currentService.id, false);
|
||||
await fetchPods(e, selectorStore.currentService.id, true);
|
||||
if (!(selectorStore.pods.length && selectorStore.pods[0])) {
|
||||
selectorStore.setCurrentPod(null);
|
||||
states.currentPod = "";
|
||||
@ -295,32 +295,25 @@ async function setSourceSelector() {
|
||||
return;
|
||||
}
|
||||
const pod = params.podId || selectorStore.pods[0].id;
|
||||
let currentPod;
|
||||
if (states.currentPod) {
|
||||
currentPod = selectorStore.pods.find(
|
||||
(d: { label: string }) => d.label === states.currentPod
|
||||
);
|
||||
} else {
|
||||
currentPod = selectorStore.pods.find((d: { id: string }) => d.id === pod);
|
||||
}
|
||||
const currentPod = selectorStore.pods.find(
|
||||
(d: { id: string }) => d.id === pod
|
||||
);
|
||||
if (!currentPod) {
|
||||
selectorStore.setCurrentProcess(null);
|
||||
states.currentProcess = "";
|
||||
return;
|
||||
}
|
||||
selectorStore.setCurrentPod(currentPod);
|
||||
states.currentPod = currentPod.label;
|
||||
const process =
|
||||
params.processId ||
|
||||
(selectorStore.processes.length && selectorStore.processes[0].id);
|
||||
let currentProcess;
|
||||
if (states.currentProcess) {
|
||||
currentProcess = selectorStore.processes.find(
|
||||
(d: { label: string }) => d.label === states.currentProcess
|
||||
);
|
||||
} else {
|
||||
currentProcess = selectorStore.processes.find(
|
||||
(d: { id: string }) => d.id === process
|
||||
);
|
||||
if (!(selectorStore.processes.length && selectorStore.processes[0])) {
|
||||
selectorStore.setCurrentProcess(null);
|
||||
states.currentProcess = "";
|
||||
return;
|
||||
}
|
||||
const process = params.processId || selectorStore.processes[0].id;
|
||||
const currentProcess = selectorStore.processes.find(
|
||||
(d: { id: string }) => d.id === process
|
||||
);
|
||||
if (currentProcess) {
|
||||
selectorStore.setCurrentProcess(currentProcess);
|
||||
states.currentProcess = currentProcess.label;
|
||||
@ -333,7 +326,7 @@ async function setDestSelector() {
|
||||
await fetchPods(
|
||||
String(params.entity),
|
||||
selectorStore.currentDestService.id,
|
||||
false
|
||||
true
|
||||
);
|
||||
if (!(selectorStore.destPods.length && selectorStore.destPods[0])) {
|
||||
selectorStore.setCurrentDestPod(null);
|
||||
@ -341,36 +334,27 @@ async function setDestSelector() {
|
||||
return;
|
||||
}
|
||||
const destPod = params.destPodId || selectorStore.destPods[0].id;
|
||||
let currentDestPod = { label: "" };
|
||||
if (states.currentDestPod) {
|
||||
currentDestPod = selectorStore.pods.find(
|
||||
(d: { label: string }) => d.label === states.currentDestPod
|
||||
);
|
||||
} else {
|
||||
currentDestPod = selectorStore.destPods.find(
|
||||
(d: { id: string }) => d.id === destPod
|
||||
);
|
||||
}
|
||||
const currentDestPod = selectorStore.destPods.find(
|
||||
(d: { id: string }) => d.id === destPod
|
||||
);
|
||||
if (!currentDestPod) {
|
||||
states.currentDestProcess = "";
|
||||
selectorStore.setCurrentProcess(null);
|
||||
return;
|
||||
}
|
||||
selectorStore.setCurrentDestPod(currentDestPod);
|
||||
states.currentDestPod = currentDestPod.label;
|
||||
const destProcess = params.destProcessId || selectorStore.destProcesses[0].id;
|
||||
let currentDestProcess;
|
||||
if (states.currentDestProcess) {
|
||||
currentDestProcess = selectorStore.destProcesses.find(
|
||||
(d: { label: string }) => d.label === states.currentProcess
|
||||
);
|
||||
} else {
|
||||
currentDestProcess = selectorStore.destProcesses.find(
|
||||
(d: { id: string }) => d.id === destProcess
|
||||
);
|
||||
}
|
||||
if (currentDestProcess) {
|
||||
selectorStore.setCurrentProcess(currentDestProcess);
|
||||
states.currentProcess = currentDestProcess.label;
|
||||
const currentDestProcess = selectorStore.destProcesses.find(
|
||||
(d: { id: string }) => d.id === destProcess
|
||||
);
|
||||
if (!currentDestProcess) {
|
||||
states.currentDestProcess = "";
|
||||
selectorStore.setCurrentProcess(null);
|
||||
return;
|
||||
}
|
||||
selectorStore.setCurrentProcess(currentDestProcess);
|
||||
states.currentDestProcess = currentDestProcess.label;
|
||||
}
|
||||
|
||||
async function getServices() {
|
||||
@ -562,6 +546,9 @@ function setTabControls(id: string) {
|
||||
case "addEvent":
|
||||
dashboardStore.addTabControls("Event");
|
||||
break;
|
||||
case "addNetworkProfiling":
|
||||
dashboardStore.addTabControls("NetworkProfiling");
|
||||
break;
|
||||
case "addTimeRange":
|
||||
dashboardStore.addTabControls("TimeRange");
|
||||
break;
|
||||
@ -603,6 +590,9 @@ function setControls(id: string) {
|
||||
case "addEvent":
|
||||
dashboardStore.addControl("Event");
|
||||
break;
|
||||
case "addNetworkProfiling":
|
||||
dashboardStore.addControl("NetworkProfiling");
|
||||
break;
|
||||
case "addTimeRange":
|
||||
dashboardStore.addControl("TimeRange");
|
||||
break;
|
||||
|
@ -58,7 +58,6 @@ limitations under the License. -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
@ -15,14 +15,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export default (d3: any, graph: any) =>
|
||||
export default (d3: any, graph: any, diff: number[]) =>
|
||||
d3
|
||||
.zoom()
|
||||
.scaleExtent([0.3, 10])
|
||||
.on("zoom", (d: any) => {
|
||||
graph
|
||||
.attr("transform", d3.zoomTransform(graph.node()))
|
||||
.attr(
|
||||
`translate(${d.transform.x},${d.transform.y})scale(${d.transform.k})`
|
||||
);
|
||||
.on("zoom", (event: any) => {
|
||||
graph.attr(
|
||||
"transform",
|
||||
`translate(${event.transform.x + diff[0]},${
|
||||
event.transform.y + diff[1]
|
||||
})scale(${event.transform.k})`
|
||||
);
|
||||
});
|
@ -103,13 +103,13 @@ limitations under the License. -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Option } from "@/types/app";
|
||||
import { TableHeader, AggregateTypes } from "./data";
|
||||
import { useEbpfStore } from "@/store/modules/ebpf";
|
||||
import { EBPFProfilingSchedule, Process } from "@/types/ebpf";
|
||||
import { ElMessage, ElTable } from "element-plus";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
|
||||
const { t } = useI18n();
|
||||
const ebpfStore = useEbpfStore();
|
||||
@ -123,8 +123,6 @@ const selectedLabels = ref<string[]>(["0"]);
|
||||
const searchText = ref<string>("");
|
||||
const aggregateType = ref<string>(AggregateTypes[0].value);
|
||||
const duration = ref<string[]>([]);
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
const attributes = (attr: { name: string; value: string }[]) => {
|
||||
return attr
|
||||
.map((d: { name: string; value: string }) => `${d.name}=${d.value}`)
|
||||
|
@ -72,17 +72,15 @@ limitations under the License. -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useEbpfStore } from "@/store/modules/ebpf";
|
||||
import { EBPFTaskList } from "@/types/ebpf";
|
||||
import { ElMessage } from "element-plus";
|
||||
import TaskDetails from "../../components/TaskDetails.vue";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
|
||||
const { t } = useI18n();
|
||||
const ebpfStore = useEbpfStore();
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
const viewDetail = ref<boolean>(false);
|
||||
|
||||
async function changeTask(item: EBPFTaskList) {
|
||||
|
@ -39,8 +39,8 @@ limitations under the License. -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { BrowserLogConstants } from "./data";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
|
||||
/*global defineProps, defineEmits, NodeListOf */
|
||||
const props = defineProps({
|
||||
@ -50,9 +50,6 @@ const columns = BrowserLogConstants;
|
||||
const emit = defineEmits(["select"]);
|
||||
const logItem = ref<any>(null);
|
||||
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
|
||||
function showSelectSpan() {
|
||||
const items: NodeListOf<any> = document.querySelectorAll(".log-item");
|
||||
|
||||
|
@ -43,8 +43,8 @@ limitations under the License. -->
|
||||
import { computed } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import dayjs from "dayjs";
|
||||
import { Option } from "@/types/app";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
|
||||
/*global defineProps */
|
||||
const props = defineProps({
|
||||
@ -60,8 +60,6 @@ const logTags = computed(() => {
|
||||
return `${d.key} = ${d.value}`;
|
||||
});
|
||||
});
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
|
@ -39,11 +39,11 @@ limitations under the License. -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { ServiceLogConstants } from "./data";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { LayoutConfig } from "@/types/dashboard";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
|
||||
/*global defineProps, defineEmits, Recordable */
|
||||
const props = defineProps({
|
||||
@ -64,8 +64,6 @@ const tags = computed(() => {
|
||||
)
|
||||
);
|
||||
});
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
|
||||
function selectLog(label: string, value: string) {
|
||||
if (label === "traceId") {
|
||||
|
59
src/views/dashboard/related/network-profiling/Content.vue
Normal file
59
src/views/dashboard/related/network-profiling/Content.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<div class="flex-h content">
|
||||
<Tasks />
|
||||
<div
|
||||
class="vis-graph ml-5"
|
||||
v-if="networkProfilingStore.nodes.length"
|
||||
v-loading="networkProfilingStore.loadNodes"
|
||||
>
|
||||
<process-topology />
|
||||
</div>
|
||||
<div class="text" v-else v-loading="networkProfilingStore.loadNodes">
|
||||
{{ t("noData") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import Tasks from "./components/Tasks.vue";
|
||||
import ProcessTopology from "./components/ProcessTopology.vue";
|
||||
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
|
||||
|
||||
const networkProfilingStore = useNetworkProfilingStore();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
height: calc(100% - 30px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vis-graph {
|
||||
height: 100%;
|
||||
flex-grow: 2;
|
||||
min-width: 700px;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
width: calc(100% - 330px);
|
||||
}
|
||||
|
||||
.text {
|
||||
width: calc(100% - 330px);
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class Orientation {
|
||||
public f0 = 0;
|
||||
public f1 = 0;
|
||||
public f2 = 0;
|
||||
public f3 = 0;
|
||||
public b0? = 0;
|
||||
public b1? = 0;
|
||||
public b2? = 0;
|
||||
public b3? = 0;
|
||||
public start_angle? = 0;
|
||||
constructor(
|
||||
f0: number,
|
||||
f1: number,
|
||||
f2: number,
|
||||
f3: number,
|
||||
b0: number,
|
||||
b1: number,
|
||||
b2: number,
|
||||
b3: number,
|
||||
start_angle: number
|
||||
) {
|
||||
this.f0 = f0;
|
||||
this.f1 = f1;
|
||||
this.f2 = f2;
|
||||
this.f3 = f3;
|
||||
this.b0 = b0;
|
||||
this.b1 = b1;
|
||||
this.b2 = b2;
|
||||
this.b3 = b3;
|
||||
this.start_angle = start_angle;
|
||||
}
|
||||
}
|
||||
|
||||
const SQRT3 = Math.sqrt(3.0);
|
||||
class Layout {
|
||||
static Pointy = new Orientation(
|
||||
SQRT3,
|
||||
SQRT3 / 2.0,
|
||||
0.0,
|
||||
3.0 / 2.0,
|
||||
SQRT3 / 3.0,
|
||||
-1.0 / 3.0,
|
||||
0.0,
|
||||
2.0 / 3.0,
|
||||
0.5
|
||||
);
|
||||
static Flat = new Orientation(
|
||||
3.0 / 2.0,
|
||||
0.0,
|
||||
SQRT3 / 2.0,
|
||||
SQRT3,
|
||||
2.0 / 3.0,
|
||||
0.0,
|
||||
-1.0 / 3.0,
|
||||
SQRT3 / 3.0,
|
||||
0.0
|
||||
);
|
||||
|
||||
static spacing(radius: number, isPointy = false): number[] {
|
||||
return isPointy
|
||||
? [SQRT3 * radius, 2 * radius * (3 / 4)]
|
||||
: [2 * radius * (3 / 4), SQRT3 * radius];
|
||||
}
|
||||
|
||||
private radius = 1;
|
||||
private orientation: Orientation = { f0: 0, f1: 0, f2: 0, f3: 0 };
|
||||
private origin = [0, 0];
|
||||
|
||||
constructor(radius: number, origin = [0, 0], orientation?: Orientation) {
|
||||
this.radius = radius; //Layout.spacing( radius, ( orientation === Layout.Pointy ) );
|
||||
this.orientation = orientation || Layout.Flat;
|
||||
this.origin = origin;
|
||||
}
|
||||
|
||||
// Same as HexToPixel, Except it takes raw coords instead of hex object.
|
||||
axialToPixel(ax: number, ay: number): number[] {
|
||||
const M = this.orientation;
|
||||
const x = (M.f0 * ax + M.f1 * ay) * this.radius;
|
||||
const y = (M.f2 * ax + M.f3 * ay) * this.radius;
|
||||
|
||||
return [x + this.origin[0], y + this.origin[1]];
|
||||
}
|
||||
|
||||
hexToPixel(h: { x: number; y: number }): number[] {
|
||||
const M = this.orientation;
|
||||
const x = (M.f0 * h.x + M.f1 * h.y) * this.radius;
|
||||
const y = (M.f2 * h.x + M.f3 * h.y) * this.radius;
|
||||
|
||||
return [x + this.origin[0], y + this.origin[1]];
|
||||
}
|
||||
}
|
||||
|
||||
class Hex extends Int16Array {
|
||||
constructor(x: number, y: number, z = null) {
|
||||
super(3);
|
||||
this.xyz(x, y, z);
|
||||
}
|
||||
|
||||
xyz(x: number, y: number, z: number | null = null): Hex {
|
||||
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);
|
||||
}
|
||||
|
||||
this[0] = x;
|
||||
this[1] = y;
|
||||
this[2] = z;
|
||||
return this;
|
||||
}
|
||||
|
||||
get x(): number {
|
||||
return this[0];
|
||||
}
|
||||
get y(): number {
|
||||
return this[1];
|
||||
}
|
||||
get z(): number {
|
||||
return this[2];
|
||||
}
|
||||
|
||||
get len(): number {
|
||||
return Math.floor(
|
||||
(Math.abs(this[0]) + Math.abs(this[1]) + Math.abs(this[2])) / 2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Hex, Orientation, Layout };
|
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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 linkElement = (graph: any) => {
|
||||
const linkEnter = graph
|
||||
.append("path")
|
||||
.attr("class", "topo-call")
|
||||
.attr("marker-end", "url(#arrow)")
|
||||
.attr("stroke", "#97B0F8")
|
||||
.attr("d", (d: any) => {
|
||||
const controlPos = computeControlPoint(
|
||||
[d.source.x, d.source.y - 5],
|
||||
[d.target.x, d.target.y - 5],
|
||||
0.5
|
||||
);
|
||||
return (
|
||||
"M" +
|
||||
d.source.x +
|
||||
" " +
|
||||
(d.source.y - 5) +
|
||||
" " +
|
||||
"Q" +
|
||||
controlPos[0] +
|
||||
" " +
|
||||
controlPos[1] +
|
||||
" " +
|
||||
d.target.x +
|
||||
" " +
|
||||
(d.target.y - 5)
|
||||
);
|
||||
});
|
||||
return linkEnter;
|
||||
};
|
||||
export const anchorElement = (graph: any, funcs: any, tip: any) => {
|
||||
const linkEnter = graph
|
||||
.append("circle")
|
||||
.attr("class", "topo-line-anchor")
|
||||
.attr("r", 5)
|
||||
.attr("fill", "#97B0F8")
|
||||
.attr("transform", (d: any) => {
|
||||
const controlPos = computeControlPoint(
|
||||
[d.source.x, d.source.y - 5],
|
||||
[d.target.x, d.target.y - 5],
|
||||
0.5
|
||||
);
|
||||
const p = quadraticBezier(
|
||||
0.5,
|
||||
{ x: d.source.x, y: d.source.y - 5 },
|
||||
{ x: controlPos[0], y: controlPos[1] },
|
||||
{ x: d.target.x, y: d.target.y - 5 }
|
||||
);
|
||||
return `translate(${p[0]}, ${p[1]})`;
|
||||
})
|
||||
.on("mouseover", function (event: unknown, d: unknown) {
|
||||
tip.html(funcs.tipHtml).show(d, this);
|
||||
})
|
||||
.on("mouseout", function () {
|
||||
tip.hide(this);
|
||||
})
|
||||
.on("click", (event: unknown, d: unknown) => {
|
||||
funcs.handleLinkClick(event, d);
|
||||
});
|
||||
return linkEnter;
|
||||
};
|
||||
export const arrowMarker = (graph: any) => {
|
||||
const defs = graph.append("defs");
|
||||
const arrow = defs
|
||||
.append("marker")
|
||||
.attr("id", "arrow")
|
||||
.attr("class", "topo-line-arrow")
|
||||
.attr("markerUnits", "strokeWidth")
|
||||
.attr("markerWidth", "8")
|
||||
.attr("markerHeight", "8")
|
||||
.attr("viewBox", "0 0 12 12")
|
||||
.attr("refX", "10")
|
||||
.attr("refY", "6")
|
||||
.attr("orient", "auto");
|
||||
const arrowPath = "M2,2 L10,6 L2,10 L6,6 L2,2";
|
||||
|
||||
arrow.append("path").attr("d", arrowPath).attr("fill", "#97B0F8");
|
||||
return arrow;
|
||||
};
|
||||
// Control Point coordinates of quadratic Bezier curve
|
||||
function computeControlPoint(ps: number[], pe: number[], arc = 0.5) {
|
||||
const deltaX = pe[0] - ps[0];
|
||||
const deltaY = pe[1] - ps[1];
|
||||
const theta = Math.atan(deltaY / deltaX);
|
||||
const len = (Math.sqrt(deltaX * deltaX + deltaY * deltaY) / 2) * arc;
|
||||
const newTheta = theta - Math.PI / 2;
|
||||
return [
|
||||
(ps[0] + pe[0]) / 2 + len * Math.cos(newTheta),
|
||||
(ps[1] + pe[1]) / 2 + len * Math.sin(newTheta),
|
||||
];
|
||||
}
|
||||
// Point coordinates of quadratic Bezier curve
|
||||
/**
|
||||
* @param t [0, 1]
|
||||
* @param ps start position
|
||||
* @param pc control position
|
||||
* @param pe end position
|
||||
* @returns a position in the line
|
||||
*/
|
||||
function quadraticBezier(
|
||||
t: number,
|
||||
ps: { x: number; y: number },
|
||||
pc: { x: number; y: number },
|
||||
pe: { x: number; y: number }
|
||||
) {
|
||||
const x = (1 - t) * (1 - t) * ps.x + 2 * t * (1 - t) * pc.x + t * t * pe.x;
|
||||
const y = (1 - t) * (1 - t) * ps.y + 2 * t * (1 - t) * pc.y + t * t * pe.y;
|
||||
return [x, y];
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 icons from "@/assets/img/icons";
|
||||
import { Node } from "@/types/topology";
|
||||
|
||||
export default (d3: any, graph: any, funcs: any, tip: any) => {
|
||||
const nodeEnter = graph
|
||||
.append("g")
|
||||
.call(
|
||||
d3
|
||||
.drag()
|
||||
.on("start", funcs.dragstart)
|
||||
.on("drag", funcs.dragged)
|
||||
.on("end", funcs.dragended)
|
||||
)
|
||||
.on("mouseover", function (event: unknown, d: Node) {
|
||||
tip.html(funcs.tipHtml).show(d, this);
|
||||
})
|
||||
.on("mouseout", function () {
|
||||
tip.hide(this);
|
||||
});
|
||||
nodeEnter
|
||||
.append("image")
|
||||
.attr("width", 35)
|
||||
.attr("height", 35)
|
||||
.attr("x", (d: any) => d.x - 15)
|
||||
.attr("y", (d: any) => d.y - 15)
|
||||
.attr("style", "cursor: move;")
|
||||
.attr("xlink:href", icons.CUBE);
|
||||
nodeEnter
|
||||
.append("text")
|
||||
.attr("fill", "#000")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("x", (d: any) => d.x + 5)
|
||||
.attr("y", (d: any) => d.y + 28)
|
||||
.text((d: { name: string }) =>
|
||||
d.name.length > 10 ? `${d.name.substring(0, 10)}...` : d.name
|
||||
);
|
||||
return nodeEnter;
|
||||
};
|
@ -0,0 +1,503 @@
|
||||
<!-- 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 ref="chart" class="process-topo"></div>
|
||||
<el-popover placement="bottom" :width="295" trigger="click">
|
||||
<template #reference>
|
||||
<div
|
||||
class="switch-icon-edit ml-5"
|
||||
title="Settings"
|
||||
@click="setConfig"
|
||||
v-if="dashboardStore.editMode"
|
||||
>
|
||||
<Icon size="middle" iconName="setting_empty" />
|
||||
</div>
|
||||
</template>
|
||||
<Settings @update="updateSettings" />
|
||||
</el-popover>
|
||||
<TimeLine @get="getDates" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import * as d3 from "d3";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ElMessage } from "element-plus";
|
||||
import router from "@/router";
|
||||
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import d3tip from "d3-tip";
|
||||
import { linkElement, anchorElement, arrowMarker } from "./Graph/linkProcess";
|
||||
import nodeElement from "./Graph/nodeProcess";
|
||||
import { Call } from "@/types/topology";
|
||||
import zoom from "../../components/utils/zoom";
|
||||
import { ProcessNode } from "@/types/ebpf";
|
||||
import { useThrottleFn } from "@vueuse/core";
|
||||
import Settings from "./Settings.vue";
|
||||
import { EntityType } from "@/views/dashboard/data";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
import { Layout } from "./Graph/layout";
|
||||
import TimeLine from "./TimeLine.vue";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
|
||||
/*global Nullable, defineProps */
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => ({ graph: {} }),
|
||||
},
|
||||
});
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStoreWithOut();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const selectorStore = useSelectorStore();
|
||||
const networkProfilingStore = useNetworkProfilingStore();
|
||||
const height = ref<number>(100);
|
||||
const width = ref<number>(100);
|
||||
const svg = ref<Nullable<any>>(null);
|
||||
const chart = ref<Nullable<HTMLDivElement>>(null);
|
||||
const tip = ref<Nullable<HTMLDivElement>>(null);
|
||||
const graph = ref<any>(null);
|
||||
const node = ref<any>(null);
|
||||
const link = ref<any>(null);
|
||||
const anchor = ref<any>(null);
|
||||
const arrow = ref<any>(null);
|
||||
const oldVal = ref<{ width: number; height: number }>({ width: 0, height: 0 });
|
||||
const config = ref<any>({});
|
||||
const diff = ref<number[]>([220, 200]);
|
||||
const radius = 210;
|
||||
const dates = ref<Nullable<{ start: number; end: number }>>(null);
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
oldVal.value = (chart.value && chart.value.getBoundingClientRect()) || {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
});
|
||||
|
||||
async function init() {
|
||||
svg.value = d3.select(chart.value).append("svg").attr("class", "process-svg");
|
||||
if (!networkProfilingStore.nodes.length) {
|
||||
return;
|
||||
}
|
||||
drawGraph();
|
||||
createLayout();
|
||||
}
|
||||
|
||||
function drawGraph() {
|
||||
const dom = chart.value?.getBoundingClientRect() || {
|
||||
height: 20,
|
||||
width: 0,
|
||||
};
|
||||
height.value = (dom.height || 40) - 20;
|
||||
width.value = dom.width;
|
||||
svg.value.attr("height", height.value).attr("width", width.value);
|
||||
tip.value = (d3tip as any)().attr("class", "d3-tip").offset([-8, 0]);
|
||||
const outNodes = networkProfilingStore.nodes.filter(
|
||||
(d: ProcessNode) => d.serviceInstanceId !== selectorStore.currentPod.id
|
||||
);
|
||||
if (outNodes.length) {
|
||||
diff.value[0] = (dom.width - radius * 4) / 2 + radius;
|
||||
} else {
|
||||
diff.value[0] = (dom.width - radius * 2) / 2 + radius;
|
||||
}
|
||||
graph.value = svg.value
|
||||
.append("g")
|
||||
.attr("class", "svg-graph")
|
||||
.attr("transform", `translate(${diff.value[0]}, ${diff.value[1]})`);
|
||||
graph.value.call(tip.value);
|
||||
node.value = graph.value.append("g").selectAll(".topo-node");
|
||||
link.value = graph.value.append("g").selectAll(".topo-call");
|
||||
anchor.value = graph.value.append("g").selectAll(".topo-line-anchor");
|
||||
arrow.value = graph.value.append("g").selectAll(".topo-line-arrow");
|
||||
svg.value.call(zoom(d3, graph.value, diff.value));
|
||||
svg.value.on("click", (event: any) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
networkProfilingStore.setNode(null);
|
||||
networkProfilingStore.setLink(null);
|
||||
dashboardStore.selectWidget(props.config);
|
||||
});
|
||||
useThrottleFn(resize, 500)();
|
||||
}
|
||||
|
||||
function hexGrid(n = 1, radius = 1, origin = [0, 0]) {
|
||||
let x, y, yn, p;
|
||||
const gLayout = new Layout(radius, origin);
|
||||
const pos = [];
|
||||
for (x = -n; x <= n; x++) {
|
||||
y = Math.max(-n, -x - n); // 0
|
||||
yn = Math.min(n, -x + n); // 1
|
||||
for (y; y <= yn; y++) {
|
||||
p = gLayout.axialToPixel(x, y);
|
||||
pos.push(p);
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
function createPolygon(radius: number, sides = 6, offset = 0) {
|
||||
const poly: number[][] = [];
|
||||
let i, rad;
|
||||
for (i = 0; i < sides; i++) {
|
||||
rad = Math.PI * 2 * (i / sides);
|
||||
poly.push([
|
||||
Math.cos(rad + offset) * radius,
|
||||
Math.sin(rad + offset) * radius,
|
||||
]);
|
||||
}
|
||||
return poly;
|
||||
}
|
||||
|
||||
function getArcPoint(angle: number, radius: number) {
|
||||
const origin = [0, 0];
|
||||
const x1 = radius + origin[0] * Math.cos((angle * Math.PI) / 180);
|
||||
const y1 = origin[1] + radius * Math.sin((angle * Math.PI) / 180);
|
||||
|
||||
return [x1, y1];
|
||||
}
|
||||
function createLayout() {
|
||||
if (!node.value || !link.value) {
|
||||
return;
|
||||
}
|
||||
const dom: any = (chart.value && chart.value.getBoundingClientRect()) || {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
if (isNaN(dom.width) || dom.width < 1) {
|
||||
return;
|
||||
}
|
||||
const p = {
|
||||
count: 1,
|
||||
radius, // layout hexagons radius 300
|
||||
};
|
||||
const polygon = createPolygon(p.radius, 6, 0);
|
||||
const origin = [0, 0];
|
||||
const vertices: any = []; // a hexagon vertices
|
||||
for (let v = 0; v < polygon.length; v++) {
|
||||
vertices.push([origin[0] + polygon[v][0], origin[1] + polygon[v][1]]);
|
||||
}
|
||||
const linePath = d3.line();
|
||||
linePath.curve(d3.curveLinearClosed);
|
||||
const hexPolygon = graph.value.append("g");
|
||||
hexPolygon
|
||||
.append("path")
|
||||
.attr("d", linePath(vertices))
|
||||
.attr("stroke", "#D5DDF6")
|
||||
.attr("stroke-width", 2)
|
||||
.style("fill", "none");
|
||||
hexPolygon
|
||||
.append("text")
|
||||
.attr("fill", "#000")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("x", 0)
|
||||
.attr("y", p.radius)
|
||||
.text(() => selectorStore.currentPod.label);
|
||||
const nodeArr = networkProfilingStore.nodes.filter(
|
||||
(d: ProcessNode) => d.isReal || d.name === "UNKNOWN_LOCAL"
|
||||
);
|
||||
const count = nodeArr.length;
|
||||
// layout
|
||||
const centers = hexGrid(p.count, 68, origin); // cube centers
|
||||
const cubeCenters = [];
|
||||
if (count > 7) {
|
||||
for (let i = 0; i < centers.length; i++) {
|
||||
// const polygon = createPolygon(68, 6, 0);
|
||||
// const vertices: any = []; // a hexagon vertices
|
||||
// for (let v = 0; v < polygon.length; v++) {
|
||||
// vertices.push([
|
||||
// centers[i][0] + polygon[v][0],
|
||||
// centers[i][1] + polygon[v][1],
|
||||
// ]);
|
||||
// }
|
||||
// const linePath = d3.line();
|
||||
// linePath.curve(d3.curveLinearClosed);
|
||||
// graph.value
|
||||
// .append("path")
|
||||
// .attr("d", linePath(vertices))
|
||||
// .attr("stroke", "#ccc")
|
||||
// .attr("stroke-width", 1)
|
||||
// .style("fill", "none");
|
||||
let c = hexGrid(1, 20, centers[i]);
|
||||
if (count < 15) {
|
||||
c = [c[0], c[5]];
|
||||
} else if (count < 22) {
|
||||
c = [c[0], c[2], c[5]];
|
||||
}
|
||||
cubeCenters.push(...c);
|
||||
}
|
||||
shuffleArray(cubeCenters);
|
||||
}
|
||||
// for (let i = 0; i < cubeCenters.length; i++) {
|
||||
// const polygon = createPolygon(20, 6, 0);
|
||||
// const vertices: any = []; // a hexagon vertices
|
||||
// for (let v = 0; v < polygon.length; v++) {
|
||||
// vertices.push([
|
||||
// cubeCenters[i][0] + polygon[v][0],
|
||||
// cubeCenters[i][1] + polygon[v][1],
|
||||
// ]);
|
||||
// }
|
||||
// const linePath = d3.line();
|
||||
// linePath.curve(d3.curveLinearClosed);
|
||||
// graph.value
|
||||
// .append("path")
|
||||
// .attr("d", linePath(vertices))
|
||||
// .attr("stroke", "#ccc")
|
||||
// .attr("stroke-width", 1)
|
||||
// .style("fill", "none");
|
||||
// }
|
||||
let cubes = count > 7 ? cubeCenters : centers;
|
||||
for (let v = 0; v < count; v++) {
|
||||
const x = cubes[v][0];
|
||||
const y = cubes[v][1];
|
||||
nodeArr[v].x = x;
|
||||
nodeArr[v].y = y;
|
||||
}
|
||||
const outNodes = networkProfilingStore.nodes.filter(
|
||||
(d: ProcessNode) => !(d.isReal || d.name === "UNKNOWN_LOCAL")
|
||||
);
|
||||
let angle = 10;
|
||||
let r = 230;
|
||||
for (let v = 0; v < outNodes.length; v++) {
|
||||
const pos = getArcPoint(angle, r); // angle is [-120, 120]
|
||||
outNodes[v].x = pos[0];
|
||||
outNodes[v].y = pos[1];
|
||||
angle = angle + 20;
|
||||
if (angle * (v + 1) > 120) {
|
||||
angle = -10;
|
||||
r = r + 60;
|
||||
}
|
||||
if (angle * (v + 1) < -120) {
|
||||
r = r + 60;
|
||||
angle = 10;
|
||||
}
|
||||
}
|
||||
drawTopology([...nodeArr, ...outNodes]);
|
||||
}
|
||||
|
||||
function drawTopology(nodeArr: any[]) {
|
||||
node.value = node.value.data(nodeArr, (d: ProcessNode) => d.id);
|
||||
node.value.exit().remove();
|
||||
node.value = nodeElement(
|
||||
d3,
|
||||
node.value.enter(),
|
||||
{
|
||||
tipHtml: (data: ProcessNode) => {
|
||||
return ` <div class="mb-5"><span class="grey">name: </span>${data.name}</div>`;
|
||||
},
|
||||
},
|
||||
tip.value
|
||||
).merge(node.value);
|
||||
// line element
|
||||
const obj = {} as any;
|
||||
const calls = networkProfilingStore.calls.reduce((prev: any[], next: any) => {
|
||||
if (
|
||||
!(
|
||||
obj[next.sourceId + next.targetId] && obj[next.targetId + next.sourceId]
|
||||
)
|
||||
) {
|
||||
obj[next.sourceId + next.targetId] = true;
|
||||
obj[next.targetId + next.sourceId] = true;
|
||||
prev.push(next);
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
|
||||
link.value = link.value.data(calls, (d: Call) => d.id);
|
||||
link.value.exit().remove();
|
||||
link.value = linkElement(link.value.enter()).merge(link.value);
|
||||
anchor.value = anchor.value.data(calls, (d: Call) => d.id);
|
||||
anchor.value.exit().remove();
|
||||
anchor.value = anchorElement(
|
||||
anchor.value.enter(),
|
||||
{
|
||||
handleLinkClick: handleLinkClick,
|
||||
tipHtml: (data: Call) => {
|
||||
const html = `<div><span class="grey">${t(
|
||||
"detectPoint"
|
||||
)}:</span>${data.detectPoints.join(" | ")}</div>`;
|
||||
return html;
|
||||
},
|
||||
},
|
||||
tip.value
|
||||
).merge(anchor.value);
|
||||
// arrow marker
|
||||
arrow.value = arrow.value.data(calls, (d: Call) => d.id);
|
||||
arrow.value.exit().remove();
|
||||
arrow.value = arrowMarker(arrow.value.enter()).merge(arrow.value);
|
||||
}
|
||||
|
||||
function shuffleArray(array: number[][]) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
}
|
||||
|
||||
function handleLinkClick(event: any, d: Call) {
|
||||
event.stopPropagation();
|
||||
networkProfilingStore.setNode(null);
|
||||
networkProfilingStore.setLink(d);
|
||||
if (!config.value.linkDashboard) {
|
||||
return;
|
||||
}
|
||||
const { dashboard } = getDashboard({
|
||||
name: config.value.linkDashboard,
|
||||
layer: dashboardStore.layerId,
|
||||
entity: EntityType[7].value,
|
||||
});
|
||||
if (!dashboard) {
|
||||
ElMessage.error(
|
||||
`The dashboard named ${config.value.linkDashboard} doesn't exist`
|
||||
);
|
||||
return;
|
||||
}
|
||||
let times: any = {};
|
||||
if (dates.value) {
|
||||
times = dates.value;
|
||||
} else {
|
||||
const { taskStartTime, fixedTriggerDuration } =
|
||||
networkProfilingStore.selectedNetworkTask;
|
||||
const startTime =
|
||||
fixedTriggerDuration > 1800
|
||||
? taskStartTime + fixedTriggerDuration * 1000 - 30 * 60 * 1000
|
||||
: taskStartTime;
|
||||
times = {
|
||||
start: startTime,
|
||||
end: taskStartTime + fixedTriggerDuration * 1000,
|
||||
};
|
||||
}
|
||||
const param = JSON.stringify({
|
||||
...times,
|
||||
step: appStore.duration.step,
|
||||
utc: appStore.utc,
|
||||
});
|
||||
const path = `/dashboard/${dashboard.layer}/${EntityType[7].value}/${d.source.serviceId}/${d.source.serviceInstanceId}/${d.source.id}/${d.target.serviceId}/${d.target.serviceInstanceId}/${d.target.id}/${dashboard.name}/duration/${param}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
}
|
||||
|
||||
function updateSettings(param: unknown) {
|
||||
config.value = param;
|
||||
}
|
||||
|
||||
function setConfig() {
|
||||
dashboardStore.selectWidget(props.config);
|
||||
}
|
||||
|
||||
function getDates(times: any) {
|
||||
dates.value = times;
|
||||
}
|
||||
|
||||
function resize() {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
const cr = entry.contentRect;
|
||||
if (
|
||||
Math.abs(cr.width - oldVal.value.width) < 5 &&
|
||||
Math.abs(cr.height - oldVal.value.height) < 5
|
||||
) {
|
||||
return;
|
||||
}
|
||||
freshNodes();
|
||||
oldVal.value = { width: cr.width, height: cr.height };
|
||||
});
|
||||
if (chart.value) {
|
||||
observer.observe(chart.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function freshNodes() {
|
||||
svg.value.selectAll(".svg-graph").remove();
|
||||
if (!networkProfilingStore.nodes.length) {
|
||||
return;
|
||||
}
|
||||
drawGraph();
|
||||
createLayout();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => networkProfilingStore.nodes,
|
||||
() => {
|
||||
freshNodes();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.process-topo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 150px;
|
||||
min-width: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.process-svg {
|
||||
width: 100%;
|
||||
height: calc(100% - 10px);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.switch-icon-edit {
|
||||
cursor: pointer;
|
||||
transition: all 0.5ms linear;
|
||||
border: 1px solid #ccc;
|
||||
color: #666;
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.range {
|
||||
right: 50px;
|
||||
}
|
||||
|
||||
.topo-line-anchor {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.topo-call {
|
||||
stroke-linecap: round;
|
||||
stroke-width: 2px;
|
||||
stroke-dasharray: 13 7;
|
||||
fill: none;
|
||||
animation: topo-dash 0.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes topo-dash {
|
||||
from {
|
||||
stroke-dashoffset: 20;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.time-ranges {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.query {
|
||||
margin-left: 510px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,88 @@
|
||||
<!-- 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="label">{{ t("linkDashboard") }}</div>
|
||||
<Selector
|
||||
:value="linkDashboard"
|
||||
:options="linkDashboards"
|
||||
size="small"
|
||||
placeholder="Please input a dashboard name for calls"
|
||||
@change="changeLinkDashboard"
|
||||
class="inputs"
|
||||
:clearable="true"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DashboardItem } from "@/types/dashboard";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { EntityType } from "@/views/dashboard/data";
|
||||
/*global defineEmits */
|
||||
const emits = defineEmits(["update"]);
|
||||
const { t } = useI18n();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const linkDashboards = ref<
|
||||
(DashboardItem & { label: string; value: string })[]
|
||||
>([]);
|
||||
const { selectedGrid } = dashboardStore;
|
||||
const linkDashboard = ref<string>(selectedGrid.linkDashboard || "");
|
||||
|
||||
onMounted(() => {
|
||||
getDashboards();
|
||||
});
|
||||
|
||||
function getDashboards() {
|
||||
const list = JSON.parse(sessionStorage.getItem("dashboards") || "[]");
|
||||
linkDashboards.value = list.reduce(
|
||||
(
|
||||
prev: (DashboardItem & { label: string; value: string })[],
|
||||
d: DashboardItem
|
||||
) => {
|
||||
if (
|
||||
d.layer === dashboardStore.layerId &&
|
||||
d.entity === EntityType[7].value
|
||||
) {
|
||||
prev.push({ ...d, label: d.name, value: d.name });
|
||||
}
|
||||
return prev;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
function changeLinkDashboard(opt: { value: string }[]) {
|
||||
linkDashboard.value = opt[0].value;
|
||||
const p = {
|
||||
...dashboardStore.selectedGrid,
|
||||
linkDashboard: opt[0].value,
|
||||
};
|
||||
dashboardStore.selectWidget(p);
|
||||
dashboardStore.setConfigs(p);
|
||||
emits("update", p);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.label {
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
margin-top: 8px;
|
||||
width: 270px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,282 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<div class="profile-task-list flex-v">
|
||||
<div class="profile-task-wrapper flex-v">
|
||||
<div class="profile-t-tool">
|
||||
<span>{{ t("taskList") }}</span>
|
||||
<span v-if="inProcess" class="new-task cp" @click="createTask">
|
||||
<Icon
|
||||
:style="{ color: '#ccc' }"
|
||||
iconName="library_add"
|
||||
size="middle"
|
||||
/>
|
||||
</span>
|
||||
<el-popconfirm
|
||||
title="Are you sure to create a task?"
|
||||
@confirm="createTask"
|
||||
v-else
|
||||
>
|
||||
<template #reference>
|
||||
<span class="new-task cp">
|
||||
<Icon iconName="library_add" size="middle" />
|
||||
</span>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
<div class="profile-t-wrapper">
|
||||
<div
|
||||
class="no-data"
|
||||
v-show="!networkProfilingStore.networkTasks.length"
|
||||
>
|
||||
{{ t("noData") }}
|
||||
</div>
|
||||
<table class="profile-t">
|
||||
<tr
|
||||
class="profile-tr cp"
|
||||
v-for="(i, index) in networkProfilingStore.networkTasks"
|
||||
@click="changeTask(i)"
|
||||
:key="index"
|
||||
>
|
||||
<td
|
||||
class="profile-td"
|
||||
:class="{
|
||||
selected:
|
||||
networkProfilingStore.selectedNetworkTask.taskId === i.taskId,
|
||||
}"
|
||||
>
|
||||
<div class="ell">
|
||||
<span class="mr-10 sm">
|
||||
{{ dateFormat(i.taskStartTime) }}
|
||||
</span>
|
||||
<span class="mr-10 sm">
|
||||
{{
|
||||
dateFormat(i.taskStartTime + i.fixedTriggerDuration * 1000)
|
||||
}}
|
||||
</span>
|
||||
<span class="ml-10" @click="viewDetail = true">
|
||||
<Icon iconName="view" size="middle" />
|
||||
</span>
|
||||
<span class="ml-5" v-if="index === 0 && inProcess">
|
||||
<Icon iconName="retry" :loading="true" size="middle" />
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="viewDetail"
|
||||
:destroy-on-close="true"
|
||||
fullscreen
|
||||
@closed="viewDetail = false"
|
||||
>
|
||||
<TaskDetails :details="networkProfilingStore.selectedNetworkTask" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { EBPFTaskList } from "@/types/ebpf";
|
||||
import { ElMessage } from "element-plus";
|
||||
import TaskDetails from "../../components/TaskDetails.vue";
|
||||
import dateFormatStep, { dateFormat } from "@/utils/dateFormat";
|
||||
import getLocalTime from "@/utils/localtime";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
|
||||
const { t } = useI18n();
|
||||
const selectorStore = useSelectorStore();
|
||||
const networkProfilingStore = useNetworkProfilingStore();
|
||||
const appStore = useAppStoreWithOut();
|
||||
const viewDetail = ref<boolean>(false);
|
||||
/*global Nullable */
|
||||
const intervalFn = ref<Nullable<any>>(null);
|
||||
const inProcess = ref<boolean>(true);
|
||||
fetchTasks();
|
||||
|
||||
async function changeTask(item: EBPFTaskList) {
|
||||
networkProfilingStore.setSelectedNetworkTask(item);
|
||||
intervalFn.value && clearInterval(intervalFn.value);
|
||||
getTopology();
|
||||
}
|
||||
async function getTopology() {
|
||||
const { taskStartTime, fixedTriggerDuration, taskId } =
|
||||
networkProfilingStore.selectedNetworkTask;
|
||||
const serviceInstanceId =
|
||||
(selectorStore.currentPod && selectorStore.currentPod.id) || "";
|
||||
const startTime =
|
||||
fixedTriggerDuration > 1800
|
||||
? taskStartTime + fixedTriggerDuration * 1000 - 30 * 60 * 1000
|
||||
: taskStartTime;
|
||||
let endTime = taskStartTime + fixedTriggerDuration * 1000;
|
||||
if (taskStartTime + fixedTriggerDuration * 1000 > new Date().getTime()) {
|
||||
endTime = new Date().getTime();
|
||||
}
|
||||
const resp = await networkProfilingStore.getProcessTopology({
|
||||
serviceInstanceId,
|
||||
duration: {
|
||||
start: dateFormatStep(
|
||||
getLocalTime(appStore.utc, new Date(startTime)),
|
||||
appStore.duration.step,
|
||||
true
|
||||
),
|
||||
end: dateFormatStep(
|
||||
getLocalTime(appStore.utc, new Date(endTime)),
|
||||
appStore.duration.step,
|
||||
true
|
||||
),
|
||||
step: appStore.duration.step,
|
||||
},
|
||||
});
|
||||
if (resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
const task = networkProfilingStore.networkTasks[0] || {};
|
||||
if (task.taskId === taskId) {
|
||||
inProcess.value =
|
||||
task.taskStartTime + task.fixedTriggerDuration * 1000 >
|
||||
new Date().getTime()
|
||||
? true
|
||||
: false;
|
||||
}
|
||||
if (!inProcess.value) {
|
||||
intervalFn.value && clearInterval(intervalFn.value);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
async function createTask() {
|
||||
if (inProcess.value) {
|
||||
return;
|
||||
}
|
||||
const serviceId =
|
||||
(selectorStore.currentService && selectorStore.currentService.id) || "";
|
||||
const serviceInstanceId =
|
||||
(selectorStore.currentPod && selectorStore.currentPod.id) || "";
|
||||
if (!serviceId) {
|
||||
return;
|
||||
}
|
||||
if (!serviceInstanceId) {
|
||||
return;
|
||||
}
|
||||
const res = await networkProfilingStore.createNetworkTask({
|
||||
serviceId,
|
||||
serviceInstanceId,
|
||||
});
|
||||
if (res.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
return;
|
||||
}
|
||||
await fetchTasks();
|
||||
}
|
||||
async function enableInterval() {
|
||||
const res = await networkProfilingStore.keepNetworkProfiling(
|
||||
networkProfilingStore.selectedNetworkTask.taskId
|
||||
);
|
||||
if (res.errors) {
|
||||
return ElMessage.error(res.errors);
|
||||
}
|
||||
if (networkProfilingStore.aliveNetwork) {
|
||||
intervalFn.value = setInterval(getTopology, 60000);
|
||||
}
|
||||
}
|
||||
async function fetchTasks() {
|
||||
intervalFn.value && clearInterval(intervalFn.value);
|
||||
const serviceId =
|
||||
(selectorStore.currentService && selectorStore.currentService.id) || "";
|
||||
const serviceInstanceId =
|
||||
(selectorStore.currentPod && selectorStore.currentPod.id) || "";
|
||||
const res = await networkProfilingStore.getTaskList({
|
||||
serviceId,
|
||||
serviceInstanceId,
|
||||
targets: ["NETWORK"],
|
||||
});
|
||||
|
||||
if (res.errors) {
|
||||
return ElMessage.error(res.errors);
|
||||
}
|
||||
await getTopology();
|
||||
if (inProcess.value) {
|
||||
enableInterval();
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => selectorStore.currentPod,
|
||||
() => {
|
||||
fetchTasks();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.profile-task-list {
|
||||
width: 330px;
|
||||
height: calc(100% - 10px);
|
||||
overflow: auto;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.item span {
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.profile-td {
|
||||
padding: 10px 0 10px 10px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
|
||||
|
||||
&.selected {
|
||||
background-color: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.profile-t-wrapper {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.profile-t {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
table-layout: fixed;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.profile-tr {
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
.profile-t-tool {
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
|
||||
background: #f3f4f9;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.new-task {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,178 @@
|
||||
<!-- 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-popover
|
||||
placement="bottom"
|
||||
:width="600"
|
||||
trigger="click"
|
||||
@after-enter="showTimeLine"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="switch-icon-edit">
|
||||
<Icon size="middle" iconName="time_range" />
|
||||
</div>
|
||||
</template>
|
||||
<div ref="timeRange" class="time-ranges"></div>
|
||||
<el-button
|
||||
class="query"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="updateTopology"
|
||||
>
|
||||
{{ t("query") }}
|
||||
</el-button>
|
||||
</el-popover>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { DataSet, Timeline } from "vis-timeline/standalone";
|
||||
import "vis-timeline/styles/vis-timeline-graph2d.css";
|
||||
import dateFormatStep from "@/utils/dateFormat";
|
||||
import getLocalTime from "@/utils/localtime";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
|
||||
/*global Nullable, defineEmits */
|
||||
const emits = defineEmits(["get"]);
|
||||
const { t } = useI18n();
|
||||
const selectorStore = useSelectorStore();
|
||||
const appStore = useAppStoreWithOut();
|
||||
const networkProfilingStore = useNetworkProfilingStore();
|
||||
const timeRange = ref<Nullable<HTMLDivElement>>(null);
|
||||
const visGraph = ref<Nullable<any>>(null);
|
||||
const task = ref<any[]>([]);
|
||||
const isUpdate = ref<boolean>(false);
|
||||
|
||||
function showTimeLine() {
|
||||
visTimeline();
|
||||
}
|
||||
function visTimeline() {
|
||||
if (!timeRange.value) {
|
||||
return;
|
||||
}
|
||||
if (!networkProfilingStore.selectedNetworkTask.taskId) {
|
||||
return;
|
||||
}
|
||||
const { taskStartTime, fixedTriggerDuration, targetType, taskId } =
|
||||
networkProfilingStore.selectedNetworkTask;
|
||||
if (task.value[0] && task.value[0].data.taskId === taskId) {
|
||||
if (isUpdate.value) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (visGraph.value) {
|
||||
visGraph.value.destroy();
|
||||
}
|
||||
isUpdate.value = false;
|
||||
let startTime = taskStartTime;
|
||||
if (fixedTriggerDuration > 1800) {
|
||||
startTime = taskStartTime + fixedTriggerDuration * 1000 - 30 * 60 * 1000;
|
||||
}
|
||||
const d = networkProfilingStore.networkTasks[0] || {};
|
||||
let endTime = taskStartTime + fixedTriggerDuration * 1000;
|
||||
if (
|
||||
taskStartTime + fixedTriggerDuration * 1000 > new Date().getTime() &&
|
||||
taskId === d.taskId
|
||||
) {
|
||||
endTime = new Date().getTime();
|
||||
}
|
||||
task.value = [
|
||||
{
|
||||
id: 1,
|
||||
content: "",
|
||||
start: new Date(startTime),
|
||||
end: new Date(endTime),
|
||||
data: networkProfilingStore.selectedNetworkTask,
|
||||
className: targetType,
|
||||
},
|
||||
];
|
||||
const items: any = new DataSet(task.value);
|
||||
items.on("update", (event: string, properties: any) => {
|
||||
task.value = properties.data;
|
||||
});
|
||||
const itemsAlwaysDraggable =
|
||||
fixedTriggerDuration > 1800
|
||||
? {
|
||||
item: true,
|
||||
range: true,
|
||||
}
|
||||
: undefined;
|
||||
const editable =
|
||||
fixedTriggerDuration > 1800
|
||||
? {
|
||||
updateTime: true,
|
||||
}
|
||||
: false;
|
||||
const options = {
|
||||
height: 150,
|
||||
width: "100%",
|
||||
locale: "en",
|
||||
editable,
|
||||
zoomMin: 1000 * 60,
|
||||
zoomMax: 1000 * 60 * 60 * 24,
|
||||
};
|
||||
const opt = itemsAlwaysDraggable
|
||||
? { ...options, itemsAlwaysDraggable }
|
||||
: options;
|
||||
visGraph.value = new Timeline(timeRange.value, items, opt);
|
||||
}
|
||||
async function updateTopology() {
|
||||
isUpdate.value = true;
|
||||
emits("get", {
|
||||
start: task.value[0].start.getTime(),
|
||||
end: task.value[0].end.getTime(),
|
||||
});
|
||||
const serviceInstanceId =
|
||||
(selectorStore.currentPod && selectorStore.currentPod.id) || "";
|
||||
|
||||
const resp = await networkProfilingStore.getProcessTopology({
|
||||
serviceInstanceId,
|
||||
duration: {
|
||||
start: dateFormatStep(
|
||||
getLocalTime(appStore.utc, new Date(task.value[0].start)),
|
||||
appStore.duration.step,
|
||||
true
|
||||
),
|
||||
end: dateFormatStep(
|
||||
getLocalTime(appStore.utc, new Date(task.value[0].end)),
|
||||
appStore.duration.step,
|
||||
true
|
||||
),
|
||||
step: appStore.duration.step,
|
||||
},
|
||||
});
|
||||
if (resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.switch-icon-edit {
|
||||
cursor: pointer;
|
||||
transition: all 0.5ms linear;
|
||||
border: 1px solid #ccc;
|
||||
color: #666;
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 50px;
|
||||
}
|
||||
</style>
|
@ -53,16 +53,14 @@ limitations under the License. -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useProfileStore } from "@/store/modules/profile";
|
||||
import { Trace } from "@/types/trace";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
|
||||
const { t } = useI18n();
|
||||
const profileStore = useProfileStore();
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
const selectedKey = ref<string>("");
|
||||
|
||||
async function selectTrace(item: Trace) {
|
||||
|
@ -40,7 +40,9 @@ limitations under the License. -->
|
||||
</a>
|
||||
</div>
|
||||
<div class="grey ell sm">
|
||||
<span class="mr-10 sm">{{ dateFormat(i.startTime) }}</span>
|
||||
<span class="mr-10 sm">
|
||||
{{ dateFormat(i.startTime) }}
|
||||
</span>
|
||||
<span class="mr-10 sm">
|
||||
{{ dateFormat(i.startTime + i.duration * 60 * 1000) }}
|
||||
</span>
|
||||
@ -122,18 +124,16 @@ limitations under the License. -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { useProfileStore } from "@/store/modules/profile";
|
||||
import { TaskLog, TaskListItem } from "@/types/profile";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
|
||||
const { t } = useI18n();
|
||||
const profileStore = useProfileStore();
|
||||
const selectorStore = useSelectorStore();
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
const viewDetail = ref<boolean>(false);
|
||||
const service = ref<string>("");
|
||||
const selectedTask = ref<TaskListItem | Record<string, never>>({});
|
||||
|
@ -94,17 +94,10 @@ import {
|
||||
import { useI18n } from "vue-i18n";
|
||||
import * as d3 from "d3";
|
||||
import d3tip from "d3-tip";
|
||||
import zoom from "../../components/D3Graph/zoom";
|
||||
import {
|
||||
simulationInit,
|
||||
simulationSkip,
|
||||
} from "../../components/D3Graph/simulation";
|
||||
import nodeElement from "../../components/D3Graph/nodeElement";
|
||||
import {
|
||||
linkElement,
|
||||
anchorElement,
|
||||
arrowMarker,
|
||||
} from "../../components/D3Graph/linkElement";
|
||||
import zoom from "../../components/utils/zoom";
|
||||
import { simulationInit, simulationSkip } from "./utils/simulation";
|
||||
import nodeElement from "./utils/nodeElement";
|
||||
import { linkElement, anchorElement, arrowMarker } from "./utils/linkElement";
|
||||
import { Node, Call } from "@/types/topology";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { useTopologyStore } from "@/store/modules/topology";
|
||||
@ -200,7 +193,7 @@ async function init() {
|
||||
link.value = graph.value.append("g").selectAll(".topo-line");
|
||||
anchor.value = graph.value.append("g").selectAll(".topo-line-anchor");
|
||||
arrow.value = graph.value.append("g").selectAll(".topo-line-arrow");
|
||||
svg.value.call(zoom(d3, graph.value));
|
||||
svg.value.call(zoom(d3, graph.value, [-100, -100]));
|
||||
svg.value.on("click", (event: any) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
@ -16,12 +16,12 @@
|
||||
*/
|
||||
export const simulationInit = (
|
||||
d3: any,
|
||||
dataNodes: any,
|
||||
dataLinks: any,
|
||||
nodes: any,
|
||||
links: any,
|
||||
ticked: any
|
||||
) => {
|
||||
const simulation = d3
|
||||
.forceSimulation(dataNodes)
|
||||
.forceSimulation(nodes)
|
||||
.force(
|
||||
"collide",
|
||||
d3.forceCollide().radius(() => 60)
|
||||
@ -31,7 +31,7 @@ export const simulationInit = (
|
||||
.force("charge", d3.forceManyBody().strength(-520))
|
||||
.force(
|
||||
"link",
|
||||
d3.forceLink(dataLinks).id((d: { id: string }) => d.id)
|
||||
d3.forceLink(links).id((d: { id: string }) => d.id)
|
||||
)
|
||||
.force(
|
||||
"center",
|
@ -120,7 +120,6 @@ limitations under the License. -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import dayjs from "dayjs";
|
||||
import { ref, defineComponent, inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
@ -130,6 +129,8 @@ import graphs from "./components/index";
|
||||
import { ElMessage } from "element-plus";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
import { LayoutConfig } from "@/types/dashboard";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TraceDetail",
|
||||
@ -137,6 +138,7 @@ export default defineComponent({
|
||||
...graphs,
|
||||
},
|
||||
setup() {
|
||||
const appStore = useAppStoreWithOut();
|
||||
/*global Recordable */
|
||||
const options: Recordable<LayoutConfig> = inject("options") || {};
|
||||
const { t } = useI18n();
|
||||
@ -144,8 +146,6 @@ export default defineComponent({
|
||||
const loading = ref<boolean>(false);
|
||||
const traceId = ref<string>("");
|
||||
const displayMode = ref<string>("List");
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
|
||||
function handleClick() {
|
||||
copy(traceId.value || traceStore.currentTrace.traceIds[0].value);
|
||||
@ -180,6 +180,7 @@ export default defineComponent({
|
||||
handleClick,
|
||||
t,
|
||||
searchTraceLogs,
|
||||
appStore,
|
||||
loading,
|
||||
traceId,
|
||||
};
|
||||
|
@ -70,7 +70,6 @@ limitations under the License. -->
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import dayjs from "dayjs";
|
||||
import { ref, computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
@ -78,6 +77,7 @@ import { ElMessage } from "element-plus";
|
||||
import { QueryOrders } from "../../data";
|
||||
import { Option } from "@/types/app";
|
||||
import { Trace } from "@/types/trace";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
|
||||
const { t } = useI18n();
|
||||
const traceStore = useTraceStore();
|
||||
@ -89,8 +89,6 @@ const total = computed(() =>
|
||||
? pageSize.value * traceStore.conditions.paging.pageNum + 1
|
||||
: pageSize.value * traceStore.conditions.paging.pageNum
|
||||
);
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
|
||||
function searchTrace() {
|
||||
loading.value = true;
|
||||
|
@ -86,10 +86,10 @@ limitations under the License. -->
|
||||
import { inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { PropType } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import copy from "@/utils/copy";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
import { LayoutConfig } from "@/types/dashboard";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
|
||||
/*global defineProps, Recordable */
|
||||
const options: Recordable<LayoutConfig> = inject("options") || {};
|
||||
@ -97,8 +97,6 @@ const props = defineProps({
|
||||
currentSpan: { type: Object as PropType<any>, default: () => ({}) },
|
||||
});
|
||||
const { t } = useI18n();
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
async function getTaceLogs() {
|
||||
const { associationWidget } = getDashboard();
|
||||
associationWidget(
|
||||
|
@ -138,11 +138,12 @@ limitations under the License. -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import dayjs from "dayjs";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ref, computed, defineComponent } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import SpanDetail from "../D3Graph/SpanDetail.vue";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
|
||||
const props = {
|
||||
data: { type: Object as PropType<any>, default: () => ({}) },
|
||||
@ -156,11 +157,10 @@ export default defineComponent({
|
||||
emits: ["select"],
|
||||
components: { SpanDetail },
|
||||
setup(props, { emit }) {
|
||||
const appStore = useAppStoreWithOut();
|
||||
const displayChildren = ref<boolean>(true);
|
||||
const showDetail = ref<boolean>(false);
|
||||
const { t } = useI18n();
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
const selfTime = computed(() => (props.data.dur ? props.data.dur : 0));
|
||||
const execTime = computed(() =>
|
||||
props.data.endTime - props.data.startTime
|
||||
@ -239,6 +239,7 @@ export default defineComponent({
|
||||
selectedItem,
|
||||
viewSpan,
|
||||
t,
|
||||
appStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -46,6 +46,9 @@ module.exports = {
|
||||
.loader("svg-sprite-loader")
|
||||
.options({ symbolId: "[name]" });
|
||||
config.resolve.alias.set("vue-i18n", "vue-i18n/dist/vue-i18n.cjs.js");
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
config.plugins.delete("preload");
|
||||
}
|
||||
},
|
||||
configureWebpack: (config) => {
|
||||
config.performance = {
|
||||
|
Loading…
Reference in New Issue
Block a user