feat: set topology legend

This commit is contained in:
Qiuxia Fan 2022-02-17 20:41:48 +08:00
parent 83805b614f
commit 6bccb79eaf
8 changed files with 222 additions and 44 deletions

View File

@ -84,6 +84,8 @@ const msg = {
endpointDashboard: "Dashboard name related with endpoints", endpointDashboard: "Dashboard name related with endpoints",
callSettings: "Call settings", callSettings: "Call settings",
nodeSettings: "Node Settings", nodeSettings: "Node Settings",
conditions: "Conditions",
legendSettings: "Legend Settings",
hourTip: "Select Hour", hourTip: "Select Hour",
minuteTip: "Select Minute", minuteTip: "Select Minute",
secondTip: "Select Second", secondTip: "Select Second",

View File

@ -83,6 +83,8 @@ const msg = {
endpointDashboard: "拓节点端点的实例的仪表板名称", endpointDashboard: "拓节点端点的实例的仪表板名称",
callSettings: "拓扑线设置", callSettings: "拓扑线设置",
nodeSettings: "拓扑点设置", nodeSettings: "拓扑点设置",
conditions: "条件",
legendSettings: "图例设置",
hourTip: "选择小时", hourTip: "选择小时",
minuteTip: "选择分钟", minuteTip: "选择分钟",
secondTip: "选择秒数", secondTip: "选择秒数",

View File

@ -344,6 +344,29 @@ export const topologyStore = defineStore({
this.setNodeMetrics(res.data.data); this.setNodeMetrics(res.data.data);
return res.data; return res.data;
}, },
async getLegendMetrics(param: {
queryStr: string;
conditions: { [key: string]: unknown };
}) {
const res: AxiosResponse = await query(param);
if (res.data.errors) {
return res.data;
}
const data = res.data.data;
const metrics = Object.keys(data);
this.nodes = this.nodes.map((d: Node | any) => {
for (const m of metrics) {
for (const val of data[m].values) {
if (d.id === val.id) {
d[m] = val.value;
}
}
}
return d;
});
return res.data;
},
async getCallServerMetrics(param: { async getCallServerMetrics(param: {
queryStr: string; queryStr: string;
conditions: { [key: string]: unknown }; conditions: { [key: string]: unknown };

View File

@ -177,3 +177,16 @@ export const ScopeType = [
{ value: "Endpoint", label: "Endpoint", key: 3 }, { value: "Endpoint", label: "Endpoint", key: 3 },
{ value: "ServiceInstance", label: "Service Instance", 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",
}

View File

@ -20,18 +20,18 @@ limitations under the License. -->
:style="`height: ${height}px`" :style="`height: ${height}px`"
> >
<div class="setting" v-show="showSetting"> <div class="setting" v-show="showSetting">
<Settings @update="updateSettings" /> <Settings @update="updateSettings" @updateNodes="freshNodes" />
</div> </div>
<div class="tool"> <div class="tool">
<span class="switch-icon ml-5" title="Settings"> <span class="switch-icon ml-5" title="Settings" @click="setConfig">
<Icon @click="setConfig" size="middle" iconName="settings" /> <Icon size="middle" iconName="settings" />
</span> </span>
<span class="switch-icon ml-5" title="Back to overview topology"> <span
<Icon class="switch-icon ml-5"
@click="backToTopology" title="Back to overview topology"
size="middle" @click="backToTopology"
iconName="keyboard_backspace" >
/> <Icon size="middle" iconName="keyboard_backspace" />
</span> </span>
</div> </div>
<div <div
@ -239,7 +239,10 @@ function update() {
topologyStore.nodeMetrics[m].values.filter( topologyStore.nodeMetrics[m].values.filter(
(val: { id: string; value: unknown }) => val.id === data.id (val: { id: string; value: unknown }) => val.id === data.id
)[0] || {}; )[0] || {};
return ` <div class="mb-5"><span class="grey">${m}: </span>${metric.value}</div>`; const val = m.includes("_sla")
? metric.value / 100
: metric.value.value;
return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</div>`;
}); });
return [ return [
` <div class="mb-5"><span class="grey">name: </span>${data.name}</div>`, ` <div class="mb-5"><span class="grey">name: </span>${data.name}</div>`,
@ -247,7 +250,8 @@ function update() {
].join(" "); ].join(" ");
}, },
}, },
tip.value tip.value,
settings.value.legend
).merge(node.value); ).merge(node.value);
// line element // line element
link.value = link.value.data(topologyStore.calls, (d: Call) => d.id); link.value = link.value.data(topologyStore.calls, (d: Call) => d.id);
@ -270,7 +274,8 @@ function update() {
(val: { id: string; value: unknown }) => val.id === data.id (val: { id: string; value: unknown }) => val.id === data.id
)[0]; )[0];
if (metric) { if (metric) {
return ` <div class="mb-5"><span class="grey">${m}: </span>${metric.value}</div>`; 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 htmlClient = linkClientMetrics.map((m) => {
@ -278,7 +283,8 @@ function update() {
(val: { id: string; value: unknown }) => val.id === data.id (val: { id: string; value: unknown }) => val.id === data.id
)[0]; )[0];
if (metric) { if (metric) {
return ` <div class="mb-5"><span class="grey">${m}: </span>${metric.value}</div>`; const val = m.includes("_sla") ? metric.value / 100 : metric.value;
return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</div>`;
} }
}); });
const html = [ const html = [
@ -433,6 +439,11 @@ function updateSettings(config: any) {
} }
} }
} }
async function freshNodes() {
svg.value.selectAll(".topo-svg-graph").remove();
await init();
update();
}
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener("resize", resize); window.removeEventListener("resize", resize);
}); });
@ -445,7 +456,7 @@ onBeforeUnmount(() => {
position: absolute; position: absolute;
top: 70px; top: 70px;
right: 0; right: 0;
width: 360px; width: 380px;
height: 700px; height: 700px;
background-color: #2b3037; background-color: #2b3037;
overflow: auto; overflow: auto;

View File

@ -24,15 +24,15 @@ limitations under the License. -->
@change="changeDepth" @change="changeDepth"
/> />
</span> </span>
<span class="switch-icon ml-5" title="Settings"> <span class="switch-icon ml-5" title="Settings" @click="setConfig">
<Icon @click="setConfig" size="middle" iconName="settings" /> <Icon size="middle" iconName="settings" />
</span> </span>
<span class="switch-icon ml-5" title="Back to overview topology"> <span
<Icon class="switch-icon ml-5"
@click="backToTopology" title="Back to overview topology"
size="middle" @click="backToTopology"
iconName="keyboard_backspace" >
/> <Icon size="middle" iconName="keyboard_backspace" />
</span> </span>
<div class="settings" v-show="showSettings"> <div class="settings" v-show="showSettings">
<Settings @update="updateConfig" /> <Settings @update="updateConfig" />

View File

@ -58,30 +58,30 @@ limitations under the License. -->
size="small" size="small"
placeholder="Select a scope" placeholder="Select a scope"
@change="changeScope(index, $event)" @change="changeScope(index, $event)"
class="item" class="item mr-5"
/> />
<el-input <el-input
v-model="item.dashboard" v-model="item.dashboard"
placeholder="Please input a dashboard name for nodes" placeholder="Please input a dashboard name for nodes"
@change="updateNodeDashboards(index, $event)" @change="updateNodeDashboards(index, $event)"
size="small" size="small"
class="item" class="item mr-5"
/> />
<span> <span>
<Icon <Icon
class="cp mr-5" class="cp mr-5"
v-show="index === items.length - 1 && items.length < 5"
iconName="add_circle_outlinecontrol_point"
size="middle"
@click="addItem"
/>
<Icon
class="cp"
v-show="items.length > 1" v-show="items.length > 1"
iconName="remove_circle_outline" iconName="remove_circle_outline"
size="middle" size="middle"
@click="deleteItem(index)" @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> </span>
</div> </div>
<div class="label">{{ t("nodeMetrics") }}</div> <div class="label">{{ t("nodeMetrics") }}</div>
@ -95,21 +95,94 @@ limitations under the License. -->
@change="changeNodeMetrics" @change="changeNodeMetrics"
/> />
</div> </div>
<div class="legend-settings">
<h5 class="title">{{ t("legendSettings") }}</h5>
<div class="label">{{ t("metrics") }}</div>
<Selector
class="item mr-5"
:value="legend.metric.name"
:options="states.nodeMetricList"
size="small"
placeholder="Select a metric"
@change="changeLegend(LegendOpt.NAME, $event)"
/>
<Selector
class="input-small mr-5"
:value="legend.metric.condition"
:options="MetricConditions"
size="small"
placeholder="Select a condition"
@change="changeLegend(LegendOpt.CONDITION, $event)"
/>
<el-input
v-model="legend.metric.value"
placeholder="Please input a value"
@change="changeLegend(LegendOpt.VALUE, $event)"
size="small"
class="item"
/>
<!-- <div class="label">{{ t("conditions") }}</div>
<Selector
class="inputs"
:value="legend.condition"
:options="LegendConditions"
size="small"
placeholder="Select a condition"
@change="changeCondition"
/> -->
<!-- <div class="label">{{ t("metrics") }}</div>
<Selector
class="item mr-5"
:value="legend.secondMetric.name"
:options="states.nodeMetricList"
size="small"
placeholder="Select a metric"
@change="changeLegendMetric(LegendOpt.NAME, $event)"
/>
<Selector
class="input-small mr-5"
:value="legend.secondMetric.value"
:options="states.nodeMetricList"
size="small"
placeholder="Select a metric"
@change="changeLegendMetric(LegendOpt.CONDITION, $event)"
/>
<el-input
v-model="legend.secondMetric.condidtion"
placeholder="Please input a value"
@change="changeLegendMetric(LegendOpt.VALUE, $event)"
size="small"
class="item"
/> -->
<el-button
@click="setLegend"
class="legend-btn"
size="small"
type="primary"
>
set legend
</el-button>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref } from "vue"; import { reactive } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useTopologyStore } from "@/store/modules/topology"; import { useTopologyStore } from "@/store/modules/topology";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { MetricCatalog, ScopeType } from "../../../data"; import {
MetricCatalog,
ScopeType,
MetricConditions,
LegendConditions,
} from "../../../data";
import { Option } from "@/types/app"; import { Option } from "@/types/app";
import { useQueryTopologyMetrics } from "@/hooks/useProcessor"; import { useQueryTopologyMetrics } from "@/hooks/useProcessor";
import { Node, Call } from "@/types/topology"; import { Node, Call } from "@/types/topology";
import { EntityType } from "../../../data"; import { EntityType, LegendOpt } from "../../../data";
/*global defineEmits */ /*global defineEmits */
const emit = defineEmits(["update"]); const emit = defineEmits(["update", "updateNodes"]);
const { t } = useI18n(); const { t } = useI18n();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const topologyStore = useTopologyStore(); const topologyStore = useTopologyStore();
@ -121,7 +194,10 @@ const items = reactive<
>([{ scope: "", dashboard: "" }]); >([{ scope: "", dashboard: "" }]);
const states = reactive<{ const states = reactive<{
linkDashboard: string; linkDashboard: string;
nodeDashboard: string; nodeDashboard: {
scope: string;
dashboard: string;
}[];
linkServerMetrics: string[]; linkServerMetrics: string[];
linkClientMetrics: string[]; linkClientMetrics: string[];
nodeMetrics: string[]; nodeMetrics: string[];
@ -129,13 +205,18 @@ const states = reactive<{
linkMetricList: Option[]; linkMetricList: Option[];
}>({ }>({
linkDashboard: "", linkDashboard: "",
nodeDashboard: "", nodeDashboard: [],
linkServerMetrics: [], linkServerMetrics: [],
linkClientMetrics: [], linkClientMetrics: [],
nodeMetrics: [], nodeMetrics: [],
nodeMetricList: [], nodeMetricList: [],
linkMetricList: [], linkMetricList: [],
}); });
const legend = reactive<{
metric: any;
condition: string;
secondMetric: any;
}>({ metric: {}, condition: "", secondMetric: {} });
getMetricList(); getMetricList();
async function getMetricList() { async function getMetricList() {
@ -159,6 +240,26 @@ async function getMetricList() {
e + "Relation" === (MetricCatalog as any)[d.catalog] e + "Relation" === (MetricCatalog as any)[d.catalog]
); );
} }
async function setLegend() {
updateSettings();
const ids = topologyStore.nodes.map((d: Node) => d.id);
const param = await useQueryTopologyMetrics([legend.metric.name], ids);
const res = await topologyStore.getLegendMetrics(param);
if (res.errors) {
ElMessage.error(res.errors);
}
emit("updateNodes");
}
function changeLegend(type: string, opt: any) {
legend.metric[type] = opt[0].value || opt;
}
function changeCondition(opt: Option[]) {
legend.condition = opt[0].value;
}
function changeLegendMetric(type: string, opt: any) {
legend.secondMetric[type] = opt[0].value || opt;
}
function changeScope(index: number, opt: Option[]) { function changeScope(index: number, opt: Option[]) {
items[index].scope = opt[0].value; items[index].scope = opt[0].value;
items[index].dashboard = ""; items[index].dashboard = "";
@ -183,6 +284,7 @@ function updateSettings() {
linkServerMetrics: states.linkServerMetrics, linkServerMetrics: states.linkServerMetrics,
linkClientMetrics: states.linkClientMetrics, linkClientMetrics: states.linkClientMetrics,
nodeMetrics: states.nodeMetrics, nodeMetrics: states.nodeMetrics,
legend,
}); });
} }
async function changeLinkServerMetrics(options: Option[]) { async function changeLinkServerMetrics(options: Option[]) {
@ -242,12 +344,16 @@ async function changeNodeMetrics(options: Option[]) {
.inputs { .inputs {
margin-top: 8px; margin-top: 8px;
width: 330px; width: 350px;
} }
.item { .item {
width: 137px; width: 140px;
margin: 5px 5px 0 0; margin-top: 5px;
}
.input-small {
width: 60px;
} }
.title { .title {
@ -258,4 +364,9 @@ async function changeNodeMetrics(options: Option[]) {
font-size: 12px; font-size: 12px;
margin-top: 10px; margin-top: 10px;
} }
.legend-btn {
margin-top: 20px;
cursor: pointer;
}
</style> </style>

View File

@ -18,7 +18,7 @@ import icons from "@/assets/img/icons";
import { Node } from "@/types/topology"; import { Node } from "@/types/topology";
icons["KAFKA-CONSUMER"] = icons.KAFKA; icons["KAFKA-CONSUMER"] = icons.KAFKA;
export default (d3: any, graph: any, funcs: any, tip: any) => { export default (d3: any, graph: any, funcs: any, tip: any, legend: any) => {
const nodeEnter = graph const nodeEnter = graph
.append("g") .append("g")
.call( .call(
@ -46,9 +46,25 @@ export default (d3: any, graph: any, funcs: any, tip: any) => {
.attr("x", 2) .attr("x", 2)
.attr("y", 10) .attr("y", 10)
.attr("style", "cursor: move;") .attr("style", "cursor: move;")
.attr("xlink:href", (d: { isReal: number; sla: number; cpm: number }) => .attr("xlink:href", (d: { [key: string]: number }) => {
d.sla < 95 && d.isReal && d.cpm > 1 ? icons.CUBEERROR : icons.CUBE if (!legend) {
); return icons.CUBE;
}
const val = legend.metric.name.includes("_sla")
? d[legend.metric.name] / 100
: d[legend.metric.name];
if (legend.metric.condition === "<") {
return val < Number(legend.metric.value) && d.isReal
? icons.CUBEERROR
: icons.CUBE;
}
if (legend.metric.condition === ">") {
return val > Number(legend.metric.value) && d.isReal
? icons.CUBEERROR
: icons.CUBE;
}
return icons.CUBE;
});
nodeEnter nodeEnter
.append("image") .append("image")
.attr("width", 32) .attr("width", 32)