mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
Plot Sysinfo (#1054)
Expands the cpuplot with memory plots and individual cpu plots. Also improves the styling and handling of multiple plots.
This commit is contained in:
parent
fae06600cc
commit
a5999aa02a
@ -4,6 +4,7 @@
|
|||||||
import { BlockComponentModel2, BlockProps } from "@/app/block/blocktypes";
|
import { BlockComponentModel2, BlockProps } from "@/app/block/blocktypes";
|
||||||
import { PlotView } from "@/app/view/plotview/plotview";
|
import { PlotView } from "@/app/view/plotview/plotview";
|
||||||
import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview";
|
import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview";
|
||||||
|
import { SysinfoView, SysinfoViewModel, makeSysinfoViewModel } from "@/app/view/sysinfo/sysinfo";
|
||||||
import { ErrorBoundary } from "@/element/errorboundary";
|
import { ErrorBoundary } from "@/element/errorboundary";
|
||||||
import { CenteredDiv } from "@/element/quickelems";
|
import { CenteredDiv } from "@/element/quickelems";
|
||||||
import { NodeModel, useDebouncedNodeInnerRect } from "@/layout/index";
|
import { NodeModel, useDebouncedNodeInnerRect } from "@/layout/index";
|
||||||
@ -16,7 +17,6 @@ import {
|
|||||||
import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos";
|
import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos";
|
||||||
import { focusedBlockId, getElemAsStr } from "@/util/focusutil";
|
import { focusedBlockId, getElemAsStr } from "@/util/focusutil";
|
||||||
import { isBlank } from "@/util/util";
|
import { isBlank } from "@/util/util";
|
||||||
import { CpuPlotView, CpuPlotViewModel, makeCpuPlotViewModel } from "@/view/cpuplot/cpuplot";
|
|
||||||
import { HelpView, HelpViewModel, makeHelpViewModel } from "@/view/helpview/helpview";
|
import { HelpView, HelpViewModel, makeHelpViewModel } from "@/view/helpview/helpview";
|
||||||
import { QuickTipsView, QuickTipsViewModel } from "@/view/quicktipsview/quicktipsview";
|
import { QuickTipsView, QuickTipsViewModel } from "@/view/quicktipsview/quicktipsview";
|
||||||
import { TermViewModel, TerminalView, makeTerminalModel } from "@/view/term/term";
|
import { TermViewModel, TerminalView, makeTerminalModel } from "@/view/term/term";
|
||||||
@ -47,8 +47,9 @@ function makeViewModel(blockId: string, blockView: string, nodeModel: NodeModel)
|
|||||||
if (blockView === "waveai") {
|
if (blockView === "waveai") {
|
||||||
return makeWaveAiViewModel(blockId);
|
return makeWaveAiViewModel(blockId);
|
||||||
}
|
}
|
||||||
if (blockView === "cpuplot") {
|
if (blockView === "cpuplot" || blockView == "sysinfo") {
|
||||||
return makeCpuPlotViewModel(blockId);
|
// "cpuplot" is for backwards compatibility with already-opened widgets
|
||||||
|
return makeSysinfoViewModel(blockId, blockView);
|
||||||
}
|
}
|
||||||
if (blockView === "help") {
|
if (blockView === "help") {
|
||||||
return makeHelpViewModel(blockId, nodeModel);
|
return makeHelpViewModel(blockId, nodeModel);
|
||||||
@ -89,8 +90,9 @@ function getViewElem(
|
|||||||
if (blockView === "waveai") {
|
if (blockView === "waveai") {
|
||||||
return <WaveAi key={blockId} blockId={blockId} model={viewModel as WaveAiModel} />;
|
return <WaveAi key={blockId} blockId={blockId} model={viewModel as WaveAiModel} />;
|
||||||
}
|
}
|
||||||
if (blockView === "cpuplot") {
|
if (blockView === "cpuplot" || blockView === "sysinfo") {
|
||||||
return <CpuPlotView key={blockId} blockId={blockId} model={viewModel as CpuPlotViewModel} />;
|
// "cpuplot" is for backwards compatibility with already opened widgets
|
||||||
|
return <SysinfoView key={blockId} blockId={blockId} model={viewModel as SysinfoViewModel} />;
|
||||||
}
|
}
|
||||||
if (blockView == "help") {
|
if (blockView == "help") {
|
||||||
return <HelpView key={blockId} model={viewModel as HelpViewModel} />;
|
return <HelpView key={blockId} model={viewModel as HelpViewModel} />;
|
||||||
|
@ -106,6 +106,9 @@
|
|||||||
--conn-icon-color-7: #dbde52;
|
--conn-icon-color-7: #dbde52;
|
||||||
--conn-icon-color-8: #58c142;
|
--conn-icon-color-8: #58c142;
|
||||||
|
|
||||||
|
--sysinfo-cpu-color: #58c142;
|
||||||
|
--sysinfo-mem-color: #53b4ea;
|
||||||
|
|
||||||
--bulb-color: rgb(255, 221, 51);
|
--bulb-color: rgb(255, 221, 51);
|
||||||
|
|
||||||
// term colors (16 + 6) form the base terminal theme
|
// term colors (16 + 6) form the base terminal theme
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
.plot-view {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: stretch;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
@ -1,302 +0,0 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
import { getConnStatusAtom, globalStore, WOS } from "@/store/global";
|
|
||||||
import * as util from "@/util/util";
|
|
||||||
import * as Plot from "@observablehq/plot";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import * as htl from "htl";
|
|
||||||
import * as jotai from "jotai";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
|
|
||||||
import { waveEventSubscribe } from "@/app/store/wps";
|
|
||||||
import { RpcApi } from "@/app/store/wshclientapi";
|
|
||||||
import { WindowRpcClient } from "@/app/store/wshrpcutil";
|
|
||||||
import "./cpuplot.less";
|
|
||||||
|
|
||||||
const DefaultNumPoints = 120;
|
|
||||||
|
|
||||||
type DataItem = {
|
|
||||||
ts: number;
|
|
||||||
[k: string]: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SysInfoMetricNames = {
|
|
||||||
cpu: "CPU %",
|
|
||||||
"mem:total": "Memory Total",
|
|
||||||
"mem:used": "Memory Used",
|
|
||||||
"mem:free": "Memory Free",
|
|
||||||
"mem:available": "Memory Available",
|
|
||||||
};
|
|
||||||
for (let i = 0; i < 32; i++) {
|
|
||||||
SysInfoMetricNames[`cpu:${i}`] = `CPU[${i}] %`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertWaveEventToDataItem(event: WaveEvent): DataItem {
|
|
||||||
const eventData: TimeSeriesData = event.data;
|
|
||||||
if (eventData == null || eventData.ts == null || eventData.values == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const dataItem = { ts: eventData.ts };
|
|
||||||
for (const key in eventData.values) {
|
|
||||||
dataItem[key] = eventData.values[key];
|
|
||||||
}
|
|
||||||
return dataItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
class CpuPlotViewModel {
|
|
||||||
viewType: string;
|
|
||||||
blockAtom: jotai.Atom<Block>;
|
|
||||||
termMode: jotai.Atom<string>;
|
|
||||||
htmlElemFocusRef: React.RefObject<HTMLInputElement>;
|
|
||||||
blockId: string;
|
|
||||||
viewIcon: jotai.Atom<string>;
|
|
||||||
viewText: jotai.Atom<string>;
|
|
||||||
viewName: jotai.Atom<string>;
|
|
||||||
dataAtom: jotai.PrimitiveAtom<Array<DataItem>>;
|
|
||||||
addDataAtom: jotai.WritableAtom<unknown, [DataItem[]], void>;
|
|
||||||
incrementCount: jotai.WritableAtom<unknown, [], Promise<void>>;
|
|
||||||
loadingAtom: jotai.PrimitiveAtom<boolean>;
|
|
||||||
numPoints: jotai.Atom<number>;
|
|
||||||
metrics: jotai.Atom<string[]>;
|
|
||||||
connection: jotai.Atom<string>;
|
|
||||||
manageConnection: jotai.Atom<boolean>;
|
|
||||||
connStatus: jotai.Atom<ConnStatus>;
|
|
||||||
|
|
||||||
constructor(blockId: string) {
|
|
||||||
this.viewType = "cpuplot";
|
|
||||||
this.blockId = blockId;
|
|
||||||
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
|
||||||
this.addDataAtom = jotai.atom(null, (get, set, points) => {
|
|
||||||
const targetLen = get(this.numPoints) + 1;
|
|
||||||
let data = get(this.dataAtom);
|
|
||||||
try {
|
|
||||||
if (data.length > targetLen) {
|
|
||||||
data = data.slice(data.length - targetLen);
|
|
||||||
}
|
|
||||||
if (data.length < targetLen) {
|
|
||||||
const defaultData = this.getDefaultData();
|
|
||||||
data = [...defaultData.slice(defaultData.length - targetLen + data.length), ...data];
|
|
||||||
}
|
|
||||||
const newData = [...data.slice(points.length), ...points];
|
|
||||||
set(this.dataAtom, newData);
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Error adding data to cpuplot", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.manageConnection = jotai.atom(true);
|
|
||||||
this.loadingAtom = jotai.atom(true);
|
|
||||||
this.numPoints = jotai.atom((get) => {
|
|
||||||
const blockData = get(this.blockAtom);
|
|
||||||
const metaNumPoints = blockData?.meta?.["graph:numpoints"];
|
|
||||||
if (metaNumPoints == null || metaNumPoints <= 0) {
|
|
||||||
return DefaultNumPoints;
|
|
||||||
}
|
|
||||||
return metaNumPoints;
|
|
||||||
});
|
|
||||||
this.metrics = jotai.atom((get) => {
|
|
||||||
const blockData = get(this.blockAtom);
|
|
||||||
const metrics = blockData?.meta?.["graph:metrics"];
|
|
||||||
if (metrics == null || !Array.isArray(metrics)) {
|
|
||||||
return ["cpu"];
|
|
||||||
}
|
|
||||||
return metrics;
|
|
||||||
});
|
|
||||||
this.viewIcon = jotai.atom((get) => {
|
|
||||||
return "chart-line"; // should not be hardcoded
|
|
||||||
});
|
|
||||||
this.viewName = jotai.atom((get) => {
|
|
||||||
return "CPU %"; // should not be hardcoded
|
|
||||||
});
|
|
||||||
this.incrementCount = jotai.atom(null, async (get, set) => {
|
|
||||||
const meta = get(this.blockAtom).meta;
|
|
||||||
const count = meta.count ?? 0;
|
|
||||||
await RpcApi.SetMetaCommand(WindowRpcClient, {
|
|
||||||
oref: WOS.makeORef("block", this.blockId),
|
|
||||||
meta: { count: count + 1 },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.connection = jotai.atom((get) => {
|
|
||||||
const blockData = get(this.blockAtom);
|
|
||||||
const connValue = blockData?.meta?.connection;
|
|
||||||
if (util.isBlank(connValue)) {
|
|
||||||
return "local";
|
|
||||||
}
|
|
||||||
return connValue;
|
|
||||||
});
|
|
||||||
this.dataAtom = jotai.atom(this.getDefaultData());
|
|
||||||
this.loadInitialData();
|
|
||||||
this.connStatus = jotai.atom((get) => {
|
|
||||||
const blockData = get(this.blockAtom);
|
|
||||||
const connName = blockData?.meta?.connection;
|
|
||||||
const connAtom = getConnStatusAtom(connName);
|
|
||||||
return get(connAtom);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadInitialData() {
|
|
||||||
globalStore.set(this.loadingAtom, true);
|
|
||||||
try {
|
|
||||||
const numPoints = globalStore.get(this.numPoints);
|
|
||||||
const connName = globalStore.get(this.connection);
|
|
||||||
const initialData = await RpcApi.EventReadHistoryCommand(WindowRpcClient, {
|
|
||||||
event: "sysinfo",
|
|
||||||
scope: connName,
|
|
||||||
maxitems: numPoints,
|
|
||||||
});
|
|
||||||
if (initialData == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newData = this.getDefaultData();
|
|
||||||
const initialDataItems: DataItem[] = initialData.map(convertWaveEventToDataItem);
|
|
||||||
// splice the initial data into the default data (replacing the newest points)
|
|
||||||
newData.splice(newData.length - initialDataItems.length, initialDataItems.length, ...initialDataItems);
|
|
||||||
globalStore.set(this.addDataAtom, newData);
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Error loading initial data for cpuplot", e);
|
|
||||||
} finally {
|
|
||||||
globalStore.set(this.loadingAtom, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultData(): DataItem[] {
|
|
||||||
// set it back one to avoid backwards line being possible
|
|
||||||
const numPoints = globalStore.get(this.numPoints);
|
|
||||||
const currentTime = Date.now() - 1000;
|
|
||||||
const points: DataItem[] = [];
|
|
||||||
for (let i = numPoints; i > -1; i--) {
|
|
||||||
points.push({ ts: currentTime - i * 1000 });
|
|
||||||
}
|
|
||||||
return points;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCpuPlotViewModel(blockId: string): CpuPlotViewModel {
|
|
||||||
const cpuPlotViewModel = new CpuPlotViewModel(blockId);
|
|
||||||
return cpuPlotViewModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plotColors = ["#58C142", "#FFC107", "#FF5722", "#2196F3", "#9C27B0", "#00BCD4", "#FFEB3B", "#795548"];
|
|
||||||
|
|
||||||
type CpuPlotViewProps = {
|
|
||||||
blockId: string;
|
|
||||||
model: CpuPlotViewModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
function CpuPlotView({ model, blockId }: CpuPlotViewProps) {
|
|
||||||
const connName = jotai.useAtomValue(model.connection);
|
|
||||||
const lastConnName = React.useRef(connName);
|
|
||||||
const connStatus = jotai.useAtomValue(model.connStatus);
|
|
||||||
const addPlotData = jotai.useSetAtom(model.addDataAtom);
|
|
||||||
const loading = jotai.useAtomValue(model.loadingAtom);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (connStatus?.status != "connected") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (lastConnName.current !== connName) {
|
|
||||||
lastConnName.current = connName;
|
|
||||||
model.loadInitialData();
|
|
||||||
}
|
|
||||||
}, [connStatus.status, connName]);
|
|
||||||
React.useEffect(() => {
|
|
||||||
const unsubFn = waveEventSubscribe({
|
|
||||||
eventType: "sysinfo",
|
|
||||||
scope: connName,
|
|
||||||
handler: (event) => {
|
|
||||||
const loading = globalStore.get(model.loadingAtom);
|
|
||||||
if (loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dataItem = convertWaveEventToDataItem(event);
|
|
||||||
addPlotData([dataItem]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log("subscribe to sysinfo", connName);
|
|
||||||
return () => {
|
|
||||||
unsubFn();
|
|
||||||
};
|
|
||||||
}, [connName]);
|
|
||||||
if (connStatus?.status != "connected") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (loading) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return <CpuPlotViewInner key={connStatus?.connection ?? "local"} blockId={blockId} model={model} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CpuPlotViewInner = React.memo(({ model }: CpuPlotViewProps) => {
|
|
||||||
const containerRef = React.useRef<HTMLInputElement>();
|
|
||||||
const plotData = jotai.useAtomValue(model.dataAtom);
|
|
||||||
const domRect = useDimensionsWithExistingRef(containerRef, 30);
|
|
||||||
const parentHeight = domRect?.height ?? 0;
|
|
||||||
const parentWidth = domRect?.width ?? 0;
|
|
||||||
const yvals = jotai.useAtomValue(model.metrics);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const marks: Plot.Markish[] = [];
|
|
||||||
marks.push(
|
|
||||||
() => htl.svg`<defs>
|
|
||||||
<linearGradient id="gradient" gradientTransform="rotate(90)">
|
|
||||||
<stop offset="0%" stop-color="#58C142" stop-opacity="0.7" />
|
|
||||||
<stop offset="100%" stop-color="#58C142" stop-opacity="0" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>`
|
|
||||||
);
|
|
||||||
if (yvals.length == 0) {
|
|
||||||
// nothing
|
|
||||||
} else if (yvals.length == 1) {
|
|
||||||
marks.push(
|
|
||||||
Plot.lineY(plotData, {
|
|
||||||
stroke: plotColors[0],
|
|
||||||
strokeWidth: 2,
|
|
||||||
x: "ts",
|
|
||||||
y: yvals[0],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
marks.push(
|
|
||||||
Plot.areaY(plotData, {
|
|
||||||
fill: "url(#gradient)",
|
|
||||||
x: "ts",
|
|
||||||
y: yvals[0],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let idx = 0;
|
|
||||||
for (const yval of yvals) {
|
|
||||||
marks.push(
|
|
||||||
Plot.lineY(plotData, {
|
|
||||||
stroke: plotColors[idx % plotColors.length],
|
|
||||||
strokeWidth: 1,
|
|
||||||
x: "ts",
|
|
||||||
y: yval,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const plot = Plot.plot({
|
|
||||||
x: { grid: true, label: "time", tickFormat: (d) => `${dayjs.unix(d / 1000).format("HH:mm:ss")}` },
|
|
||||||
y: { label: "%", domain: [0, 100] },
|
|
||||||
width: parentWidth,
|
|
||||||
height: parentHeight,
|
|
||||||
marks: marks,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (plot !== undefined) {
|
|
||||||
containerRef.current.append(plot);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (plot !== undefined) {
|
|
||||||
plot.remove();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [plotData, parentHeight, parentWidth]);
|
|
||||||
|
|
||||||
return <div className="plot-view" ref={containerRef} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
export { CpuPlotView, CpuPlotViewModel, makeCpuPlotViewModel };
|
|
33
frontend/app/view/sysinfo/sysinfo.less
Normal file
33
frontend/app/view/sysinfo/sysinfo.less
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
.scrollable {
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
.sysinfo-view {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
&.two-columns {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sysinfo-plot-content {
|
||||||
|
min-height: 100px;
|
||||||
|
svg {
|
||||||
|
[aria-label="tip"] {
|
||||||
|
g {
|
||||||
|
path {
|
||||||
|
color: var(--border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
502
frontend/app/view/sysinfo/sysinfo.tsx
Normal file
502
frontend/app/view/sysinfo/sysinfo.tsx
Normal file
@ -0,0 +1,502 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { getConnStatusAtom, globalStore, WOS } from "@/store/global";
|
||||||
|
import * as util from "@/util/util";
|
||||||
|
import * as Plot from "@observablehq/plot";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import * as htl from "htl";
|
||||||
|
import * as jotai from "jotai";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
|
||||||
|
import { waveEventSubscribe } from "@/app/store/wps";
|
||||||
|
import { RpcApi } from "@/app/store/wshclientapi";
|
||||||
|
import { WindowRpcClient } from "@/app/store/wshrpcutil";
|
||||||
|
import { atoms } from "@/store/global";
|
||||||
|
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
|
||||||
|
import "./sysinfo.less";
|
||||||
|
|
||||||
|
const DefaultNumPoints = 120;
|
||||||
|
|
||||||
|
type DataItem = {
|
||||||
|
ts: number;
|
||||||
|
[k: string]: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function defaultCpuMeta(name: string): TimeSeriesMeta {
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
label: "%",
|
||||||
|
miny: 0,
|
||||||
|
maxy: 100,
|
||||||
|
color: "var(--sysinfo-cpu-color)",
|
||||||
|
decimalPlaces: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultMemMeta(name: string, maxY: string): TimeSeriesMeta {
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
label: "GB",
|
||||||
|
miny: 0,
|
||||||
|
maxy: maxY,
|
||||||
|
color: "var(--sysinfo-mem-color)",
|
||||||
|
decimalPlaces: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlotTypes: Object = {
|
||||||
|
CPU: function (dataItem: DataItem): Array<string> {
|
||||||
|
return ["cpu"];
|
||||||
|
},
|
||||||
|
Mem: function (dataItem: DataItem): Array<string> {
|
||||||
|
return ["mem:used"];
|
||||||
|
},
|
||||||
|
"CPU + Mem": function (dataItem: DataItem): Array<string> {
|
||||||
|
return ["cpu", "mem:used"];
|
||||||
|
},
|
||||||
|
"All CPU": function (dataItem: DataItem): Array<string> {
|
||||||
|
return Object.keys(dataItem)
|
||||||
|
.filter((item) => item.startsWith("cpu") && item != "cpu")
|
||||||
|
.sort((a, b) => {
|
||||||
|
const valA = parseInt(a.replace("cpu:", ""));
|
||||||
|
const valB = parseInt(b.replace("cpu:", ""));
|
||||||
|
return valA - valB;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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++) {
|
||||||
|
DefaultPlotMeta[`cpu:${i}`] = defaultCpuMeta(`Core ${i}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertWaveEventToDataItem(event: WaveEvent): DataItem {
|
||||||
|
const eventData: TimeSeriesData = event.data;
|
||||||
|
if (eventData == null || eventData.ts == null || eventData.values == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const dataItem = { ts: eventData.ts };
|
||||||
|
for (const key in eventData.values) {
|
||||||
|
dataItem[key] = eventData.values[key];
|
||||||
|
}
|
||||||
|
return dataItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SysinfoViewModel {
|
||||||
|
viewType: string;
|
||||||
|
blockAtom: jotai.Atom<Block>;
|
||||||
|
termMode: jotai.Atom<string>;
|
||||||
|
htmlElemFocusRef: React.RefObject<HTMLInputElement>;
|
||||||
|
blockId: string;
|
||||||
|
viewIcon: jotai.Atom<string>;
|
||||||
|
viewText: jotai.Atom<string>;
|
||||||
|
viewName: jotai.Atom<string>;
|
||||||
|
dataAtom: jotai.PrimitiveAtom<Array<DataItem>>;
|
||||||
|
addDataAtom: jotai.WritableAtom<unknown, [DataItem[]], void>;
|
||||||
|
incrementCount: jotai.WritableAtom<unknown, [], Promise<void>>;
|
||||||
|
loadingAtom: jotai.PrimitiveAtom<boolean>;
|
||||||
|
numPoints: jotai.Atom<number>;
|
||||||
|
metrics: jotai.Atom<string[]>;
|
||||||
|
connection: jotai.Atom<string>;
|
||||||
|
manageConnection: jotai.Atom<boolean>;
|
||||||
|
connStatus: jotai.Atom<ConnStatus>;
|
||||||
|
plotMetaAtom: jotai.PrimitiveAtom<Map<string, TimeSeriesMeta>>;
|
||||||
|
endIconButtons: jotai.Atom<IconButtonDecl[]>;
|
||||||
|
plotTypeSelectedAtom: jotai.Atom<string>;
|
||||||
|
|
||||||
|
constructor(blockId: string, viewType: string) {
|
||||||
|
this.viewType = viewType;
|
||||||
|
this.blockId = blockId;
|
||||||
|
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
||||||
|
this.addDataAtom = jotai.atom(null, (get, set, points) => {
|
||||||
|
const targetLen = get(this.numPoints) + 1;
|
||||||
|
let data = get(this.dataAtom);
|
||||||
|
try {
|
||||||
|
if (data.length > targetLen) {
|
||||||
|
data = data.slice(data.length - targetLen);
|
||||||
|
}
|
||||||
|
if (data.length < targetLen) {
|
||||||
|
const defaultData = this.getDefaultData();
|
||||||
|
data = [...defaultData.slice(defaultData.length - targetLen + data.length), ...data];
|
||||||
|
}
|
||||||
|
const newData = [...data.slice(points.length), ...points];
|
||||||
|
set(this.dataAtom, newData);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Error adding data to sysinfo", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.plotMetaAtom = jotai.atom(new Map(Object.entries(DefaultPlotMeta)));
|
||||||
|
this.manageConnection = jotai.atom(true);
|
||||||
|
this.loadingAtom = jotai.atom(true);
|
||||||
|
this.numPoints = jotai.atom((get) => {
|
||||||
|
const blockData = get(this.blockAtom);
|
||||||
|
const metaNumPoints = blockData?.meta?.["graph:numpoints"];
|
||||||
|
if (metaNumPoints == null || metaNumPoints <= 0) {
|
||||||
|
return DefaultNumPoints;
|
||||||
|
}
|
||||||
|
return metaNumPoints;
|
||||||
|
});
|
||||||
|
this.metrics = jotai.atom((get) => {
|
||||||
|
let plotType = get(this.plotTypeSelectedAtom);
|
||||||
|
const plotData = get(this.dataAtom);
|
||||||
|
try {
|
||||||
|
const metrics = PlotTypes[plotType](plotData[plotData.length - 1]);
|
||||||
|
if (metrics == null || !Array.isArray(metrics)) {
|
||||||
|
return ["cpu"];
|
||||||
|
}
|
||||||
|
return metrics;
|
||||||
|
} catch (e) {
|
||||||
|
return ["cpu"];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.plotTypeSelectedAtom = jotai.atom((get) => {
|
||||||
|
const blockData = get(this.blockAtom);
|
||||||
|
const plotType = blockData?.meta?.["sysinfo:type"];
|
||||||
|
if (plotType == null || typeof plotType != "string") {
|
||||||
|
return "CPU";
|
||||||
|
}
|
||||||
|
return plotType;
|
||||||
|
});
|
||||||
|
this.viewIcon = jotai.atom((get) => {
|
||||||
|
return "chart-line"; // should not be hardcoded
|
||||||
|
});
|
||||||
|
this.viewName = jotai.atom((get) => {
|
||||||
|
return get(this.plotTypeSelectedAtom);
|
||||||
|
});
|
||||||
|
this.incrementCount = jotai.atom(null, async (get, set) => {
|
||||||
|
const meta = get(this.blockAtom).meta;
|
||||||
|
const count = meta.count ?? 0;
|
||||||
|
await RpcApi.SetMetaCommand(WindowRpcClient, {
|
||||||
|
oref: WOS.makeORef("block", this.blockId),
|
||||||
|
meta: { count: count + 1 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.connection = jotai.atom((get) => {
|
||||||
|
const blockData = get(this.blockAtom);
|
||||||
|
const connValue = blockData?.meta?.connection;
|
||||||
|
if (util.isBlank(connValue)) {
|
||||||
|
return "local";
|
||||||
|
}
|
||||||
|
return connValue;
|
||||||
|
});
|
||||||
|
this.dataAtom = jotai.atom(this.getDefaultData());
|
||||||
|
this.loadInitialData();
|
||||||
|
this.connStatus = jotai.atom((get) => {
|
||||||
|
const blockData = get(this.blockAtom);
|
||||||
|
const connName = blockData?.meta?.connection;
|
||||||
|
const connAtom = getConnStatusAtom(connName);
|
||||||
|
return get(connAtom);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadInitialData() {
|
||||||
|
globalStore.set(this.loadingAtom, true);
|
||||||
|
try {
|
||||||
|
const numPoints = globalStore.get(this.numPoints);
|
||||||
|
const connName = globalStore.get(this.connection);
|
||||||
|
const initialData = await RpcApi.EventReadHistoryCommand(WindowRpcClient, {
|
||||||
|
event: "sysinfo",
|
||||||
|
scope: connName,
|
||||||
|
maxitems: numPoints,
|
||||||
|
});
|
||||||
|
if (initialData == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newData = this.getDefaultData();
|
||||||
|
const initialDataItems: DataItem[] = initialData.map(convertWaveEventToDataItem);
|
||||||
|
// splice the initial data into the default data (replacing the newest points)
|
||||||
|
newData.splice(newData.length - initialDataItems.length, initialDataItems.length, ...initialDataItems);
|
||||||
|
globalStore.set(this.addDataAtom, newData);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Error loading initial data for sysinfo", e);
|
||||||
|
} finally {
|
||||||
|
globalStore.set(this.loadingAtom, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSettingsMenuItems(): ContextMenuItem[] {
|
||||||
|
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
||||||
|
const termThemes = fullConfig?.termthemes ?? {};
|
||||||
|
const termThemeKeys = Object.keys(termThemes);
|
||||||
|
const plotData = globalStore.get(this.dataAtom);
|
||||||
|
|
||||||
|
termThemeKeys.sort((a, b) => {
|
||||||
|
return termThemes[a]["display:order"] - termThemes[b]["display:order"];
|
||||||
|
});
|
||||||
|
const fullMenu: ContextMenuItem[] = [];
|
||||||
|
let submenu: ContextMenuItem[];
|
||||||
|
if (plotData.length == 0) {
|
||||||
|
submenu = [];
|
||||||
|
} else {
|
||||||
|
submenu = Object.keys(PlotTypes).map((plotType) => {
|
||||||
|
const dataTypes = PlotTypes[plotType](plotData[plotData.length - 1]);
|
||||||
|
const currentlySelected = globalStore.get(this.plotTypeSelectedAtom);
|
||||||
|
const menuItem: ContextMenuItem = {
|
||||||
|
label: plotType,
|
||||||
|
type: "radio",
|
||||||
|
checked: currentlySelected == plotType,
|
||||||
|
click: async () => {
|
||||||
|
await RpcApi.SetMetaCommand(WindowRpcClient, {
|
||||||
|
oref: WOS.makeORef("block", this.blockId),
|
||||||
|
meta: { "graph:metrics": dataTypes, "sysinfo:type": plotType },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return menuItem;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fullMenu.push({
|
||||||
|
label: "Plot Type",
|
||||||
|
submenu: submenu,
|
||||||
|
});
|
||||||
|
fullMenu.push({ type: "separator" });
|
||||||
|
return fullMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultData(): DataItem[] {
|
||||||
|
// set it back one to avoid backwards line being possible
|
||||||
|
const numPoints = globalStore.get(this.numPoints);
|
||||||
|
const currentTime = Date.now() - 1000;
|
||||||
|
const points: DataItem[] = [];
|
||||||
|
for (let i = numPoints; i > -1; i--) {
|
||||||
|
points.push({ ts: currentTime - i * 1000 });
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSysinfoViewModel(blockId: string, viewType: string): SysinfoViewModel {
|
||||||
|
const sysinfoViewModel = new SysinfoViewModel(blockId, viewType);
|
||||||
|
return sysinfoViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plotColors = ["#58C142", "#FFC107", "#FF5722", "#2196F3", "#9C27B0", "#00BCD4", "#FFEB3B", "#795548"];
|
||||||
|
|
||||||
|
type SysinfoViewProps = {
|
||||||
|
blockId: string;
|
||||||
|
model: SysinfoViewModel;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 SysinfoView({ model, blockId }: SysinfoViewProps) {
|
||||||
|
const connName = jotai.useAtomValue(model.connection);
|
||||||
|
const lastConnName = React.useRef(connName);
|
||||||
|
const connStatus = jotai.useAtomValue(model.connStatus);
|
||||||
|
const addPlotData = jotai.useSetAtom(model.addDataAtom);
|
||||||
|
const loading = jotai.useAtomValue(model.loadingAtom);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (connStatus?.status != "connected") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (lastConnName.current !== connName) {
|
||||||
|
lastConnName.current = connName;
|
||||||
|
model.loadInitialData();
|
||||||
|
}
|
||||||
|
}, [connStatus.status, connName]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
const unsubFn = waveEventSubscribe({
|
||||||
|
eventType: "sysinfo",
|
||||||
|
scope: connName,
|
||||||
|
handler: (event) => {
|
||||||
|
const loading = globalStore.get(model.loadingAtom);
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dataItem = convertWaveEventToDataItem(event);
|
||||||
|
addPlotData([dataItem]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log("subscribe to sysinfo", connName);
|
||||||
|
return () => {
|
||||||
|
unsubFn();
|
||||||
|
};
|
||||||
|
}, [connName]);
|
||||||
|
if (connStatus?.status != "connected") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <SysinfoViewInner key={connStatus?.connection ?? "local"} blockId={blockId} model={model} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SingleLinePlotProps = {
|
||||||
|
plotData: Array<DataItem>;
|
||||||
|
yval: string;
|
||||||
|
yvalMeta: TimeSeriesMeta;
|
||||||
|
blockId: string;
|
||||||
|
defaultColor: string;
|
||||||
|
title?: boolean;
|
||||||
|
sparkline?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SingleLinePlot({
|
||||||
|
plotData,
|
||||||
|
yval,
|
||||||
|
yvalMeta,
|
||||||
|
blockId,
|
||||||
|
defaultColor,
|
||||||
|
title = false,
|
||||||
|
sparkline = false,
|
||||||
|
}: SingleLinePlotProps) {
|
||||||
|
const containerRef = React.useRef<HTMLInputElement>();
|
||||||
|
const domRect = useDimensionsWithExistingRef(containerRef, 300);
|
||||||
|
const plotHeight = domRect?.height ?? 0;
|
||||||
|
const plotWidth = domRect?.width ?? 0;
|
||||||
|
const marks: Plot.Markish[] = [];
|
||||||
|
let decimalPlaces = yvalMeta?.decimalPlaces ?? 0;
|
||||||
|
let color = yvalMeta?.color;
|
||||||
|
if (!color) {
|
||||||
|
color = defaultColor;
|
||||||
|
}
|
||||||
|
marks.push(
|
||||||
|
() => htl.svg`<defs>
|
||||||
|
<linearGradient id="gradient-${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>
|
||||||
|
</defs>`
|
||||||
|
);
|
||||||
|
|
||||||
|
marks.push(
|
||||||
|
Plot.lineY(plotData, {
|
||||||
|
stroke: color,
|
||||||
|
strokeWidth: 2,
|
||||||
|
x: "ts",
|
||||||
|
y: yval,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// only add the gradient for single items
|
||||||
|
marks.push(
|
||||||
|
Plot.areaY(plotData, {
|
||||||
|
fill: `url(#gradient-${blockId}-${yval})`,
|
||||||
|
x: "ts",
|
||||||
|
y: yval,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (title) {
|
||||||
|
marks.push(
|
||||||
|
Plot.text([yvalMeta.name], {
|
||||||
|
frameAnchor: "top-left",
|
||||||
|
dx: 4,
|
||||||
|
fill: "var(--grey-text-color)",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const labelY = yvalMeta?.label ?? "?";
|
||||||
|
marks.push(
|
||||||
|
Plot.ruleX(
|
||||||
|
plotData,
|
||||||
|
Plot.pointerX({ x: "ts", py: yval, stroke: "var(--grey-text-color)", strokeWidth: 1, strokeDasharray: 2 })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
marks.push(
|
||||||
|
Plot.ruleY(
|
||||||
|
plotData,
|
||||||
|
Plot.pointerX({ px: "ts", y: yval, stroke: "var(--grey-text-color)", strokeWidth: 1, strokeDasharray: 2 })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
marks.push(
|
||||||
|
Plot.tip(
|
||||||
|
plotData,
|
||||||
|
Plot.pointerX({
|
||||||
|
x: "ts",
|
||||||
|
y: yval,
|
||||||
|
fill: "var(--main-bg-color)",
|
||||||
|
anchor: "middle",
|
||||||
|
dy: -30,
|
||||||
|
title: (d) =>
|
||||||
|
`${dayjs.unix(d.ts / 1000).format("HH:mm:ss")} ${Number(d[yval]).toFixed(decimalPlaces)}${labelY}`,
|
||||||
|
textPadding: 3,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
marks.push(
|
||||||
|
Plot.dot(
|
||||||
|
plotData,
|
||||||
|
Plot.pointerX({ x: "ts", y: yval, fill: color, r: 3, stroke: "var(--main-text-color)", strokeWidth: 1 })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let maxY = resolveDomainBound(yvalMeta?.maxy, plotData[plotData.length - 1]) ?? 100;
|
||||||
|
let minY = resolveDomainBound(yvalMeta?.miny, plotData[plotData.length - 1]) ?? 0;
|
||||||
|
const plot = Plot.plot({
|
||||||
|
axis: !sparkline,
|
||||||
|
x: {
|
||||||
|
grid: true,
|
||||||
|
label: "time",
|
||||||
|
tickFormat: (d) => `${dayjs.unix(d / 1000).format("HH:mm:ss")}`,
|
||||||
|
},
|
||||||
|
y: { label: labelY, domain: [minY, maxY] },
|
||||||
|
width: plotWidth,
|
||||||
|
height: plotHeight,
|
||||||
|
marks: marks,
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
containerRef.current.append(plot);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
plot.remove();
|
||||||
|
};
|
||||||
|
}, [plot, plotWidth, plotHeight]);
|
||||||
|
|
||||||
|
return <div ref={containerRef} className="sysinfo-plot-content" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SysinfoViewInner = React.memo(({ model }: SysinfoViewProps) => {
|
||||||
|
const plotData = jotai.useAtomValue(model.dataAtom);
|
||||||
|
const yvals = jotai.useAtomValue(model.metrics);
|
||||||
|
const plotMeta = jotai.useAtomValue(model.plotMetaAtom);
|
||||||
|
const osRef = React.useRef<OverlayScrollbarsComponentRef>();
|
||||||
|
let title = false;
|
||||||
|
let cols2 = false;
|
||||||
|
if (yvals.length > 1) {
|
||||||
|
title = true;
|
||||||
|
}
|
||||||
|
if (yvals.length > 2) {
|
||||||
|
cols2 = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverlayScrollbarsComponent ref={osRef} className="scrollable" options={{ scrollbars: { autoHide: "leave" } }}>
|
||||||
|
<div className={clsx("sysinfo-view", { "two-columns": cols2 })}>
|
||||||
|
{yvals.map((yval, idx) => {
|
||||||
|
return (
|
||||||
|
<SingleLinePlot
|
||||||
|
key={`plot-${model.blockId}-${yval}`}
|
||||||
|
plotData={plotData}
|
||||||
|
yval={yval}
|
||||||
|
yvalMeta={plotMeta.get(yval)}
|
||||||
|
blockId={model.blockId}
|
||||||
|
defaultColor={"var(--accent-color)"}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export { makeSysinfoViewModel, SysinfoView, SysinfoViewModel };
|
9
frontend/types/custom.d.ts
vendored
9
frontend/types/custom.d.ts
vendored
@ -332,6 +332,15 @@ 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;
|
||||||
|
decimalPlaces?: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
1
frontend/types/gotypes.d.ts
vendored
1
frontend/types/gotypes.d.ts
vendored
@ -306,6 +306,7 @@ declare global {
|
|||||||
"graph:*"?: boolean;
|
"graph:*"?: boolean;
|
||||||
"graph:numpoints"?: number;
|
"graph:numpoints"?: number;
|
||||||
"graph:metrics"?: string[];
|
"graph:metrics"?: string[];
|
||||||
|
"sysinfo:type"?: string;
|
||||||
"bg:*"?: boolean;
|
"bg:*"?: boolean;
|
||||||
bg?: string;
|
bg?: string;
|
||||||
"bg:opacity"?: number;
|
"bg:opacity"?: number;
|
||||||
|
@ -64,6 +64,8 @@ const (
|
|||||||
MetaKey_GraphNumPoints = "graph:numpoints"
|
MetaKey_GraphNumPoints = "graph:numpoints"
|
||||||
MetaKey_GraphMetrics = "graph:metrics"
|
MetaKey_GraphMetrics = "graph:metrics"
|
||||||
|
|
||||||
|
MetaKey_SysinfoType = "sysinfo:type"
|
||||||
|
|
||||||
MetaKey_BgClear = "bg:*"
|
MetaKey_BgClear = "bg:*"
|
||||||
MetaKey_Bg = "bg"
|
MetaKey_Bg = "bg"
|
||||||
MetaKey_BgOpacity = "bg:opacity"
|
MetaKey_BgOpacity = "bg:opacity"
|
||||||
|
@ -64,6 +64,8 @@ type MetaTSType struct {
|
|||||||
GraphNumPoints int `json:"graph:numpoints,omitempty"`
|
GraphNumPoints int `json:"graph:numpoints,omitempty"`
|
||||||
GraphMetrics []string `json:"graph:metrics,omitempty"`
|
GraphMetrics []string `json:"graph:metrics,omitempty"`
|
||||||
|
|
||||||
|
SysinfoType string `json:"sysinfo:type,omitempty"`
|
||||||
|
|
||||||
// for tabs
|
// for tabs
|
||||||
BgClear bool `json:"bg:*,omitempty"`
|
BgClear bool `json:"bg:*,omitempty"`
|
||||||
Bg string `json:"bg,omitempty"`
|
Bg string `json:"bg,omitempty"`
|
||||||
|
@ -41,13 +41,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defwidget@cpuplot": {
|
"defwidget@sysinfo": {
|
||||||
"display:order": 5,
|
"display:order": 5,
|
||||||
"icon": "chart-line",
|
"icon": "chart-line",
|
||||||
"label": "cpu",
|
"label": "sysinfo",
|
||||||
"blockdef": {
|
"blockdef": {
|
||||||
"meta": {
|
"meta": {
|
||||||
"view": "cpuplot"
|
"view": "sysinfo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ func GetStarterLayout() PortableLayout {
|
|||||||
}, Focused: true},
|
}, Focused: true},
|
||||||
{IndexArr: []int{1}, BlockDef: &waveobj.BlockDef{
|
{IndexArr: []int{1}, BlockDef: &waveobj.BlockDef{
|
||||||
Meta: waveobj.MetaMapType{
|
Meta: waveobj.MetaMapType{
|
||||||
waveobj.MetaKey_View: "cpuplot",
|
waveobj.MetaKey_View: "sysinfo",
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
{IndexArr: []int{1, 1}, BlockDef: &waveobj.BlockDef{
|
{IndexArr: []int{1, 1}, BlockDef: &waveobj.BlockDef{
|
||||||
|
@ -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) {
|
||||||
|
@ -83,7 +83,7 @@ func MakePlotData(ctx context.Context, blockId string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
viewName := block.Meta.GetString(waveobj.MetaKey_View, "")
|
viewName := block.Meta.GetString(waveobj.MetaKey_View, "")
|
||||||
if viewName != "cpuplot" {
|
if viewName != "cpuplot" && viewName != "sysinfo" {
|
||||||
return fmt.Errorf("invalid view type: %s", viewName)
|
return fmt.Errorf("invalid view type: %s", viewName)
|
||||||
}
|
}
|
||||||
return filestore.WFS.MakeFile(ctx, blockId, "cpuplotdata", nil, filestore.FileOptsType{})
|
return filestore.WFS.MakeFile(ctx, blockId, "cpuplotdata", nil, filestore.FileOptsType{})
|
||||||
@ -95,7 +95,7 @@ func SavePlotData(ctx context.Context, blockId string, history string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
viewName := block.Meta.GetString(waveobj.MetaKey_View, "")
|
viewName := block.Meta.GetString(waveobj.MetaKey_View, "")
|
||||||
if viewName != "cpuplot" {
|
if viewName != "cpuplot" && viewName != "sysinfo" {
|
||||||
return fmt.Errorf("invalid view type: %s", viewName)
|
return fmt.Errorf("invalid view type: %s", viewName)
|
||||||
}
|
}
|
||||||
// todo: interpret the data being passed
|
// todo: interpret the data being passed
|
||||||
|
Loading…
Reference in New Issue
Block a user