This commit is contained in:
easonyipj 2022-09-18 01:45:36 +08:00
commit 9e3c3c254b
19 changed files with 352 additions and 338 deletions

View File

@ -19,7 +19,7 @@ limitations under the License. -->
#app { #app {
color: #2c3e50; color: #2c3e50;
height: 100%; height: 100%;
overflow: auto; overflow: hidden;
min-width: 1024px; min-width: 1024px;
} }
</style> </style>

View File

@ -18,7 +18,7 @@ limitations under the License. -->
<div class="app-config"> <div class="app-config">
<span class="red" v-show="timeRange">{{ t("timeTips") }}</span> <span class="red" v-show="timeRange">{{ t("timeTips") }}</span>
<TimePicker <TimePicker
:value="time" :value="[appStore.durationRow.start, appStore.durationRow.end]"
position="bottom" position="bottom"
format="YYYY-MM-DD HH:mm" format="YYYY-MM-DD HH:mm"
@input="changeTimeRange" @input="changeTimeRange"
@ -55,17 +55,12 @@ import { useI18n } from "vue-i18n";
import timeFormat from "@/utils/timeFormat"; import timeFormat from "@/utils/timeFormat";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import getLocalTime from "@/utils/localtime";
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const route = useRoute(); const route = useRoute();
const pageName = ref<string>(""); const pageName = ref<string>("");
const timeRange = ref<number>(0); const timeRange = ref<number>(0);
const time = ref<Date[]>([
appStore.durationRow.start,
appStore.durationRow.end,
]);
resetDuration(); resetDuration();
getVersion(); getVersion();
@ -73,15 +68,13 @@ const setConfig = (value: string) => {
pageName.value = value || ""; pageName.value = value || "";
}; };
const handleReload = () => { function handleReload() {
const gap = const gap =
appStore.duration.end.getTime() - appStore.duration.start.getTime(); appStore.duration.end.getTime() - appStore.duration.start.getTime();
const dates: Date[] = [ const dates: Date[] = [new Date(new Date().getTime() - gap), new Date()];
getLocalTime(appStore.utc, new Date(new Date().getTime() - gap)),
getLocalTime(appStore.utc, new Date()),
];
appStore.setDuration(timeFormat(dates)); appStore.setDuration(timeFormat(dates));
}; }
function changeTimeRange(val: Date[] | any) { function changeTimeRange(val: Date[] | any) {
timeRange.value = timeRange.value =
val[1].getTime() - val[0].getTime() > 60 * 24 * 60 * 60 * 1000 ? 1 : 0; val[1].getTime() - val[0].getTime() > 60 * 24 * 60 * 60 * 1000 ? 1 : 0;
@ -114,7 +107,6 @@ function resetDuration() {
step: d.step, step: d.step,
}); });
appStore.updateUTC(d.utc); appStore.updateUTC(d.utc);
time.value = [new Date(d.start), new Date(d.end)];
} }
} }
</script> </script>

View File

