mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-10-14 11:21:29 +00:00
feat: implement Topology on the dashboard (#14)
This commit is contained in:
@@ -22,7 +22,7 @@ limitations under the License. -->
|
||||
<span class="label">{{ t("language") }}</span>
|
||||
<el-switch
|
||||
v-model="lang"
|
||||
:change="setLang"
|
||||
@change="setLang"
|
||||
active-text="En"
|
||||
inactive-text="Zh"
|
||||
style="height: 25px"
|
||||
|
@@ -23,7 +23,17 @@ limitations under the License. -->
|
||||
:destroy-on-close="true"
|
||||
@closed="dashboardStore.setConfigPanel(false)"
|
||||
>
|
||||
<config-edit />
|
||||
<TopologyConfig v-if="dashboardStore.selectedGrid.type === 'Topology'" />
|
||||
<Widget v-else />
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="dashboardStore.showTopology"
|
||||
:destroy-on-close="true"
|
||||
fullscreen
|
||||
@closed="dashboardStore.setTopology(false)"
|
||||
custom-class="dark-dialog"
|
||||
>
|
||||
<Topology />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -32,7 +42,9 @@ import { useI18n } from "vue-i18n";
|
||||
import GridLayout from "./panel/Layout.vue";
|
||||
// import { LayoutConfig } from "@/types/dashboard";
|
||||
import Tool from "./panel/Tool.vue";
|
||||
import ConfigEdit from "./configuration/ConfigEdit.vue";
|
||||
import Widget from "./configuration/Widget.vue";
|
||||
import TopologyConfig from "./configuration/Topology.vue";
|
||||
import Topology from "./related/topology/Index.vue";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
|
||||
|
@@ -39,14 +39,14 @@ limitations under the License. -->
|
||||
<el-table-column prop="date" label="Date" />
|
||||
<el-table-column label="Operations">
|
||||
<template #default="scope">
|
||||
<el-button size="mini" @click="handleEdit(scope.$index, scope.row)">
|
||||
<el-button size="small" @click="handleEdit(scope.$index, scope.row)">
|
||||
{{ t("view") }}
|
||||
</el-button>
|
||||
<el-button size="mini" @click="handleEdit(scope.$index, scope.row)">
|
||||
<el-button size="small" @click="handleEdit(scope.$index, scope.row)">
|
||||
{{ t("edit") }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(scope.$index, scope.row)"
|
||||
>
|
||||
|
@@ -18,7 +18,7 @@ limitations under the License. -->
|
||||
<div class="item">
|
||||
<div class="label">{{ t("name") }}</div>
|
||||
<el-input
|
||||
size="small"
|
||||
size="default"
|
||||
v-model="states.name"
|
||||
placeholder="Please input name"
|
||||
/>
|
||||
@@ -28,7 +28,6 @@ limitations under the License. -->
|
||||
<Selector
|
||||
v-model="states.selectedLayer"
|
||||
:options="states.layers"
|
||||
size="small"
|
||||
placeholder="Select a layer"
|
||||
@change="changeLayer"
|
||||
class="selectors"
|
||||
@@ -39,14 +38,13 @@ limitations under the License. -->
|
||||
<Selector
|
||||
v-model="states.entity"
|
||||
:options="EntityType"
|
||||
size="small"
|
||||
placeholder="Select a entity"
|
||||
@change="changeEntity"
|
||||
class="selectors"
|
||||
/>
|
||||
</div>
|
||||
<div class="btn">
|
||||
<el-button class="create" size="small" type="primary" @click="onCreate">
|
||||
<el-button class="create" size="default" type="primary" @click="onCreate">
|
||||
{{ t("create") }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
@@ -20,6 +20,7 @@ limitations under the License. -->
|
||||
placeholder="Please input dashboard name"
|
||||
@change="changeDashboard"
|
||||
class="selectors"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div>{{ t("metrics") }}</div>
|
||||
@@ -31,7 +32,7 @@ limitations under the License. -->
|
||||
<Selector
|
||||
:value="metric"
|
||||
:options="states.metricList"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="Select a metric"
|
||||
@change="changeMetrics(index, $event)"
|
||||
class="selectors"
|
||||
@@ -39,7 +40,7 @@ limitations under the License. -->
|
||||
<Selector
|
||||
:value="states.metricTypes[index]"
|
||||
:options="states.metricTypeList[index]"
|
||||
size="mini"
|
||||
size="small"
|
||||
:disabled="
|
||||
dashboardStore.selectedGrid.graph.type && !states.isTable && index !== 0
|
||||
"
|
||||
|
@@ -18,7 +18,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="state.unit"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="Please input Unit"
|
||||
@change="changeStandardOpt({ unit: state.unit })"
|
||||
/>
|
||||
@@ -28,7 +28,7 @@ limitations under the License. -->
|
||||
<Selector
|
||||
:value="state.sortOrder"
|
||||
:options="SortOrder"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="Select a sort order"
|
||||
class="selector"
|
||||
@change="changeStandardOpt({ sortOrder: state.sortOrder })"
|
||||
@@ -39,7 +39,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="state.max"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="auto"
|
||||
@change="changeStandardOpt({ max: state.max })"
|
||||
/>
|
||||
@@ -49,7 +49,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="state.min"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="auto"
|
||||
@change="changeStandardOpt({ min: state.min })"
|
||||
/>
|
||||
@@ -59,7 +59,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="state.plus"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="none"
|
||||
@change="changeStandardOpt({ plus: state.plus })"
|
||||
/>
|
||||
@@ -69,7 +69,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="state.minus"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="none"
|
||||
@change="changeStandardOpt({ minus: state.minus })"
|
||||
/>
|
||||
@@ -79,7 +79,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="state.multiply"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="none"
|
||||
@change="changeStandardOpt({ multiply: state.multiply })"
|
||||
/>
|
||||
@@ -89,7 +89,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="state.divide"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="none"
|
||||
@change="changeStandardOpt({ divide: state.divide })"
|
||||
/>
|
||||
@@ -99,7 +99,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="state.milliseconds"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="none"
|
||||
@change="changeStandardOpt({ milliseconds: state.milliseconds })"
|
||||
/>
|
||||
@@ -109,7 +109,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="state.seconds"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="none"
|
||||
@change="changeStandardOpt({ seconds: state.seconds })"
|
||||
/>
|
||||
|
52
src/views/dashboard/configuration/Topology.vue
Normal file
52
src/views/dashboard/configuration/Topology.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<!-- 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>
|
||||
<WidgetOptions />
|
||||
<TopologyOptions />
|
||||
<div class="footer">
|
||||
<el-button size="small">
|
||||
{{ t("cancel") }}
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" @click="applyConfig">
|
||||
{{ t("apply") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import WidgetOptions from "./WidgetOptions.vue";
|
||||
import TopologyOptions from "./graph-styles/TopologyItem.vue";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
|
||||
const { t } = useI18n();
|
||||
const dashboardStore = useDashboardStore();
|
||||
|
||||
function applyConfig() {
|
||||
dashboardStore.setConfigs(dashboardStore.selectedGrid);
|
||||
dashboardStore.setConfigPanel(false);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-top: 1px solid #eee;
|
||||
padding: 10px;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
@@ -60,10 +60,10 @@ limitations under the License. -->
|
||||
</el-collapse>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<el-button size="mini">
|
||||
<el-button size="small">
|
||||
{{ t("cancel") }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="primary" @click="applyConfig">
|
||||
<el-button size="small" type="primary" @click="applyConfig">
|
||||
{{ t("apply") }}
|
||||
</el-button>
|
||||
</div>
|
@@ -18,7 +18,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="title"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="Please input title"
|
||||
@change="updateWidgetConfig({ title })"
|
||||
/>
|
||||
@@ -28,7 +28,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="tips"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="Please input tips"
|
||||
@change="updateWidgetConfig({ tips })"
|
||||
/>
|
||||
|
@@ -27,7 +27,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="tableHeaderCol1"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="none"
|
||||
@change="updateConfig({ tableHeaderCol1 })"
|
||||
/>
|
||||
@@ -37,7 +37,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="tableHeaderCol2"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="none"
|
||||
@change="updateConfig({ tableHeaderCol2 })"
|
||||
/>
|
||||
|
@@ -18,7 +18,7 @@ limitations under the License. -->
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="topN"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="none"
|
||||
type="number"
|
||||
:min="1"
|
||||
|
145
src/views/dashboard/configuration/graph-styles/TopologyItem.vue
Normal file
145
src/views/dashboard/configuration/graph-styles/TopologyItem.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<!-- 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="item">
|
||||
<span class="label">{{ t("backgroundColors") }}</span>
|
||||
<Selector
|
||||
:value="backgroundColor"
|
||||
:options="colors"
|
||||
size="small"
|
||||
placeholder="Select a color"
|
||||
class="input"
|
||||
@change="changeConfig({ backgroundColor: $event[0].value })"
|
||||
/>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">{{ t("fontSize") }}</span>
|
||||
<el-slider
|
||||
class="slider"
|
||||
v-model="fontSize"
|
||||
show-input
|
||||
input-size="small"
|
||||
:min="12"
|
||||
:max="30"
|
||||
:step="1"
|
||||
@change="changeConfig({ fontSize })"
|
||||
/>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">{{ t("fontColors") }}</span>
|
||||
<Selector
|
||||
:value="fontColor"
|
||||
:options="colors"
|
||||
size="small"
|
||||
placeholder="Select a color"
|
||||
class="input"
|
||||
@change="changeConfig({ fontColor: $event[0].value })"
|
||||
/>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">{{ t("iconTheme") }}</span>
|
||||
<el-switch
|
||||
v-model="iconTheme"
|
||||
active-text="Light"
|
||||
inactive-text="Dark"
|
||||
@change="changeConfig({ iconTheme })"
|
||||
/>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">{{ t("content") }}</span>
|
||||
<el-input
|
||||
class="input"
|
||||
v-model="content"
|
||||
size="small"
|
||||
@change="changeConfig({ content })"
|
||||
/>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">{{ t("defaultDepth") }}</span>
|
||||
<Selector
|
||||
class="input"
|
||||
size="small"
|
||||
:value="depth"
|
||||
:options="DepthList"
|
||||
@change="changeDepth($event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ref } from "vue";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { DepthList } from "../../data";
|
||||
import { Option } from "@/types/app";
|
||||
import { useTopologyStore } from "@/store/modules/topology";
|
||||
|
||||
const { t } = useI18n();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const topologyStore = useTopologyStore();
|
||||
const { selectedGrid } = dashboardStore;
|
||||
const iconTheme = ref(selectedGrid.graph.iconTheme || true);
|
||||
const backgroundColor = ref(selectedGrid.graph.backgroundColor || "green");
|
||||
const fontColor = ref(selectedGrid.graph.fontColor || "white");
|
||||
const content = ref<string>(selectedGrid.graph.content);
|
||||
const fontSize = ref<number>(selectedGrid.graph.fontSize);
|
||||
const depth = ref<string>(selectedGrid.graph.depth || "2");
|
||||
const colors = [
|
||||
{
|
||||
label: "Green",
|
||||
value: "green",
|
||||
},
|
||||
{ label: "Blue", value: "blue" },
|
||||
{ label: "Red", value: "red" },
|
||||
{ label: "Grey", value: "grey" },
|
||||
{ label: "White", value: "white" },
|
||||
{ label: "Black", value: "black" },
|
||||
{ label: "Orange", value: "orange" },
|
||||
];
|
||||
topologyStore.setDefaultDepth(depth.value);
|
||||
function changeConfig(param: { [key: string]: unknown }) {
|
||||
const { selectedGrid } = dashboardStore;
|
||||
const graph = {
|
||||
...selectedGrid.graph,
|
||||
...param,
|
||||
};
|
||||
dashboardStore.selectWidget({ ...selectedGrid, graph });
|
||||
}
|
||||
function changeDepth(opt: Option[]) {
|
||||
const val = opt[0].value;
|
||||
topologyStore.setDefaultDepth(val);
|
||||
changeConfig({ depth: val });
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.slider {
|
||||
width: 500px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
143
src/views/dashboard/controls/Topology.vue
Normal file
143
src/views/dashboard/controls/Topology.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<!-- 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="topology">
|
||||
<div class="header flex-h">
|
||||
<div>{{ data.widget?.title || "" }}</div>
|
||||
<div>
|
||||
<el-tooltip :content="data.widget?.tips">
|
||||
<span>
|
||||
<Icon
|
||||
iconName="info_outline"
|
||||
size="sm"
|
||||
class="operation"
|
||||
v-show="data.widget?.tips"
|
||||
/>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-popover placement="bottom" trigger="click" :width="100">
|
||||
<template #reference>
|
||||
<span>
|
||||
<Icon iconName="ellipsis_v" size="middle" class="operation" />
|
||||
</span>
|
||||
</template>
|
||||
<div class="tools" @click="editConfig">
|
||||
<span>{{ t("edit") }}</span>
|
||||
</div>
|
||||
<div class="tools" @click="removeTopo">
|
||||
<span>{{ t("delete") }}</span>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="body"
|
||||
@click="ViewTopology"
|
||||
:style="{ backgroundColor: Colors[data.graph.backgroundColor] }"
|
||||
>
|
||||
<Icon
|
||||
:iconName="data.graph.iconTheme ? 'topology-light' : 'topology-dark'"
|
||||
size="middle"
|
||||
/>
|
||||
<div
|
||||
:style="{
|
||||
color: Colors[data.graph.fontColor],
|
||||
fontSize: data.graph.fontSize + 'px',
|
||||
}"
|
||||
>
|
||||
{{ data.graph.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { Colors } from "../data";
|
||||
/*global defineProps */
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => ({ graph: {} }),
|
||||
},
|
||||
activeIndex: { type: String, default: "" },
|
||||
});
|
||||
const { t } = useI18n();
|
||||
const dashboardStore = useDashboardStore();
|
||||
|
||||
function editConfig() {
|
||||
dashboardStore.setConfigPanel(true);
|
||||
dashboardStore.selectWidget(props.data);
|
||||
}
|
||||
function ViewTopology() {
|
||||
dashboardStore.setTopology(true);
|
||||
}
|
||||
function removeTopo() {
|
||||
dashboardStore.removeControls(props.data);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.topology {
|
||||
font-size: 12px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 30px;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #eee;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.operation {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tools {
|
||||
padding: 5px 0;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
color: #333;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-box-align: center;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-top: 20px;
|
||||
}
|
||||
</style>
|
@@ -18,20 +18,20 @@ limitations under the License. -->
|
||||
<div>{{ data.widget?.title || "" }}</div>
|
||||
<div>
|
||||
<el-tooltip :content="data.widget?.tips">
|
||||
<Icon
|
||||
iconName="info_outline"
|
||||
size="sm"
|
||||
class="operation"
|
||||
v-show="data.widget?.tips"
|
||||
/>
|
||||
<span>
|
||||
<Icon
|
||||
iconName="info_outline"
|
||||
size="sm"
|
||||
class="operation"
|
||||
v-show="data.widget?.tips"
|
||||
/>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-popover
|
||||
placement="bottom"
|
||||
trigger="click"
|
||||
:style="{ width: '100px' }"
|
||||
>
|
||||
<el-popover placement="bottom" trigger="click" :width="100">
|
||||
<template #reference>
|
||||
<Icon iconName="ellipsis_v" size="middle" class="operation" />
|
||||
<span>
|
||||
<Icon iconName="ellipsis_v" size="middle" class="operation" />
|
||||
</span>
|
||||
</template>
|
||||
<div class="tools" @click="editConfig">
|
||||
<span>{{ t("edit") }}</span>
|
||||
@@ -99,6 +99,7 @@ export default defineComponent({
|
||||
|
||||
async function queryMetrics() {
|
||||
const params = await useQueryProcessor(props.data);
|
||||
|
||||
if (!params) {
|
||||
state.source = {};
|
||||
return;
|
||||
@@ -137,15 +138,18 @@ export default defineComponent({
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => selectorStore.currentService,
|
||||
() => [selectorStore.currentService, selectorStore.currentDestService],
|
||||
() => {
|
||||
if (dashboardStore.entity === EntityType[0].value) {
|
||||
if (
|
||||
dashboardStore.entity === EntityType[0].value ||
|
||||
dashboardStore.entity === EntityType[4].value
|
||||
) {
|
||||
queryMetrics();
|
||||
}
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => selectorStore.currentPod,
|
||||
() => [selectorStore.currentPod, selectorStore.currentDestPod],
|
||||
() => {
|
||||
if (dashboardStore.entity === EntityType[0].value) {
|
||||
return;
|
||||
|
@@ -142,7 +142,7 @@ export enum MetricCatalog {
|
||||
export const EntityType = [
|
||||
{ value: "Service", label: "Service", key: 1 },
|
||||
{ value: "All", label: "All", key: 10 },
|
||||
{ value: "Endpoint", label: "Service Endpoint", key: 3 },
|
||||
{ value: "Endpoint", label: "Endpoint", key: 3 },
|
||||
{ value: "ServiceInstance", label: "Service Instance", key: 3 },
|
||||
{ value: "ServiceRelation", label: "Service Relation", key: 2 },
|
||||
{
|
||||
@@ -152,6 +152,7 @@ export const EntityType = [
|
||||
},
|
||||
{ value: "EndpointRelation", label: "Endpoint Relation", key: 4 },
|
||||
];
|
||||
export const hasTopology = ["All", "Service", "ServiceRelation", "Endpoint"];
|
||||
export const TableEntity: any = {
|
||||
InstanceList: EntityType[3].value,
|
||||
EndpointList: EntityType[2].value,
|
||||
@@ -165,8 +166,40 @@ export const ToolIcons = [
|
||||
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
||||
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
|
||||
// { name: "insert_image", content: "Add Image", id: "addImage" },
|
||||
{ name: "save_alt", content: "Export", id: "export" },
|
||||
{ name: "folder_open", content: "Import", id: "import" },
|
||||
{ name: "settings", content: "Settings", id: "settings" },
|
||||
{ name: "save", content: "Apply", id: "applay" },
|
||||
// { name: "save_alt", content: "Export", id: "export" },
|
||||
// { name: "folder_open", content: "Import", id: "import" },
|
||||
// { name: "settings", content: "Settings", id: "settings" },
|
||||
{ name: "device_hub", content: "Add Topology", id: "topology" },
|
||||
// { name: "save", content: "Apply", id: "apply" },
|
||||
];
|
||||
export const ScopeType = [
|
||||
{ value: "Service", label: "Service", key: 1 },
|
||||
{ value: "Endpoint", label: "Endpoint", key: 3 },
|
||||
{ value: "ServiceInstance", label: "Service Instance", key: 3 },
|
||||
];
|
||||
export const LegendConditions = [
|
||||
{ label: "&&", value: "and" },
|
||||
{ label: "||", value: "or" },
|
||||
];
|
||||
export const MetricConditions = [
|
||||
{ label: ">", value: ">" },
|
||||
{ label: "<", value: "<" },
|
||||
];
|
||||
export enum LegendOpt {
|
||||
NAME = "name",
|
||||
VALUE = "value",
|
||||
CONDITION = "condition",
|
||||
}
|
||||
export const DepthList = ["1", "2", "3", "4", "5"].map((item: string) => ({
|
||||
value: item,
|
||||
label: item,
|
||||
}));
|
||||
export const Colors: any = {
|
||||
green: "#67C23A",
|
||||
blue: "#409EFF",
|
||||
red: "#F56C6C",
|
||||
grey: "#909399",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
orange: "#E6A23C",
|
||||
};
|
||||
|
@@ -42,10 +42,11 @@ import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { LayoutConfig } from "@/types/dashboard";
|
||||
import Widget from "../controls/Widget.vue";
|
||||
import Tab from "../controls/Tab.vue";
|
||||
import Topology from "../controls/Topology.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Layout",
|
||||
components: { Widget, Tab },
|
||||
components: { Widget, Tab, Topology },
|
||||
setup() {
|
||||
const dashboardStore = useDashboardStore();
|
||||
function layoutUpdatedEvent(newLayout: LayoutConfig[]) {
|
||||
|
@@ -20,7 +20,7 @@ limitations under the License. -->
|
||||
<Selector
|
||||
v-model="states.currentService"
|
||||
:options="selectorStore.services"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="Select a service"
|
||||
@change="changeService"
|
||||
class="selectors"
|
||||
@@ -29,7 +29,7 @@ limitations under the License. -->
|
||||
<div class="selectors-item" v-if="states.key === 3 || states.key === 4">
|
||||
<span class="label">
|
||||
{{
|
||||
dashboardStore.entity === "Endpoint"
|
||||
["EndpointRelation", "Endpoint"].includes(dashboardStore.entity)
|
||||
? "$Endpoint"
|
||||
: "$ServiceInstance"
|
||||
}}
|
||||
@@ -37,48 +37,58 @@ limitations under the License. -->
|
||||
<Selector
|
||||
v-model="states.currentPod"
|
||||
:options="selectorStore.pods"
|
||||
size="mini"
|
||||
size="small"
|
||||
placeholder="Select a data"
|
||||
@change="changePods"
|
||||
class="selectorPod"
|
||||
/>
|
||||
</div>
|
||||
<div class="selectors-item" v-if="states.key === 2">
|
||||
<div class="selectors-item" v-if="states.key === 2 || states.key === 4">
|
||||
<span class="label">$DestinationService</span>
|
||||
<Selector
|
||||
v-model="selectorStore.currentDestService"
|
||||
:options="selectorStore.services"
|
||||
size="mini"
|
||||
v-model="states.currentDestService"
|
||||
:options="selectorStore.destServices"
|
||||
size="small"
|
||||
placeholder="Select a service"
|
||||
@change="changeService"
|
||||
@change="changeDestService"
|
||||
class="selectors"
|
||||
/>
|
||||
</div>
|
||||
<div class="selectors-item" v-if="states.key === 4">
|
||||
<span class="label">$DestinationServiceInstance</span>
|
||||
<span class="label">
|
||||
{{
|
||||
dashboardStore.entity === "EndpointRelation"
|
||||
? "$DestinationEndpoint"
|
||||
: "$DestinationServiceInstance"
|
||||
}}</span
|
||||
>
|
||||
<Selector
|
||||
v-model="states.currentPod"
|
||||
:options="selectorStore.pods"
|
||||
size="mini"
|
||||
v-model="states.currentDestPod"
|
||||
:options="selectorStore.destPods"
|
||||
size="small"
|
||||
placeholder="Select a data"
|
||||
@change="changePods"
|
||||
class="selectors"
|
||||
:borderRadius="4"
|
||||
class="selectorPod"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-icons">
|
||||
<el-tooltip
|
||||
<span
|
||||
@click="clickIcons(t)"
|
||||
v-for="(t, index) in ToolIcons"
|
||||
:key="index"
|
||||
class="item"
|
||||
:content="t.content"
|
||||
placement="top"
|
||||
:title="t.content"
|
||||
>
|
||||
<span class="icon-btn" @click="clickIcons(t)">
|
||||
<Icon size="sm" :iconName="t.name" />
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<Icon
|
||||
class="icon-btn"
|
||||
size="sm"
|
||||
:iconName="t.name"
|
||||
v-if="
|
||||
t.id !== 'topology' ||
|
||||
(t.id === 'topology' && hasTopology.includes(dashboardStore.entity))
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -88,7 +98,7 @@ import { reactive, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import { EntityType, ToolIcons } from "../data";
|
||||
import { EntityType, ToolIcons, hasTopology } from "../data";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Option } from "@/types/app";
|
||||
@@ -105,12 +115,16 @@ const states = reactive<{
|
||||
key: number;
|
||||
currentService: string;
|
||||
currentPod: string;
|
||||
currentDestService: string;
|
||||
currentDestPod: string;
|
||||
}>({
|
||||
destService: "",
|
||||
destPod: "",
|
||||
key: (type && type.key) || 0,
|
||||
currentService: "",
|
||||
currentPod: "",
|
||||
currentDestService: "",
|
||||
currentDestPod: "",
|
||||
});
|
||||
|
||||
dashboardStore.setLayer(String(params.layerId));
|
||||
@@ -127,56 +141,136 @@ function initSelector() {
|
||||
}
|
||||
|
||||
async function setSelector() {
|
||||
if (params.podId) {
|
||||
await selectorStore.getService(String(params.serviceId));
|
||||
states.currentService = selectorStore.currentService.value;
|
||||
await fetchPods(String(params.entity), false);
|
||||
const currentPod = selectorStore.pods.filter(
|
||||
(d: { id: string }) => d.id === String(params.podId)
|
||||
)[0];
|
||||
selectorStore.setCurrentPod(currentPod);
|
||||
states.currentPod = currentPod.label;
|
||||
if (
|
||||
[
|
||||
EntityType[2].value,
|
||||
EntityType[3].value,
|
||||
EntityType[5].value,
|
||||
EntityType[6].value,
|
||||
].includes(String(params.entity))
|
||||
) {
|
||||
setSourceSelector();
|
||||
if (
|
||||
[EntityType[2].value, EntityType[3].value].includes(String(params.entity))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setDestSelector();
|
||||
return;
|
||||
}
|
||||
// entity=Service with serviceId
|
||||
// entity=Service/ServiceRelation
|
||||
const json = await selectorStore.fetchServices(dashboardStore.layerId);
|
||||
if (json.errors) {
|
||||
ElMessage.error(json.errors);
|
||||
return;
|
||||
}
|
||||
const currentService = selectorStore.services.filter(
|
||||
(d: { id: string }) => d.id === String(params.serviceId)
|
||||
)[0];
|
||||
let currentService, currentDestService;
|
||||
for (const d of selectorStore.services) {
|
||||
if (d.id === String(params.serviceId)) {
|
||||
currentService = d;
|
||||
}
|
||||
if (d.id === String(params.destServiceId)) {
|
||||
currentDestService = d;
|
||||
}
|
||||
}
|
||||
selectorStore.setCurrentService(currentService);
|
||||
selectorStore.setCurrentDestService(currentDestService);
|
||||
states.currentService = selectorStore.currentService.value;
|
||||
states.currentDestService = selectorStore.currentDestService.value;
|
||||
}
|
||||
|
||||
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);
|
||||
if (!(selectorStore.pods.length && selectorStore.pods[0])) {
|
||||
selectorStore.setCurrentPod(null);
|
||||
states.currentPod = "";
|
||||
return;
|
||||
}
|
||||
const pod = params.podId || selectorStore.pods[0].id;
|
||||
const currentPod = selectorStore.pods.filter(
|
||||
(d: { id: string }) => d.id === pod
|
||||
)[0];
|
||||
if (currentPod) {
|
||||
selectorStore.setCurrentPod(currentPod);
|
||||
states.currentPod = currentPod.label;
|
||||
}
|
||||
}
|
||||
|
||||
async function setDestSelector() {
|
||||
await selectorStore.getService(String(params.destServiceId), true);
|
||||
states.currentDestService = selectorStore.currentDestService.value;
|
||||
await fetchPods(
|
||||
String(params.entity),
|
||||
selectorStore.currentDestService.id,
|
||||
false
|
||||
);
|
||||
if (!(selectorStore.destPods.length && selectorStore.destPods[0])) {
|
||||
selectorStore.setCurrentDestPod(null);
|
||||
states.currentDestPod = "";
|
||||
return;
|
||||
}
|
||||
const destPod = params.destPodId || selectorStore.destPods[0].id;
|
||||
const currentDestPod = selectorStore.destPods.filter(
|
||||
(d: { id: string }) => d.id === destPod
|
||||
)[0];
|
||||
if (currentDestPod) {
|
||||
selectorStore.setCurrentDestPod(currentDestPod);
|
||||
states.currentDestPod = currentDestPod.label;
|
||||
}
|
||||
}
|
||||
|
||||
async function getServices() {
|
||||
if (!dashboardStore.layerId) {
|
||||
return;
|
||||
}
|
||||
if (dashboardStore.entity === EntityType[1].value) {
|
||||
return;
|
||||
}
|
||||
const json = await selectorStore.fetchServices(dashboardStore.layerId);
|
||||
if (json.errors) {
|
||||
ElMessage.error(json.errors);
|
||||
return;
|
||||
}
|
||||
if (dashboardStore.entity === EntityType[1].value) {
|
||||
return;
|
||||
}
|
||||
selectorStore.setCurrentService(
|
||||
selectorStore.services.length ? selectorStore.services[0] : null
|
||||
);
|
||||
selectorStore.setCurrentDestService(
|
||||
selectorStore.services.length ? selectorStore.services[1] : null
|
||||
);
|
||||
states.currentService = selectorStore.currentService.value;
|
||||
fetchPods(dashboardStore.entity, true);
|
||||
states.currentDestService = selectorStore.currentDestService.value;
|
||||
const e = dashboardStore.entity.split("Relation")[0];
|
||||
if (
|
||||
[EntityType[2].value, EntityType[3].value].includes(dashboardStore.entity)
|
||||
) {
|
||||
fetchPods(e, selectorStore.currentService.id, true);
|
||||
}
|
||||
if (
|
||||
[EntityType[5].value, EntityType[6].value].includes(dashboardStore.entity)
|
||||
) {
|
||||
fetchPods(dashboardStore.entity, selectorStore.currentDestService.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function changeService(service: Service[]) {
|
||||
if (service[0]) {
|
||||
states.currentService = service[0].value;
|
||||
selectorStore.setCurrentService(service[0]);
|
||||
fetchPods(dashboardStore.entity, true);
|
||||
fetchPods(dashboardStore.entity, selectorStore.currentService.id, true);
|
||||
} else {
|
||||
selectorStore.setCurrentService("");
|
||||
selectorStore.setCurrentService(null);
|
||||
}
|
||||
}
|
||||
|
||||
function changeDestService(service: Service[]) {
|
||||
if (service[0]) {
|
||||
states.currentDestService = service[0].value;
|
||||
selectorStore.setCurrentDestService(service[0]);
|
||||
} else {
|
||||
selectorStore.setCurrentDestService(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +293,9 @@ function clickIcons(t: { id: string; content: string; name: string }) {
|
||||
case "addImage":
|
||||
dashboardStore.addControl("Image");
|
||||
break;
|
||||
case "topology":
|
||||
dashboardStore.addControl("Topology");
|
||||
break;
|
||||
case "settings":
|
||||
dashboardStore.setConfigPanel(true);
|
||||
break;
|
||||
@@ -207,11 +304,11 @@ function clickIcons(t: { id: string; content: string; name: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPods(type: string, setPod: boolean) {
|
||||
async function fetchPods(type: string, serviceId: string, setPod: boolean) {
|
||||
let resp;
|
||||
switch (type) {
|
||||
case "Endpoint":
|
||||
resp = await selectorStore.getEndpoints();
|
||||
case EntityType[2].value:
|
||||
resp = await selectorStore.getEndpoints({ serviceId });
|
||||
if (setPod) {
|
||||
selectorStore.setCurrentPod(
|
||||
selectorStore.pods.length ? selectorStore.pods[0] : null
|
||||
@@ -219,8 +316,8 @@ async function fetchPods(type: string, setPod: boolean) {
|
||||
states.currentPod = selectorStore.currentPod.label;
|
||||
}
|
||||
break;
|
||||
case "ServiceInstance":
|
||||
resp = await selectorStore.getServiceInstances();
|
||||
case EntityType[3].value:
|
||||
resp = await selectorStore.getServiceInstances({ serviceId });
|
||||
if (setPod) {
|
||||
selectorStore.setCurrentPod(
|
||||
selectorStore.pods.length ? selectorStore.pods[0] : null
|
||||
@@ -228,6 +325,27 @@ async function fetchPods(type: string, setPod: boolean) {
|
||||
states.currentPod = selectorStore.currentPod.label;
|
||||
}
|
||||
break;
|
||||
case EntityType[6].value:
|
||||
resp = await selectorStore.getEndpoints({ serviceId, isRelation: true });
|
||||
if (setPod) {
|
||||
selectorStore.setCurrentDestPod(
|
||||
selectorStore.destPods.length ? selectorStore.destPods[0] : null
|
||||
);
|
||||
states.currentDestPod = selectorStore.currentDestPod.label;
|
||||
}
|
||||
break;
|
||||
case EntityType[5].value:
|
||||
resp = await selectorStore.getServiceInstances({
|
||||
serviceId,
|
||||
isRelation: true,
|
||||
});
|
||||
if (setPod) {
|
||||
selectorStore.setCurrentDestPod(
|
||||
selectorStore.destPods.length ? selectorStore.destPods[0] : null
|
||||
);
|
||||
states.currentDestPod = selectorStore.currentDestPod.label;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
resp = {};
|
||||
}
|
||||
@@ -258,9 +376,13 @@ watch(
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.tool-icons {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
padding: 3px;
|
||||
text-align: center;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
@@ -268,9 +390,6 @@ watch(
|
||||
cursor: pointer;
|
||||
background-color: #eee;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
30
src/views/dashboard/related/topology/Index.vue
Normal file
30
src/views/dashboard/related/topology/Index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<!-- 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>
|
||||
<PodTopology v-if="isSankey" />
|
||||
<Graph v-else />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import Graph from "./components/Graph.vue";
|
||||
import PodTopology from "./components/PodTopology.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)
|
||||
);
|
||||
</script>
|
586
src/views/dashboard/related/topology/components/Graph.vue
Normal file
586
src/views/dashboard/related/topology/components/Graph.vue
Normal file
@@ -0,0 +1,586 @@
|
||||
<!-- 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="micro-topo-chart"
|
||||
v-loading="loading"
|
||||
:style="`height: ${height}px`"
|
||||
>
|
||||
<div class="setting" v-show="showSetting">
|
||||
<Settings @update="updateSettings" @updateNodes="freshNodes" />
|
||||
</div>
|
||||
<div class="tool">
|
||||
<span class="label">{{ t("currentDepth") }}</span>
|
||||
<Selector
|
||||
class="inputs"
|
||||
:value="depth"
|
||||
:options="DepthList"
|
||||
placeholder="Select a option"
|
||||
@change="changeDepth"
|
||||
/>
|
||||
<span class="switch-icon ml-5" title="Settings" @click="setConfig">
|
||||
<Icon size="middle" iconName="settings" />
|
||||
</span>
|
||||
<span
|
||||
class="switch-icon ml-5"
|
||||
title="Back to overview topology"
|
||||
@click="backToTopology"
|
||||
>
|
||||
<Icon size="middle" iconName="keyboard_backspace" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="operations-list"
|
||||
v-if="topologyStore.node"
|
||||
:style="{
|
||||
top: operationsPos.y + 'px',
|
||||
left: operationsPos.x + 'px',
|
||||
}"
|
||||
>
|
||||
<span
|
||||
v-for="(item, index) of items"
|
||||
:key="index"
|
||||
@click="item.func(item.dashboard)"
|
||||
>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onBeforeUnmount, reactive } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import * as d3 from "d3";
|
||||
import d3tip from "d3-tip";
|
||||
import zoom from "../utils/zoom";
|
||||
import { simulationInit, simulationSkip } from "../utils/simulation";
|
||||
import nodeElement from "../utils/nodeElement";
|
||||
import { linkElement, anchorElement, arrowMarker } from "../utils/linkElement";
|
||||
import topoLegend from "../utils/legend";
|
||||
import { Node, Call } from "@/types/topology";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { useTopologyStore } from "@/store/modules/topology";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { EntityType, DepthList } from "../../../data";
|
||||
import router from "@/router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import Settings from "./Settings.vue";
|
||||
import { Option } from "@/types/app";
|
||||
import { Service } from "@/types/selector";
|
||||
|
||||
/*global Nullable */
|
||||
const { t } = useI18n();
|
||||
const selectorStore = useSelectorStore();
|
||||
const topologyStore = useTopologyStore();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const height = ref<number>(document.body.clientHeight - 90);
|
||||
const width = ref<number>(document.body.clientWidth - 40);
|
||||
const loading = ref<boolean>(false);
|
||||
const simulation = ref<any>(null);
|
||||
const svg = ref<Nullable<any>>(null);
|
||||
const chart = ref<Nullable<HTMLDivElement>>(null);
|
||||
const tip = ref<any>(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 legend = ref<any>(null);
|
||||
const showSetting = ref<boolean>(false);
|
||||
const settings = ref<any>({});
|
||||
const operationsPos = reactive<{ x: number; y: number }>({ x: NaN, y: NaN });
|
||||
const items = ref<
|
||||
{ id: string; title: string; func: any; dashboard?: string }[]
|
||||
>([
|
||||
{ id: "inspect", title: "Inspect", func: handleInspect },
|
||||
{ id: "alarm", title: "Alarm", func: handleGoAlarm },
|
||||
]);
|
||||
const depth = ref<string>(topologyStore.defaultDepth);
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
const resp = await getTopology();
|
||||
loading.value = false;
|
||||
if (resp && resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
window.addEventListener("resize", resize);
|
||||
svg.value = d3
|
||||
.select(chart.value)
|
||||
.append("svg")
|
||||
.attr("class", "topo-svg")
|
||||
.attr("height", height.value)
|
||||
.attr("width", width.value);
|
||||
await init();
|
||||
update();
|
||||
});
|
||||
async function init() {
|
||||
tip.value = (d3tip as any)().attr("class", "d3-tip").offset([-8, 0]);
|
||||
graph.value = svg.value.append("g").attr("class", "topo-svg-graph");
|
||||
graph.value.call(tip.value);
|
||||
simulation.value = simulationInit(
|
||||
d3,
|
||||
topologyStore.nodes,
|
||||
topologyStore.calls,
|
||||
ticked
|
||||
);
|
||||
node.value = graph.value.append("g").selectAll(".topo-node");
|
||||
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));
|
||||
// legend
|
||||
legend.value = graph.value.append("g").attr("class", "topo-legend");
|
||||
topoLegend(legend.value, height.value, width.value, settings.value.legend);
|
||||
svg.value.on("click", (event: any) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(null);
|
||||
});
|
||||
}
|
||||
function ticked() {
|
||||
link.value.attr(
|
||||
"d",
|
||||
(d: Call | any) =>
|
||||
`M${d.source.x} ${d.source.y} Q ${(d.source.x + d.target.x) / 2} ${
|
||||
(d.target.y + d.source.y) / 2 - d.loopFactor * 90
|
||||
} ${d.target.x} ${d.target.y}`
|
||||
);
|
||||
anchor.value.attr(
|
||||
"transform",
|
||||
(d: Call | any) =>
|
||||
`translate(${(d.source.x + d.target.x) / 2}, ${
|
||||
(d.target.y + d.source.y) / 2 - d.loopFactor * 45
|
||||
})`
|
||||
);
|
||||
node.value.attr(
|
||||
"transform",
|
||||
(d: Node | any) => `translate(${d.x - 22},${d.y - 22})`
|
||||
);
|
||||
}
|
||||
function dragstart(d: any) {
|
||||
node.value._groups[0].forEach((g: any) => {
|
||||
g.__data__.fx = g.__data__.x;
|
||||
g.__data__.fy = g.__data__.y;
|
||||
});
|
||||
if (!d.active) {
|
||||
simulation.value.alphaTarget(0.1).restart();
|
||||
}
|
||||
d.subject.fx = d.subject.x;
|
||||
d.subject.fy = d.subject.y;
|
||||
d.sourceEvent.stopPropagation();
|
||||
}
|
||||
function dragged(d: any) {
|
||||
d.subject.fx = d.x;
|
||||
d.subject.fy = d.y;
|
||||
}
|
||||
function dragended(d: any) {
|
||||
if (!d.active) {
|
||||
simulation.value.alphaTarget(0);
|
||||
}
|
||||
}
|
||||
function handleNodeClick(d: Node & { x: number; y: number }) {
|
||||
topologyStore.setNode(d);
|
||||
topologyStore.setLink(null);
|
||||
operationsPos.x = d.x;
|
||||
operationsPos.y = d.y + 30;
|
||||
if (d.layer === String(dashboardStore.layerId)) {
|
||||
return;
|
||||
}
|
||||
items.value = [
|
||||
{ id: "inspect", title: "Inspect", func: handleInspect },
|
||||
{ id: "alarm", title: "Alarm", func: handleGoAlarm },
|
||||
];
|
||||
}
|
||||
function handleLinkClick(event: any, d: Call) {
|
||||
if (
|
||||
d.source.layer !== dashboardStore.layerId ||
|
||||
d.target.layer !== dashboardStore.layerId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(d);
|
||||
if (!settings.value.linkDashboard) {
|
||||
return;
|
||||
}
|
||||
const e =
|
||||
dashboardStore.entity === EntityType[1].value
|
||||
? EntityType[0].value
|
||||
: dashboardStore.entity;
|
||||
const path = `/dashboard/${dashboardStore.layerId}/${e}Relation/${d.source.id}/${d.target.id}/${settings.value.linkDashboard}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
window.open(routeUrl.href, "_blank");
|
||||
}
|
||||
function update() {
|
||||
// node element
|
||||
if (!node.value || !link.value) {
|
||||
return;
|
||||
}
|
||||
node.value = node.value.data(topologyStore.nodes, (d: Node) => d.id);
|
||||
node.value.exit().remove();
|
||||
node.value = nodeElement(
|
||||
d3,
|
||||
node.value.enter(),
|
||||
{
|
||||
dragstart: dragstart,
|
||||
dragged: dragged,
|
||||
dragended: dragended,
|
||||
handleNodeClick: handleNodeClick,
|
||||
tipHtml: (data: Node) => {
|
||||
const nodeMetrics: string[] = settings.value.nodeMetrics || [];
|
||||
const html = nodeMetrics.map((m) => {
|
||||
const metric =
|
||||
topologyStore.nodeMetrics[m].values.filter(
|
||||
(val: { id: string; value: unknown }) => val.id === data.id
|
||||
)[0] || {};
|
||||
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
|
||||
return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</div>`;
|
||||
});
|
||||
return [
|
||||
` <div class="mb-5"><span class="grey">name: </span>${data.name}</div>`,
|
||||
...html,
|
||||
].join(" ");
|
||||
},
|
||||
},
|
||||
tip.value,
|
||||
settings.value.legend
|
||||
).merge(node.value);
|
||||
// line element
|
||||
link.value = link.value.data(topologyStore.calls, (d: Call) => d.id);
|
||||
link.value.exit().remove();
|
||||
link.value = linkElement(link.value.enter()).merge(link.value);
|
||||
// anchorElement
|
||||
anchor.value = anchor.value.data(topologyStore.calls, (d: Call) => d.id);
|
||||
anchor.value.exit().remove();
|
||||
anchor.value = anchorElement(
|
||||
anchor.value.enter(),
|
||||
{
|
||||
handleLinkClick: handleLinkClick,
|
||||
tipHtml: (data: Call) => {
|
||||
const linkClientMetrics: string[] =
|
||||
settings.value.linkClientMetrics || [];
|
||||
const linkServerMetrics: string[] =
|
||||
settings.value.linkServerMetrics || [];
|
||||
const htmlServer = linkServerMetrics.map((m) => {
|
||||
const metric = topologyStore.linkServerMetrics[m].values.filter(
|
||||
(val: { id: string; value: unknown }) => val.id === data.id
|
||||
)[0];
|
||||
if (metric) {
|
||||
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
|
||||
return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</div>`;
|
||||
}
|
||||
});
|
||||
const htmlClient = linkClientMetrics.map((m) => {
|
||||
const metric = topologyStore.linkClientMetrics[m].values.filter(
|
||||
(val: { id: string; value: unknown }) => val.id === data.id
|
||||
)[0];
|
||||
if (metric) {
|
||||
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
|
||||
return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</div>`;
|
||||
}
|
||||
});
|
||||
const html = [
|
||||
...htmlServer,
|
||||
...htmlClient,
|
||||
`<div><span class="grey">${t(
|
||||
"detectPoint"
|
||||
)}:</span>${data.detectPoints.join(" | ")}</div>`,
|
||||
].join(" ");
|
||||
|
||||
return html;
|
||||
},
|
||||
},
|
||||
tip.value
|
||||
).merge(anchor.value);
|
||||
// arrow marker
|
||||
arrow.value = arrow.value.data(topologyStore.calls, (d: Call) => d.id);
|
||||
arrow.value.exit().remove();
|
||||
arrow.value = arrowMarker(arrow.value.enter()).merge(arrow.value);
|
||||
// force element
|
||||
simulation.value.nodes(topologyStore.nodes);
|
||||
simulation.value
|
||||
.force("link")
|
||||
.links(topologyStore.calls)
|
||||
.id((d: Call) => d.id);
|
||||
simulationSkip(d3, simulation.value, ticked);
|
||||
const loopMap: any = {};
|
||||
for (let i = 0; i < topologyStore.calls.length; i++) {
|
||||
const link: any = topologyStore.calls[i];
|
||||
link.loopFactor = 1;
|
||||
for (let j = 0; j < topologyStore.calls.length; j++) {
|
||||
if (i === j || loopMap[i]) {
|
||||
continue;
|
||||
}
|
||||
const otherLink = topologyStore.calls[j];
|
||||
if (
|
||||
link.source.id === otherLink.target.id &&
|
||||
link.target.id === otherLink.source.id
|
||||
) {
|
||||
link.loopFactor = -1;
|
||||
loopMap[j] = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async function handleInspect() {
|
||||
svg.value.selectAll(".topo-svg-graph").remove();
|
||||
const id = topologyStore.node.id;
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(null);
|
||||
loading.value = true;
|
||||
const resp = await topologyStore.getServicesTopology([id]);
|
||||
loading.value = false;
|
||||
|
||||
if (resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
await init();
|
||||
update();
|
||||
}
|
||||
function handleGoEndpoint(name: string) {
|
||||
const path = `/dashboard/${dashboardStore.layerId}/Endpoint/${topologyStore.node.id}/${name}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
}
|
||||
function handleGoInstance(name: string) {
|
||||
const path = `/dashboard/${dashboardStore.layerId}/ServiceInstance/${topologyStore.node.id}/${name}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
}
|
||||
function handleGoDashboard(name: string) {
|
||||
const path = `/dashboard/${dashboardStore.layerId}/Service/${topologyStore.node.id}/${name}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
}
|
||||
function handleGoAlarm() {
|
||||
const path = `/alarm`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
}
|
||||
async function backToTopology() {
|
||||
svg.value.selectAll(".topo-svg-graph").remove();
|
||||
loading.value = true;
|
||||
const resp = await getTopology();
|
||||
loading.value = false;
|
||||
|
||||
if (resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
await init();
|
||||
update();
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(null);
|
||||
}
|
||||
async function getTopology() {
|
||||
const ids = selectorStore.services.map((d: Service) => d.id);
|
||||
const serviceIds =
|
||||
dashboardStore.entity === EntityType[0].value
|
||||
? [selectorStore.currentService.id]
|
||||
: ids;
|
||||
const resp = await topologyStore.getDepthServiceTopology(
|
||||
serviceIds,
|
||||
Number(depth.value)
|
||||
);
|
||||
return resp;
|
||||
}
|
||||
function setConfig() {
|
||||
showSetting.value = !showSetting.value;
|
||||
}
|
||||
function resize() {
|
||||
height.value = document.body.clientHeight - 90;
|
||||
width.value = document.body.clientWidth - 40;
|
||||
svg.value.attr("height", height.value).attr("width", width.value);
|
||||
}
|
||||
function updateSettings(config: any) {
|
||||
items.value = [
|
||||
{ id: "inspect", title: "Inspect", func: handleInspect },
|
||||
{ id: "alarm", title: "Alarm", func: handleGoAlarm },
|
||||
];
|
||||
settings.value = config;
|
||||
for (const item of config.nodeDashboard) {
|
||||
if (item.scope === EntityType[0].value) {
|
||||
items.value.push({
|
||||
id: "dashboard",
|
||||
title: "Dashboard",
|
||||
func: handleGoDashboard,
|
||||
...item,
|
||||
});
|
||||
}
|
||||
if (item.scope === EntityType[2].value) {
|
||||
items.value.push({
|
||||
id: "endpoint",
|
||||
title: "Endpoint",
|
||||
func: handleGoEndpoint,
|
||||
...item,
|
||||
});
|
||||
}
|
||||
if (item.scope === EntityType[3].value) {
|
||||
items.value.push({
|
||||
id: "instance",
|
||||
title: "Service Instance",
|
||||
func: handleGoInstance,
|
||||
...item,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
async function freshNodes() {
|
||||
svg.value.selectAll(".topo-svg-graph").remove();
|
||||
await init();
|
||||
update();
|
||||
}
|
||||
|
||||
async function changeDepth(opt: Option[]) {
|
||||
depth.value = opt[0].value;
|
||||
await getTopology();
|
||||
freshNodes();
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resize);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.micro-topo-chart {
|
||||
position: relative;
|
||||
|
||||
.setting {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: 0;
|
||||
width: 400px;
|
||||
height: 700px;
|
||||
background-color: #2b3037;
|
||||
overflow: auto;
|
||||
padding: 0 15px;
|
||||
border-radius: 3px;
|
||||
color: #ccc;
|
||||
transition: all 0.5ms linear;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #ccc;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.operations-list {
|
||||
position: absolute;
|
||||
padding: 10px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
height: 30px;
|
||||
width: 140px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
span:hover {
|
||||
color: #409eff;
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.tool {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.switch-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.5ms linear;
|
||||
background-color: #252a2f99;
|
||||
color: #ddd;
|
||||
display: inline-block;
|
||||
padding: 5px 8px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.topo-svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.topo-line {
|
||||
stroke-linecap: round;
|
||||
stroke-width: 3px;
|
||||
stroke-dasharray: 13 7;
|
||||
fill: none;
|
||||
animation: topo-dash 0.5s linear infinite;
|
||||
}
|
||||
|
||||
.topo-line-anchor {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.topo-text {
|
||||
font-family: "Lato", "Source Han Sans CN", "Microsoft YaHei", sans-serif;
|
||||
fill: #ddd;
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.d3-tip {
|
||||
line-height: 1;
|
||||
padding: 8px;
|
||||
color: #eee;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
z-index: 9999;
|
||||
background: #252a2f;
|
||||
}
|
||||
|
||||
.d3-tip:after {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
width: 100%;
|
||||
line-height: 0.8;
|
||||
color: #252a2f;
|
||||
content: "\25BC";
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.d3-tip.n:after {
|
||||
margin: -2px 0 0 0;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
}
|
||||
@keyframes topo-dash {
|
||||
from {
|
||||
stroke-dashoffset: 20;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
274
src/views/dashboard/related/topology/components/PodTopology.vue
Normal file
274
src/views/dashboard/related/topology/components/PodTopology.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<!-- 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="tool">
|
||||
<span v-show="dashboardStore.entity === EntityType[2].value">
|
||||
<span class="label">{{ t("currentDepth") }}</span>
|
||||
<Selector
|
||||
class="inputs"
|
||||
:value="depth"
|
||||
:options="DepthList"
|
||||
placeholder="Select a option"
|
||||
@change="changeDepth"
|
||||
/>
|
||||
</span>
|
||||
<span class="switch-icon ml-5" title="Settings" @click="setConfig">
|
||||
<Icon size="middle" iconName="settings" />
|
||||
</span>
|
||||
<span
|
||||
class="switch-icon ml-5"
|
||||
title="Back to overview topology"
|
||||
@click="backToTopology"
|
||||
>
|
||||
<Icon size="middle" iconName="keyboard_backspace" />
|
||||
</span>
|
||||
<div class="settings" v-show="showSettings">
|
||||
<Settings @update="updateConfig" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="sankey"
|
||||
:style="`height:${height}px;width:${width}px;`"
|
||||
v-loading="loading"
|
||||
@click="handleClick"
|
||||
>
|
||||
<Sankey @click="selectNodeLink" />
|
||||
</div>
|
||||
<div
|
||||
class="operations-list"
|
||||
v-if="topologyStore.node"
|
||||
:style="{
|
||||
top: operationsPos.y + 'px',
|
||||
left: operationsPos.x + 'px',
|
||||
}"
|
||||
>
|
||||
<i v-for="(item, index) of items" :key="index" @click="item.func">
|
||||
<span
|
||||
v-if="
|
||||
['alarm', 'inspect'].includes(item.id) ||
|
||||
(item.id === 'dashboard' && settings.nodeDashboard)
|
||||
"
|
||||
>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</i>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ref, onMounted, reactive } from "vue";
|
||||
import { Option } from "@/types/app";
|
||||
import { useTopologyStore } from "@/store/modules/topology";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { EntityType, DepthList } from "../../../data";
|
||||
import { ElMessage } from "element-plus";
|
||||
import Sankey from "./Sankey.vue";
|
||||
import Settings from "./Settings.vue";
|
||||
import router from "@/router";
|
||||
|
||||
const { t } = useI18n();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const selectorStore = useSelectorStore();
|
||||
const topologyStore = useTopologyStore();
|
||||
const loading = ref<boolean>(false);
|
||||
const height = ref<number>(document.body.clientHeight - 150);
|
||||
const width = ref<number>(document.body.clientWidth - 40);
|
||||
const showSettings = ref<boolean>(false);
|
||||
const settings = ref<any>({});
|
||||
const operationsPos = reactive<{ x: number; y: number }>({ x: NaN, y: NaN });
|
||||
const depth = ref<string>(topologyStore.defaultDepth);
|
||||
const items = [
|
||||
{ id: "inspect", title: "Inspect", func: inspect },
|
||||
{ id: "dashboard", title: "View Dashboard", func: goDashboard },
|
||||
{ id: "alarm", title: "View Alarm", func: goAlarm },
|
||||
];
|
||||
|
||||
onMounted(async () => {
|
||||
loadTopology(selectorStore.currentPod && selectorStore.currentPod.id);
|
||||
});
|
||||
|
||||
async function loadTopology(id: string) {
|
||||
loading.value = true;
|
||||
const resp = await getTopology(id);
|
||||
loading.value = false;
|
||||
if (resp && resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
}
|
||||
|
||||
function inspect() {
|
||||
const id = topologyStore.node.id;
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(null);
|
||||
loadTopology(id);
|
||||
}
|
||||
|
||||
function goAlarm() {
|
||||
const path = `/alarm`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
topologyStore.setNode(null);
|
||||
}
|
||||
function goDashboard() {
|
||||
const entity =
|
||||
dashboardStore.entity === EntityType[2].value
|
||||
? EntityType[2].value
|
||||
: EntityType[3].value;
|
||||
const path = `/dashboard/${dashboardStore.layerId}/${entity}/${topologyStore.node.serviceId}/${topologyStore.node.id}/${settings.value.nodeDashboard}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
window.open(routeUrl.href, "_blank");
|
||||
topologyStore.setNode(null);
|
||||
}
|
||||
|
||||
function setConfig() {
|
||||
topologyStore.setNode(null);
|
||||
showSettings.value = !showSettings.value;
|
||||
}
|
||||
|
||||
function updateConfig(config: any) {
|
||||
settings.value = config;
|
||||
}
|
||||
|
||||
function backToTopology() {
|
||||
loadTopology(selectorStore.currentPod.id);
|
||||
topologyStore.setNode(null);
|
||||
}
|
||||
|
||||
function selectNodeLink(d: any) {
|
||||
if (d.dataType === "edge") {
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(d.data);
|
||||
if (!settings.value.linkDashboard) {
|
||||
return;
|
||||
}
|
||||
const { sourceObj, targetObj } = d.data;
|
||||
const entity =
|
||||
dashboardStore.entity === EntityType[2].value
|
||||
? EntityType[6].value
|
||||
: EntityType[5].value;
|
||||
const path = `/dashboard/${dashboardStore.layerId}/${entity}/${sourceObj.serviceId}/${sourceObj.id}/${targetObj.serviceId}/${targetObj.id}/${settings.value.linkDashboard}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
window.open(routeUrl.href, "_blank");
|
||||
return;
|
||||
}
|
||||
topologyStore.setNode(d.data);
|
||||
topologyStore.setLink(null);
|
||||
operationsPos.x = d.event.event.clientX;
|
||||
operationsPos.y = d.event.event.clientY;
|
||||
}
|
||||
|
||||
async function changeDepth(opt: Option[]) {
|
||||
depth.value = opt[0].value;
|
||||
loadTopology(selectorStore.currentPod.id);
|
||||
}
|
||||
|
||||
async function getTopology(id: string) {
|
||||
let resp;
|
||||
switch (dashboardStore.entity) {
|
||||
case EntityType[2].value:
|
||||
resp = await topologyStore.updateEndpointTopology(
|
||||
[id],
|
||||
Number(depth.value)
|
||||
);
|
||||
break;
|
||||
case EntityType[4].value:
|
||||
resp = await topologyStore.getInstanceTopology();
|
||||
break;
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
function handleClick(event: any) {
|
||||
if (event.target.nodeName === "svg") {
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(null);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.sankey {
|
||||
margin-top: 10px;
|
||||
background-color: #333840;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.settings {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 0;
|
||||
width: 400px;
|
||||
height: 700px;
|
||||
background-color: #2b3037;
|
||||
overflow: auto;
|
||||
padding: 0 15px;
|
||||
border-radius: 3px;
|
||||
color: #ccc;
|
||||
transition: all 0.5ms linear;
|
||||
z-index: 99;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tool {
|
||||
text-align: right;
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.switch-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.5ms linear;
|
||||
background-color: #252a2f99;
|
||||
color: #ddd;
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #ccc;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.operations-list {
|
||||
position: absolute;
|
||||
padding: 10px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
height: 30px;
|
||||
width: 140px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
span:hover {
|
||||
color: #409eff;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
i {
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
</style>
|
131
src/views/dashboard/related/topology/components/Sankey.vue
Normal file
131
src/views/dashboard/related/topology/components/Sankey.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
|
||||
<template>
|
||||
<Graph :option="option" @select="clickChart" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { useTopologyStore } from "@/store/modules/topology";
|
||||
import { Node, Call } from "@/types/topology";
|
||||
|
||||
/*global defineEmits */
|
||||
const emit = defineEmits(["click"]);
|
||||
const topologyStore = useTopologyStore();
|
||||
const option = computed(() => getOption());
|
||||
|
||||
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 linkTooltip(param.data);
|
||||
}
|
||||
return nodeTooltip(param.data);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
function linkTooltip(data: Call) {
|
||||
const clientMetrics: string[] = Object.keys(topologyStore.linkClientMetrics);
|
||||
const serverMetrics: string[] = Object.keys(topologyStore.linkServerMetrics);
|
||||
const htmlServer = serverMetrics.map((m) => {
|
||||
const metric = topologyStore.linkServerMetrics[m].values.filter(
|
||||
(val: { id: string; value: unknown }) => val.id === data.id
|
||||
)[0];
|
||||
if (metric) {
|
||||
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
|
||||
return ` <div><span>${m}: </span>${val}</div>`;
|
||||
}
|
||||
});
|
||||
const htmlClient = clientMetrics.map((m) => {
|
||||
const metric = topologyStore.linkClientMetrics[m].values.filter(
|
||||
(val: { id: string; value: unknown }) => val.id === data.id
|
||||
)[0];
|
||||
if (metric) {
|
||||
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
|
||||
return ` <div><span>${m}: </span>${val}</div>`;
|
||||
}
|
||||
});
|
||||
const html = [
|
||||
`<div>${data.sourceObj.serviceName} -> ${data.targetObj.serviceName}</div>`,
|
||||
...htmlServer,
|
||||
...htmlClient,
|
||||
].join(" ");
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function nodeTooltip(data: Node) {
|
||||
const nodeMetrics: string[] = Object.keys(topologyStore.nodeMetrics);
|
||||
const html = nodeMetrics.map((m) => {
|
||||
const metric =
|
||||
topologyStore.nodeMetrics[m].values.filter(
|
||||
(val: { id: string; value: unknown }) => val.id === data.id
|
||||
)[0] || {};
|
||||
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
|
||||
return ` <div><span>${m}: </span>${val}</div>`;
|
||||
});
|
||||
return [` <div><span>name: </span>${data.serviceName}</div>`, ...html].join(
|
||||
" "
|
||||
);
|
||||
}
|
||||
|
||||
function clickChart(param: any) {
|
||||
emit("click", param);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.sankey {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
402
src/views/dashboard/related/topology/components/Settings.vue
Normal file
402
src/views/dashboard/related/topology/components/Settings.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<!-- 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="link-settings">
|
||||
<h5 class="title">{{ t("callSettings") }}</h5>
|
||||
<div class="label">{{ t("linkDashboard") }}</div>
|
||||
<el-input
|
||||
v-model="states.linkDashboard"
|
||||
placeholder="Please input a dashboard name for calls"
|
||||
@change="updateSettings"
|
||||
size="small"
|
||||
class="inputs"
|
||||
/>
|
||||
<div class="label">{{ t("linkServerMetrics") }}</div>
|
||||
<Selector
|
||||
class="inputs"
|
||||
:multiple="true"
|
||||
:value="states.linkServerMetrics"
|
||||
:options="states.linkMetricList"
|
||||
size="small"
|
||||
placeholder="Select metrics"
|
||||
@change="changeLinkServerMetrics"
|
||||
/>
|
||||
<span v-show="dashboardStore.entity !== EntityType[2].value">
|
||||
<div class="label">
|
||||
{{ t("linkClientMetrics") }}
|
||||
</div>
|
||||
<Selector
|
||||
class="inputs"
|
||||
:multiple="true"
|
||||
:value="states.linkClientMetrics"
|
||||
:options="states.linkMetricList"
|
||||
size="small"
|
||||
placeholder="Select metrics"
|
||||
@change="changeLinkClientMetrics"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="node-settings">
|
||||
<h5 class="title">{{ t("nodeSettings") }}</h5>
|
||||
<div class="label">{{ t("nodeDashboard") }}</div>
|
||||
<el-input
|
||||
v-show="!isServer"
|
||||
v-model="states.nodeDashboard"
|
||||
placeholder="Please input a dashboard name for nodes"
|
||||
@change="updateSettings"
|
||||
size="small"
|
||||
class="inputs"
|
||||
/>
|
||||
<div
|
||||
v-show="isServer"
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="metric-item"
|
||||
>
|
||||
<Selector
|
||||
:value="item.scope"
|
||||
:options="ScopeType"
|
||||
size="small"
|
||||
placeholder="Select a scope"
|
||||
@change="changeScope(index, $event)"
|
||||
class="item mr-5"
|
||||
/>
|
||||
<el-input
|
||||
v-model="item.dashboard"
|
||||
placeholder="Please input a dashboard name for nodes"
|
||||
@change="updateNodeDashboards(index, $event)"
|
||||
size="small"
|
||||
class="item mr-5"
|
||||
/>
|
||||
<span>
|
||||
<Icon
|
||||
class="cp mr-5"
|
||||
v-show="items.length > 1"
|
||||
iconName="remove_circle_outline"
|
||||
size="middle"
|
||||
@click="deleteItem(index)"
|
||||
/>
|
||||
<Icon
|
||||
class="cp"
|
||||
v-show="index === items.length - 1 && items.length < 5"
|
||||
iconName="add_circle_outlinecontrol_point"
|
||||
size="middle"
|
||||
@click="addItem"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="label">{{ t("nodeMetrics") }}</div>
|
||||
<Selector
|
||||
class="inputs"
|
||||
:multiple="true"
|
||||
:value="states.nodeMetrics"
|
||||
:options="states.nodeMetricList"
|
||||
size="small"
|
||||
placeholder="Select metrics"
|
||||
@change="changeNodeMetrics"
|
||||
/>
|
||||
</div>
|
||||
<div class="legend-settings" v-show="isServer">
|
||||
<h5 class="title">{{ t("legendSettings") }}</h5>
|
||||
<div class="label">{{ t("conditions") }}</div>
|
||||
<div v-for="(metric, index) of legend.metric" :key="metric.name + index">
|
||||
<Selector
|
||||
class="item"
|
||||
:value="metric.name"
|
||||
:options="states.nodeMetricList"
|
||||
size="small"
|
||||
placeholder="Select a metric"
|
||||
@change="changeLegend(LegendOpt.NAME, $event, index)"
|
||||
/>
|
||||
<Selector
|
||||
class="input-small"
|
||||
:value="metric.condition"
|
||||
:options="MetricConditions"
|
||||
size="small"
|
||||
placeholder="Select a condition"
|
||||
@change="changeLegend(LegendOpt.CONDITION, $event, index)"
|
||||
/>
|
||||
<el-input
|
||||
v-model="metric.value"
|
||||
placeholder="Please input a value"
|
||||
@change="changeLegend(LegendOpt.VALUE, $event, index)"
|
||||
size="small"
|
||||
class="item"
|
||||
/>
|
||||
<span>
|
||||
<Icon
|
||||
class="cp delete"
|
||||
iconName="remove_circle_outline"
|
||||
size="middle"
|
||||
@click="deleteMetric(index)"
|
||||
v-show="legend.metric.length > 1"
|
||||
/>
|
||||
<Icon
|
||||
class="cp"
|
||||
iconName="add_circle_outlinecontrol_point"
|
||||
size="middle"
|
||||
v-show="
|
||||
index === legend.metric.length - 1 && legend.metric.length < 5
|
||||
"
|
||||
@click="addMetric"
|
||||
/>
|
||||
</span>
|
||||
<div v-show="index !== legend.metric.length - 1">&&</div>
|
||||
</div>
|
||||
<!-- <div class="label">{{ t("conditions") }}</div>
|
||||
<Selector
|
||||
class="inputs"
|
||||
:value="legend.condition"
|
||||
:options="LegendConditions"
|
||||
size="small"
|
||||
placeholder="Select a condition"
|
||||
@change="changeCondition"
|
||||
/> -->
|
||||
<el-button
|
||||
@click="setLegend"
|
||||
class="legend-btn"
|
||||
size="small"
|
||||
type="primary"
|
||||
>
|
||||
{{ t("setLegend") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { useTopologyStore } from "@/store/modules/topology";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { MetricCatalog, ScopeType, MetricConditions } from "../../../data";
|
||||
import { Option } from "@/types/app";
|
||||
import { useQueryTopologyMetrics } from "@/hooks/useProcessor";
|
||||
import { Node, Call } from "@/types/topology";
|
||||
import { EntityType, LegendOpt } from "../../../data";
|
||||
|
||||
/*global defineEmits */
|
||||
const emit = defineEmits(["update", "updateNodes"]);
|
||||
const { t } = useI18n();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const topologyStore = useTopologyStore();
|
||||
const items = reactive<
|
||||
{
|
||||
scope: string;
|
||||
dashboard: string;
|
||||
}[]
|
||||
>([{ scope: "", dashboard: "" }]);
|
||||
const states = reactive<{
|
||||
linkDashboard: string;
|
||||
nodeDashboard: {
|
||||
scope: string;
|
||||
dashboard: string;
|
||||
}[];
|
||||
linkServerMetrics: string[];
|
||||
linkClientMetrics: string[];
|
||||
nodeMetrics: string[];
|
||||
nodeMetricList: Option[];
|
||||
linkMetricList: Option[];
|
||||
}>({
|
||||
linkDashboard: "",
|
||||
nodeDashboard: [],
|
||||
linkServerMetrics: [],
|
||||
linkClientMetrics: [],
|
||||
nodeMetrics: [],
|
||||
nodeMetricList: [],
|
||||
linkMetricList: [],
|
||||
});
|
||||
const isServer = [EntityType[0].value, EntityType[1].value].includes(
|
||||
dashboardStore.entity
|
||||
);
|
||||
const legend = reactive<{
|
||||
metric: { name: string; condition: string; value: string }[];
|
||||
}>({ metric: [{ name: "", condition: "", value: "" }] });
|
||||
|
||||
getMetricList();
|
||||
async function getMetricList() {
|
||||
const json = await dashboardStore.fetchMetricList();
|
||||
if (json.errors) {
|
||||
ElMessage.error(json.errors);
|
||||
return;
|
||||
}
|
||||
const entity =
|
||||
dashboardStore.entity === EntityType[1].value
|
||||
? EntityType[0].value
|
||||
: dashboardStore.entity === EntityType[4].value
|
||||
? EntityType[3].value
|
||||
: dashboardStore.entity;
|
||||
states.nodeMetricList = (json.data.metrics || []).filter(
|
||||
(d: { catalog: string }) => entity === (MetricCatalog as any)[d.catalog]
|
||||
);
|
||||
const e =
|
||||
dashboardStore.entity === EntityType[1].value
|
||||
? EntityType[0].value
|
||||
: dashboardStore.entity === EntityType[4].value
|
||||
? EntityType[3].value
|
||||
: dashboardStore.entity;
|
||||
states.linkMetricList = (json.data.metrics || []).filter(
|
||||
(d: { catalog: string }) =>
|
||||
e + "Relation" === (MetricCatalog as any)[d.catalog]
|
||||
);
|
||||
}
|
||||
async function setLegend() {
|
||||
const metrics = legend.metric.filter(
|
||||
(d: any) => d.name && d.value && d.condition
|
||||
);
|
||||
const names = metrics.map((d: any) => d.name);
|
||||
|
||||
emit("update", {
|
||||
linkDashboard: states.linkDashboard,
|
||||
nodeDashboard: isServer
|
||||
? items.filter((d: { scope: string; dashboard: string }) => d.dashboard)
|
||||
: states.nodeDashboard,
|
||||
linkServerMetrics: states.linkServerMetrics,
|
||||
linkClientMetrics: states.linkClientMetrics,
|
||||
nodeMetrics: states.nodeMetrics,
|
||||
legend: metrics,
|
||||
});
|
||||
const ids = topologyStore.nodes.map((d: Node) => d.id);
|
||||
const param = await useQueryTopologyMetrics(names, ids);
|
||||
const res = await topologyStore.getLegendMetrics(param);
|
||||
|
||||
if (res.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
}
|
||||
emit("updateNodes");
|
||||
}
|
||||
function changeLegend(type: string, opt: any, index: number) {
|
||||
(legend.metric[index] as any)[type] = opt[0].value || opt;
|
||||
}
|
||||
function changeScope(index: number, opt: Option[]) {
|
||||
items[index].scope = opt[0].value;
|
||||
items[index].dashboard = "";
|
||||
}
|
||||
function updateNodeDashboards(index: number, content: string) {
|
||||
items[index].dashboard = content;
|
||||
updateSettings();
|
||||
}
|
||||
function addItem() {
|
||||
items.push({ scope: "", dashboard: "" });
|
||||
}
|
||||
function deleteItem(index: number) {
|
||||
items.splice(index, 1);
|
||||
updateSettings();
|
||||
}
|
||||
function updateSettings() {
|
||||
emit("update", {
|
||||
linkDashboard: states.linkDashboard,
|
||||
nodeDashboard: isServer
|
||||
? items.filter((d: { scope: string; dashboard: string }) => d.dashboard)
|
||||
: states.nodeDashboard,
|
||||
linkServerMetrics: states.linkServerMetrics,
|
||||
linkClientMetrics: states.linkClientMetrics,
|
||||
nodeMetrics: states.nodeMetrics,
|
||||
legend: legend.metric,
|
||||
});
|
||||
}
|
||||
async function changeLinkServerMetrics(options: Option[]) {
|
||||
states.linkServerMetrics = options.map((d: Option) => d.value);
|
||||
updateSettings();
|
||||
if (!states.linkServerMetrics.length) {
|
||||
topologyStore.setLinkServerMetrics({});
|
||||
return;
|
||||
}
|
||||
const idsS = topologyStore.calls
|
||||
.filter((i: Call) => i.detectPoints.includes("SERVER"))
|
||||
.map((b: Call) => b.id);
|
||||
const param = await useQueryTopologyMetrics(states.linkServerMetrics, idsS);
|
||||
const res = await topologyStore.getCallServerMetrics(param);
|
||||
|
||||
if (res.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
}
|
||||
}
|
||||
async function changeLinkClientMetrics(options: Option[]) {
|
||||
states.linkClientMetrics = options.map((d: Option) => d.value);
|
||||
updateSettings();
|
||||
if (!states.linkClientMetrics.length) {
|
||||
topologyStore.setLinkClientMetrics({});
|
||||
return;
|
||||
}
|
||||
const idsC = topologyStore.calls
|
||||
.filter((i: Call) => i.detectPoints.includes("CLIENT"))
|
||||
.map((b: Call) => b.id);
|
||||
const param = await useQueryTopologyMetrics(states.linkClientMetrics, idsC);
|
||||
const res = await topologyStore.getCallClientMetrics(param);
|
||||
|
||||
if (res.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
}
|
||||
}
|
||||
async function changeNodeMetrics(options: Option[]) {
|
||||
states.nodeMetrics = options.map((d: Option) => d.value);
|
||||
updateSettings();
|
||||
if (!states.nodeMetrics.length) {
|
||||
topologyStore.setNodeMetrics({});
|
||||
return;
|
||||
}
|
||||
const ids = topologyStore.nodes.map((d: Node) => d.id);
|
||||
const param = await useQueryTopologyMetrics(states.nodeMetrics, ids);
|
||||
const res = await topologyStore.getNodeMetrics(param);
|
||||
|
||||
if (res.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
}
|
||||
}
|
||||
function deleteMetric(index: number) {
|
||||
legend.metric.splice(index, 1);
|
||||
}
|
||||
function addMetric() {
|
||||
legend.metric.push({ name: "", condition: "", value: "" });
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.link-settings {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
margin-top: 8px;
|
||||
width: 370px;
|
||||
}
|
||||
|
||||
.item {
|
||||
width: 130px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.input-small {
|
||||
width: 45px;
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.legend-btn {
|
||||
margin: 20px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.delete {
|
||||
margin: 0 3px;
|
||||
}
|
||||
</style>
|
53
src/views/dashboard/related/topology/utils/legend.ts
Normal file
53
src/views/dashboard/related/topology/utils/legend.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
export default function topoLegend(
|
||||
graph: any,
|
||||
clientHeight: number,
|
||||
clientWidth: number,
|
||||
config: any
|
||||
) {
|
||||
for (const item of ["CUBE", "CUBEERROR"]) {
|
||||
graph
|
||||
.append("image")
|
||||
.attr("width", 30)
|
||||
.attr("height", 30)
|
||||
.attr("x", clientWidth - (item === "CUBEERROR" ? 340 : 440))
|
||||
.attr("y", clientHeight - 50)
|
||||
.attr("xlink:href", () =>
|
||||
item === "CUBEERROR" ? icons.CUBEERROR : icons.CUBE
|
||||
);
|
||||
graph
|
||||
.append("text")
|
||||
.attr("x", clientWidth - (item === "CUBEERROR" ? 310 : 410))
|
||||
.attr("y", clientHeight - 30)
|
||||
.text(() => {
|
||||
const l = config || [];
|
||||
const str = l
|
||||
.map((d: any) => `${d.name} ${d.condition} ${d.value}`)
|
||||
.join(" and ");
|
||||
return item === "CUBEERROR"
|
||||
? config
|
||||
? `Unhealthy (${str})`
|
||||
: "Unhealthy"
|
||||
: "Healthy";
|
||||
})
|
||||
.style("fill", "#efeff1")
|
||||
.style("font-size", "11px");
|
||||
}
|
||||
}
|
60
src/views/dashboard/related/topology/utils/linkElement.ts
Normal file
60
src/views/dashboard/related/topology/utils/linkElement.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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-line")
|
||||
.attr("marker-end", "url(#arrow)")
|
||||
.attr("stroke", "#217EF25f");
|
||||
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", "#217EF25f")
|
||||
.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", "6")
|
||||
.attr("markerHeight", "6")
|
||||
.attr("viewBox", "0 0 12 12")
|
||||
.attr("refX", "5")
|
||||
.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", "#217EF25f");
|
||||
return arrow;
|
||||
};
|
96
src/views/dashboard/related/topology/utils/nodeElement.ts
Normal file
96
src/views/dashboard/related/topology/utils/nodeElement.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
icons["KAFKA-CONSUMER"] = icons.KAFKA;
|
||||
export default (d3: any, graph: any, funcs: any, tip: any, legend: 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: any, d: Node) {
|
||||
tip.html(funcs.tipHtml).show(d, this);
|
||||
})
|
||||
.on("mouseout", function () {
|
||||
tip.hide(this);
|
||||
})
|
||||
.on("click", (event: any, d: Node | any) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
funcs.handleNodeClick(d);
|
||||
});
|
||||
nodeEnter
|
||||
.append("image")
|
||||
.attr("width", 49)
|
||||
.attr("height", 49)
|
||||
.attr("x", 2)
|
||||
.attr("y", 10)
|
||||
.attr("style", "cursor: move;")
|
||||
.attr("xlink:href", (d: { [key: string]: number }) => {
|
||||
if (!legend) {
|
||||
return icons.CUBE;
|
||||
}
|
||||
if (!legend.length) {
|
||||
return icons.CUBE;
|
||||
}
|
||||
let c = true;
|
||||
for (const l of legend) {
|
||||
const val = l.name.includes("_sla") ? d[l.name] / 100 : d[l.name];
|
||||
if (l.condition === "<") {
|
||||
c = c && val < Number(l.value);
|
||||
} else {
|
||||
c = c && val > Number(l.value);
|
||||
}
|
||||
}
|
||||
return c && d.isReal ? icons.CUBEERROR : icons.CUBE;
|
||||
});
|
||||
nodeEnter
|
||||
.append("image")
|
||||
.attr("width", 32)
|
||||
.attr("height", 32)
|
||||
.attr("x", 6)
|
||||
.attr("y", -10)
|
||||
.attr("style", "opacity: 0.5;")
|
||||
.attr("xlink:href", icons.LOCAL);
|
||||
nodeEnter
|
||||
.append("image")
|
||||
.attr("width", 18)
|
||||
.attr("height", 18)
|
||||
.attr("x", 13)
|
||||
.attr("y", -7)
|
||||
.attr("xlink:href", (d: { type: string }) =>
|
||||
!d.type || d.type === "N/A"
|
||||
? icons.UNDEFINED
|
||||
: icons[d.type.toUpperCase().replace("-", "")]
|
||||
);
|
||||
nodeEnter
|
||||
.append("text")
|
||||
.attr("class", "topo-text")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("x", 22)
|
||||
.attr("y", 70)
|
||||
.text((d: { name: string }) =>
|
||||
d.name.length > 20 ? `${d.name.substring(0, 20)}...` : d.name
|
||||
);
|
||||
return nodeEnter;
|
||||
};
|
56
src/views/dashboard/related/topology/utils/simulation.ts
Normal file
56
src/views/dashboard/related/topology/utils/simulation.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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 simulationInit = (
|
||||
d3: any,
|
||||
dataNodes: any,
|
||||
dataLinks: any,
|
||||
ticked: any
|
||||
) => {
|
||||
const simulation = d3
|
||||
.forceSimulation(dataNodes)
|
||||
.force(
|
||||
"collide",
|
||||
d3.forceCollide().radius(() => 60)
|
||||
)
|
||||
.force("yPos", d3.forceY().strength(1))
|
||||
.force("xPos", d3.forceX().strength(1))
|
||||
.force("charge", d3.forceManyBody().strength(-520))
|
||||
.force(
|
||||
"link",
|
||||
d3.forceLink(dataLinks).id((d: { id: string }) => d.id)
|
||||
)
|
||||
.force(
|
||||
"center",
|
||||
d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2 - 20)
|
||||
)
|
||||
.on("tick", ticked)
|
||||
.stop();
|
||||
simulationSkip(d3, simulation, ticked);
|
||||
return simulation;
|
||||
};
|
||||
|
||||
export const simulationSkip = (d3: any, simulation: any, ticked: any) => {
|
||||
d3.timeout(() => {
|
||||
const n = Math.ceil(
|
||||
Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())
|
||||
);
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
simulation.tick();
|
||||
ticked();
|
||||
}
|
||||
});
|
||||
};
|
28
src/views/dashboard/related/topology/utils/zoom.ts
Normal file
28
src/views/dashboard/related/topology/utils/zoom.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 default (d3: any, graph: any) =>
|
||||
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})`
|
||||
);
|
||||
});
|
Reference in New Issue
Block a user