feat: add graph for instance and endpoint topology

This commit is contained in:
Qiuxia Fan 2022-02-15 13:46:56 +08:00
parent 41ad830ea8
commit 06a4a4ac96
8 changed files with 202 additions and 16 deletions

View File

@ -19,6 +19,7 @@ import {
LineSeriesOption, LineSeriesOption,
HeatmapSeriesOption, HeatmapSeriesOption,
PieSeriesOption, PieSeriesOption,
SankeySeriesOption,
} from "echarts/charts"; } from "echarts/charts";
import { import {
TitleComponentOption, TitleComponentOption,
@ -46,6 +47,7 @@ export type ECOption = echarts.ComposeOption<
| LegendComponentOption | LegendComponentOption
| HeatmapSeriesOption | HeatmapSeriesOption
| PieSeriesOption | PieSeriesOption
| SankeySeriesOption
>; >;
export function useECharts( export function useECharts(

View File

@ -28,8 +28,8 @@ interface MetricVal {
[key: string]: { values: { id: string; value: unknown }[] }; [key: string]: { values: { id: string; value: unknown }[] };
} }
interface TopologyState { interface TopologyState {
node: Node | null; node: Nullable<Node>;
call: Call | null; call: Nullable<Call>;
calls: Call[]; calls: Call[];
nodes: Node[]; nodes: Node[];
nodeMetrics: MetricVal; nodeMetrics: MetricVal;
@ -76,6 +76,43 @@ export const topologyStore = defineStore({
return c; return c;
}); });
}, },
setInstanceTopology(data: { nodes: Node[]; calls: Call[] }) {
for (const call of data.calls) {
for (const node of data.nodes) {
if (call.source === node.id) {
call.sourceObj = node;
}
if (call.target === node.id) {
call.targetObj = node;
}
}
call.value = call.value || 1;
}
this.calls = data.calls;
this.nodes = data.nodes;
},
setEndpointTopology(data: { nodes: Node[]; calls: Call[] }) {
const obj = {} as any;
let nodes = [];
let calls = [];
nodes = data.nodes.reduce((prev: Node[], next: Node) => {
if (!obj[next.id]) {
obj[next.id] = true;
prev.push(next);
}
return prev;
}, []);
calls = data.calls.reduce((prev: Call[], next: Call) => {
if (!obj[next.id]) {
obj[next.id] = true;
next.value = next.value || 1;
prev.push(next);
}
return prev;
}, []);
this.calls = calls;
this.nodes = nodes;
},
setNodeMetrics(m: { id: string; value: unknown }[]) { setNodeMetrics(m: { id: string; value: unknown }[]) {
this.nodeMetrics = m; this.nodeMetrics = m;
}, },
@ -135,7 +172,7 @@ export const topologyStore = defineStore({
duration, duration,
}); });
if (!res.data.errors) { if (!res.data.errors) {
this.setTopology(res.data.data.topology); this.setEndpointTopology(res.data.data.topology);
} }
return res.data; return res.data;
}, },
@ -151,7 +188,7 @@ export const topologyStore = defineStore({
duration, duration,
}); });
if (!res.data.errors) { if (!res.data.errors) {
this.setTopology(res.data.data.topology); this.setInstanceTopology(res.data.data.topology);
} }
return res.data; return res.data;
}, },

View File

