feat: allow different types of plots

This commit is contained in:
Sylvia Crowe 2024-09-25 15:22:17 -07:00
parent dea651216d
commit bae31cc438
3 changed files with 142 additions and 40 deletions

View File

@ -11,6 +11,7 @@ import * as htl from "htl";
import * as jotai from "jotai"; import * as jotai from "jotai";
import * as React from "react"; import * as React from "react";
import { ContextMenuModel } from "@/app/store/contextmenu";
import { waveEventSubscribe } from "@/app/store/wps"; import { waveEventSubscribe } from "@/app/store/wps";
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
import { WindowRpcClient } from "@/app/store/wshrpcutil"; import { WindowRpcClient } from "@/app/store/wshrpcutil";
@ -23,15 +24,33 @@ type DataItem = {
[k: string]: number; [k: string]: number;
}; };
const SysInfoMetricNames = { function defaultCpuMeta(name: string): TimeSeriesMeta {
cpu: "CPU %", return {
"mem:total": "Memory Total", name: name,
"mem:used": "Memory Used", label: "%",
"mem:free": "Memory Free", miny: 0,
"mem:available": "Memory Available", maxy: 100,
};
}
function defaultMemMeta(name: string, maxY: string): TimeSeriesMeta {
return {
name: name,
label: "GB",
miny: 0,
maxy: maxY,
};
}
const DefaultPlotMeta = {
cpu: defaultCpuMeta("CPU %"),
"mem:total": defaultMemMeta("Memory Total", "mem:total"),
"mem:used": defaultMemMeta("Memory Used", "mem:total"),
"mem:free": defaultMemMeta("Memory Free", "mem:total"),
"mem:available": defaultMemMeta("Memory Available", "mem:total"),
}; };
for (let i = 0; i < 32; i++) { for (let i = 0; i < 32; i++) {
SysInfoMetricNames[`cpu:${i}`] = `CPU[${i}] %`; DefaultPlotMeta[`cpu:${i}`] = defaultCpuMeta(`CPU[${i}] %`);
} }
function convertWaveEventToDataItem(event: WaveEvent): DataItem { function convertWaveEventToDataItem(event: WaveEvent): DataItem {
@ -64,6 +83,8 @@ class CpuPlotViewModel {
connection: jotai.Atom<string>; connection: jotai.Atom<string>;
manageConnection: jotai.Atom<boolean>; manageConnection: jotai.Atom<boolean>;
connStatus: jotai.Atom<ConnStatus>; connStatus: jotai.Atom<ConnStatus>;
plotMetaAtom: jotai.PrimitiveAtom<Map<string, TimeSeriesMeta>>;
endIconButtons: jotai.Atom<IconButtonDecl[]>;
constructor(blockId: string) { constructor(blockId: string) {
this.viewType = "cpuplot"; this.viewType = "cpuplot";
@ -86,6 +107,16 @@ class CpuPlotViewModel {
console.log("Error adding data to cpuplot", e); console.log("Error adding data to cpuplot", e);
} }
}); });
this.plotMetaAtom = jotai.atom(new Map(Object.entries(DefaultPlotMeta)));
this.endIconButtons = jotai.atom((get) => {
return [
{
elemtype: "iconbutton",
icon: "wrench",
click: (e) => this.handleContextMenu(e),
},
];
});
this.manageConnection = jotai.atom(true); this.manageConnection = jotai.atom(true);
this.loadingAtom = jotai.atom(true); this.loadingAtom = jotai.atom(true);
this.numPoints = jotai.atom((get) => { this.numPoints = jotai.atom((get) => {
@ -108,7 +139,16 @@ class CpuPlotViewModel {
return "chart-line"; // should not be hardcoded return "chart-line"; // should not be hardcoded
}); });
this.viewName = jotai.atom((get) => { this.viewName = jotai.atom((get) => {
return "CPU %"; // should not be hardcoded const metrics = get(this.metrics);
const meta = get(this.plotMetaAtom);
if (metrics.length == 0) {
return "unknown";
}
const metaSelected = meta.get(metrics[0]);
if (!metaSelected) {
return "unknown";
}
return metaSelected.name;
}); });
this.incrementCount = jotai.atom(null, async (get, set) => { this.incrementCount = jotai.atom(null, async (get, set) => {
const meta = get(this.blockAtom).meta; const meta = get(this.blockAtom).meta;
@ -161,6 +201,31 @@ class CpuPlotViewModel {
} }
} }
handleContextMenu(e: React.MouseEvent<HTMLDivElement>) {
e.preventDefault();
e.stopPropagation();
const plotData = globalStore.get(this.dataAtom);
if (plotData.length == 0) {
return;
}
const menu = Object.keys(plotData[plotData.length - 1])
.filter((dataType) => dataType !== "ts")
.map((dataType) => {
const menuItem: ContextMenuItem = {
label: dataType,
click: async () => {
await RpcApi.SetMetaCommand(WindowRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "graph:metrics": [dataType] },
});
},
};
return menuItem;
});
ContextMenuModel.showContextMenu(menu, e);
}
getDefaultData(): DataItem[] { getDefaultData(): DataItem[] {
// set it back one to avoid backwards line being possible // set it back one to avoid backwards line being possible
const numPoints = globalStore.get(this.numPoints); const numPoints = globalStore.get(this.numPoints);
@ -185,6 +250,16 @@ type CpuPlotViewProps = {
model: CpuPlotViewModel; model: CpuPlotViewModel;
}; };
function resolveDomainBound(value: number | string, dataItem: DataItem): number | undefined {
if (typeof value == "number") {
return value;
} else if (typeof value == "string") {
return dataItem?.[value];
} else {
return undefined;
}
}
function CpuPlotView({ model, blockId }: CpuPlotViewProps) { function CpuPlotView({ model, blockId }: CpuPlotViewProps) {
const connName = jotai.useAtomValue(model.connection); const connName = jotai.useAtomValue(model.connection);
const lastConnName = React.useRef(connName); const lastConnName = React.useRef(connName);
@ -234,52 +309,69 @@ const CpuPlotViewInner = React.memo(({ model }: CpuPlotViewProps) => {
const parentHeight = useHeight(containerRef); const parentHeight = useHeight(containerRef);
const parentWidth = useWidth(containerRef); const parentWidth = useWidth(containerRef);
const yvals = jotai.useAtomValue(model.metrics); const yvals = jotai.useAtomValue(model.metrics);
const plotMeta = jotai.useAtomValue(model.plotMetaAtom);
React.useEffect(() => { React.useEffect(() => {
if (yvals.length == 0) {
// don't bother creating plots if none are selected
return;
}
const singleItem = yvals.length == 1;
const marks: Plot.Markish[] = []; const marks: Plot.Markish[] = [];
marks.push( yvals.forEach((yval, idx) => {
() => htl.svg`<defs> // use rotating colors for
<linearGradient id="gradient" gradientTransform="rotate(90)"> // color not configured
<stop offset="0%" stop-color="#58C142" stop-opacity="0.7" /> // plotting multiple items
<stop offset="100%" stop-color="#58C142" stop-opacity="0" /> let color = plotMeta.get(yval)?.color;
if (!color || !singleItem) {
color = plotColors[idx];
}
marks.push(
() => htl.svg`<defs>
<linearGradient id="gradient-${model.blockId}-${yval}" gradientTransform="rotate(90)">
<stop offset="0%" stop-color="${color}" stop-opacity="0.7" />
<stop offset="100%" stop-color="${color}" stop-opacity="0" />
</linearGradient> </linearGradient>
</defs>` </defs>`
); );
if (yvals.length == 0) {
// nothing
} else if (yvals.length == 1) {
marks.push( marks.push(
Plot.lineY(plotData, { Plot.lineY(plotData, {
stroke: plotColors[0], stroke: color,
strokeWidth: 2, strokeWidth: singleItem ? 2 : 1,
x: "ts", x: "ts",
y: yvals[0], y: yval,
}) })
); );
marks.push(
Plot.areaY(plotData, { // only add the gradient for single items
fill: "url(#gradient)", if (singleItem) {
x: "ts",
y: yvals[0],
})
);
} else {
let idx = 0;
for (const yval of yvals) {
marks.push( marks.push(
Plot.lineY(plotData, { Plot.areaY(plotData, {
stroke: plotColors[idx % plotColors.length], fill: `url(#gradient-${model.blockId}-${yvals[0]})`,
strokeWidth: 1,
x: "ts", x: "ts",
y: yval, y: yval,
}) })
); );
idx++;
} }
});
// use the largest configured yval.maxYs. if none is found, use 100
const maxYs = yvals.map((yval) => resolveDomainBound(plotMeta.get(yval)?.maxy, plotData[plotData.length - 1]));
let maxY = Math.max(...maxYs.filter(Number.isFinite));
if (!Number.isFinite(maxY)) {
maxY = 100;
} }
// use the smalles configured yval.minYs. if none is found, use 0
const minYs = yvals.map((yval) => resolveDomainBound(plotMeta.get(yval)?.miny, plotData[plotData.length - 1]));
let minY = Math.min(...minYs.filter(Number.isFinite));
if (!Number.isFinite(maxY)) {
minY = 0;
}
const labelY = plotMeta.get(yvals[0])?.label ?? "?";
const plot = Plot.plot({ const plot = Plot.plot({
x: { grid: true, label: "time", tickFormat: (d) => `${dayjs.unix(d / 1000).format("HH:mm:ss")}` }, x: { grid: true, label: "time", tickFormat: (d) => `${dayjs.unix(d / 1000).format("HH:mm:ss")}` },
y: { label: "%", domain: [0, 100] }, y: { label: labelY, domain: [minY, maxY] },
width: parentWidth, width: parentWidth,
height: parentHeight, height: parentHeight,
marks: marks, marks: marks,
@ -294,7 +386,7 @@ const CpuPlotViewInner = React.memo(({ model }: CpuPlotViewProps) => {
plot.remove(); plot.remove();
} }
}; };
}, [plotData, parentHeight, parentWidth]); }, [plotData, parentHeight, parentWidth, yvals, plotMeta, model.blockId]);
return <div className="plot-view" ref={containerRef} />; return <div className="plot-view" ref={containerRef} />;
}); });

View File

@ -295,6 +295,14 @@ declare global {
command: string; command: string;
msgFn: (msg: RpcMessage) => void; msgFn: (msg: RpcMessage) => void;
}; };
type TimeSeriesMeta = {
name?: string;
color?: string;
label?: string;
maxy?: string | number;
miny?: string | number;
};
} }
export {}; export {};

View File

@ -16,6 +16,8 @@ import (
"github.com/wavetermdev/waveterm/pkg/wshutil" "github.com/wavetermdev/waveterm/pkg/wshutil"
) )
const BYTES_PER_GB = 1073741824
func getCpuData(values map[string]float64) { func getCpuData(values map[string]float64) {
percentArr, err := cpu.Percent(0, false) percentArr, err := cpu.Percent(0, false)
if err != nil { if err != nil {
@ -38,10 +40,10 @@ func getMemData(values map[string]float64) {
if err != nil { if err != nil {
return return
} }
values["mem:total"] = float64(memData.Total) values["mem:total"] = float64(memData.Total) / BYTES_PER_GB
values["mem:available"] = float64(memData.Available) values["mem:available"] = float64(memData.Available) / BYTES_PER_GB
values["mem:used"] = float64(memData.Used) values["mem:used"] = float64(memData.Used) / BYTES_PER_GB
values["mem:free"] = float64(memData.Free) values["mem:free"] = float64(memData.Free) / BYTES_PER_GB
} }
func generateSingleServerData(client *wshutil.WshRpc, connName string) { func generateSingleServerData(client *wshutil.WshRpc, connName string) {