Skip to content

Commit 601b4f6

Browse files
craig[bot]jasonlmfong
andcommitted
Merge #158546
158546: ui: add new cluster explorer page to advanced debug r=jasonlmfong a=jasonlmfong This new page contains a new hexagon heatmap powered by d3. This is used to display node activity like CPU percentage and write bytes. This also features a uplot histogram for range level drill-down once a node is selected. This histogram is similar to the existing BarGraphTimeSeries component. Epic: None Release note(ui): The cluster explorer page adds a new interactive way to visualize a cluster. Co-authored-by: Jason Fong <[email protected]>
2 parents 14a4e11 + 1c60347 commit 601b4f6

File tree

10 files changed

+1120
-0
lines changed

10 files changed

+1120
-0
lines changed

pkg/ui/pnpm-lock.yaml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/index.tsx

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ import { Visualization } from "../visualization";
1717

1818
import styles from "./bargraph.module.scss";
1919
import { getStackedBarOpts, stack } from "./bars";
20+
import { categoricalBarTooltipPlugin, BarMetadata } from "./plugins";
2021

2122
const cx = classNames.bind(styles);
2223

24+
export type { BarMetadata };
25+
2326
export type BarGraphTimeSeriesProps = {
2427
alignedData?: AlignedData;
2528
colourPalette?: string[]; // Series colour palette.
@@ -113,3 +116,121 @@ export const BarGraphTimeSeries: React.FC<BarGraphTimeSeriesProps> = ({
113116
</Visualization>
114117
);
115118
};
119+
120+
export type BarGraphDataPoint = {
121+
label: string;
122+
value: number;
123+
databases?: string[];
124+
tables?: string[];
125+
indexes?: string[];
126+
};
127+
128+
export type BarGraphProps = {
129+
data: Array<BarGraphDataPoint>;
130+
colourPalette?: string[];
131+
preCalcGraphSize?: boolean;
132+
title: string;
133+
tooltip?: React.ReactNode;
134+
yAxisUnits: AxisUnits;
135+
};
136+
137+
// Simple bar graph component for categorical data
138+
export const BarGraph: React.FC<BarGraphProps> = ({
139+
data,
140+
colourPalette = ["#2196F3"],
141+
preCalcGraphSize = true,
142+
title,
143+
tooltip,
144+
yAxisUnits,
145+
}) => {
146+
const graphRef = useRef<HTMLDivElement>(null);
147+
148+
useEffect(() => {
149+
if (!data || data.length === 0) return;
150+
151+
const xValues = data.map((_, i) => i);
152+
const yValues = data.map(d => d.value);
153+
const plotData: AlignedData = [xValues, yValues];
154+
155+
const yAxisDomain = calculateYAxisDomain(yAxisUnits, yValues);
156+
157+
// Extract labels and metadata for tooltip
158+
const labels = data.map(d => d.label);
159+
const metadata: BarMetadata[] = data.map(d => ({
160+
databases: d.databases,
161+
tables: d.tables,
162+
indexes: d.indexes,
163+
}));
164+
165+
// Standard uPlot bar chart configuration
166+
const opts: Options = {
167+
id: "chart",
168+
class: cx("bargraph"),
169+
width: 800,
170+
height: 450,
171+
legend: {
172+
show: false,
173+
},
174+
cursor: {
175+
points: {
176+
show: false,
177+
},
178+
},
179+
series: [
180+
{},
181+
{
182+
label: "Value",
183+
fill: colourPalette[0],
184+
stroke: colourPalette[0],
185+
paths: uPlot.paths.bars({ size: [0.9, 80] }),
186+
points: { show: false },
187+
},
188+
],
189+
axes: [
190+
{
191+
// X-axis: categorical labels
192+
values: (_u, splits) => splits.map(i => labels[Math.round(i)] || ""),
193+
},
194+
{
195+
// Y-axis: numeric values with units
196+
label: yAxisDomain.label,
197+
values: (_u, vals) =>
198+
vals.map(v => yAxisDomain.tickFormat(v as number)),
199+
splits: () => [
200+
yAxisDomain.extent[0],
201+
...yAxisDomain.ticks,
202+
yAxisDomain.extent[1],
203+
],
204+
},
205+
],
206+
scales: {
207+
x: {
208+
range: () => [-0.5, data.length - 0.5],
209+
},
210+
y: {
211+
range: () => [yAxisDomain.extent[0], yAxisDomain.extent[1]],
212+
},
213+
},
214+
plugins: [categoricalBarTooltipPlugin(yAxisUnits, labels, metadata)],
215+
};
216+
217+
const plot = new uPlot(opts, plotData, graphRef.current);
218+
219+
return () => {
220+
plot?.destroy();
221+
};
222+
}, [data, yAxisUnits, colourPalette]);
223+
224+
return (
225+
<Visualization
226+
title={title}
227+
loading={!data || data.length === 0}
228+
preCalcGraphSize={preCalcGraphSize}
229+
tooltip={tooltip}
230+
>
231+
<div className={cx("bargraph")}>
232+
<div ref={graphRef} />
233+
</div>
234+
</Visualization>
235+
);
236+
};

pkg/ui/workspaces/cluster-ui/src/graphs/bargraph/plugins.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,133 @@ function getFormattedValue(value: number, yAxisUnits: AxisUnits): string {
101101
}
102102
}
103103