@ -141,13 +141,13 @@ const filterMenus = (menus: any[]) => {
.side-bar { .side-bar {
background: #252a2f; background: #252a2f;
height: 100%; height: 100%;
min-height: 700px;
position: relative;
margin-bottom: 100px; margin-bottom: 100px;
overflow-y: auto;
overflow-x: hidden;
} }
.el-menu-vertical:not(.el-menu--collapse) { .el-menu-vertical:not(.el-menu--collapse) {
width: 200px; width: 220px;
font-size: 16px; font-size: 16px;
} }
@ -173,7 +173,7 @@ span.collapse {
.menu-control { .menu-control {
position: absolute; position: absolute;
top: 7px; top: 7px;
left: 200px; left: 220px;
cursor: pointer; cursor: pointer;
transition: all 0.2s linear; transition: all 0.2s linear;
z-index: 99; z-index: 99;

View File

@ -152,6 +152,7 @@ const msg = {
text: "Text", text: "Text",
query: "Query", query: "Query",
postgreSQL: "PostgreSQL", postgreSQL: "PostgreSQL",
endpointTips: "The table shows up to 20 pieces of endpoints.",
seconds: "Seconds", seconds: "Seconds",
hourTip: "Select Hour", hourTip: "Select Hour",
minuteTip: "Select Minute", minuteTip: "Select Minute",

View File

@ -152,6 +152,7 @@ const msg = {
enableAssociate: "Activar asociación", enableAssociate: "Activar asociación",
query: "Consulta", query: "Consulta",
postgreSQL: "PostgreSQL", postgreSQL: "PostgreSQL",
endpointTips: "Aquí, la tabla muestra hasta 20 punto final.",
seconds: "Segundos", seconds: "Segundos",
hourTip: "Seleccione Hora", hourTip: "Seleccione Hora",
minuteTip: "Seleccione Minuto", minuteTip: "Seleccione Minuto",

View File

@ -149,6 +149,7 @@ const msg = {
text: "文本", text: "文本",
query: "查询", query: "查询",
postgreSQL: "PostgreSQL", postgreSQL: "PostgreSQL",
endpointTips: "这里最多展示20条endpoints。",
seconds: "秒", seconds: "秒",
hourTip: "选择小时", hourTip: "选择小时",
minuteTip: "选择分钟", minuteTip: "选择分钟",

View File

@ -92,15 +92,21 @@ export const networkProfilingStore = defineStore({
} }
return prev; return prev;
}, []); }, []);
calls = calls.map((d: any) => { const param = {} as any;
d.sourceId = d.source; calls = data.calls.reduce((prev: (Call | any)[], next: Call | any) => {
d.targetId = d.target; if (param[next.targetId + next.sourceId]) {
d.source = d.sourceObj; next.lowerArc = true;
d.target = d.targetObj; }
delete d.sourceObj; param[next.sourceId + next.targetId] = true;
delete d.targetObj; next.sourceId = next.source;
return d; next.targetId = next.target;
}); next.source = next.sourceObj;
next.target = next.targetObj;
delete next.sourceObj;
delete next.targetObj;
prev.push(next);
return prev;
}, []);
this.calls = calls; this.calls = calls;
this.nodes = data.nodes; this.nodes = data.nodes;
}, },

2
src/types/ebpf.d.ts vendored
View File

@ -74,4 +74,6 @@ export type ProcessNode = {
serviceInstanceName: string; serviceInstanceName: string;
name: string; name: string;
isReal: boolean; isReal: boolean;
x?: number;
y?: number;
}; };

View File

