merge main to dev 0.9

This commit is contained in:
sawka 2024-10-18 09:29:39 -07:00
commit 0590ba2509
30 changed files with 838 additions and 374 deletions

156
.github/workflows/testdriver.yml vendored Normal file
View File

@ -0,0 +1,156 @@
name: TestDriver.ai
on:
push:
branches:
- main
tags:
- "v[0-9]+.[0-9]+.[0-9]+*"
pull_request:
branches:
- main
schedule:
- cron: 0 21 * * *
workflow_dispatch: null
env:
GO_VERSION: "1.22"
NODE_VERSION: "20"
permissions:
contents: read # To allow the action to read repository contents
pull-requests: write # To allow the action to create/update pull request comments
jobs:
build_and_upload:
name: Test Onboarding
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
# General build dependencies
- uses: actions/setup-go@v5
with:
go-version: ${{env.GO_VERSION}}
cache-dependency-path: |
go.sum
- uses: actions/setup-node@v4
with:
node-version: ${{env.NODE_VERSION}}
- name: Install Yarn
run: |
corepack enable
yarn install
- name: Install Task
uses: arduino/setup-task@v2
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build
run: task package
env:
USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one.
CSC_IDENTITY_AUTO_DISCOVERY: false # disable codesign
shell: powershell # electron-builder's Windows code signing package has some compatibility issues with pwsh, so we need to use Windows Powershell
# Upload .exe as an artifact
- name: Upload .exe artifact
id: upload
uses: actions/upload-artifact@v4
with:
name: windows-exe
path: make/*.exe
- uses: testdriverai/action@main
id: testdriver
env:
FORCE_COLOR: "3"
with:
key: ${{ secrets.DASHCAM_API }}
prerun: |
$headers = @{
Authorization = "token ${{ secrets.GITHUB_TOKEN }}"
}
$downloadFolder = "./download"
$artifactFileName = "waveterm.exe"
$artifactFilePath = "$downloadFolder/$artifactFileName"
Write-Host "Starting the artifact download process..."
# Create the download directory if it doesn't exist
if (-not (Test-Path -Path $downloadFolder)) {
Write-Host "Creating download folder..."
mkdir $downloadFolder
} else {
Write-Host "Download folder already exists."
}
# Fetch the artifact upload URL
Write-Host "Fetching the artifact upload URL..."
$artifactUrl = (Invoke-RestMethod -Uri "https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" -Headers $headers).artifacts[0].archive_download_url
if ($artifactUrl) {
Write-Host "Artifact URL successfully fetched: $artifactUrl"
} else {
Write-Error "Failed to fetch the artifact URL."
exit 1
}
# Download the artifact (zipped file)
Write-Host "Starting artifact download..."
$artifactZipPath = "$env:TEMP\artifact.zip"
try {
Invoke-WebRequest -Uri $artifactUrl `
-Headers $headers `
-OutFile $artifactZipPath `
-MaximumRedirection 5
Write-Host "Artifact downloaded successfully to $artifactZipPath"
} catch {
Write-Error "Error downloading artifact: $_"
exit 1
}
# Unzip the artifact
$artifactUnzipPath = "$env:TEMP\artifact"
Write-Host "Unzipping the artifact to $artifactUnzipPath..."
try {
Expand-Archive -Path $artifactZipPath -DestinationPath $artifactUnzipPath -Force
Write-Host "Artifact unzipped successfully to $artifactUnzipPath"
} catch {
Write-Error "Failed to unzip the artifact: $_"
exit 1
}
# Find the installer or app executable
$artifactInstallerPath = Get-ChildItem -Path $artifactUnzipPath -Filter *.exe -Recurse | Select-Object -First 1
if ($artifactInstallerPath) {
Write-Host "Executable file found: $($artifactInstallerPath.FullName)"
} else {
Write-Error "Executable file not found. Exiting."
exit 1
}
# Run the installer and log the result
Write-Host "Running the installer: $($artifactInstallerPath.FullName)..."
try {
Start-Process -FilePath $artifactInstallerPath.FullName -Wait
Write-Host "Installer ran successfully."
} catch {
Write-Error "Failed to run the installer: $_"
exit 1
}
# Optional: If the app executable is different from the installer, find and launch it
$wavePath = Join-Path $env:USERPROFILE "AppData\Local\Programs\waveterm\Wave.exe"
Write-Host "Launching the application: $($wavePath)"
Start-Process -FilePath $wavePath
Write-Host "Application launched."
prompt: |
1. /run testdriver/onboarding.yml
2. /generate desktop 20

BIN
assets/appicon-windows.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,20 @@
<svg width="1024" height="727" viewBox="0 0 1024 727" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M328.87 332.526C280.49 332.526 253.744 364.034 239.191 434.533L20.5 403.025C47.2464 172.231 136.925 60.3787 314.317 60.3787C445.688 60.3787 574.307 160.022 637.24 160.022C686.012 160.022 712.365 126.151 726.918 58.0156L945.609 89.5233C921.223 320.711 829.184 432.563 651.793 432.563C518.454 432.17 394.556 332.526 328.87 332.526Z"
fill="url(#paint0_linear_1814_3217)" />
<path
d="M390.87 558.061C342.49 558.061 315.744 589.569 301.191 660.067L82.5 628.559C109.246 397.765 198.925 285.519 376.317 285.519C507.295 285.519 636.307 385.162 699.239 385.162C748.012 385.162 774.365 351.292 788.918 283.156L1007.61 314.664C983.223 545.852 891.184 657.704 713.793 657.704C580.454 657.704 456.556 558.061 390.87 558.061Z"
fill="url(#paint1_linear_1814_3217)" />
<defs>
<linearGradient id="paint0_linear_1814_3217" x1="20.5503" y1="246.309" x2="945.797" y2="246.309"
gradientUnits="userSpaceOnUse">
<stop offset="0.1418" stop-color="#1F4D22" />
<stop offset="0.8656" stop-color="#418D31" />
</linearGradient>
<linearGradient id="paint1_linear_1814_3217" x1="82.5673" y1="471.774" x2="1007.81" y2="471.774"
gradientUnits="userSpaceOnUse">
<stop offset="0.2223" stop-color="#418D31" />
<stop offset="0.7733" stop-color="#58C142" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 295 KiB

View File

@ -3,6 +3,8 @@ const pkg = require("./package.json");
const fs = require("fs");
const path = require("path");
const windowsShouldSign = !!process.env.SM_CODE_SIGNING_CERT_SHA1_HASH;
/**
* @type {import('electron-builder').Configuration}
* @see https://www.electron.build/configuration/configuration
@ -47,7 +49,6 @@ const config = {
arch: ["universal", "arm64", "x64"],
},
],
icon: "build/icons.icns",
category: "public.app-category.developer-tools",
minimumSystemVersion: "10.15.0",
mergeASARs: true,
@ -57,7 +58,6 @@ const config = {
artifactName: "${name}-${platform}-${arch}-${version}.${ext}",
category: "TerminalEmulator",
executableName: pkg.name,
icon: "build/icons.icns",
target: ["zip", "deb", "rpm", "AppImage", "pacman"],
synopsis: pkg.description,
description: null,
@ -73,12 +73,13 @@ const config = {
afterInstall: "build/deb-postinstall.tpl",
},
win: {
icon: "build/icons.icns",
publisherName: "Command Line Inc",
target: ["nsis", "msi", "zip"],
certificateSubjectName: "Command Line Inc",
certificateSha1: process.env.SM_CODE_SIGNING_CERT_SHA1_HASH,
signingHashAlgorithms: ["sha256"],
signtoolOptions: windowsShouldSign && {
signingHashAlgorithms: ["sha256"],
publisherName: "Command Line Inc",
certificateSubjectName: "Command Line Inc",
certificateSha1: process.env.SM_CODE_SIGNING_CERT_SHA1_HASH,
},
},
appImage: {
license: "LICENSE",

View File

@ -17,7 +17,10 @@ export async function initDocsite() {
console.log("Embedded docsite is running, using embedded version for help view");
docsiteUrl = docsiteEmbeddedUrl;
} else {
console.log("Embedded docsite is not running, using web version for help view", response);
console.log(
"Embedded docsite is not running, using web version for help view",
"status: " + response?.status
);
docsiteUrl = docsiteWebUrl;
}
} catch (error) {

View File

@ -657,7 +657,7 @@ async function relaunchBrowserWindows(): Promise<void> {
}
for (const win of wins) {
await win.waveReadyPromise;
console.log("show", win.waveWindowId);
console.log("show window", win.waveWindowId);
win.show();
}
}

View File

@ -4,6 +4,7 @@
import { BlockComponentModel2, BlockProps } from "@/app/block/blocktypes";
import { PlotView } from "@/app/view/plotview/plotview";
import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview";
import { SysinfoView, SysinfoViewModel, makeSysinfoViewModel } from "@/app/view/sysinfo/sysinfo";
import { ErrorBoundary } from "@/element/errorboundary";
import { CenteredDiv } from "@/element/quickelems";
import { NodeModel, useDebouncedNodeInnerRect } from "@/layout/index";
@ -16,7 +17,6 @@ import {
import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos";
import { focusedBlockId, getElemAsStr } from "@/util/focusutil";
import { isBlank } from "@/util/util";
import { CpuPlotView, CpuPlotViewModel, makeCpuPlotViewModel } from "@/view/cpuplot/cpuplot";
import { HelpView, HelpViewModel, makeHelpViewModel } from "@/view/helpview/helpview";
import { QuickTipsView, QuickTipsViewModel } from "@/view/quicktipsview/quicktipsview";
import { TermViewModel, TerminalView, makeTerminalModel } from "@/view/term/term";
@ -47,8 +47,9 @@ function makeViewModel(blockId: string, blockView: string, nodeModel: NodeModel)
if (blockView === "waveai") {
return makeWaveAiViewModel(blockId);
}
if (blockView === "cpuplot") {
return makeCpuPlotViewModel(blockId);
if (blockView === "cpuplot" || blockView == "sysinfo") {
// "cpuplot" is for backwards compatibility with already-opened widgets
return makeSysinfoViewModel(blockId, blockView);
}
if (blockView === "help") {
return makeHelpViewModel(blockId, nodeModel);
@ -89,8 +90,9 @@ function getViewElem(
if (blockView === "waveai") {
return <WaveAi key={blockId} blockId={blockId} model={viewModel as WaveAiModel} />;
}
if (blockView === "cpuplot") {
return <CpuPlotView key={blockId} blockId={blockId} model={viewModel as CpuPlotViewModel} />;
if (blockView === "cpuplot" || blockView === "sysinfo") {
// "cpuplot" is for backwards compatibility with already opened widgets
return <SysinfoView key={blockId} blockId={blockId} model={viewModel as SysinfoViewModel} />;
}
if (blockView == "help") {
return <HelpView key={blockId} model={viewModel as HelpViewModel} />;

View File

@ -12,6 +12,7 @@ const dlog = debug("wave:ws");
const WarnWebSocketSendSize = 1024 * 1024; // 1MB
const MaxWebSocketSendSize = 5 * 1024 * 1024; // 5MB
const reconnectHandlers: (() => void)[] = [];
const StableConnTime = 2000;
function addWSReconnectHandler(handler: () => void) {
reconnectHandlers.push(handler);
@ -45,6 +46,7 @@ class WSControl {
lastReconnectTime: number = 0;
eoOpts: ElectronOverrideOpts;
noReconnect: boolean = false;
onOpenTimeoutId: NodeJS.Timeout = null;
constructor(
baseHostPort: string,
@ -80,9 +82,15 @@ class WSControl {
}
: null
);
this.wsConn.onopen = this.onopen.bind(this);
this.wsConn.onmessage = this.onmessage.bind(this);
this.wsConn.onclose = this.onclose.bind(this);
this.wsConn.onopen = (e: Event) => {
this.onopen(e);
};
this.wsConn.onmessage = (e: MessageEvent) => {
this.onmessage(e);
};
this.wsConn.onclose = (e: CloseEvent) => {
this.onclose(e);
};
// turns out onerror is not necessary (onclose always follows onerror)
// this.wsConn.onerror = this.onerror;
}
@ -118,8 +126,11 @@ class WSControl {
}, timeout * 1000);
}
onclose(event: any) {
onclose(event: CloseEvent) {
// console.log("close", event);
if (this.onOpenTimeoutId) {
clearTimeout(this.onOpenTimeoutId);
}
if (event.wasClean) {
dlog("connection closed");
} else {
@ -132,15 +143,18 @@ class WSControl {
}
}
onopen() {
onopen(e: Event) {
dlog("connection open");
this.open = true;
this.opening = false;
this.onOpenTimeoutId = setTimeout(() => {
this.reconnectTimes = 0;
dlog("clear reconnect times");
}, StableConnTime);
for (let handler of reconnectHandlers) {
handler();
}
this.runMsgQueue();
// reconnectTimes is reset in onmessage:hello
}
runMsgQueue() {
@ -157,7 +171,7 @@ class WSControl {
}, 100);
}
onmessage(event: any) {
onmessage(event: MessageEvent) {
let eventData = null;
if (event.data != null) {
eventData = JSON.parse(event.data);
@ -173,10 +187,6 @@ class WSControl {
// nothing
return;
}
if (eventData.type == "hello") {
this.reconnectTimes = 0;
return;
}
if (this.messageCallback) {
try {
this.messageCallback(eventData);

View File

@ -106,6 +106,9 @@
--conn-icon-color-7: #dbde52;
--conn-icon-color-8: #58c142;
--sysinfo-cpu-color: #58c142;
--sysinfo-mem-color: #53b4ea;
--bulb-color: rgb(255, 221, 51);
// term colors (16 + 6) form the base terminal theme

View File

@ -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%;
}

View File

@ -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 { TabRpcClient } 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(TabRpcClient, {
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(TabRpcClient, {
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 };

View File

@ -6,7 +6,7 @@ import { WebView, WebViewModel } from "@/app/view/webview/webview";
import { NodeModel } from "@/layout/index";
import { fireAndForget } from "@/util/util";
import { atom, useAtomValue } from "jotai";
import { useEffect } from "react";
import { useCallback } from "react";
import "./helpview.less";
class HelpViewModel extends WebViewModel {
@ -48,21 +48,35 @@ function makeHelpViewModel(blockId: string, nodeModel: NodeModel) {
return new HelpViewModel(blockId, nodeModel);
}
const baseUrlRegex = /http[s]?:\/\/([^:\/])+(:\d+)?/;
function HelpView({ model }: { model: HelpViewModel }) {
const homepageUrl = useAtomValue(model.homepageUrl);
useEffect(
() =>
// Effect to update the docsite base url when the app restarts, since the webserver port is dynamic
const onFailLoad = useCallback(
(url: string) =>
fireAndForget(async () => {
const curDocsiteUrl = getApi().getDocsiteUrl();
if (curDocsiteUrl !== homepageUrl) {
await model.setHomepageUrl(curDocsiteUrl, "block");
const newDocsiteUrl = getApi().getDocsiteUrl();
// Correct the homepage URL, if necessary
if (newDocsiteUrl !== homepageUrl) {
await model.setHomepageUrl(newDocsiteUrl, "block");
}
// Correct the base URL of the current page, if necessary
const newBaseUrl = baseUrlRegex.exec(newDocsiteUrl)?.[0];
const curBaseUrl = baseUrlRegex.exec(url)?.[0];
console.log("fix-docsite-url", url, newDocsiteUrl, homepageUrl, curBaseUrl, newBaseUrl);
if (curBaseUrl && newBaseUrl && curBaseUrl !== newBaseUrl) {
model.loadUrl(url.replace(curBaseUrl, newBaseUrl), "fix-fail-load");
}
}),
[]
[homepageUrl]
);
return (
<div className="help-view">
<WebView blockId={model.blockId} model={model} />
<WebView blockId={model.blockId} model={model} onFailLoad={onFailLoad} />
</div>
);
}

View 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);
}
}
}
}
}
}
}

View 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 { TabRpcClient } 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(TabRpcClient, {
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(TabRpcClient, {
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(TabRpcClient, {
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 };

View File

@ -60,7 +60,6 @@ export class WebViewModel implements ViewModel {
this.homepageUrl = atom((get) => {
const defaultUrl = get(defaultUrlAtom);
const pinnedUrl = get(this.blockAtom).meta.pinnedurl;
console.log("homepageUrl", pinnedUrl, defaultUrl);
return pinnedUrl ?? defaultUrl;
});
this.urlWrapperClassName = atom("");
@ -468,9 +467,10 @@ function makeWebViewModel(blockId: string, nodeModel: NodeModel): WebViewModel {
interface WebViewProps {
blockId: string;
model: WebViewModel;
onFailLoad?: (url: string) => void;
}
const WebView = memo(({ model }: WebViewProps) => {
const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
const blockData = useAtomValue(model.blockAtom);
const defaultUrl = useAtomValue(model.homepageUrl);
const defaultSearchAtom = getSettingsKeyAtom("web:defaultsearch");
@ -555,6 +555,10 @@ const WebView = memo(({ model }: WebViewProps) => {
console.warn("Suppressed ERR_ABORTED error", e);
} else {
console.error(`Failed to load ${e.validatedURL}: ${e.errorDescription}`);
if (onFailLoad) {
const curUrl = model.webviewRef?.current.getURL();
onFailLoad(curUrl);
}
}
};
const webviewFocus = () => {

View File

@ -366,6 +366,15 @@ declare global {
initResolve: () => void;
waveReadyResolve: () => void;
};
type TimeSeriesMeta = {
name?: string;
color?: string;
label?: string;
maxy?: string | number;
miny?: string | number;
decimalPlaces?: number;
};
}
export {};

View File

@ -322,6 +322,7 @@ declare global {
"graph:*"?: boolean;
"graph:numpoints"?: number;
"graph:metrics"?: string[];
"sysinfo:type"?: string;
"bg:*"?: boolean;
bg?: string;
"bg:opacity"?: number;

View File

@ -7,7 +7,7 @@
"productName": "Wave",
"description": "Open-Source AI-Native Terminal Built for Seamless Workflows",
"license": "Apache-2.0",
"version": "0.8.12-beta.0",
"version": "0.8.12-beta.3",
"homepage": "https://waveterm.dev",
"build": {
"appId": "dev.commandline.waveterm"

View File

@ -366,7 +366,6 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
bc.ShellProc.Cmd.Write(ic.InputData)
}
if ic.TermSize != nil {
log.Printf("SETTERMSIZE: %dx%d\n", ic.TermSize.Rows, ic.TermSize.Cols)
err = setTermSize(ctx, bc.BlockId, *ic.TermSize)
if err != nil {
log.Printf("error setting pty size: %v\n", err)

View File

@ -64,6 +64,8 @@ const (
MetaKey_GraphNumPoints = "graph:numpoints"
MetaKey_GraphMetrics = "graph:metrics"
MetaKey_SysinfoType = "sysinfo:type"
MetaKey_BgClear = "bg:*"
MetaKey_Bg = "bg"
MetaKey_BgOpacity = "bg:opacity"

View File

@ -64,6 +64,8 @@ type MetaTSType struct {
GraphNumPoints int `json:"graph:numpoints,omitempty"`
GraphMetrics []string `json:"graph:metrics,omitempty"`
SysinfoType string `json:"sysinfo:type,omitempty"`
// for tabs
BgClear bool `json:"bg:*,omitempty"`
Bg string `json:"bg,omitempty"`

View File

@ -41,13 +41,13 @@
}
}
},
"defwidget@cpuplot": {
"defwidget@sysinfo": {
"display:order": 5,
"icon": "chart-line",
"label": "cpu",
"label": "sysinfo",
"blockdef": {
"meta": {
"view": "cpuplot"
"view": "sysinfo"
}
}
}

View File

@ -40,10 +40,10 @@ func RunWebSocketServer(listener net.Listener) {
Handler: gr,
}
server.SetKeepAlivesEnabled(false)
log.Printf("Running websocket server on %s\n", listener.Addr())
log.Printf("[websocket] running websocket server on %s\n", listener.Addr())
err := server.Serve(listener)
if err != nil {
log.Printf("[error] trying to run websocket server: %v\n", err)
log.Printf("[websocket] error trying to run websocket server: %v\n", err)
}
}
@ -81,7 +81,7 @@ func processWSCommand(jmsg map[string]any, outputCh chan any, rpcInputCh chan []
r := recover()
if r != nil {
rtnErr = fmt.Errorf("panic: %v", r)
log.Printf("panic in processMessage: %v\n", r)
log.Printf("[websocket] panic in processMessage: %v\n", r)
debug.PrintStack()
}
if rtnErr == nil {
@ -108,7 +108,7 @@ func processWSCommand(jmsg map[string]any, outputCh chan any, rpcInputCh chan []
msgBytes, err := json.Marshal(rpcMsg)
if err != nil {
// this really should never fail since we just unmarshalled this value
log.Printf("error marshalling rpc message: %v\n", err)
log.Printf("[websocket] error marshalling rpc message: %v\n", err)
return
}
rpcInputCh <- msgBytes
@ -125,7 +125,7 @@ func processWSCommand(jmsg map[string]any, outputCh chan any, rpcInputCh chan []
msgBytes, err := json.Marshal(rpcMsg)
if err != nil {
// this really should never fail since we just unmarshalled this value
log.Printf("error marshalling rpc message: %v\n", err)
log.Printf("[websocket] error marshalling rpc message: %v\n", err)
return
}
rpcInputCh <- msgBytes
@ -152,7 +152,7 @@ func processMessage(jmsg map[string]any, outputCh chan any, rpcInputCh chan []by
processWSCommand(jmsg, outputCh, rpcInputCh)
}
func ReadLoop(conn *websocket.Conn, outputCh chan any, closeCh chan any, rpcInputCh chan []byte) {
func ReadLoop(conn *websocket.Conn, outputCh chan any, closeCh chan any, rpcInputCh chan []byte, routeId string) {
readWait := wsReadWaitTimeout
conn.SetReadLimit(64 * 1024)
conn.SetReadDeadline(time.Now().Add(readWait))
@ -160,13 +160,13 @@ func ReadLoop(conn *websocket.Conn, outputCh chan any, closeCh chan any, rpcInpu
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Printf("ReadPump error: %v\n", err)
log.Printf("[websocket] ReadPump error (%s): %v\n", routeId, err)
break
}
jmsg := map[string]any{}
err = json.Unmarshal(message, &jmsg)
if err != nil {
log.Printf("Error unmarshalling json: %v\n", err)
log.Printf("[websocket] error unmarshalling json: %v\n", err)
break
}
conn.SetReadDeadline(time.Now().Add(readWait))
@ -197,7 +197,7 @@ func WritePing(conn *websocket.Conn) error {
return nil
}
func WriteLoop(conn *websocket.Conn, outputCh chan any, closeCh chan any) {
func WriteLoop(conn *websocket.Conn, outputCh chan any, closeCh chan any, routeId string) {
ticker := time.NewTicker(wsInitialPingTime)
defer ticker.Stop()
initialPing := true
@ -211,7 +211,7 @@ func WriteLoop(conn *websocket.Conn, outputCh chan any, closeCh chan any) {
} else {
barr, err = json.Marshal(msg)
if err != nil {
log.Printf("cannot marshal websocket message: %v\n", err)
log.Printf("[websocket] cannot marshal websocket message: %v\n", err)
// just loop again
break
}
@ -219,14 +219,14 @@ func WriteLoop(conn *websocket.Conn, outputCh chan any, closeCh chan any) {
err = conn.WriteMessage(websocket.TextMessage, barr)
if err != nil {
conn.Close()
log.Printf("WritePump error: %v\n", err)
log.Printf("[websocket] WritePump error (%s): %v\n", routeId, err)
return
}
case <-ticker.C:
err := WritePing(conn)
if err != nil {
log.Printf("WritePump error: %v\n", err)
log.Printf("[websocket] WritePump error (%s): %v\n", routeId, err)
return
}
if initialPing {
@ -249,6 +249,7 @@ func HandleWsInternal(w http.ResponseWriter, r *http.Request) error {
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(fmt.Sprintf("error validating authkey: %v", err)))
log.Printf("[websocket] error validating authkey: %v\n", err)
return err
}
conn, err := WebSocketUpgrader.Upgrade(w, r, nil)
@ -257,7 +258,6 @@ func HandleWsInternal(w http.ResponseWriter, r *http.Request) error {
}
defer conn.Close()
wsConnId := uuid.New().String()
log.Printf("New websocket connection: tabid:%s connid:%s\n", tabId, wsConnId)
outputCh := make(chan any, 100)
closeCh := make(chan any)
eventbus.RegisterWSChannel(wsConnId, tabId, outputCh)
@ -268,6 +268,7 @@ func HandleWsInternal(w http.ResponseWriter, r *http.Request) error {
routeId = wshutil.MakeTabRouteId(tabId)
}
defer eventbus.UnregisterWSChannel(wsConnId)
log.Printf("[websocket] new connection: tabid:%s connid:%s routeid:%s\n", tabId, wsConnId, routeId)
// we create a wshproxy to handle rpc messages to/from the window
wproxy := wshutil.MakeRpcProxy()
wshutil.DefaultRouter.RegisterRoute(routeId, wproxy)
@ -292,12 +293,12 @@ func HandleWsInternal(w http.ResponseWriter, r *http.Request) error {
go func() {
// read loop
defer wg.Done()
ReadLoop(conn, outputCh, closeCh, wproxy.FromRemoteCh)
ReadLoop(conn, outputCh, closeCh, wproxy.FromRemoteCh, routeId)
}()
go func() {
// write loop
defer wg.Done()
WriteLoop(conn, outputCh, closeCh)
WriteLoop(conn, outputCh, closeCh, routeId)
}()
wg.Wait()
close(wproxy.FromRemoteCh)

View File

@ -38,7 +38,7 @@ func GetStarterLayout() PortableLayout {
}, Focused: true},
{IndexArr: []int{1}, BlockDef: &waveobj.BlockDef{
Meta: waveobj.MetaMapType{
waveobj.MetaKey_View: "cpuplot",
waveobj.MetaKey_View: "sysinfo",
},
}},
{IndexArr: []int{1, 1}, BlockDef: &waveobj.BlockDef{

View File

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

View File

@ -83,7 +83,7 @@ func MakePlotData(ctx context.Context, blockId string) error {
return err
}
viewName := block.Meta.GetString(waveobj.MetaKey_View, "")
if viewName != "cpuplot" {
if viewName != "cpuplot" && viewName != "sysinfo" {
return fmt.Errorf("invalid view type: %s", viewName)
}
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
}
viewName := block.Meta.GetString(waveobj.MetaKey_View, "")
if viewName != "cpuplot" {
if viewName != "cpuplot" && viewName != "sysinfo" {
return fmt.Errorf("invalid view type: %s", viewName)
}
// todo: interpret the data being passed
@ -422,7 +422,6 @@ func (ws *WshServer) EventPublishCommand(ctx context.Context, data wps.WaveEvent
}
func (ws *WshServer) EventSubCommand(ctx context.Context, data wps.SubscriptionRequest) error {
// log.Printf("EventSubCommand: %v\n", data)
rpcSource := wshutil.GetRpcSourceFromContext(ctx)
if rpcSource == "" {
return fmt.Errorf("no rpc source set")

12
testdriver/onboarding.yml Normal file
View File

@ -0,0 +1,12 @@
version: 4.0.65
steps:
- prompt: complete the onboarding of wave terminal
commands:
- command: hover-text
text: Continue
description: button to complete onboarding
action: click
- command: hover-text
text: Get Started
description: button to complete onboarding
action: click