104+
export interface BarMetadata {
105+
databases?: string[];
106+
tables?: string[];
107+
indexes?: string[];
108+
}
109+
110+
// Tooltip plugin for categorical bar charts (non-time-series)
111+
export function categoricalBarTooltipPlugin(
112+
yAxis: AxisUnits,
113+
labels: string[],
114+
metadata?: BarMetadata[],
115+
): Plugin {
116+
const cursorToolTip = {
117+
tooltip: document.createElement("div"),
118+
};
119+
120+
function escapeHtml(str: string): string {
121+
const div = document.createElement("div");
122+
div.textContent = str;
123+
return div.innerHTML;
124+
}
125+
126+
function setCursor(u: uPlot) {
127+
const { tooltip } = cursorToolTip;
128+
const { left = 0, idx } = u.cursor;
129+
130+
if (idx === null || idx === undefined || idx < 0 || idx >= labels.length) {
131+
tooltip.style.display = "none";
132+
return;
133+
}
134+
135+
const label = labels[idx];
136+
const value = u.data[1][idx];
137+
const meta = metadata?.[idx];
138+
139+
// Build tooltip content
140+
let content = `<div style="font-weight: bold; margin-bottom: 8px;">${escapeHtml(label)}</div>`;
141+
content += `<div style="margin-bottom: 8px;">Value: ${getFormattedValue(value, yAxis)}</div>`;
142+
143+
// Add metadata if available
144+
if (meta) {
145+
if (meta.databases && meta.databases.length > 0) {
146+
content += `<div style="margin-top: 8px;"><strong>Database${meta.databases.length > 1 ? "s" : ""}:</strong></div>`;
147+
content += meta.databases
148+
.map(db => `<div style="margin-left: 8px;">• ${escapeHtml(db)}</div>`)
149+
.join("");
150+
} else if (label.toLowerCase().includes("range")) {
151+
content += `<div style="margin-top: 8px;"><strong>Database:</strong> System range</div>`;
152+
}
153+
154+
if (meta.tables && meta.tables.length > 0) {
155+
content += `<div style="margin-top: 8px;"><strong>Table${meta.tables.length > 1 ? "s" : ""}:</strong></div>`;
156+
content += meta.tables
157+
.map(
158+
table =>
159+
`<div style="margin-left: 8px;">• ${escapeHtml(table)}</div>`,
160+
)
161+
.join("");
162+
}
163+
164+
if (meta.indexes && meta.indexes.length > 0) {
165+
content += `<div style="margin-top: 8px;"><strong>Index${meta.indexes.length > 1 ? "es" : ""}:</strong></div>`;
166+
content += meta.indexes
167+
.map(
168+
index =>
169+
`<div style="margin-left: 8px;">• ${escapeHtml(index)}</div>`,
170+
)
171+
.join("");
172+
}
173+
}
174+
175+
tooltip.innerHTML = content;
176+
177+
// Position tooltip
178+
const tooltipWidth = tooltip.offsetWidth;
179+
const plotWidth = u.over.offsetWidth;
180+
let tooltipLeft = left + 20;
181+
182+
// Adjust if tooltip goes off the right edge
183+
if (tooltipLeft + tooltipWidth > plotWidth) {
184+
tooltipLeft = left - tooltipWidth - 20;
185+
}
186+
187+
tooltip.style.left = `${tooltipLeft}px`;
188+
tooltip.style.top = `20px`;
189+
tooltip.style.display = "";
190+
}
191+
192+
function ready(u: uPlot) {
193+
const plot = u.root.querySelector(".u-over");
194+
const { tooltip } = cursorToolTip;
195+
196+
plot?.addEventListener("mouseleave", () => {
197+
tooltip.style.display = "none";
198+
});
199+
}
200+
201+
function init(u: uPlot) {
202+
const plot = u.root.querySelector(".u-over");
203+
const { tooltip } = cursorToolTip;
204+
205+
tooltip.style.display = "none";
206+
tooltip.style.pointerEvents = "none";
207+
tooltip.style.position = "absolute";
208+
tooltip.style.padding = "12px";
209+
tooltip.style.minWidth = "150px";
210+
tooltip.style.maxWidth = "350px";
211+
tooltip.style.background = "#fff";
212+
tooltip.style.borderRadius = "4px";
213+
tooltip.style.boxShadow = "0 2px 4px rgba(0,0,0,0.1)";
214+
tooltip.style.border = "1px solid #999";
215+
tooltip.style.zIndex = "1000";
216+
tooltip.style.fontSize = "12px";
217+
tooltip.style.lineHeight = "1.4";
218+
219+
plot?.appendChild(tooltip);
220+
}
221+
222+
return {
223+
hooks: {
224+
init,
225+
ready,
226+
setCursor,
227+
},
228+
};
229+
}
230+
104231
// Tooltip legend plugin for bar charts.
105232
export function barTooltipPlugin(yAxis: AxisUnits, timezone: string): Plugin {
106233
const cursorToolTip = {

pkg/ui/workspaces/cluster-ui/src/graphs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55

66
export * from "./visualization";
77
export * from "./utils/domain";
8+
export * from "./bargraph";

pkg/ui/workspaces/db-console/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"d3-format": "^3.1.0",
3939
"d3-geo": "^3.1.1",
4040
"d3-geo-projection": "^4.0.0",
41+
"d3-interpolate": "^3.0.1",
4142
"d3-scale": "^4.0.2",
4243
"d3-selection": "^3.0.0",
4344
"d3-shape": "^3.2.0",
@@ -101,6 +102,7 @@
101102
"@types/d3-drag": "^3.0.7",
102103
"@types/d3-format": "^3.0.4",
103104
"@types/d3-geo": "^3.1.0",
105+
"@types/d3-interpolate": "^3.0.1",
104106
"@types/d3-scale": "^4.0.8",
105107
"@types/d3-selection": "^3.0.10",
106108
"@types/d3-shape": "^3.1.7",

pkg/ui/workspaces/db-console/src/app.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import { IndexDetailsPage } from "src/views/databases/indexDetailsPage";
5858
import Raft from "src/views/devtools/containers/raft";
5959
import RaftMessages from "src/views/devtools/containers/raftMessages";
6060
import RaftRanges from "src/views/devtools/containers/raftRanges";
61+
import ClusterExplorerPage from "src/views/explorer/explorer";
6162
import HotRangesPage from "src/views/hotRanges/index";
6263
import JobDetails from "src/views/jobs/jobDetails";
6364
import JobsPage from "src/views/jobs/jobsPage";
@@ -357,6 +358,13 @@ export const App: React.FC<AppProps> = (props: AppProps) => {
357358
component={StatementInsightDetailsPage}
358359
/>
359360

361+
{/* cluster explorer */}
362+
<Route
363+
exact
364+
path="/cluster-explorer"
365+
component={ClusterExplorerPage}
366+
/>
367+
360368
{/* debug pages */}
361369
<Route exact path="/debug" component={Debug} />
362370
<Route

0 commit comments

Comments
 (0)