From 99b5083ea58cf415dcc360f7bb1627d4921d7fb1 Mon Sep 17 00:00:00 2001 From: Fine0830 Date: Wed, 29 Apr 2026 20:58:36 +0800 Subject: [PATCH] fix: add templates to optimize log content and update topology tooltips (#550) --- .../related/log/LogTable/LogService.vue | 50 +++++++- .../components/ProcessTopology.vue | 32 +++-- .../related/topology/pod/InstanceMap.vue | 24 ++-- .../related/topology/service/HierarchyMap.vue | 26 ++-- .../related/topology/service/ServiceMap.vue | 117 +++++++++++------- 5 files changed, 174 insertions(+), 75 deletions(-) diff --git a/src/views/dashboard/related/log/LogTable/LogService.vue b/src/views/dashboard/related/log/LogTable/LogService.vue index c04b4980..549029e6 100644 --- a/src/views/dashboard/related/log/LogTable/LogService.vue +++ b/src/views/dashboard/related/log/LogTable/LogService.vue @@ -27,7 +27,12 @@ limitations under the License. --> - + + + @@ -59,10 +64,41 @@ limitations under the License. --> } return (props.data.tags.find((d: { key: string; value: string }) => d.key === "level") || {}).value || ""; }); - const highlightKeywords = (content: string) => { - const keywords = Object.values(logStore.conditions.keywordsOfContent || {}); - const regex = new RegExp(keywords.join("|"), "gi"); - return `${content}`.replace(regex, (match) => `${match}`); + type HighlightPart = { + text: string; + highlight: boolean; + }; + + const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + + const highlightKeywords = (content: string): HighlightPart[] => { + const text = `${content || ""}`; + const keywords = [ + ...new Set(Object.values(logStore.conditions.keywordsOfContent || {}).map((keyword) => `${keyword}`.trim())), + ].filter(Boolean); + + if (!keywords.length) { + return [{ text, highlight: false }]; + } + + const regex = new RegExp(keywords.map(escapeRegExp).join("|"), "gi"); + const parts: HighlightPart[] = []; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = regex.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push({ text: text.slice(lastIndex, match.index), highlight: false }); + } + parts.push({ text: match[0], highlight: true }); + lastIndex = regex.lastIndex; + } + + if (lastIndex < text.length) { + parts.push({ text: text.slice(lastIndex), highlight: false }); + } + + return parts.length ? parts : [{ text, highlight: false }]; }; function getDataValue(label: string) { @@ -165,4 +201,8 @@ limitations under the License. --> .warning { color: var(--sw-orange); } + + .keyword-highlight { + color: var(--sw-red); + } diff --git a/src/views/dashboard/related/network-profiling/components/ProcessTopology.vue b/src/views/dashboard/related/network-profiling/components/ProcessTopology.vue index d4a0f28b..e25191e6 100644 --- a/src/views/dashboard/related/network-profiling/components/ProcessTopology.vue +++ b/src/views/dashboard/related/network-profiling/components/ProcessTopology.vue @@ -461,14 +461,28 @@ limitations under the License. --> } } - function showNodeTip(d: ProcessNode, event: MouseEvent) { - const tipHtml = `
name: ${d.name}
`; + type TooltipRow = { + label: string; + value: unknown; + }; + function renderTooltipRows(rows: TooltipRow[]) { + tooltip.value.html(""); + const row = tooltip.value.selectAll("div").data(rows).enter().append("div").attr("class", "mb-5"); + + row + .append("span") + .attr("class", "grey") + .text((d: TooltipRow) => `${d.label}: `); + row.append("span").text((d: TooltipRow) => `${d.value ?? ""}`); + } + + function showNodeTip(d: ProcessNode, event: MouseEvent) { tooltip.value .style("top", event.offsetY + "px") .style("left", event.offsetX + "px") - .style("visibility", "visible") - .html(tipHtml); + .style("visibility", "visible"); + renderTooltipRows([{ label: "name", value: d.name }]); } function hideNodeTip() { @@ -487,14 +501,14 @@ limitations under the License. --> if (types.includes("tls")) { l = "TLS"; } - const tipHtml = `
${t("detectPoint")}: ${link.detectPoints.join(" | ")}
-
Type: ${l}
`; - tooltip.value .style("top", event.offsetY + "px") .style("left", event.offsetX + "px") - .style("visibility", "visible") - .html(tipHtml); + .style("visibility", "visible"); + renderTooltipRows([ + { label: t("detectPoint"), value: link.detectPoints.join(" | ") }, + { label: "Type", value: l }, + ]); } function hideLinkTip() { diff --git a/src/views/dashboard/related/topology/pod/InstanceMap.vue b/src/views/dashboard/related/topology/pod/InstanceMap.vue index 01deabd6..f4d2e705 100644 --- a/src/views/dashboard/related/topology/pod/InstanceMap.vue +++ b/src/views/dashboard/related/topology/pod/InstanceMap.vue @@ -122,27 +122,33 @@ limitations under the License. --> ).dashboard || {}; const exprssions = dashboard.expressions || []; const nodeMetricConfig = dashboard.expressionsConfig || []; - const html = exprssions.map((m: string, index: number) => { + const metrics = exprssions.map((m: string, index: number) => { const metric = topologyStore.hierarchyInstanceNodeMetrics[data.layer || ""][m].values.find( (val: { id: string; value: unknown }) => val.id === data.id, ) || null; const opt: MetricConfigOpt = nodeMetricConfig[index] || {}; - return `
${opt.label || m}: ${metric?.value || NaN} ${ - opt.unit || "" - }
`; + return { + label: opt.label || m, + value: `${metric?.value || NaN} ${opt.unit || ""}`, + }; }); - const tipHtml = [ - `
name: ${data.name}
layer: ${data.layer}
`, - ...html, - ].join(" "); popover.value .style("top", event.offsetY + 10 + "px") .style("left", event.offsetX + 10 + "px") .style("visibility", "visible") - .html(tipHtml); + .html(""); + + const rows = [{ label: "name", value: data.name }, { label: "layer", value: data.layer }, ...metrics]; + + const row = popover.value.selectAll("div").data(rows).enter().append("div").attr("class", "mb-5"); + row + .append("span") + .attr("class", "grey") + .text((d: { label: string }) => `${d.label}: `); + row.append("span").text((d: { value: unknown }) => `${d.value ?? ""}`); } function hideTip() { diff --git a/src/views/dashboard/related/topology/service/HierarchyMap.vue b/src/views/dashboard/related/topology/service/HierarchyMap.vue index e6a8a65a..7c2c81ef 100644 --- a/src/views/dashboard/related/topology/service/HierarchyMap.vue +++ b/src/views/dashboard/related/topology/service/HierarchyMap.vue @@ -130,26 +130,32 @@ limitations under the License. --> ).dashboard || {}; const exprssions = dashboard.expressions || []; const nodeMetricConfig = dashboard.expressionsConfig || []; - const html = exprssions.map((m: string, index: number) => { + const metrics = exprssions.map((m: string, index: number) => { const metric = topologyStore.hierarchyNodeMetrics[data.layer || ""][m].values.find( (val: { id: string; value: unknown }) => val.id === data.id, ) || null; const opt: MetricConfigOpt = nodeMetricConfig[index] || {}; - return `
${opt.label || m}: ${metric?.value || NaN} ${ - opt.unit || "" - }
`; - }); - const tipHtml = [ - `
name: ${data.name}
layer: ${data.layer}
`, - ...html, - ].join(" "); + return { + label: opt.label || m, + value: `${metric?.value || NaN} ${opt.unit || ""}`, + }; + }); popover.value .style("top", event.offsetY + 10 + "px") .style("left", event.offsetX + 10 + "px") .style("visibility", "visible") - .html(tipHtml); + .html(""); + + const rows = [{ label: "name", value: data.name }, { label: "layer", value: data.layer }, ...metrics]; + + const row = popover.value.selectAll("div").data(rows).enter().append("div").attr("class", "mb-5"); + row + .append("span") + .attr("class", "grey") + .text((d: { label: string }) => `${d.label}: `); + row.append("span").text((d: { value: unknown }) => `${d.value ?? ""}`); } function hideTip() { diff --git a/src/views/dashboard/related/topology/service/ServiceMap.vue b/src/views/dashboard/related/topology/service/ServiceMap.vue index eae23fbe..1c241d38 100644 --- a/src/views/dashboard/related/topology/service/ServiceMap.vue +++ b/src/views/dashboard/related/topology/service/ServiceMap.vue @@ -304,70 +304,103 @@ limitations under the License. --> } return Number(d[legendMQE.expression]) && d.isReal ? icons.CUBEERROR : icons.CUBE; } + + type TooltipRow = { + label: string; + value: unknown; + }; + + function isTooltipRow(row: TooltipRow | null): row is TooltipRow { + return Boolean(row); + } + + function renderTooltipRows(rows: TooltipRow[]) { + tooltip.value.html(""); + const row = tooltip.value.selectAll("div").data(rows).enter().append("div").attr("class", "mb-5"); + + row + .append("span") + .attr("class", "grey") + .text((d: TooltipRow) => `${d.label}: `); + row.append("span").text((d: TooltipRow) => `${d.value ?? ""}`); + } + function showNodeTip(event: MouseEvent, data: Node) { const nodeMetrics: string[] = settings.value.nodeExpressions || []; const nodeMetricConfig = settings.value.nodeMetricConfig || []; - const html = nodeMetrics.map((m, index) => { + const metrics = nodeMetrics.map((m, index) => { const metric = topologyStore.nodeMetricValue[m]?.values?.find((val: { id: string; value: unknown }) => val.id === data.id) || null; const opt: MetricConfigOpt = nodeMetricConfig[index] || {}; - return `
${opt.label || m}: ${metric?.value || NaN} ${ - opt.unit || "unknown" - }
`; + + return { + label: opt.label || m, + value: `${metric?.value || NaN} ${opt.unit || "unknown"}`, + }; }); - let tipHtml = `
name: ${ - data.name - }
type: ${data.type || "UNKNOWN"}
`; - if (data.isReal) { - tipHtml = [tipHtml, ...html].join(" "); - } + const rows = [ + { label: "name", value: data.name }, + { label: "type", value: data.type || "UNKNOWN" }, + ]; + const tipRows = data.isReal ? [...rows, ...metrics] : rows; + tooltip.value .style("top", event.offsetY + 10 + "px") .style("left", event.offsetX + 10 + "px") - .style("visibility", "visible") - .html(tipHtml); + .style("visibility", "visible"); + renderTooltipRows(tipRows); } function showLinkTip(event: MouseEvent, data: Call) { const linkClientMetrics: string[] = settings.value.linkClientExpressions || []; const linkServerMetricConfig: MetricConfigOpt[] = settings.value.linkServerMetricConfig || []; const linkClientMetricConfig: MetricConfigOpt[] = settings.value.linkClientMetricConfig || []; const linkServerMetrics: string[] = settings.value.linkServerExpressions || []; - const htmlServer = linkServerMetrics.map((m, index) => { - const metric = topologyStore.linkServerMetrics[m]?.values?.find( - (val: { id: string; value: unknown }) => val.id === data.id, - ); - if (metric) { - const opt: MetricConfigOpt = linkServerMetricConfig[index] || {}; - return `
${opt.label || m}: ${metric.value || NaN} ${ - opt.unit || "" - }
`; - } - }); - const htmlClient = linkClientMetrics.map((m: string, index: number) => { - const opt: MetricConfigOpt = linkClientMetricConfig[index] || {}; - const metric = topologyStore.linkClientMetrics[m]?.values?.find( - (val: { id: string; value: unknown }) => val.id === data.id, - ); - if (metric) { - return `
${opt.label || m}: ${metric.value || NaN} ${ - opt.unit || "" - }
`; - } - }); - const html = [ - ...htmlServer, - ...htmlClient, - `
${t("detectPoint")}:${data.detectPoints.join(" | ")}
`, - ].join(" "); + const serverRows = linkServerMetrics + .map((m, index): TooltipRow | null => { + const metric = topologyStore.linkServerMetrics[m]?.values?.find( + (val: { id: string; value: unknown }) => val.id === data.id, + ); + if (metric) { + const opt: MetricConfigOpt = linkServerMetricConfig[index] || {}; + return { + label: opt.label || m, + value: `${metric.value || NaN} ${opt.unit || ""}`, + }; + } + return null; + }) + .filter(isTooltipRow); + const clientRows = linkClientMetrics + .map((m: string, index: number): TooltipRow | null => { + const opt: MetricConfigOpt = linkClientMetricConfig[index] || {}; + const metric = topologyStore.linkClientMetrics[m]?.values?.find( + (val: { id: string; value: unknown }) => val.id === data.id, + ); + if (metric) { + return { + label: opt.label || m, + value: `${metric.value || NaN} ${opt.unit || ""}`, + }; + } + return null; + }) + .filter(isTooltipRow); + const rows = [ + ...serverRows, + ...clientRows, + { + label: t("detectPoint"), + value: data.detectPoints.join(" | "), + }, + ]; tooltip.value .style("top", event.offsetY + "px") .style("left", event.offsetX + "px") - .style("visibility", "visible") - .html(html); + .style("visibility", "visible"); + renderTooltipRows(rows); } - function hideTip() { tooltip.value.style("visibility", "hidden"); }