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"