mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-07-18 14:45:25 +00:00
feat: set node metrics
This commit is contained in:
parent
9f77830a1a
commit
72080f7bd5
37
src/graphql/fetch.ts
Normal file
37
src/graphql/fetch.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* 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 axios, { AxiosResponse } from "axios";
|
||||||
|
import { cancelToken } from "@/utils/cancelToken";
|
||||||
|
|
||||||
|
async function query(param: {
|
||||||
|
queryStr: string;
|
||||||
|
conditions: { [key: string]: unknown };
|
||||||
|
}) {
|
||||||
|
const res: AxiosResponse = await axios.post(
|
||||||
|
"/graphql",
|
||||||
|
{ query: param.queryStr, variables: { ...param.conditions } },
|
||||||
|
{ cancelToken: cancelToken() }
|
||||||
|
);
|
||||||
|
if (res.data.errors) {
|
||||||
|
res.data.errors = res.data.errors
|
||||||
|
.map((e: { message: string }) => e.message)
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default query;
|
@ -27,7 +27,7 @@ const query: { [key: string]: string } = {
|
|||||||
...dashboard,
|
...dashboard,
|
||||||
...topology,
|
...topology,
|
||||||
};
|
};
|
||||||
class Graph {
|
class Graphql {
|
||||||
private queryData = "";
|
private queryData = "";
|
||||||
public query(queryData: string) {
|
public query(queryData: string) {
|
||||||
this.queryData = queryData;
|
this.queryData = queryData;
|
||||||
@ -57,4 +57,4 @@ class Graph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Graph();
|
export default new Graphql();
|
||||||
|
@ -258,3 +258,28 @@ export function usePodsSource(
|
|||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
export function useQueryNodesMetrics(metrics: string[], ids: string[]) {
|
||||||
|
const appStore = useAppStoreWithOut();
|
||||||
|
const conditions: { [key: string]: unknown } = {
|
||||||
|
duration: appStore.durationTime,
|
||||||
|
ids,
|
||||||
|
};
|
||||||
|
const variables: string[] = [`$duration: Duration!`, `$ids: [ID!]!`];
|
||||||
|
const fragmentList = metrics.map((d: string, index: number) => {
|
||||||
|
conditions[`m${index}`] = d;
|
||||||
|
variables.push(`$m${index}: String!`);
|
||||||
|
|
||||||
|
return `${d}: getValues(metric: {
|
||||||
|
name: $m${index}
|
||||||
|
ids: $ids
|
||||||
|
}, duration: $duration) {
|
||||||
|
values {
|
||||||
|
id
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
});
|
||||||
|
const queryStr = `query queryData(${variables}) {${fragmentList.join(" ")}}`;
|
||||||
|
|
||||||
|
return { queryStr, conditions };
|
||||||
|
}
|
||||||
|
@ -18,6 +18,7 @@ import { defineStore } from "pinia";
|
|||||||
import { store } from "@/store";
|
import { store } from "@/store";
|
||||||
import { LayoutConfig } from "@/types/dashboard";
|
import { LayoutConfig } from "@/types/dashboard";
|
||||||
import graphql from "@/graphql";
|
import graphql from "@/graphql";
|
||||||
|
import query from "@/graphql/fetch";
|
||||||
import {
|
import {
|
||||||
ConfigData,
|
ConfigData,
|
||||||
ConfigData1,
|
ConfigData1,
|
||||||
@ -29,8 +30,7 @@ import { useAppStoreWithOut } from "@/store/modules/app";
|
|||||||
import { useSelectorStore } from "@/store/modules/selectors";
|
import { useSelectorStore } from "@/store/modules/selectors";
|
||||||
import { NewControl } from "../data";
|
import { NewControl } from "../data";
|
||||||
import { Duration } from "@/types/app";
|
import { Duration } from "@/types/app";
|
||||||
import axios, { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { cancelToken } from "@/utils/cancelToken";
|
|
||||||
interface DashboardState {
|
interface DashboardState {
|
||||||
showConfig: boolean;
|
showConfig: boolean;
|
||||||
layout: LayoutConfig[];
|
layout: LayoutConfig[];
|
||||||
@ -212,11 +212,7 @@ export const dashboardStore = defineStore({
|
|||||||
queryStr: string;
|
queryStr: string;
|
||||||
conditions: { [key: string]: unknown };
|
conditions: { [key: string]: unknown };
|
||||||
}) {
|
}) {
|
||||||
const res: AxiosResponse = await axios.post(
|
const res: AxiosResponse = await query(param);
|
||||||
"/graphql",
|
|
||||||
{ query: param.queryStr, variables: { ...param.conditions } },
|
|
||||||
{ cancelToken: cancelToken() }
|
|
||||||
);
|
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -18,15 +18,17 @@ import { defineStore } from "pinia";
|
|||||||
import { store } from "@/store";
|
import { store } from "@/store";
|
||||||
import { Node, Call } from "@/types/topology";
|
import { Node, Call } from "@/types/topology";
|
||||||
import graphql from "@/graphql";
|
import graphql from "@/graphql";
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import { useSelectorStore } from "@/store/modules/selectors";
|
import { useSelectorStore } from "@/store/modules/selectors";
|
||||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import query from "@/graphql/fetch";
|
||||||
|
|
||||||
interface TopologyState {
|
interface TopologyState {
|
||||||
node: Node | null;
|
node: Node | null;
|
||||||
call: Call | null;
|
call: Call | null;
|
||||||
calls: Call[];
|
calls: Call[];
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
|
nodeMetrics: { id: string; value: unknown }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const topologyStore = defineStore({
|
export const topologyStore = defineStore({
|
||||||
@ -36,6 +38,7 @@ export const topologyStore = defineStore({
|
|||||||
nodes: [],
|
nodes: [],
|
||||||
node: null,
|
node: null,
|
||||||
call: null,
|
call: null,
|
||||||
|
nodeMetrics: [],
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setNode(node: Node) {
|
setNode(node: Node) {
|
||||||
@ -48,75 +51,6 @@ export const topologyStore = defineStore({
|
|||||||
this.nodes = data.nodes;
|
this.nodes = data.nodes;
|
||||||
this.calls = data.calls;
|
this.calls = data.calls;
|
||||||
},
|
},
|
||||||
async setMetrics(data: { nodes: Node[]; calls: Call[] }, params: any) {
|
|
||||||
const ids = data.nodes.map((i: Node) => i.id);
|
|
||||||
const idsC = data.calls
|
|
||||||
.filter((i: Call) => i.detectPoints.includes("CLIENT"))
|
|
||||||
.map((b: any) => b.id);
|
|
||||||
const idsS = data.calls
|
|
||||||
.filter((i: Call) => i.detectPoints.includes("SERVER"))
|
|
||||||
.map((b: any) => b.id);
|
|
||||||
const res: AxiosResponse = await graphql
|
|
||||||
.query("queryTopoInfo")
|
|
||||||
.params({ ...params, ids, idsC, idsS });
|
|
||||||
const resInfo = res.data.data;
|
|
||||||
if (!resInfo.sla) {
|
|
||||||
return this.setTopology(data);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < resInfo.sla.values.length; i += 1) {
|
|
||||||
for (let j = 0; j < data.nodes.length; j += 1) {
|
|
||||||
if (data.nodes[j].id === resInfo.sla.values[i].id) {
|
|
||||||
data.nodes[j] = {
|
|
||||||
...data.nodes[j],
|
|
||||||
isGroupActive: true,
|
|
||||||
sla: resInfo.sla.values[i].value
|
|
||||||
? resInfo.sla.values[i].value / 100
|
|
||||||
: -1,
|
|
||||||
cpm: resInfo.nodeCpm.values[i]
|
|
||||||
? resInfo.nodeCpm.values[i].value
|
|
||||||
: -1,
|
|
||||||
latency: resInfo.nodeLatency.values[i]
|
|
||||||
? resInfo.nodeLatency.values[i].value
|
|
||||||
: -1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!resInfo.cpmC) {
|
|
||||||
return this.setTopology(data);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < resInfo.cpmC.values.length; i += 1) {
|
|
||||||
for (let j = 0; j < data.calls.length; j += 1) {
|
|
||||||
if (data.calls[j].id === resInfo.cpmC.values[i].id) {
|
|
||||||
data.calls[j] = {
|
|
||||||
...data.calls[j],
|
|
||||||
isGroupActive: true,
|
|
||||||
cpm: resInfo.cpmC.values[i] ? resInfo.cpmC.values[i].value : "",
|
|
||||||
latency: resInfo.latencyC.values[i]
|
|
||||||
? resInfo.latencyC.values[i].value
|
|
||||||
: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!resInfo.cpmS) {
|
|
||||||
return this.setTopology(data);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < resInfo.cpmS.values.length; i += 1) {
|
|
||||||
for (let j = 0; j < data.calls.length; j += 1) {
|
|
||||||
if (data.calls[j].id === resInfo.cpmS.values[i].id) {
|
|
||||||
data.calls[j] = {
|
|
||||||
...data.calls[j],
|
|
||||||
cpm: resInfo.cpmS.values[i] ? resInfo.cpmS.values[i].value : "",
|
|
||||||
latency: resInfo.latencyS.values[i]
|
|
||||||
? resInfo.latencyS.values[i].value
|
|
||||||
: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.setTopology(data);
|
|
||||||
},
|
|
||||||
async getServiceTopology() {
|
async getServiceTopology() {
|
||||||
const serviceId = useSelectorStore().currentService.id;
|
const serviceId = useSelectorStore().currentService.id;
|
||||||
const duration = useAppStoreWithOut().durationTime;
|
const duration = useAppStoreWithOut().durationTime;
|
||||||
@ -127,7 +61,7 @@ export const topologyStore = defineStore({
|
|||||||
duration,
|
duration,
|
||||||
});
|
});
|
||||||
if (!res.data.errors) {
|
if (!res.data.errors) {
|
||||||
this.setMetrics(res.data.data.topology, { duration });
|
this.setTopology(res.data.data.topology);
|
||||||
}
|
}
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
@ -139,7 +73,7 @@ export const topologyStore = defineStore({
|
|||||||
duration,
|
duration,
|
||||||
});
|
});
|
||||||
if (!res.data.errors) {
|
if (!res.data.errors) {
|
||||||
this.setMetrics(res.data.data.topology, { duration });
|
this.setTopology(res.data.data.topology);
|
||||||
}
|
}
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
@ -173,6 +107,18 @@ export const topologyStore = defineStore({
|
|||||||
}
|
}
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
|
async getNodeMetrics(param: {
|
||||||
|
queryStr: string;
|
||||||
|
conditions: { [key: string]: unknown };
|
||||||
|
}) {
|
||||||
|
const res: AxiosResponse = await query(param);
|
||||||
|
|
||||||
|
if (res.data.errors) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
this.nodeMetrics = res.data.data;
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -122,8 +122,9 @@ onMounted(async () => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
topologyStore.setNode(null);
|
topologyStore.setNode(null);
|
||||||
showSetting.value = false;
|
// showSetting.value = false;
|
||||||
});
|
});
|
||||||
|
update();
|
||||||
});
|
});
|
||||||
function ticked() {
|
function ticked() {
|
||||||
link.value.attr(
|
link.value.attr(
|
||||||
@ -182,6 +183,9 @@ function handleLinkClick(event: any, d: Call) {
|
|||||||
}
|
}
|
||||||
function update() {
|
function update() {
|
||||||
// node element
|
// node element
|
||||||
|
if (!node.value || !link.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
node.value = node.value.data(topologyStore.nodes, (d: Node) => d.id);
|
node.value = node.value.data(topologyStore.nodes, (d: Node) => d.id);
|
||||||
node.value.exit().remove();
|
node.value.exit().remove();
|
||||||
node.value = nodeElement(
|
node.value = nodeElement(
|
||||||
@ -192,9 +196,25 @@ function update() {
|
|||||||
dragged: dragged,
|
dragged: dragged,
|
||||||
dragended: dragended,
|
dragended: dragended,
|
||||||
handleNodeClick: handleNodeClick,
|
handleNodeClick: handleNodeClick,
|
||||||
|
tipHtml: (data: Node) => {
|
||||||
|
const nodeMetrics: string[] = settings.value.nodeMetrics;
|
||||||
|
if (!nodeMetrics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const html = nodeMetrics.map((m) => {
|
||||||
|
const metric =
|
||||||
|
topologyStore.nodeMetrics[m].values.filter(
|
||||||
|
(val: { id: string; value: unknown }) => val.id === data.id
|
||||||
|
)[0] || {};
|
||||||
|
return ` <div class="mb-5"><span class="grey">${m}: </span>${metric.value}</div>`;
|
||||||
|
});
|
||||||
|
return [
|
||||||
|
` <div class="mb-5"><span class="grey">name: </span>${data.name}</div>`,
|
||||||
|
...html,
|
||||||
|
].join(" ");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
tip.value,
|
tip.value
|
||||||
t
|
|
||||||
).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);
|
||||||
@ -207,7 +227,7 @@ function update() {
|
|||||||
anchor.value.enter(),
|
anchor.value.enter(),
|
||||||
{
|
{
|
||||||
handleLinkClick: handleLinkClick,
|
handleLinkClick: handleLinkClick,
|
||||||
$tip: (data: any) =>
|
$tip: (data: Call) =>
|
||||||
`
|
`
|
||||||
<div class="mb-5"><span class="grey">${t("cpm")}: </span>${
|
<div class="mb-5"><span class="grey">${t("cpm")}: </span>${
|
||||||
data.cpm
|
data.cpm
|
||||||
@ -254,22 +274,19 @@ function update() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function handleGoEndpoint() {
|
function handleGoEndpoint() {
|
||||||
const node = topologyStore.node;
|
const path = `/dashboard/${dashboardStore.layerId}/Endpoint/${topologyStore.node.id}/${settings.value.endpointDashboard}`;
|
||||||
const path = `/dashboard/${dashboardStore.layerId}/Endpoint/${node.id}/${settings.value.endpointDashboard}`;
|
|
||||||
const routeUrl = router.resolve({ path });
|
const routeUrl = router.resolve({ path });
|
||||||
|
|
||||||
window.open(routeUrl.href, "_blank");
|
window.open(routeUrl.href, "_blank");
|
||||||
}
|
}
|
||||||
function handleGoInstance() {
|
function handleGoInstance() {
|
||||||
const node = topologyStore.node;
|
const path = `/dashboard/${dashboardStore.layerId}/ServiceInstance/${topologyStore.node.id}/${settings.value.instanceDashboard}`;
|
||||||
const path = `/dashboard/${dashboardStore.layerId}/ServiceInstance/${node.id}/${settings.value.instanceDashboard}`;
|
|
||||||
const routeUrl = router.resolve({ path });
|
const routeUrl = router.resolve({ path });
|
||||||
|
|
||||||
window.open(routeUrl.href, "_blank");
|
window.open(routeUrl.href, "_blank");
|
||||||
}
|
}
|
||||||
function handleGoDashboard() {
|
function handleGoDashboard() {
|
||||||
const node = topologyStore.node;
|
const path = `/dashboard/${dashboardStore.layerId}/Service/${topologyStore.node.id}/${settings.value.nodeDashboard}`;
|
||||||
const path = `/dashboard/${dashboardStore.layerId}/Service/${node.id}/${settings.value.nodeDashboard}`;
|
|
||||||
const routeUrl = router.resolve({ path });
|
const routeUrl = router.resolve({ path });
|
||||||
|
|
||||||
window.open(routeUrl.href, "_blank");
|
window.open(routeUrl.href, "_blank");
|
||||||
@ -312,12 +329,12 @@ function updateSettings(config: any) {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener("resize", resize);
|
window.removeEventListener("resize", resize);
|
||||||
});
|
});
|
||||||
watch(
|
// watch(
|
||||||
() => [topologyStore.calls, topologyStore.nodes],
|
// () => [topologyStore.calls, topologyStore.nodes],
|
||||||
() => {
|
// () => {
|
||||||
update();
|
// update();
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.micro-topo-chart {
|
.micro-topo-chart {
|
||||||
|
@ -76,14 +76,18 @@ limitations under the License. -->
|
|||||||
import { reactive } 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 { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { MetricCatalog } from "../../data";
|
import { MetricCatalog } from "../../data";
|
||||||
import { Option } from "@/types/app";
|
import { Option } from "@/types/app";
|
||||||
|
import { useQueryNodesMetrics } from "@/hooks/useProcessor";
|
||||||
|
import { Node, Call } from "@/types/topology";
|
||||||
|
|
||||||
/*global defineEmits */
|
/*global defineEmits */
|
||||||
const emit = defineEmits(["update"]);
|
const emit = defineEmits(["update"]);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const dashboardStore = useDashboardStore();
|
const dashboardStore = useDashboardStore();
|
||||||
|
const topologyStore = useTopologyStore();
|
||||||
const states = reactive<{
|
const states = reactive<{
|
||||||
linkDashboard: string;
|
linkDashboard: string;
|
||||||
nodeDashboard: string;
|
nodeDashboard: string;
|
||||||
@ -130,13 +134,21 @@ function updateSettings() {
|
|||||||
nodeMetrics: states.nodeMetrics,
|
nodeMetrics: states.nodeMetrics,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function changeLinkMetrics(options: Option[]) {
|
async function changeLinkMetrics(options: Option[]) {
|
||||||
states.linkMetrics = options.map((d: Option) => d.value);
|
states.linkMetrics = options.map((d: Option) => d.value);
|
||||||
updateSettings();
|
updateSettings();
|
||||||
}
|
}
|
||||||
function changeNodeMetrics(options: Option[]) {
|
async function changeNodeMetrics(options: Option[]) {
|
||||||
states.nodeMetrics = options.map((d: Option) => d.value);
|
states.nodeMetrics = options.map((d: Option) => d.value);
|
||||||
updateSettings();
|
updateSettings();
|
||||||
|
|
||||||
|
const ids = topologyStore.nodes.map((d: Node) => d.id);
|
||||||
|
const param = await useQueryNodesMetrics(states.nodeMetrics, ids);
|
||||||
|
const res = await topologyStore.getNodeMetrics(param);
|
||||||
|
|
||||||
|
if (res.errors) {
|
||||||
|
ElMessage.error(res.errors);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -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, t: any) => {
|
export default (d3: any, graph: any, funcs: any, tip: any) => {
|
||||||
const nodeEnter = graph
|
const nodeEnter = graph
|
||||||
.append("g")
|
.append("g")
|
||||||
.call(
|
.call(
|
||||||
@ -29,22 +29,7 @@ export default (d3: any, graph: any, funcs: any, tip: any, t: any) => {
|
|||||||
.on("end", funcs.dragended)
|
.on("end", funcs.dragended)
|
||||||
)
|
)
|
||||||
.on("mouseover", function (event: any, d: Node) {
|
.on("mouseover", function (event: any, d: Node) {
|
||||||
tip
|
tip.html(funcs.tipHtml).show(d, this);
|
||||||
.html(() => {
|
|
||||||
return `
|
|
||||||
<div class="mb-5"><span class="grey">${t("name")}: </span>${
|
|
||||||
d.name
|
|
||||||
}</div>
|
|
||||||
<div class="mb-5"><span class="grey">${t("cpm")}: </span>${
|
|
||||||
d.cpm
|
|
||||||
}</div>
|
|
||||||
<div class="mb-5"><span class="grey">${t("latency")}: </span>${
|
|
||||||
d.latency
|
|
||||||
}</div>
|
|
||||||
<div><span class="grey">${t("sla")}: </span>${d.sla}</div>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.show(d, this);
|
|
||||||
})
|
})
|
||||||
.on("mouseout", function () {
|
.on("mouseout", function () {
|
||||||
tip.hide(this);
|
tip.hide(this);
|
||||||
|
Loading…
Reference in New Issue
Block a user