feat: implement Topology on the dashboard (#14)

This commit is contained in:
Fine0830
2022-02-19 23:05:57 +08:00
committed by GitHub
parent 7472d70720
commit f53b422782
81 changed files with 4886 additions and 232 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 })"
/>

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

View File

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

View File

@@ -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 })"
/>

View File

@@ -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 })"
/>

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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