@ -20,7 +20,9 @@ export interface Call {
id: string; id: string;
detectPoints: string[]; detectPoints: string[];
type?: string; type?: string;
layer?: string; sourceObj?: any;
targetObj?: any;
value?: number;
} }
export interface Node { export interface Node {
id: string; id: string;

View File

@ -16,7 +16,13 @@
*/ */
import * as echarts from "echarts/core"; import * as echarts from "echarts/core";
import { BarChart, LineChart, PieChart, HeatmapChart } from "echarts/charts"; import {
BarChart,
LineChart,
PieChart,
HeatmapChart,
SankeyChart,
} from "echarts/charts";
import { import {
TitleComponent, TitleComponent,
@ -39,6 +45,7 @@ echarts.use([
LineChart, LineChart,
PieChart, PieChart,
HeatmapChart, HeatmapChart,
SankeyChart,
SVGRenderer, SVGRenderer,
DataZoomComponent, DataZoomComponent,
VisualMapComponent, VisualMapComponent,

View File

@ -32,7 +32,7 @@ limitations under the License. -->
@closed="dashboardStore.setTopology(false)" @closed="dashboardStore.setTopology(false)"
custom-class="dark-dialog" custom-class="dark-dialog"
> >
<Graph /> <Topology />
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
@ -42,7 +42,7 @@ import GridLayout from "./panel/Layout.vue";
// import { LayoutConfig } from "@/types/dashboard"; // import { LayoutConfig } from "@/types/dashboard";
import Tool from "./panel/Tool.vue"; import Tool from "./panel/Tool.vue";
import ConfigEdit from "./configuration/ConfigEdit.vue"; import ConfigEdit from "./configuration/ConfigEdit.vue";
import Graph from "./related/topology/Graph.vue"; import Topology from "./related/topology/Index.vue";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";

View File

@ -17,7 +17,7 @@ limitations under the License. -->
ref="chart" ref="chart"
class="micro-topo-chart" class="micro-topo-chart"
v-loading="loading" v-loading="loading"
:style="`height: ${height}`" :style="`height: ${height}px`"
> >
<div class="setting" v-show="showSetting"> <div class="setting" v-show="showSetting">
<Settings @update="updateSettings" /> <Settings @update="updateSettings" />
@ -182,7 +182,6 @@ function handleNodeClick(d: Node & { x: number; y: number }) {
topologyStore.setLink(null); topologyStore.setLink(null);
operationsPos.x = d.x; operationsPos.x = d.x;
operationsPos.y = d.y + 30; operationsPos.y = d.y + 30;
console.log(d.layer === String(dashboardStore.layerId));
if (d.layer === String(dashboardStore.layerId)) { if (d.layer === String(dashboardStore.layerId)) {
return; return;
} }
@ -322,7 +321,9 @@ async function handleInspect() {
const id = topologyStore.node.id; const id = topologyStore.node.id;
topologyStore.setNode(null); topologyStore.setNode(null);
topologyStore.setLink(null); topologyStore.setLink(null);
loading.value = true;
const resp = await topologyStore.getServiceTopology(id); const resp = await topologyStore.getServiceTopology(id);
loading.value = false;
if (resp.errors) { if (resp.errors) {
ElMessage.error(resp.errors); ElMessage.error(resp.errors);
@ -356,7 +357,9 @@ function handleGoAlarm() {
} }
async function backToTopology() { async function backToTopology() {
svg.value.selectAll(".topo-svg-graph").remove(); svg.value.selectAll(".topo-svg-graph").remove();
loading.value = true;
const resp = await getTopology(); const resp = await getTopology();
loading.value = false;
if (resp.errors) { if (resp.errors) {
ElMessage.error(resp.errors); ElMessage.error(resp.errors);
@ -377,12 +380,6 @@ async function getTopology() {
case EntityType[1].value: case EntityType[1].value:
resp = await topologyStore.getServicesTopology(); resp = await topologyStore.getServicesTopology();
break; break;
case EntityType[2].value:
resp = await topologyStore.getEndpointTopology();
break;
case EntityType[4].value:
resp = await topologyStore.getInstanceTopology();
break;
} }
return resp; return resp;
} }

View File

@ -0,0 +1,34 @@
<!-- 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 :style="`height:${height}px;width:${width}px;`" v-if="isSankey">
<Sankey />
</div>
<Graph v-else />
</template>
<script lang="ts" setup>
import { ref } from "vue";
import Graph from "./Graph.vue";
import Sankey from "./Sankey.vue";
import { EntityType } from "../../data";
import { useDashboardStore } from "@/store/modules/dashboard";
const dashboardStore = useDashboardStore();
const isSankey = ref<boolean>(
[EntityType[2].value, EntityType[4].value].includes(dashboardStore.entity)
);
const height = ref<number>(document.body.clientHeight - 90);
const width = ref<number>(document.body.clientWidth - 40);
</script>

View File

@ -0,0 +1,107 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<Graph :option="option" v-loading="loading" />
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import { useTopologyStore } from "@/store/modules/topology";
import { useDashboardStore } from "@/store/modules/dashboard";
import { EntityType } from "../../data";
import { ElMessage } from "element-plus";
const topologyStore = useTopologyStore();
const dashboardStore = useDashboardStore();
const loading = ref<boolean>(false);
const option = computed(() => getOption());
onMounted(async () => {
loading.value = true;
const resp = await getTopology();
loading.value = false;
if (resp && resp.errors) {
ElMessage.error(resp.errors);
}
});
function getOption() {
return {
tooltip: {
trigger: "item",
},
series: {
type: "sankey",
left: 40,
top: 20,
right: 300,
bottom: 40,
emphasis: { focus: "adjacency" },
data: topologyStore.nodes,
links: topologyStore.calls,
label: {
color: "#fff",
formatter: (param: any) => param.data.name,
},
color: [
"#3fe1da",
"#6be6c1",
"#3fcfdc",
"#626c91",
"#3fbcde",
"#a0a7e6",
"#3fa9e1",
"#96dee8",
"#bf99f8",
],
itemStyle: {
borderWidth: 0,
},
lineStyle: {
color: "source",
opacity: 0.12,
},
tooltip: {
position: "bottom",
formatter: (param: { data: any; dataType: string }) => {
if (param.dataType === "edge") {
return `${param.data.sourceObj.serviceName} -> ${param.data.targetObj.serviceName}`;
}
return param.data.serviceName;
},
},
},
};
}
async function getTopology() {
let resp;
switch (dashboardStore.entity) {
case EntityType[2].value:
resp = await topologyStore.getEndpointTopology();
break;
case EntityType[4].value:
resp = await topologyStore.getInstanceTopology();
break;
}
return resp;
}
</script>
<style lang="scss" scoped>
.sankey {
width: 100%;
height: 100%;
}
</style>