@ -46,7 +46,7 @@ limitations under the License. -->
ref="multipleTableRef" ref="multipleTableRef"
:default-sort="{ prop: 'name', order: 'ascending' }" :default-sort="{ prop: 'name', order: 'ascending' }"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
height="637px" height="calc(100% - 60px)"
size="small" size="small"
> >
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
@ -156,7 +156,7 @@ import { isEmptyObject } from "@/utils/is";
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const pageSize = 18; const pageSize = 20;
const dashboards = ref<DashboardItem[]>([]); const dashboards = ref<DashboardItem[]>([]);
const searchText = ref<string>(""); const searchText = ref<string>("");
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
@ -500,12 +500,13 @@ function changePage(pageIndex: number) {
} }
.table { .table {
padding: 20px; padding: 20px 10px;
background-color: #fff; background-color: #fff;
box-shadow: 0px 1px 4px 0px #00000029; box-shadow: 0px 1px 4px 0px #00000029;
border-radius: 5px; border-radius: 5px;
width: 100%; width: 100%;
overflow: hidden; height: 100%;
overflow: auto;
} }
.toggle-selection { .toggle-selection {

View File

@ -228,6 +228,11 @@ export default defineComponent({
watch( watch(
() => [selectorStore.currentProcess, selectorStore.currentDestProcess], () => [selectorStore.currentProcess, selectorStore.currentDestProcess],
() => { () => {
if (
!(selectorStore.currentDestProcess && selectorStore.currentProcess)
) {
return;
}
if (dashboardStore.entity === EntityType[7].value) { if (dashboardStore.entity === EntityType[7].value) {
queryMetrics(); queryMetrics();
} }

View File

@ -17,17 +17,17 @@ limitations under the License. -->
<div class="search"> <div class="search">
<el-input <el-input
v-model="searchText" v-model="searchText"
placeholder="Please input endpoint name" placeholder="Search for more endpoints"
size="small"
@change="searchList" @change="searchList"
class="inputs" class="inputs"
> >
<template #append> <template #append>
<el-button size="small" @click="searchList"> <el-button @click="searchList" class="btn">
<Icon size="sm" iconName="search" /> <Icon size="middle" iconName="search" />
</el-button> </el-button>
</template> </template>
</el-input> </el-input>
<span class="ml-5 tips">{{ t("endpointTips") }}</span>
</div> </div>
<div class="list"> <div class="list">
<el-table v-loading="chartLoading" :data="endpoints" style="width: 100%"> <el-table v-loading="chartLoading" :data="endpoints" style="width: 100%">
@ -56,6 +56,7 @@ limitations under the License. -->
import { ref, watch, computed } from "vue"; import { ref, watch, computed } from "vue";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
import type { PropType } from "vue"; import type { PropType } from "vue";
import { EndpointListConfig } from "@/types/dashboard"; import { EndpointListConfig } from "@/types/dashboard";
import { Endpoint } from "@/types/selector"; import { Endpoint } from "@/types/selector";
@ -92,6 +93,7 @@ const props = defineProps({
intervalTime: { type: Array as PropType<string[]>, default: () => [] }, intervalTime: { type: Array as PropType<string[]>, default: () => [] },
}); });
const { t } = useI18n();
const selectorStore = useSelectorStore(); const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const chartLoading = ref<boolean>(false); const chartLoading = ref<boolean>(false);
@ -191,11 +193,7 @@ watch(
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./style.scss"; @import "./style.scss";
.chart { .tips {
height: 60px; color: rgba(255, 0, 0, 0.5);
}
.inputs {
width: 300px;
} }
</style> </style>

View File

@ -18,12 +18,11 @@ limitations under the License. -->
<el-input <el-input
v-model="searchText" v-model="searchText"
placeholder="Please input instance name" placeholder="Please input instance name"
size="small"
@change="searchList" @change="searchList"
class="inputs" class="inputs"
> >
<template #append> <template #append>
<el-button size="small" @click="searchList"> <el-button class="btn" @click="searchList">
<Icon size="sm" iconName="search" /> <Icon size="sm" iconName="search" />
</el-button> </el-button>
</template> </template>
@ -243,14 +242,6 @@ watch(
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./style.scss"; @import "./style.scss";
.chart {
height: 60px;
}
.inputs {
width: 300px;
}
.attributes { .attributes {
max-height: 400px; max-height: 400px;
overflow: auto; overflow: auto;

View File

@ -18,12 +18,11 @@ limitations under the License. -->
<el-input <el-input
v-model="searchText" v-model="searchText"
placeholder="Please input service name" placeholder="Please input service name"
size="small"
@change="searchList" @change="searchList"
class="inputs mt-5" class="inputs mt-5"
> >
<template #append> <template #append>
<el-button size="small" @click="searchList"> <el-button class="btn" @click="searchList">
<Icon size="sm" iconName="search" /> <Icon size="sm" iconName="search" />
</el-button> </el-button>
</template> </template>
@ -286,8 +285,4 @@ watch(
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./style.scss"; @import "./style.scss";
.inputs {
width: 300px;
}
</style> </style>

View File

@ -23,6 +23,7 @@
.list { .list {
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
height: calc(100% - 90px);
} }
.pagination { .pagination {
@ -40,9 +41,21 @@
} }
.search { .search {
text-align: right; margin-top: 5px;
} }
.input-with-search { .input-with-search {
width: 400px; width: 400px;
} }
.btn {
margin-top: -12px;
}
.chart {
height: 60px;
}
.inputs {
width: 300px;
}

View File

@ -19,11 +19,11 @@ export default (d3: any, graph: any, diff: number[]) =>
d3 d3
.zoom() .zoom()
.scaleExtent([0.3, 10]) .scaleExtent([0.3, 10])
.on("zoom", (event: any) => { .on("zoom", (d: any) => {
graph.attr( graph.attr(
"transform", "transform",
`translate(${event.transform.x + diff[0]},${ `translate(${d.transform.x + diff[0]},${
event.transform.y + diff[1] d.transform.y + diff[1]
})scale(${event.transform.k})` })scale(${d.transform.k})`
); );
}); });

View File

@ -53,7 +53,7 @@ const { t } = useI18n();
height: 100%; height: 100%;
flex-grow: 2; flex-grow: 2;
min-width: 700px; min-width: 700px;
overflow: auto; overflow: hidden;
position: relative; position: relative;
width: calc(100% - 330px); width: calc(100% - 330px);
} }

View File

@ -17,96 +17,6 @@
import icons from "@/assets/img/icons"; import icons from "@/assets/img/icons";
import { Call } from "@/types/topology"; import { Call } from "@/types/topology";
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: Call) => {
const controlPos = computeControlPoint(
[d.source.x, d.source.y - 5],
[d.target.x, d.target.y - 5],
0.5
);
if (d.lowerArc) {
controlPos[1] =
Math.abs(controlPos[1]) < 50
? -controlPos[1] + 90
: -controlPos[1] - 10;
}
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("g")
.attr("class", "topo-line-anchor")
.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);
});
linkEnter
.append("image")
.attr("width", 15)
.attr("height", 15)
.attr("x", (d: Call) => {
const p = getMidpoint(d);
return p[0] - 8;
})
.attr("y", (d: Call) => {
const p = getMidpoint(d);
return p[1] - 13;
})
.attr("xlink:href", (d: Call) => {
const types = [...d.sourceComponents, ...d.targetComponents];
if (types.includes("tcp") || types.includes("http")) {
return icons.HTTPDARK;
}
if (types.includes("https") || types.includes("tls")) {
return icons.HTTPS;
}
});
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 // Control Point coordinates of quadratic Bezier curve
function computeControlPoint(ps: number[], pe: number[], arc = 0.5) { function computeControlPoint(ps: number[], pe: number[], arc = 0.5) {
const deltaX = pe[0] - ps[0]; const deltaX = pe[0] - ps[0];
@ -137,15 +47,20 @@ function quadraticBezier(
const y = (1 - t) * (1 - t) * ps.y + 2 * t * (1 - t) * pc.y + t * t * pe.y; const y = (1 - t) * (1 - t) * ps.y + 2 * t * (1 - t) * pc.y + t * t * pe.y;
return [x, y]; return [x, y];
} }
function getMidpoint(d: Call) { export function getMidpoint(d: Call) {
if (isNaN(d.source.x) || isNaN(d.source.y)) {
return [0, 0];
}
if (isNaN(d.target.x) || isNaN(d.target.y)) {
return [0, 0];
}
const controlPos = computeControlPoint( const controlPos = computeControlPoint(
[d.source.x, d.source.y], [d.source.x, d.source.y],
[d.target.x, d.target.y], [d.target.x, d.target.y],
0.5 0.5
); );
if (d.lowerArc) { if (d.lowerArc) {
controlPos[1] = controlPos[1] = -controlPos[1];
Math.abs(controlPos[1]) < 50 ? -controlPos[1] + 100 : -controlPos[1] - 10;
} }
const p = quadraticBezier( const p = quadraticBezier(
0.5, 0.5,
@ -155,3 +70,43 @@ function getMidpoint(d: Call) {
); );
return p; return p;
} }
export function linkPath(d: Call) {
if (isNaN(d.source.x) || isNaN(d.source.y)) {
return;
}
if (isNaN(d.target.x) || isNaN(d.target.y)) {
return;
}
const controlPos = computeControlPoint(
[d.source.x, d.source.y - 5],
[d.target.x, d.target.y - 5],
0.5
);
if (d.lowerArc) {
controlPos[1] = -controlPos[1] - 10;
}
return (
"M" +
d.source.x +
" " +
(d.source.y - 5) +
" " +
"Q" +
controlPos[0] +
" " +
controlPos[1] +
" " +
d.target.x +
" " +
(d.target.y - 5)
);
}
export function getAnchor(d: Call) {
const types = [...d.sourceComponents, ...d.targetComponents];
if (types.includes("tcp") || types.includes("http")) {
return icons.HTTPDARK;
}
if (types.includes("https") || types.includes("tls")) {
return icons.HTTPS;
}
}

View File

@ -1,54 +0,0 @@
/**
* 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)
.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

@ -13,13 +13,103 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div ref="chart" class="process-topo"></div> <div ref="chart" class="process-topo">
<el-popover <svg
placement="bottom" class="process-svg"
:width="295" :width="width"
trigger="click" :height="height"
v-if="dashboardStore.editMode" @click="clickTopology"
> >
<g class="svg-graph" :transform="`translate(${diff[0]}, ${diff[1]})`">
<g class="hex-polygon">
<path
:d="getHexPolygonVertices()"
stroke="#D5DDF6"
stroke-width="2"
fill="none"
/>
<text :x="0" :y="radius - 15" fill="#000" text-anchor="middle">
{{ selectorStore.currentPod.label }}
</text>
</g>
<g class="nodes">
<g
v-for="(node, index) in nodeList"
:key="index"
class="node"
@mouseover="showNodeTip(node, $event)"
@mouseout="hideNodeTip"
@mousedown="startMoveNode($event, node)"
@mouseup="stopMoveNode($event)"
>
<image
:href="icons.CUBE"
style="cursor: 'move'"
width="35"
height="35"
:x="(node.x || 0) - 15"
:y="(node.y || 0) - 15"
/>
<text
:x="node.x"
:y="(node.y || 0) + 28"
fill="#000"
text-anchor="middle"
>
{{
node.name.length > 10
? `${node.name.substring(0, 10)}...`
: node.name
}}
</text>
</g>
</g>
<g class="calls">
<path
v-for="(call, index) in networkProfilingStore.calls"
:key="index"
class="topo-call"
marker-end="url(#arrow)"
stroke="#97B0F8"
:d="linkPath(call)"
/>
</g>
<g class="anchors">
<image
v-for="(call, index) in networkProfilingStore.calls"
:key="index"
class="topo-line-anchor"
:href="getAnchor(call)"
width="15"
height="15"
:x="getMidpoint(call)[0] - 8"
:y="getMidpoint(call)[1] - 13"
@click="handleLinkClick($event, call)"
@mouseover="showLinkTip(call, $event)"
@mouseout="hideLinkTip"
/>
</g>
<g class="arrows">
<defs v-for="(_, index) in networkProfilingStore.calls" :key="index">
<marker
id="arrow"
markerUnits="strokeWidth"
markerWidth="8"
markerHeight="8"
viewBox="0 0 12 12"
refX="10"
refY="6"
orient="auto"
>
<path d="M2,2 L10,6 L2,10 L6,6 L2,2" fill="#97B0F8" />
</marker>
</defs>
</g>
</g>
</svg>
<div id="tooltip"></div>
</div>
<el-popover placement="bottom" :width="295" trigger="click">
<template #reference> <template #reference>
<div class="switch-icon-edit ml-5" title="Settings" @click="setConfig"> <div class="switch-icon-edit ml-5" title="Settings" @click="setConfig">
<Icon size="middle" iconName="setting_empty" /> <Icon size="middle" iconName="setting_empty" />
@ -39,9 +129,7 @@ import router from "@/router";
import { useNetworkProfilingStore } from "@/store/modules/network-profiling"; import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import d3tip from "d3-tip"; import { linkPath, getAnchor, getMidpoint } from "./Graph/linkProcess";
import { linkElement, anchorElement, arrowMarker } from "./Graph/linkProcess";
import nodeElement from "./Graph/nodeProcess";
import { Call } from "@/types/topology"; import { Call } from "@/types/topology";
import zoom from "../../components/utils/zoom"; import zoom from "../../components/utils/zoom";
import { ProcessNode } from "@/types/ebpf"; import { ProcessNode } from "@/types/ebpf";
@ -52,6 +140,7 @@ import getDashboard from "@/hooks/useDashboardsSession";
import { Layout } from "./Graph/layout"; import { Layout } from "./Graph/layout";
import TimeLine from "./TimeLine.vue"; import TimeLine from "./TimeLine.vue";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import icons from "@/assets/img/icons";
/*global Nullable, defineProps */ /*global Nullable, defineProps */
const props = defineProps({ const props = defineProps({
@ -67,19 +156,18 @@ const selectorStore = useSelectorStore();
const networkProfilingStore = useNetworkProfilingStore(); const networkProfilingStore = useNetworkProfilingStore();
const height = ref<number>(100); const height = ref<number>(100);
const width = ref<number>(100); const width = ref<number>(100);
const svg = ref<Nullable<any>>(null);
const chart = ref<Nullable<HTMLDivElement>>(null); const chart = ref<Nullable<HTMLDivElement>>(null);
const tip = ref<Nullable<HTMLDivElement>>(null); const tooltip = ref<Nullable<any>>(null);
const graph = ref<any>(null); const svg = ref<Nullable<any>>(null);
const node = ref<any>(null); const graph = ref<Nullable<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 oldVal = ref<{ width: number; height: number }>({ width: 0, height: 0 });
const config = ref<any>(props.config || {}); const config = ref<any>(props.config || {});
const diff = ref<number[]>([220, 200]); const diff = ref<number[]>([220, 200]);
const radius = 210; const radius = 210;
const dates = ref<Nullable<{ start: number; end: number }>>(null); const dates = ref<Nullable<{ start: number; end: number }>>(null);
const nodeList = ref<ProcessNode[]>([]);
const currentNode = ref<Nullable<ProcessNode>>(null);
const origin = [0, 0];
onMounted(() => { onMounted(() => {
init(); init();
@ -90,12 +178,14 @@ onMounted(() => {
}); });
async function init() { async function init() {
svg.value = d3.select(chart.value).append("svg").attr("class", "process-svg");
if (!networkProfilingStore.nodes.length) { if (!networkProfilingStore.nodes.length) {
return; return;
} }
drawGraph(); svg.value = d3.select(".process-svg");
createLayout(); graph.value = d3.select(".svg-graph");
tooltip.value = d3.select("#tooltip");
freshNodes();
useThrottleFn(resize, 500)();
} }
function drawGraph() { function drawGraph() {
@ -105,27 +195,16 @@ function drawGraph() {
}; };
height.value = (dom.height || 40) - 20; height.value = (dom.height || 40) - 20;
width.value = dom.width; 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]);
diff.value[0] = (dom.width - radius * 2) / 2 + radius; 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.call(zoom(d3, graph.value, diff.value));
svg.value.on("click", (event: any) => { }
function clickTopology(event: MouseEvent) {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
networkProfilingStore.setNode(null); networkProfilingStore.setNode(null);
networkProfilingStore.setLink(null); networkProfilingStore.setLink(null);
dashboardStore.selectWidget(props.config); dashboardStore.selectWidget(props.config);
});
useThrottleFn(resize, 500)();
} }
function hexGrid(n = 1, radius = 1, origin = [0, 0]) { function hexGrid(n = 1, radius = 1, origin = [0, 0]) {
@ -157,7 +236,6 @@ function createPolygon(radius: number, sides = 6, offset = 0) {
} }
function getCirclePoint(radius: number, p = 1) { function getCirclePoint(radius: number, p = 1) {
const data = []; const data = [];
const origin = [0, 0];
for (let index = 0; index < 360; index = index + p) { for (let index = 0; index < 360; index = index + p) {
if (index < 230 || index > 310) { if (index < 230 || index > 310) {
let x = radius * Math.cos((Math.PI * 2 * index) / 360); let x = radius * Math.cos((Math.PI * 2 * index) / 360);
@ -167,10 +245,22 @@ function getCirclePoint(radius: number, p = 1) {
} }
return data; return data;
} }
function createLayout() {
if (!node.value || !link.value) { function getHexPolygonVertices() {
return; const p = {
count: 1,
radius, // layout hexagons radius 300
};
const polygon = createPolygon(p.radius, 6, 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);
return linePath(vertices) || "";
}
function createLayout() {
const dom: any = (chart.value && chart.value.getBoundingClientRect()) || { const dom: any = (chart.value && chart.value.getBoundingClientRect()) || {
width: 0, width: 0,
height: 0, height: 0,
@ -182,28 +272,6 @@ function createLayout() {
count: 1, count: 1,
radius, // layout hexagons radius 300 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 - 15)
.text(() => selectorStore.currentPod.label);
const nodeArr = networkProfilingStore.nodes.filter( const nodeArr = networkProfilingStore.nodes.filter(
(d: ProcessNode) => d.isReal || d.name === "UNKNOWN_LOCAL" (d: ProcessNode) => d.isReal || d.name === "UNKNOWN_LOCAL"
); );
@ -278,67 +346,11 @@ function createLayout() {
outNodes[v].x = pointArr[v][0]; outNodes[v].x = pointArr[v][0];
outNodes[v].y = pointArr[v][1]; outNodes[v].y = pointArr[v][1];
} }
drawTopology([...nodeArr, ...outNodes]); nodeList.value = [...nodeArr, ...outNodes];
} const drag: any = d3.drag().on("drag", (d: ProcessNode) => {
moveNode(d);
function drawTopology(nodeArr: any[]) { });
node.value = node.value.data(nodeArr, (d: ProcessNode) => d.id); d3.selectAll(".node").call(drag);
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.targetId + next.sourceId]) {
next.lowerArc = true;
}
obj[next.sourceId + next.targetId] = 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 types = [...data.sourceComponents, ...data.targetComponents];
let l = "TCP";
if (types.includes("https")) {
l = "HTTPS";
}
if (types.includes("http")) {
l = "HTTP";
}
if (types.includes("tls")) {
l = "TLS";
}
const html = `<div><span class="grey">${t(
"detectPoint"
)}: </span>${data.detectPoints.join(" | ")}</div>
<div><span class="grey">Type: </span>${l}</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[][]) { function shuffleArray(array: number[][]) {
@ -347,7 +359,6 @@ function shuffleArray(array: number[][]) {
[array[i], array[j]] = [array[j], array[i]]; [array[i], array[j]] = [array[j], array[i]];
} }
} }
function handleLinkClick(event: any, d: Call) { function handleLinkClick(event: any, d: Call) {
event.stopPropagation(); event.stopPropagation();
networkProfilingStore.setNode(null); networkProfilingStore.setNode(null);
@ -423,13 +434,100 @@ function resize() {
} }
async function freshNodes() { async function freshNodes() {
svg.value.selectAll(".svg-graph").remove();
if (!networkProfilingStore.nodes.length) { if (!networkProfilingStore.nodes.length) {
return; return;
} }
drawGraph(); drawGraph();
createLayout(); createLayout();
} }
function startMoveNode(event: MouseEvent, d: ProcessNode) {
event.stopPropagation();
currentNode.value = d;
}
function stopMoveNode(event: MouseEvent) {
event.stopPropagation();
currentNode.value = null;
}
function moveNode(d: ProcessNode) {
if (!currentNode.value) {
return;
}
const inNode =
currentNode.value.isReal || currentNode.value.name === "UNKNOWN_LOCAL";
const diff = inNode ? -20 : 20;
const inside = posInHex(d.x || 0, d.y || 0, diff);
if (inNode) {
if (!inside) {
return;
}
} else {
if (inside) {
return;
}
}
nodeList.value = nodeList.value.map((node: ProcessNode) => {
if (currentNode.value && node.id === currentNode.value.id) {
node.x = d.x;
node.y = d.y;
}
return node;
});
}
function posInHex(posX: number, posY: number, diff: number) {
const halfSideLen = (radius + diff) / 2;
const mathSqrt3 = Math.sqrt(3);
const dx = Math.abs(origin[0] - posX);
const dy = Math.abs(origin[1] - posY);
if (dx < halfSideLen) {
return dy <= halfSideLen * mathSqrt3;
} else {
const maxY = -mathSqrt3 * (dx - halfSideLen) + halfSideLen * mathSqrt3;
return dy < maxY;
}
}
function showNodeTip(d: ProcessNode, event: MouseEvent) {
const tipHtml = ` <div class="mb-5"><span class="grey">name: </span>${d.name}</div>`;
tooltip.value
.style("top", event.offsetY + "px")
.style("left", event.offsetX + "px")
.style("visibility", "visible")
.html(tipHtml);
}
function hideNodeTip() {
tooltip.value.style("visibility", "hidden");
}
function showLinkTip(link: Call, event: MouseEvent) {
const types = [...link.sourceComponents, ...link.targetComponents];
let l = "TCP";
if (types.includes("https")) {
l = "HTTPS";
}
if (types.includes("http")) {
l = "HTTP";
}
if (types.includes("tls")) {
l = "TLS";
}
const tipHtml = `<div><span class="grey">${t(
"detectPoint"
)}: </span>${link.detectPoints.join(" | ")}</div>
<div><span class="grey">Type: </span>${l}</div>`;
tooltip.value
.style("top", event.offsetY + "px")
.style("left", event.offsetX + "px")
.style("visibility", "visible")
.html(tipHtml);
}
function hideLinkTip() {
tooltip.value.style("visibility", "hidden");
}
watch( watch(
() => networkProfilingStore.nodes, () => networkProfilingStore.nodes,
@ -500,4 +598,13 @@ watch(
.query { .query {
margin-left: 510px; margin-left: 510px;
} }
#tooltip {
position: absolute;
visibility: hidden;
padding: 5px;
border: 1px solid #000;
border-radius: 3px;
background-color: #fff;
}
</style> </style>