diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx
index b8d97e302..180f195cb 100644
--- a/frontend/app/block/block.tsx
+++ b/frontend/app/block/block.tsx
@@ -10,6 +10,7 @@ import { atoms, globalStore, setBlockFocus, useBlockAtom } from "@/store/global"
import * as services from "@/store/services";
import * as WOS from "@/store/wos";
import * as util from "@/util/util";
+import { CpuPlotView, makeCpuPlotViewModel } from "@/view/cpuplot";
import { PlotView } from "@/view/plotview";
import { PreviewView, makePreviewModel } from "@/view/preview";
import { TerminalView, makeTerminalModel } from "@/view/term/term";
@@ -521,6 +522,10 @@ function getViewElemAndModel(
const waveAiModel = makeWaveAiViewModel(blockId);
viewElem = ;
viewModel = waveAiModel;
+ } else if (blockView === "cpuplot") {
+ const cpuPlotModel = makeCpuPlotViewModel(blockId);
+ viewElem = ;
+ viewModel = cpuPlotModel;
}
if (viewModel == null) {
viewElem = Invalid View "{blockView}";
diff --git a/frontend/app/hook/useWidth.tsx b/frontend/app/hook/useWidth.tsx
new file mode 100644
index 000000000..68d0d204c
--- /dev/null
+++ b/frontend/app/hook/useWidth.tsx
@@ -0,0 +1,45 @@
+import debounce from "lodash.debounce";
+import { useCallback, useEffect, useState } from "react";
+
+const useWidth = (ref: React.RefObject, delay = 0) => {
+ const [width, setWidth] = useState(null);
+
+ const updateWidth = useCallback(() => {
+ if (ref.current) {
+ const element = ref.current;
+ const style = window.getComputedStyle(element);
+ const paddingLeft = parseFloat(style.paddingLeft);
+ const paddingRight = parseFloat(style.paddingRight);
+ const marginLeft = parseFloat(style.marginLeft);
+ const marginRight = parseFloat(style.marginRight);
+ const parentWidth = element.clientWidth - paddingLeft - paddingRight - marginLeft - marginRight;
+ setWidth(parentWidth);
+ }
+ }, []);
+
+ const fUpdateWidth = useCallback(delay > 0 ? debounce(updateWidth, delay) : updateWidth, [updateWidth, delay]);
+
+ useEffect(() => {
+ const resizeObserver = new ResizeObserver(() => {
+ fUpdateWidth();
+ });
+
+ if (ref.current) {
+ resizeObserver.observe(ref.current);
+ fUpdateWidth();
+ }
+
+ return () => {
+ if (ref.current) {
+ resizeObserver.unobserve(ref.current);
+ }
+ if (delay > 0) {
+ fUpdateWidth.cancel();
+ }
+ };
+ }, [fUpdateWidth]);
+
+ return width;
+};
+
+export { useWidth };
diff --git a/frontend/app/store/wshserver.ts b/frontend/app/store/wshserver.ts
index 6f4a6f48b..d464f28f9 100644
--- a/frontend/app/store/wshserver.ts
+++ b/frontend/app/store/wshserver.ts
@@ -102,6 +102,11 @@ class WshServerType {
return WOS.wshServerRpcHelper_call("setview", data, opts);
}
+ // command "streamcpudata" [responsestream]
+ StreamCpuDataCommand(data: CpuDataRequest, opts?: WshRpcCommandOpts): AsyncGenerator {
+ return WOS.wshServerRpcHelper_responsestream("streamcpudata", data, opts);
+ }
+
// command "streamtest" [responsestream]
StreamTestCommand(opts?: WshRpcCommandOpts): AsyncGenerator {
return WOS.wshServerRpcHelper_responsestream("streamtest", null, opts);
diff --git a/frontend/app/view/cpuplot.less b/frontend/app/view/cpuplot.less
new file mode 100644
index 000000000..4f950dc63
--- /dev/null
+++ b/frontend/app/view/cpuplot.less
@@ -0,0 +1,9 @@
+// Copyright 2024, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+.plot-view {
+ display: flex;
+ justify-content: center;
+ align-items: stretch;
+ width: 100%;
+}
diff --git a/frontend/app/view/cpuplot.tsx b/frontend/app/view/cpuplot.tsx
new file mode 100644
index 000000000..826f32bb0
--- /dev/null
+++ b/frontend/app/view/cpuplot.tsx
@@ -0,0 +1,135 @@
+// Copyright 2024, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import { useHeight } from "@/app/hook/useHeight";
+import { useWidth } from "@/app/hook/useWidth";
+import { WshServer } from "@/store/wshserver";
+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 "./cpuplot.less";
+
+type Point = {
+ time: number;
+ value: number;
+};
+
+class CpuPlotViewModel {
+ blockAtom: jotai.Atom;
+ termMode: jotai.Atom;
+ htmlElemFocusRef: React.RefObject;
+ blockId: string;
+ viewIcon: jotai.Atom;
+ viewText: jotai.Atom;
+ viewName: jotai.Atom;
+ dataAtom: jotai.PrimitiveAtom>;
+ addDataAtom: jotai.WritableAtom;
+ width: number;
+
+ constructor(blockId: string) {
+ this.blockId = blockId;
+ this.width = 100;
+ this.dataAtom = jotai.atom(this.getDefaultData());
+ this.addDataAtom = jotai.atom(null, (get, set, point) => {
+ // not efficient but should be okay for a demo?
+ const data = get(this.dataAtom);
+ set(this.dataAtom, [...data.slice(1), point]);
+ });
+
+ this.viewIcon = jotai.atom((get) => {
+ return "chart-line"; // should not be hardcoded
+ });
+ this.viewName = jotai.atom((get) => {
+ return "CPU %"; // should not be hardcoded
+ });
+ }
+
+ getDefaultData(): Array {
+ // set it back one to avoid backwards line being possible
+ const currentTime = Date.now() / 1000 - 1;
+ const points = [];
+ for (let i = this.width; i > -1; i--) {
+ points.push({ time: currentTime - i, value: 0 });
+ }
+ return points;
+ }
+}
+
+function makeCpuPlotViewModel(blockId: string): CpuPlotViewModel {
+ const cpuPlotViewModel = new CpuPlotViewModel(blockId);
+ return cpuPlotViewModel;
+}
+
+function CpuPlotView({ model }: { model: CpuPlotViewModel }) {
+ const containerRef = React.useRef();
+ const plotData = jotai.useAtomValue(model.dataAtom);
+ const addPlotData = jotai.useSetAtom(model.addDataAtom);
+ const parentHeight = useHeight(containerRef);
+ const parentWidth = useWidth(containerRef);
+
+ React.useEffect(() => {
+ console.log("plotData:", plotData);
+ }, [plotData]);
+
+ React.useEffect(() => {
+ const temp = async () => {
+ const dataGen = WshServer.StreamCpuDataCommand(
+ { id: model.blockId },
+ { timeout: 999999999, noresponse: false }
+ );
+ try {
+ for await (const datum of dataGen) {
+ addPlotData(datum);
+ }
+ } catch (e) {
+ console.log(e);
+ }
+ };
+ temp();
+ }, []);
+
+ React.useEffect(() => {
+ const plot = Plot.plot({
+ x: { grid: true, label: "time", tickFormat: (d) => `${dayjs.unix(d).format("HH:mm:ss")}` },
+ y: { label: "%", domain: [0, 100] },
+ width: parentWidth,
+ height: parentHeight,
+ marks: [
+ () => htl.svg`
+
+
+
+
+ `,
+ Plot.lineY(plotData, {
+ stroke: "#58C142",
+ strokeWidth: 2,
+ x: "time",
+ y: "value",
+ }),
+ Plot.areaY(plotData, {
+ fill: "url(#gradient)",
+ x: "time",
+ y: "value",
+ }),
+ ],
+ });
+
+ if (plot !== undefined) {
+ containerRef.current.append(plot);
+ }
+
+ return () => {
+ if (plot !== undefined) {
+ plot.remove();
+ }
+ };
+ }, [plotData, parentHeight, parentWidth]);
+
+ return ;
+}
+
+export { CpuPlotView, CpuPlotViewModel, makeCpuPlotViewModel };
diff --git a/frontend/app/view/directorypreview.tsx b/frontend/app/view/directorypreview.tsx
index 21d45307d..399a970a8 100644
--- a/frontend/app/view/directorypreview.tsx
+++ b/frontend/app/view/directorypreview.tsx
@@ -426,8 +426,10 @@ function TableBody({
label: "Open Preview in New Block",
click: async () => {
const blockDef = {
- view: "preview",
- meta: { file: path },
+ meta: {
+ view: "preview",
+ file: path,
+ },
};
await createBlock(blockDef);
},
@@ -438,10 +440,10 @@ function TableBody({
label: "Open Terminal in New Block",
click: async () => {
const termBlockDef: BlockDef = {
- controller: "shell",
- view: "term",
meta: {
- cwd: path,
+ controller: "shell",
+ view: "term",
+ "cmd:cwd": path,
},
};
await createBlock(termBlockDef);
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts
index f280b9666..51ec5cb0a 100644
--- a/frontend/types/gotypes.d.ts
+++ b/frontend/types/gotypes.d.ts
@@ -131,6 +131,17 @@ declare global {
meta: MetaType;
};
+ // wshrpc.CpuDataRequest
+ type CpuDataRequest = {
+ id: string;
+ };
+
+ // wshrpc.CpuDataType
+ type CpuDataType = {
+ time: number;
+ value: number;
+ };
+
// wstore.FileDef
type FileDef = {
filetype?: string;
diff --git a/go.mod b/go.mod
index 704d53251..c89d332f3 100644
--- a/go.mod
+++ b/go.mod
@@ -18,6 +18,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0
github.com/sashabaranov/go-openai v1.27.1
github.com/sawka/txwrap v0.2.0
+ github.com/shirou/gopsutil/v4 v4.24.6
github.com/spf13/cobra v1.8.1
github.com/wavetermdev/htmltoken v0.1.0
github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94
@@ -27,11 +28,17 @@ require (
require (
github.com/felixge/httpsnoop v1.0.3 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
+ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
+ github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/spf13/pflag v1.0.5 // indirect
- github.com/stretchr/testify v1.8.4 // indirect
+ github.com/tklauser/go-sysconf v0.3.12 // indirect
+ github.com/tklauser/numcpus v0.6.1 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
diff --git a/go.sum b/go.sum
index cc33522f1..ad7d2d664 100644
--- a/go.sum
+++ b/go.sum
@@ -12,12 +12,17 @@ github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBd
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
@@ -37,17 +42,27 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sashabaranov/go-openai v1.27.1 h1:7Nx6db5NXbcoutNmAUQulEQZEpHG/SkzfexP2X5RWMk=
github.com/sashabaranov/go-openai v1.27.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sawka/txwrap v0.2.0 h1:V3LfvKVLULxcYSxdMguLwFyQFMEU9nFDJopg0ZkL+94=
github.com/sawka/txwrap v0.2.0/go.mod h1:wwQ2SQiN4U+6DU/iVPhbvr7OzXAtgZlQCIGuvOswEfA=
+github.com/shirou/gopsutil/v4 v4.24.6 h1:9qqCSYF2pgOU+t+NgJtp7Co5+5mHF/HyKBUckySQL64=
+github.com/shirou/gopsutil/v4 v4.24.6/go.mod h1:aoebb2vxetJ/yIDZISmduFvVNPHqXQ9SEJwRXxkf0RA=
+github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
+github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
+github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
+github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -55,25 +70,36 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
+github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
+github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
+github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/wavetermdev/htmltoken v0.1.0 h1:RMdA9zTfnYa5jRC4RRG3XNoV5NOP8EDxpaVPjuVz//Q=
github.com/wavetermdev/htmltoken v0.1.0/go.mod h1:5FM0XV6zNYiNza2iaTcFGj+hnMtgqumFHO31Z8euquk=
github.com/wavetermdev/ssh_config v0.0.0-20240306041034-17e2087ebde2 h1:onqZrJVap1sm15AiIGTfWzdr6cEF0KdtddeuuOVhzyY=
github.com/wavetermdev/ssh_config v0.0.0-20240306041034-17e2087ebde2/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94 h1:/SPCxd4KHlS4eRTreYEXWFRr8WfRFBcChlV5cgkaO58=
github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94/go.mod h1:ywoo7DXdYueQ0tTPhVoB+wzRTgERSE19EA3mR6KGRaI=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/package.json b/package.json
index ef42d23d5..ff02c9dbd 100644
--- a/package.json
+++ b/package.json
@@ -89,6 +89,7 @@
"css-tree": "^2.3.1",
"dayjs": "^1.11.12",
"electron-updater": "6.3.1",
+ "htl": "^0.3.1",
"html-to-image": "^1.11.11",
"immer": "^10.1.1",
"jotai": "^2.9.1",
diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go
index 818bb9ff8..5ae98aabb 100644
--- a/pkg/wconfig/settingsconfig.go
+++ b/pkg/wconfig/settingsconfig.go
@@ -304,6 +304,15 @@ func applyDefaultSettings(settings *SettingsConfigType) {
},
},
},
+ {
+ Icon: "chart-line",
+ Label: "cpu",
+ BlockDef: wstore.BlockDef{
+ Meta: map[string]any{
+ wstore.MetaKey_View: "cpuplot",
+ },
+ },
+ },
}
if settings.Widgets == nil {
settings.Widgets = defaultWidgets
diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go
index 4d06eab15..677563d44 100644
--- a/pkg/wshrpc/wshclient/wshclient.go
+++ b/pkg/wshrpc/wshclient/wshclient.go
@@ -125,6 +125,11 @@ func SetViewCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockSetViewData, opts
return err
}
+// command "streamcpudata", wshserver.StreamCpuDataCommand
+func StreamCpuDataCommand(w *wshutil.WshRpc, data wshrpc.CpuDataRequest, opts *wshrpc.WshRpcCommandOpts) chan wshrpc.RespOrErrorUnion[wshrpc.CpuDataType] {
+ return sendRpcRequestResponseStreamHelper[wshrpc.CpuDataType](w, "streamcpudata", data, opts)
+}
+
// command "streamtest", wshserver.StreamTestCommand
func StreamTestCommand(w *wshutil.WshRpc, opts *wshrpc.WshRpcCommandOpts) chan wshrpc.RespOrErrorUnion[int] {
return sendRpcRequestResponseStreamHelper[int](w, "streamtest", nil, opts)
diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go
index d45da0775..dab222964 100644
--- a/pkg/wshrpc/wshrpctypes.go
+++ b/pkg/wshrpc/wshrpctypes.go
@@ -43,6 +43,7 @@ const (
Command_EventUnsubAll = "eventunsuball"
Command_StreamTest = "streamtest"
Command_StreamWaveAi = "streamwaveai"
+ Command_StreamCpuData = "streamcpudata"
)
type RespOrErrorUnion[T any] struct {
@@ -72,6 +73,7 @@ type WshRpcInterface interface {
EventUnsubAllCommand(ctx context.Context) error
StreamTestCommand(ctx context.Context) chan RespOrErrorUnion[int]
StreamWaveAiCommand(ctx context.Context, request OpenAiStreamRequest) chan RespOrErrorUnion[OpenAIPacketType]
+ StreamCpuDataCommand(ctx context.Context, request CpuDataRequest) chan RespOrErrorUnion[CpuDataType]
}
// for frontend
@@ -228,3 +230,12 @@ type OpenAIUsageType struct {
CompletionTokens int `json:"completion_tokens,omitempty"`
TotalTokens int `json:"total_tokens,omitempty"`
}
+
+type CpuDataRequest struct {
+ Id string `json:"id"`
+}
+
+type CpuDataType struct {
+ Time int64 `json:"time"`
+ Value float64 `json:"value"`
+}
diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go
index 44876a530..b2ab795e8 100644
--- a/pkg/wshrpc/wshserver/wshserver.go
+++ b/pkg/wshrpc/wshserver/wshserver.go
@@ -8,12 +8,14 @@ package wshserver
import (
"context"
"encoding/base64"
+ "encoding/json"
"fmt"
"io/fs"
"log"
"strings"
"time"
+ "github.com/shirou/gopsutil/v4/cpu"
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
"github.com/wavetermdev/thenextwave/pkg/eventbus"
"github.com/wavetermdev/thenextwave/pkg/filestore"
@@ -67,6 +69,71 @@ func (ws *WshServer) StreamWaveAiCommand(ctx context.Context, request wshrpc.Ope
return waveai.RunLocalCompletionStream(ctx, request)
}
+func (ws *WshServer) StreamCpuDataCommand(ctx context.Context, request wshrpc.CpuDataRequest) chan wshrpc.RespOrErrorUnion[wshrpc.CpuDataType] {
+ rtn := make(chan wshrpc.RespOrErrorUnion[wshrpc.CpuDataType])
+ go func() {
+ defer close(rtn)
+ MakePlotData(ctx, request.Id)
+ // we can use the err from MakePlotData to determine if a routine is already running
+ // but we still need a way to close it or get data from it
+ for {
+ now := time.Now()
+ percent, err := cpu.Percent(0, false)
+ if err != nil {
+ rtn <- wshrpc.RespOrErrorUnion[wshrpc.CpuDataType]{Error: err}
+ }
+ var value float64
+ if len(percent) > 0 {
+ value = percent[0]
+ } else {
+ value = 0.0
+ }
+ cpuData := wshrpc.CpuDataType{Time: now.UnixMilli() / 1000, Value: value}
+ rtn <- wshrpc.RespOrErrorUnion[wshrpc.CpuDataType]{Response: cpuData}
+ time.Sleep(time.Second * 1)
+ // this will end the goroutine if the block is closed
+ err = SavePlotData(ctx, request.Id, "")
+ if err != nil {
+ rtn <- wshrpc.RespOrErrorUnion[wshrpc.CpuDataType]{Error: err}
+ return
+ }
+ }
+ }()
+
+ return rtn
+}
+
+func MakePlotData(ctx context.Context, blockId string) error {
+ block, err := wstore.DBMustGet[*wstore.Block](ctx, blockId)
+ if err != nil {
+ return err
+ }
+ viewName := block.Meta.GetString(wstore.MetaKey_View, "")
+ if viewName != "cpuplot" {
+ return fmt.Errorf("invalid view type: %s", viewName)
+ }
+ return filestore.WFS.MakeFile(ctx, blockId, "cpuplotdata", nil, filestore.FileOptsType{})
+}
+
+func SavePlotData(ctx context.Context, blockId string, history string) error {
+ block, err := wstore.DBMustGet[*wstore.Block](ctx, blockId)
+ if err != nil {
+ return err
+ }
+ viewName := block.Meta.GetString(wstore.MetaKey_View, "")
+ if viewName != "cpuplot" {
+ return fmt.Errorf("invalid view type: %s", viewName)
+ }
+ // todo: interpret the data being passed
+ // for now, this is just to throw an error if the block was closed
+ historyBytes, err := json.Marshal(history)
+ if err != nil {
+ return fmt.Errorf("unable to serialize plot data: %v", err)
+ }
+ // ignore MakeFile error (already exists is ok)
+ return filestore.WFS.WriteFile(ctx, blockId, "cpuplotdata", historyBytes)
+}
+
func (ws *WshServer) GetMetaCommand(ctx context.Context, data wshrpc.CommandGetMetaData) (waveobj.MetaMapType, error) {
log.Printf("calling meta: %s\n", data.ORef)
obj, err := wstore.DBGetORef(ctx, data.ORef)
diff --git a/yarn.lock b/yarn.lock
index 8b9125d9c..3ba2073eb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7934,6 +7934,13 @@ __metadata:
languageName: node
linkType: hard
+"htl@npm:^0.3.1":
+ version: 0.3.1
+ resolution: "htl@npm:0.3.1"
+ checksum: 10c0/36a22e10e0f11982c4e142c8c7bd389b3e9e7d70379c12f7572140a668a2a0328198f53fcb582281be761db91240ffb60261840256ebb10131739454baf82560
+ languageName: node
+ linkType: hard
+
"html-escaper@npm:^2.0.0":
version: 2.0.2
resolution: "html-escaper@npm:2.0.2"
@@ -12099,6 +12106,7 @@ __metadata:
electron-vite: "npm:^2.3.0"
eslint: "npm:^9.8.0"
eslint-config-prettier: "npm:^9.1.0"
+ htl: "npm:^0.3.1"
html-to-image: "npm:^1.11.11"
immer: "npm:^10.1.1"
jotai: "npm:^2.9.1"