feat: Implement the network profiling widget (#132)

This commit is contained in:
Fine0830 2022-08-23 13:41:05 +08:00 committed by GitHub
parent ffabc7c7a7
commit a4fc5192ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1899 additions and 139 deletions

30
package-lock.json generated
View File

@ -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": {

View File

@ -1,6 +1,6 @@
{
"name": "skywalking-booster-ui",
"version": "9.1.0",
"version": "9.3.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -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

View File

@ -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 {

View File

@ -150,6 +150,7 @@ const msg = {
duplicateName: "Duplicate name",
enableAssociate: "Enable association",
text: "Text",
query: "Query",
postgreSQL: "PostgreSQL",
seconds: "Seconds",
hourTip: "Select Hour",

View File

@ -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",

View File

@ -147,6 +147,7 @@ const msg = {
nameTip: "该名称仅支持中文和英文、横线和下划线, 并且限制长度为300个字符",
duplicateName: "重复的名称",
text: "文本",
query: "查询",
postgreSQL: "PostgreSQL",
seconds: "秒",
hourTip: "选择小时",

View File

@ -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",
},
],
},
],

View File

@ -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]);

View 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);
}

View File

@ -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);

View File

@ -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) {

View File

@ -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>({});

View 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>

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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" },

View File

@ -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;

View File

@ -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";

View File

@ -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})`
);
});

View File

@ -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}`)

View File

@ -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) {

View File

@ -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");

View File

@ -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 {

View File

@ -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") {

View 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>

View File

@ -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 };

View File

@ -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];
}

View File

@ -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;
};

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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) {

View File

@ -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>>({});

View File

@ -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();

View File

@ -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",

View File

@ -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,
};

View File

@ -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;

View File

@ -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(

View File

@ -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,
};
},
});

View File

@ -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 = {