mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
stickers and terminal serialization (#57)
This commit is contained in:
parent
b6c85e38f6
commit
4ded6d94b6
@ -1,8 +1,13 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { LayoutTreeActionType, LayoutTreeInsertNodeAction, newLayoutNode } from "@/faraday/index";
|
||||||
|
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
|
||||||
|
import { layoutTreeStateReducer } from "@/faraday/lib/layoutState";
|
||||||
|
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
import * as rxjs from "rxjs";
|
import * as rxjs from "rxjs";
|
||||||
|
import * as services from "./services";
|
||||||
import * as WOS from "./wos";
|
import * as WOS from "./wos";
|
||||||
import { WSControl } from "./ws";
|
import { WSControl } from "./ws";
|
||||||
|
|
||||||
@ -62,8 +67,6 @@ const atoms = {
|
|||||||
workspace: workspaceAtom,
|
workspace: workspaceAtom,
|
||||||
};
|
};
|
||||||
|
|
||||||
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
|
||||||
|
|
||||||
// key is "eventType" or "eventType|oref"
|
// key is "eventType" or "eventType|oref"
|
||||||
const eventSubjects = new Map<string, SubjectWithRef<WSEventType>>();
|
const eventSubjects = new Map<string, SubjectWithRef<WSEventType>>();
|
||||||
const fileSubjects = new Map<string, SubjectWithRef<WSFileEventData>>();
|
const fileSubjects = new Map<string, SubjectWithRef<WSFileEventData>>();
|
||||||
@ -215,9 +218,58 @@ function getApi(): ElectronApi {
|
|||||||
return (window as any).api;
|
return (window as any).api;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createBlock(blockDef: BlockDef) {
|
||||||
|
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
|
||||||
|
const blockId = await services.ObjectService.CreateBlock(blockDef, rtOpts);
|
||||||
|
const insertNodeAction: LayoutTreeInsertNodeAction<TabLayoutData> = {
|
||||||
|
type: LayoutTreeActionType.InsertNode,
|
||||||
|
node: newLayoutNode<TabLayoutData>(undefined, undefined, undefined, { blockId }),
|
||||||
|
};
|
||||||
|
const activeTabId = globalStore.get(atoms.uiContext).activetabid;
|
||||||
|
const layoutStateAtom = getLayoutStateAtomForTab(
|
||||||
|
activeTabId,
|
||||||
|
WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", activeTabId))
|
||||||
|
);
|
||||||
|
const curState = globalStore.get(layoutStateAtom);
|
||||||
|
globalStore.set(layoutStateAtom, layoutTreeStateReducer(curState, insertNodeAction));
|
||||||
|
}
|
||||||
|
|
||||||
|
// when file is not found, returns {data: null, fileInfo: null}
|
||||||
|
async function fetchWaveFile(
|
||||||
|
zoneId: string,
|
||||||
|
fileName: string,
|
||||||
|
offset?: number
|
||||||
|
): Promise<{ data: Uint8Array; fileInfo: WaveFile }> {
|
||||||
|
let usp = new URLSearchParams();
|
||||||
|
usp.set("zoneid", zoneId);
|
||||||
|
usp.set("name", fileName);
|
||||||
|
if (offset != null) {
|
||||||
|
usp.set("offset", offset.toString());
|
||||||
|
}
|
||||||
|
const resp = await fetch(getBackendHostPort() + "/wave/file?" + usp.toString());
|
||||||
|
if (!resp.ok) {
|
||||||
|
if (resp.status === 404) {
|
||||||
|
return { data: null, fileInfo: null };
|
||||||
|
}
|
||||||
|
throw new Error("error getting wave file: " + resp.statusText);
|
||||||
|
}
|
||||||
|
if (resp.status == 204) {
|
||||||
|
return { data: null, fileInfo: null };
|
||||||
|
}
|
||||||
|
let fileInfo64 = resp.headers.get("X-ZoneFileInfo");
|
||||||
|
if (fileInfo64 == null) {
|
||||||
|
throw new Error(`missing zone file info for ${zoneId}:${fileName}`);
|
||||||
|
}
|
||||||
|
let fileInfo = JSON.parse(atob(fileInfo64));
|
||||||
|
const data = await resp.arrayBuffer();
|
||||||
|
return { data: new Uint8Array(data), fileInfo };
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
WOS,
|
WOS,
|
||||||
atoms,
|
atoms,
|
||||||
|
createBlock,
|
||||||
|
fetchWaveFile,
|
||||||
getApi,
|
getApi,
|
||||||
getBackendHostPort,
|
getBackendHostPort,
|
||||||
getEventORefSubject,
|
getEventORefSubject,
|
||||||
|
@ -7,6 +7,10 @@ import * as WOS from "./wos";
|
|||||||
|
|
||||||
// blockservice.BlockService (block)
|
// blockservice.BlockService (block)
|
||||||
class BlockServiceType {
|
class BlockServiceType {
|
||||||
|
SaveTerminalState(arg2: string, arg3: string, arg4: string, arg5: number): Promise<void> {
|
||||||
|
return WOS.callBackendService("block", "SaveTerminalState", Array.from(arguments))
|
||||||
|
}
|
||||||
|
|
||||||
// send command to block
|
// send command to block
|
||||||
SendCommand(blockid: string, cmd: BlockCommand): Promise<void> {
|
SendCommand(blockid: string, cmd: BlockCommand): Promise<void> {
|
||||||
return WOS.callBackendService("block", "SendCommand", Array.from(arguments))
|
return WOS.callBackendService("block", "SendCommand", Array.from(arguments))
|
||||||
|
@ -27,4 +27,5 @@
|
|||||||
|
|
||||||
/* z-index values */
|
/* z-index values */
|
||||||
--zindex-header-hover: 100;
|
--zindex-header-hover: 100;
|
||||||
|
--zindex-termstickers: 20;
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-left: 4px solid transparent;
|
border-left: 4px solid transparent;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
border-left: 4px solid var(--accent-color);
|
border-left: 4px solid var(--accent-color);
|
||||||
@ -84,4 +85,30 @@
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.term-stickers {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: var(--zindex-termstickers);
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.term-sticker-image {
|
||||||
|
img {
|
||||||
|
object-fit: contain;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-sticker-svg {
|
||||||
|
svg {
|
||||||
|
object-fit: contain;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,8 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import {
|
import { WOS, atoms, globalStore, sendWSCommand, useBlockAtom } from "@/store/global";
|
||||||
WOS,
|
|
||||||
atoms,
|
|
||||||
getBackendHostPort,
|
|
||||||
getFileSubject,
|
|
||||||
globalStore,
|
|
||||||
sendWSCommand,
|
|
||||||
useBlockAtom,
|
|
||||||
} from "@/store/global";
|
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import { base64ToArray } from "@/util/util";
|
|
||||||
import { FitAddon } from "@xterm/addon-fit";
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
import type { ITheme } from "@xterm/xterm";
|
import type { ITheme } from "@xterm/xterm";
|
||||||
import { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm";
|
||||||
@ -20,9 +11,10 @@ import { produce } from "immer";
|
|||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { IJsonView } from "./ijson";
|
import { IJsonView } from "./ijson";
|
||||||
|
import { TermStickers } from "./termsticker";
|
||||||
|
import { TermWrap } from "./termwrap";
|
||||||
|
|
||||||
import "public/xterm.css";
|
import "public/xterm.css";
|
||||||
import { debounce } from "throttle-debounce";
|
|
||||||
import "./term.less";
|
import "./term.less";
|
||||||
|
|
||||||
function getThemeFromCSSVars(el: Element): ITheme {
|
function getThemeFromCSSVars(el: Element): ITheme {
|
||||||
@ -149,7 +141,7 @@ function setBlockFocus(blockId: string) {
|
|||||||
|
|
||||||
const TerminalView = ({ blockId }: { blockId: string }) => {
|
const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||||
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||||
const termRef = React.useRef<Terminal>(null);
|
const termRef = React.useRef<TermWrap>(null);
|
||||||
const initialLoadRef = React.useRef<InitialLoadDataType>({ loaded: false, heldData: [] });
|
const initialLoadRef = React.useRef<InitialLoadDataType>({ loaded: false, heldData: [] });
|
||||||
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
|
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||||
@ -161,8 +153,7 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
|||||||
});
|
});
|
||||||
const isFocused = jotai.useAtomValue(isFocusedAtom);
|
const isFocused = jotai.useAtomValue(isFocusedAtom);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log("terminal created");
|
const termWrap = new TermWrap(blockId, connectElemRef.current, {
|
||||||
const newTerm = new Terminal({
|
|
||||||
theme: getThemeFromCSSVars(connectElemRef.current),
|
theme: getThemeFromCSSVars(connectElemRef.current),
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: "Hack",
|
fontFamily: "Hack",
|
||||||
@ -170,93 +161,22 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
|||||||
fontWeight: "normal",
|
fontWeight: "normal",
|
||||||
fontWeightBold: "bold",
|
fontWeightBold: "bold",
|
||||||
});
|
});
|
||||||
termRef.current = newTerm;
|
(window as any).term = termWrap;
|
||||||
const newFitAddon = new FitAddon();
|
termRef.current = termWrap;
|
||||||
newTerm.loadAddon(newFitAddon);
|
termWrap.addFocusListener(() => {
|
||||||
newTerm.open(connectElemRef.current);
|
|
||||||
newFitAddon.fit();
|
|
||||||
sendWSCommand({
|
|
||||||
wscommand: "setblocktermsize",
|
|
||||||
blockid: blockId,
|
|
||||||
termsize: { rows: newTerm.rows, cols: newTerm.cols },
|
|
||||||
});
|
|
||||||
connectElemRef.current.addEventListener(
|
|
||||||
"keydown",
|
|
||||||
(ev) => {
|
|
||||||
if (ev.code == "Escape" && ev.metaKey) {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": "html" } };
|
|
||||||
services.BlockService.SendCommand(blockId, metaCmd);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
newTerm.onData((data) => {
|
|
||||||
const b64data = btoa(data);
|
|
||||||
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data };
|
|
||||||
services.BlockService.SendCommand(blockId, inputCmd);
|
|
||||||
});
|
|
||||||
newTerm.textarea.addEventListener("focus", () => {
|
|
||||||
setBlockFocus(blockId);
|
setBlockFocus(blockId);
|
||||||
});
|
});
|
||||||
const mainFileSubject = getFileSubject(blockId, "main");
|
|
||||||
mainFileSubject.subscribe((msg: WSFileEventData) => {
|
|
||||||
if (msg.fileop != "append") {
|
|
||||||
console.log("bad fileop for terminal", msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const decodedData = base64ToArray(msg.data64);
|
|
||||||
if (initialLoadRef.current.loaded) {
|
|
||||||
newTerm.write(decodedData);
|
|
||||||
} else {
|
|
||||||
initialLoadRef.current.heldData.push(decodedData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// load data from filestore
|
|
||||||
const startTs = Date.now();
|
|
||||||
let loadedBytes = 0;
|
|
||||||
const localTerm = termRef.current; // avoids devmode double effect running issue (terminal gets created twice)
|
|
||||||
const usp = new URLSearchParams();
|
|
||||||
usp.set("zoneid", blockId);
|
|
||||||
usp.set("name", "main");
|
|
||||||
fetch(getBackendHostPort() + "/wave/file?" + usp.toString())
|
|
||||||
.then((resp) => {
|
|
||||||
if (resp.ok) {
|
|
||||||
return resp.arrayBuffer();
|
|
||||||
}
|
|
||||||
console.log("error loading file", resp.status, resp.statusText);
|
|
||||||
})
|
|
||||||
.then((data: ArrayBuffer) => {
|
|
||||||
const uint8View = new Uint8Array(data);
|
|
||||||
localTerm.write(uint8View);
|
|
||||||
loadedBytes = uint8View.byteLength;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
initialLoadRef.current.heldData.forEach((data) => {
|
|
||||||
localTerm.write(data);
|
|
||||||
});
|
|
||||||
initialLoadRef.current.loaded = true;
|
|
||||||
initialLoadRef.current.heldData = [];
|
|
||||||
console.log(`terminal loaded file ${loadedBytes} bytes, ${Date.now() - startTs}ms`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const resize_debounced = debounce(50, () => {
|
|
||||||
handleResize(newFitAddon, blockId, newTerm);
|
|
||||||
});
|
|
||||||
const rszObs = new ResizeObserver(() => {
|
const rszObs = new ResizeObserver(() => {
|
||||||
resize_debounced();
|
termWrap.handleResize_debounced();
|
||||||
});
|
});
|
||||||
rszObs.observe(connectElemRef.current);
|
rszObs.observe(connectElemRef.current);
|
||||||
|
termWrap.initTerminal();
|
||||||
return () => {
|
return () => {
|
||||||
newTerm.dispose();
|
termWrap.dispose();
|
||||||
mainFileSubject.release();
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleHtmlKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (event.code === "Escape" && event.metaKey) {
|
if (event.code === "Escape" && event.metaKey) {
|
||||||
// reset term:mode
|
// reset term:mode
|
||||||
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": null } };
|
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": null } };
|
||||||
@ -280,15 +200,24 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isFocused && termMode == "term") {
|
if (isFocused && termMode == "term") {
|
||||||
termRef.current?.focus();
|
termRef.current?.terminal.focus();
|
||||||
}
|
}
|
||||||
if (isFocused && termMode == "html") {
|
if (isFocused && termMode == "html") {
|
||||||
htmlElemFocusRef.current?.focus();
|
htmlElemFocusRef.current?.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let stickerConfig = {
|
||||||
|
charWidth: 8,
|
||||||
|
charHeight: 16,
|
||||||
|
rows: termRef.current?.terminal.rows ?? 24,
|
||||||
|
cols: termRef.current?.terminal.cols ?? 80,
|
||||||
|
blockId: blockId,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx("view-term", "term-mode-" + termMode, isFocused ? "is-focused" : null)}>
|
<div className={clsx("view-term", "term-mode-" + termMode, isFocused ? "is-focused" : null)}>
|
||||||
|
<TermStickers config={stickerConfig} />
|
||||||
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
||||||
<div
|
<div
|
||||||
key="htmlElem"
|
key="htmlElem"
|
||||||
@ -305,7 +234,7 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
|||||||
type="text"
|
type="text"
|
||||||
value={""}
|
value={""}
|
||||||
ref={htmlElemFocusRef}
|
ref={htmlElemFocusRef}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleHtmlKeyDown}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
204
frontend/app/view/term/termsticker.tsx
Normal file
204
frontend/app/view/term/termsticker.tsx
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { createBlock, getBackendHostPort } from "@/store/global";
|
||||||
|
import * as services from "@/store/services";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import * as jotai from "jotai";
|
||||||
|
import * as React from "react";
|
||||||
|
import GaugeChart from "react-gauge-chart";
|
||||||
|
import "./term.less";
|
||||||
|
|
||||||
|
type StickerType = {
|
||||||
|
position: "absolute";
|
||||||
|
top?: number;
|
||||||
|
left?: number;
|
||||||
|
right?: number;
|
||||||
|
bottom?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
color?: string;
|
||||||
|
opacity?: number;
|
||||||
|
pointerevents?: boolean;
|
||||||
|
fontsize?: number;
|
||||||
|
transform?: string;
|
||||||
|
|
||||||
|
stickertype: "icon" | "image" | "gauge";
|
||||||
|
icon?: string;
|
||||||
|
imgsrc?: string;
|
||||||
|
clickcmd?: string;
|
||||||
|
clickblockdef?: BlockDef;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StickerTermConfig = {
|
||||||
|
charWidth: number;
|
||||||
|
charHeight: number;
|
||||||
|
rows: number;
|
||||||
|
cols: number;
|
||||||
|
blockId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function convertWidthDimToPx(dim: number, config: StickerTermConfig) {
|
||||||
|
if (dim == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return dim * config.charWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertHeightDimToPx(dim: number, config: StickerTermConfig) {
|
||||||
|
if (dim == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return dim * config.charHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
var valueAtom = jotai.atom(Math.random() * 100);
|
||||||
|
|
||||||
|
function GaugeSticker() {
|
||||||
|
let [value, setValue] = jotai.useAtom(valueAtom);
|
||||||
|
React.useEffect(() => {
|
||||||
|
let interval = setInterval(() => {
|
||||||
|
var amt = Math.random() * 10 - 5;
|
||||||
|
setValue((value) => Math.max(0, Math.min(100, value + amt)));
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
});
|
||||||
|
return <GaugeChart id="gauge-chart1" nrOfLevels={20} percent={value / 100} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TermSticker({ sticker, config }: { sticker: StickerType; config: StickerTermConfig }) {
|
||||||
|
let style: React.CSSProperties = {
|
||||||
|
position: sticker.position,
|
||||||
|
top: convertHeightDimToPx(sticker.top, config),
|
||||||
|
left: convertWidthDimToPx(sticker.left, config),
|
||||||
|
right: convertWidthDimToPx(sticker.right, config),
|
||||||
|
bottom: convertHeightDimToPx(sticker.bottom, config),
|
||||||
|
width: convertWidthDimToPx(sticker.width, config),
|
||||||
|
height: convertHeightDimToPx(sticker.height, config),
|
||||||
|
color: sticker.color,
|
||||||
|
fontSize: sticker.fontsize,
|
||||||
|
transform: sticker.transform,
|
||||||
|
opacity: sticker.opacity,
|
||||||
|
fill: sticker.color,
|
||||||
|
stroke: sticker.color,
|
||||||
|
};
|
||||||
|
if (sticker.pointerevents) {
|
||||||
|
style.pointerEvents = "auto";
|
||||||
|
}
|
||||||
|
if (style.width != null) {
|
||||||
|
style.overflowX = "hidden";
|
||||||
|
}
|
||||||
|
if (style.height != null) {
|
||||||
|
style.overflowY = "hidden";
|
||||||
|
}
|
||||||
|
let clickHandler = null;
|
||||||
|
if (sticker.pointerevents && (sticker.clickcmd || sticker.clickblockdef)) {
|
||||||
|
style.cursor = "pointer";
|
||||||
|
clickHandler = () => {
|
||||||
|
console.log("clickHandler", sticker.clickcmd, sticker.clickblockdef);
|
||||||
|
if (sticker.clickcmd) {
|
||||||
|
const b64data = btoa(sticker.clickcmd);
|
||||||
|
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data };
|
||||||
|
services.BlockService.SendCommand(config.blockId, inputCmd);
|
||||||
|
}
|
||||||
|
if (sticker.clickblockdef) {
|
||||||
|
createBlock(sticker.clickblockdef);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (sticker.stickertype == "icon") {
|
||||||
|
return (
|
||||||
|
<div className="term-sticker" style={style} onClick={clickHandler}>
|
||||||
|
<i className={clsx("fa", "fa-" + sticker.icon)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (sticker.stickertype == "image") {
|
||||||
|
if (sticker.imgsrc == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const streamingUrl = getBackendHostPort() + "/wave/stream-file?path=" + encodeURIComponent(sticker.imgsrc);
|
||||||
|
return (
|
||||||
|
<div className="term-sticker term-sticker-image" style={style} onClick={clickHandler}>
|
||||||
|
<img src={streamingUrl} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (sticker.stickertype == "gauge") {
|
||||||
|
return (
|
||||||
|
<div className="term-sticker term-sticker-gauge" style={style}>
|
||||||
|
<GaugeSticker />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TermStickers({ config }: { config: StickerTermConfig }) {
|
||||||
|
let stickers: StickerType[] = [];
|
||||||
|
if (config.blockId.startsWith("d1eaddcb")) {
|
||||||
|
stickers.push({
|
||||||
|
position: "absolute",
|
||||||
|
top: 5,
|
||||||
|
right: 7,
|
||||||
|
stickertype: "icon",
|
||||||
|
icon: "paw",
|
||||||
|
color: "#40cc40aa",
|
||||||
|
fontsize: 30,
|
||||||
|
transform: "rotate(-18deg)",
|
||||||
|
pointerevents: true,
|
||||||
|
clickcmd: "ls\n",
|
||||||
|
});
|
||||||
|
stickers.push({
|
||||||
|
position: "absolute",
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
stickertype: "icon",
|
||||||
|
icon: "paw",
|
||||||
|
color: "#4040ccaa",
|
||||||
|
fontsize: 30,
|
||||||
|
transform: "rotate(-20deg)",
|
||||||
|
pointerevents: true,
|
||||||
|
clickcmd: "git status\n",
|
||||||
|
});
|
||||||
|
stickers.push({
|
||||||
|
position: "absolute",
|
||||||
|
top: 10,
|
||||||
|
right: 5,
|
||||||
|
stickertype: "icon",
|
||||||
|
icon: "paw",
|
||||||
|
color: "#cc4040aa",
|
||||||
|
fontsize: 30,
|
||||||
|
transform: "rotate(-15deg)",
|
||||||
|
pointerevents: true,
|
||||||
|
clickcmd: "cd ~/work/wails/thenextwave\n",
|
||||||
|
});
|
||||||
|
stickers.push({
|
||||||
|
position: "absolute",
|
||||||
|
top: 18,
|
||||||
|
right: 8,
|
||||||
|
stickertype: "image",
|
||||||
|
width: 12,
|
||||||
|
height: 6,
|
||||||
|
imgsrc: "~/Downloads/natureicon.png",
|
||||||
|
opacity: 0.8,
|
||||||
|
pointerevents: true,
|
||||||
|
clickblockdef: { view: "preview", meta: { file: "~/" } },
|
||||||
|
});
|
||||||
|
stickers.push({
|
||||||
|
position: "absolute",
|
||||||
|
top: 2,
|
||||||
|
right: 25,
|
||||||
|
width: 20,
|
||||||
|
stickertype: "gauge",
|
||||||
|
opacity: 0.7,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="term-stickers">
|
||||||
|
{stickers.map((sticker, i) => (
|
||||||
|
<TermSticker key={i} sticker={sticker} config={config} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
172
frontend/app/view/term/termwrap.ts
Normal file
172
frontend/app/view/term/termwrap.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { fetchWaveFile, getFileSubject, sendWSCommand } from "@/store/global";
|
||||||
|
import * as services from "@/store/services";
|
||||||
|
import { base64ToArray } from "@/util/util";
|
||||||
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
|
import { SerializeAddon } from "@xterm/addon-serialize";
|
||||||
|
import * as TermTypes from "@xterm/xterm";
|
||||||
|
import { Terminal } from "@xterm/xterm";
|
||||||
|
import { debounce } from "throttle-debounce";
|
||||||
|
|
||||||
|
export class TermWrap {
|
||||||
|
blockId: string;
|
||||||
|
ptyOffset: number;
|
||||||
|
dataBytesProcessed: number;
|
||||||
|
terminal: Terminal;
|
||||||
|
connectElem: HTMLDivElement;
|
||||||
|
fitAddon: FitAddon;
|
||||||
|
serializeAddon: SerializeAddon;
|
||||||
|
mainFileSubject: SubjectWithRef<Uint8Array>;
|
||||||
|
loaded: boolean;
|
||||||
|
heldData: Uint8Array[];
|
||||||
|
handleResize_debounced: () => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
blockId: string,
|
||||||
|
connectElem: HTMLDivElement,
|
||||||
|
options?: TermTypes.ITerminalOptions & TermTypes.ITerminalInitOnlyOptions
|
||||||
|
) {
|
||||||
|
this.blockId = blockId;
|
||||||
|
this.ptyOffset = 0;
|
||||||
|
this.dataBytesProcessed = 0;
|
||||||
|
this.terminal = new Terminal(options);
|
||||||
|
this.fitAddon = new FitAddon();
|
||||||
|
this.serializeAddon = new SerializeAddon();
|
||||||
|
this.terminal.loadAddon(this.fitAddon);
|
||||||
|
this.terminal.loadAddon(this.serializeAddon);
|
||||||
|
this.connectElem = connectElem;
|
||||||
|
this.mainFileSubject = null;
|
||||||
|
this.loaded = false;
|
||||||
|
this.heldData = [];
|
||||||
|
this.handleResize_debounced = debounce(50, this.handleResize.bind(this));
|
||||||
|
this.terminal.open(this.connectElem);
|
||||||
|
this.handleResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initTerminal() {
|
||||||
|
this.connectElem.addEventListener("keydown", this.keydownListener.bind(this), true);
|
||||||
|
this.terminal.onData(this.handleTermData.bind(this));
|
||||||
|
this.mainFileSubject = getFileSubject(this.blockId, "main");
|
||||||
|
this.mainFileSubject.subscribe(this.handleNewFileSubjectData.bind(this));
|
||||||
|
try {
|
||||||
|
await this.loadInitialTerminalData();
|
||||||
|
} finally {
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
this.runProcessIdleTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.terminal.dispose();
|
||||||
|
this.mainFileSubject.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTermData(data: string) {
|
||||||
|
const b64data = btoa(data);
|
||||||
|
if (b64data.length < 512) {
|
||||||
|
const wsCmd: BlockInputWSCommand = { wscommand: "blockinput", blockid: this.blockId, inputdata64: b64data };
|
||||||
|
sendWSCommand(wsCmd);
|
||||||
|
} else {
|
||||||
|
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data };
|
||||||
|
services.BlockService.SendCommand(this.blockId, inputCmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addFocusListener(focusFn: () => void) {
|
||||||
|
this.terminal.textarea.addEventListener("focus", focusFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
keydownListener(ev: KeyboardEvent) {
|
||||||
|
if (ev.code == "Escape" && ev.metaKey) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": "html" } };
|
||||||
|
services.BlockService.SendCommand(this.blockId, metaCmd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNewFileSubjectData(msg: WSFileEventData) {
|
||||||
|
if (msg.fileop != "append") {
|
||||||
|
console.log("bad fileop for terminal", msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const decodedData = base64ToArray(msg.data64);
|
||||||
|
if (this.loaded) {
|
||||||
|
this.doTerminalWrite(decodedData, null);
|
||||||
|
} else {
|
||||||
|
this.heldData.push(decodedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doTerminalWrite(data: string | Uint8Array, setPtyOffset?: number): Promise<void> {
|
||||||
|
let resolve: () => void = null;
|
||||||
|
let prtn = new Promise<void>((presolve, _) => {
|
||||||
|
resolve = presolve;
|
||||||
|
});
|
||||||
|
this.terminal.write(data, () => {
|
||||||
|
if (setPtyOffset != null) {
|
||||||
|
this.ptyOffset = setPtyOffset;
|
||||||
|
} else {
|
||||||
|
this.ptyOffset += data.length;
|
||||||
|
this.dataBytesProcessed += data.length;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
return prtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadInitialTerminalData(): Promise<void> {
|
||||||
|
let startTs = Date.now();
|
||||||
|
const { data: cacheData, fileInfo: cacheFile } = await fetchWaveFile(this.blockId, "cache:term:full");
|
||||||
|
let ptyOffset = 0;
|
||||||
|
if (cacheFile != null) {
|
||||||
|
ptyOffset = cacheFile.meta["ptyoffset"] ?? 0;
|
||||||
|
if (cacheData.byteLength > 0) {
|
||||||
|
this.doTerminalWrite(cacheData, ptyOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { data: mainData, fileInfo: mainFile } = await fetchWaveFile(this.blockId, "main", ptyOffset);
|
||||||
|
console.log(
|
||||||
|
`terminal loaded cachefile:${cacheData?.byteLength ?? 0} main:${mainData?.byteLength ?? 0} bytes, ${Date.now() - startTs}ms`
|
||||||
|
);
|
||||||
|
if (mainFile != null) {
|
||||||
|
await this.doTerminalWrite(mainData, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize() {
|
||||||
|
const oldRows = this.terminal.rows;
|
||||||
|
const oldCols = this.terminal.cols;
|
||||||
|
this.fitAddon.fit();
|
||||||
|
if (oldRows !== this.terminal.rows || oldCols !== this.terminal.cols) {
|
||||||
|
const wsCommand: SetBlockTermSizeWSCommand = {
|
||||||
|
wscommand: "setblocktermsize",
|
||||||
|
blockid: this.blockId,
|
||||||
|
termsize: { rows: this.terminal.rows, cols: this.terminal.cols },
|
||||||
|
};
|
||||||
|
sendWSCommand(wsCommand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processAndCacheData() {
|
||||||
|
if (this.dataBytesProcessed < 10 * 1024) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const serializedOutput = this.serializeAddon.serialize();
|
||||||
|
console.log("idle timeout term", this.dataBytesProcessed, serializedOutput.length);
|
||||||
|
services.BlockService.SaveTerminalState(this.blockId, serializedOutput, "full", this.ptyOffset);
|
||||||
|
this.dataBytesProcessed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
runProcessIdleTimeout() {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.requestIdleCallback(() => {
|
||||||
|
this.processAndCacheData();
|
||||||
|
this.runProcessIdleTimeout();
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
@ -3,41 +3,13 @@
|
|||||||
|
|
||||||
import { TabBar } from "@/app/tab/tabbar";
|
import { TabBar } from "@/app/tab/tabbar";
|
||||||
import { TabContent } from "@/app/tab/tabcontent";
|
import { TabContent } from "@/app/tab/tabcontent";
|
||||||
import { atoms } from "@/store/global";
|
import { atoms, createBlock } from "@/store/global";
|
||||||
import * as services from "@/store/services";
|
|
||||||
import * as WOS from "@/store/wos";
|
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
import { CenteredDiv } from "../element/quickelems";
|
import { CenteredDiv } from "../element/quickelems";
|
||||||
|
|
||||||
import { LayoutTreeActionType, LayoutTreeInsertNodeAction, newLayoutNode } from "@/faraday/index";
|
|
||||||
import { getLayoutStateAtomForTab, useLayoutTreeStateReducerAtom } from "@/faraday/lib/layoutAtom";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import "./workspace.less";
|
import "./workspace.less";
|
||||||
|
|
||||||
function Widgets() {
|
function Widgets() {
|
||||||
const windowData = jotai.useAtomValue(atoms.waveWindow);
|
|
||||||
const activeTabAtom = useMemo(() => {
|
|
||||||
return getLayoutStateAtomForTab(
|
|
||||||
windowData.activetabid,
|
|
||||||
WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", windowData.activetabid))
|
|
||||||
);
|
|
||||||
}, [windowData.activetabid]);
|
|
||||||
const [, dispatchLayoutStateAction] = useLayoutTreeStateReducerAtom(activeTabAtom);
|
|
||||||
|
|
||||||
function addBlockToTab(blockId: string) {
|
|
||||||
const insertNodeAction: LayoutTreeInsertNodeAction<TabLayoutData> = {
|
|
||||||
type: LayoutTreeActionType.InsertNode,
|
|
||||||
node: newLayoutNode<TabLayoutData>(undefined, undefined, undefined, { blockId }),
|
|
||||||
};
|
|
||||||
dispatchLayoutStateAction(insertNodeAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createBlock(blockDef: BlockDef) {
|
|
||||||
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
|
|
||||||
const blockId = await services.ObjectService.CreateBlock(blockDef, rtOpts);
|
|
||||||
addBlockToTab(blockId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clickTerminal() {
|
async function clickTerminal() {
|
||||||
const termBlockDef = {
|
const termBlockDef = {
|
||||||
controller: "shell",
|
controller: "shell",
|
||||||
|
2
frontend/types/custom.d.ts
vendored
2
frontend/types/custom.d.ts
vendored
@ -10,6 +10,8 @@ declare global {
|
|||||||
isDev: () => boolean;
|
isDev: () => boolean;
|
||||||
isDevServer: () => boolean;
|
isDevServer: () => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
80
frontend/types/gotypes.d.ts
vendored
80
frontend/types/gotypes.d.ts
vendored
@ -4,12 +4,14 @@
|
|||||||
// generated by cmd/generate/main-generate.go
|
// generated by cmd/generate/main-generate.go
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
||||||
// wstore.Block
|
// wstore.Block
|
||||||
type Block = WaveObj & {
|
type Block = WaveObj & {
|
||||||
blockdef: BlockDef;
|
blockdef: BlockDef;
|
||||||
controller: string;
|
controller: string;
|
||||||
view: string;
|
view: string;
|
||||||
runtimeopts?: RuntimeOpts;
|
runtimeopts?: RuntimeOpts;
|
||||||
|
stickers?: StickerType[];
|
||||||
meta: MetaType;
|
meta: MetaType;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -29,28 +31,20 @@ declare global {
|
|||||||
|
|
||||||
type BlockCommand = {
|
type BlockCommand = {
|
||||||
command: string;
|
command: string;
|
||||||
} & (
|
} & ( BlockAppendIJsonCommand | ResolveIdsCommand | BlockInputCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockAppendFileCommand );
|
||||||
| BlockSetMetaCommand
|
|
||||||
| BlockGetMetaCommand
|
|
||||||
| BlockMessageCommand
|
|
||||||
| BlockAppendFileCommand
|
|
||||||
| BlockAppendIJsonCommand
|
|
||||||
| BlockInputCommand
|
|
||||||
| BlockSetViewCommand
|
|
||||||
);
|
|
||||||
|
|
||||||
// wstore.BlockDef
|
// wstore.BlockDef
|
||||||
type BlockDef = {
|
type BlockDef = {
|
||||||
controller?: string;
|
controller?: string;
|
||||||
view?: string;
|
view?: string;
|
||||||
files?: { [key: string]: FileDef };
|
files?: {[key: string]: FileDef};
|
||||||
meta?: MetaType;
|
meta?: MetaType;
|
||||||
};
|
};
|
||||||
|
|
||||||
// wshutil.BlockGetMetaCommand
|
// wshutil.BlockGetMetaCommand
|
||||||
type BlockGetMetaCommand = {
|
type BlockGetMetaCommand = {
|
||||||
command: "getmeta";
|
command: "getmeta";
|
||||||
oid: string;
|
oref: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// wshutil.BlockInputCommand
|
// wshutil.BlockInputCommand
|
||||||
@ -61,6 +55,13 @@ declare global {
|
|||||||
termsize?: TermSize;
|
termsize?: TermSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// webcmd.BlockInputWSCommand
|
||||||
|
type BlockInputWSCommand = {
|
||||||
|
wscommand: "blockinput";
|
||||||
|
blockid: string;
|
||||||
|
inputdata64: string;
|
||||||
|
};
|
||||||
|
|
||||||
// wshutil.BlockMessageCommand
|
// wshutil.BlockMessageCommand
|
||||||
type BlockMessageCommand = {
|
type BlockMessageCommand = {
|
||||||
command: "message";
|
command: "message";
|
||||||
@ -70,7 +71,7 @@ declare global {
|
|||||||
// wshutil.BlockSetMetaCommand
|
// wshutil.BlockSetMetaCommand
|
||||||
type BlockSetMetaCommand = {
|
type BlockSetMetaCommand = {
|
||||||
command: "setmeta";
|
command: "setmeta";
|
||||||
oid?: string;
|
oref?: string;
|
||||||
meta: MetaType;
|
meta: MetaType;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -106,6 +107,14 @@ declare global {
|
|||||||
mimetype?: string;
|
mimetype?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// filestore.FileOptsType
|
||||||
|
type FileOptsType = {
|
||||||
|
maxsize?: number;
|
||||||
|
circular?: boolean;
|
||||||
|
ijson?: boolean;
|
||||||
|
ijsonbudget?: number;
|
||||||
|
};
|
||||||
|
|
||||||
// fileservice.FullFile
|
// fileservice.FullFile
|
||||||
type FullFile = {
|
type FullFile = {
|
||||||
info: FileInfo;
|
info: FileInfo;
|
||||||
@ -118,7 +127,7 @@ declare global {
|
|||||||
meta?: MetaType;
|
meta?: MetaType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MetaType = { [key: string]: any };
|
type MetaType = {[key: string]: any}
|
||||||
|
|
||||||
// tsgenmeta.MethodMeta
|
// tsgenmeta.MethodMeta
|
||||||
type MethodMeta = {
|
type MethodMeta = {
|
||||||
@ -139,6 +148,12 @@ declare global {
|
|||||||
y: number;
|
y: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// wshutil.ResolveIdsCommand
|
||||||
|
type ResolveIdsCommand = {
|
||||||
|
command: "resolveids";
|
||||||
|
ids: string[];
|
||||||
|
};
|
||||||
|
|
||||||
// wstore.RuntimeOpts
|
// wstore.RuntimeOpts
|
||||||
type RuntimeOpts = {
|
type RuntimeOpts = {
|
||||||
termsize?: TermSize;
|
termsize?: TermSize;
|
||||||
@ -152,6 +167,27 @@ declare global {
|
|||||||
termsize: TermSize;
|
termsize: TermSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// wstore.StickerClickOptsType
|
||||||
|
type StickerClickOptsType = {
|
||||||
|
sendinput?: string;
|
||||||
|
createblock?: BlockDef;
|
||||||
|
};
|
||||||
|
|
||||||
|
// wstore.StickerDisplayOptsType
|
||||||
|
type StickerDisplayOptsType = {
|
||||||
|
icon: string;
|
||||||
|
imgsrc: string;
|
||||||
|
svgblob?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// wstore.StickerType
|
||||||
|
type StickerType = {
|
||||||
|
stickertype: string;
|
||||||
|
style: MetaType;
|
||||||
|
clickopts?: StickerClickOptsType;
|
||||||
|
display: StickerDisplayOptsType;
|
||||||
|
};
|
||||||
|
|
||||||
// wstore.Tab
|
// wstore.Tab
|
||||||
type Tab = WaveObj & {
|
type Tab = WaveObj & {
|
||||||
name: string;
|
name: string;
|
||||||
@ -174,7 +210,7 @@ declare global {
|
|||||||
|
|
||||||
type WSCommandType = {
|
type WSCommandType = {
|
||||||
wscommand: string;
|
wscommand: string;
|
||||||
} & SetBlockTermSizeWSCommand;
|
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand );
|
||||||
|
|
||||||
// eventbus.WSEventType
|
// eventbus.WSEventType
|
||||||
type WSEventType = {
|
type WSEventType = {
|
||||||
@ -191,6 +227,17 @@ declare global {
|
|||||||
data64: string;
|
data64: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// filestore.WaveFile
|
||||||
|
type WaveFile = {
|
||||||
|
zoneid: string;
|
||||||
|
name: string;
|
||||||
|
opts: FileOptsType;
|
||||||
|
createdts: number;
|
||||||
|
size: number;
|
||||||
|
modts: number;
|
||||||
|
meta: MetaType;
|
||||||
|
};
|
||||||
|
|
||||||
// waveobj.WaveObj
|
// waveobj.WaveObj
|
||||||
type WaveObj = {
|
type WaveObj = {
|
||||||
otype: string;
|
otype: string;
|
||||||
@ -233,7 +280,7 @@ declare global {
|
|||||||
workspaceid: string;
|
workspaceid: string;
|
||||||
activetabid: string;
|
activetabid: string;
|
||||||
activeblockid?: string;
|
activeblockid?: string;
|
||||||
activeblockmap: { [key: string]: string };
|
activeblockmap: {[key: string]: string};
|
||||||
pos: Point;
|
pos: Point;
|
||||||
winsize: WinSize;
|
winsize: WinSize;
|
||||||
lastfocusts: number;
|
lastfocusts: number;
|
||||||
@ -246,6 +293,7 @@ declare global {
|
|||||||
tabids: string[];
|
tabids: string[];
|
||||||
meta: MetaType;
|
meta: MetaType;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {}
|
||||||
|
@ -55,6 +55,7 @@
|
|||||||
"@react-hook/resize-observer": "^2.0.1",
|
"@react-hook/resize-observer": "^2.0.1",
|
||||||
"@tanstack/react-table": "^8.17.3",
|
"@tanstack/react-table": "^8.17.3",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/addon-serialize": "^0.13.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@ -69,6 +70,7 @@
|
|||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-frame-component": "^5.2.7",
|
"react-frame-component": "^5.2.7",
|
||||||
|
"react-gauge-chart": "^0.5.1",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
@ -402,7 +402,6 @@ func (bc *BlockController) Run(bdata *wstore.Block) {
|
|||||||
}
|
}
|
||||||
inputUnion.InputData = inputBuf[:nw]
|
inputUnion.InputData = inputBuf[:nw]
|
||||||
}
|
}
|
||||||
log.Printf("INPUT: %s | %q\n", bc.BlockId, string(inputUnion.InputData))
|
|
||||||
bc.ShellInputCh <- inputUnion
|
bc.ShellInputCh <- inputUnion
|
||||||
default:
|
default:
|
||||||
log.Printf("unknown command type %T\n", cmd)
|
log.Printf("unknown command type %T\n", cmd)
|
||||||
|
@ -185,6 +185,9 @@ func (s *FileStore) Stat(ctx context.Context, zoneId string, name string) (*Wave
|
|||||||
return withLockRtn(s, zoneId, name, func(entry *CacheEntry) (*WaveFile, error) {
|
return withLockRtn(s, zoneId, name, func(entry *CacheEntry) (*WaveFile, error) {
|
||||||
file, err := entry.loadFileForRead(ctx)
|
file, err := entry.loadFileForRead(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == fs.ErrNotExist {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("error getting file: %v", err)
|
return nil, fmt.Errorf("error getting file: %v", err)
|
||||||
}
|
}
|
||||||
return file.DeepCopy(), nil
|
return file.DeepCopy(), nil
|
||||||
|
@ -4,13 +4,16 @@
|
|||||||
package blockservice
|
package blockservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/filestore"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BlockService struct{}
|
type BlockService struct{}
|
||||||
@ -38,3 +41,24 @@ func (bs *BlockService) SendCommand(blockId string, cmd wshutil.BlockCommand) er
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bs *BlockService) SaveTerminalState(ctx context.Context, blockId string, state string, stateType string, ptyOffset int64) error {
|
||||||
|
_, err := wstore.DBMustGet[*wstore.Block](ctx, blockId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if stateType != "full" && stateType != "preview" {
|
||||||
|
return fmt.Errorf("invalid state type: %q", stateType)
|
||||||
|
}
|
||||||
|
// ignore MakeFile error (already exists is ok)
|
||||||
|
filestore.WFS.MakeFile(ctx, blockId, "cache:term:"+stateType, nil, filestore.FileOptsType{})
|
||||||
|
err = filestore.WFS.WriteFile(ctx, blockId, "cache:term:"+stateType, []byte(state))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot save terminal state: %w", err)
|
||||||
|
}
|
||||||
|
err = filestore.WFS.WriteMeta(ctx, blockId, "cache:term:"+stateType, filestore.FileMeta{"ptyoffset": ptyOffset}, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot save terminal state meta: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/eventbus"
|
"github.com/wavetermdev/thenextwave/pkg/eventbus"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/filestore"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/service"
|
"github.com/wavetermdev/thenextwave/pkg/service"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||||
@ -364,6 +365,7 @@ func GenerateWaveObjTypes(tsTypesMap map[reflect.Type]string) {
|
|||||||
GenerateTSType(reflect.TypeOf(wstore.UIContext{}), tsTypesMap)
|
GenerateTSType(reflect.TypeOf(wstore.UIContext{}), tsTypesMap)
|
||||||
GenerateTSType(reflect.TypeOf(eventbus.WSEventType{}), tsTypesMap)
|
GenerateTSType(reflect.TypeOf(eventbus.WSEventType{}), tsTypesMap)
|
||||||
GenerateTSType(reflect.TypeOf(eventbus.WSFileEventData{}), tsTypesMap)
|
GenerateTSType(reflect.TypeOf(eventbus.WSFileEventData{}), tsTypesMap)
|
||||||
|
GenerateTSType(reflect.TypeOf(filestore.WaveFile{}), tsTypesMap)
|
||||||
for _, rtype := range wstore.AllWaveObjTypes() {
|
for _, rtype := range wstore.AllWaveObjTypes() {
|
||||||
GenerateTSType(rtype, tsTypesMap)
|
GenerateTSType(rtype, tsTypesMap)
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -102,6 +103,15 @@ func marshalReturnValue(data any, err error) []byte {
|
|||||||
func handleWaveFile(w http.ResponseWriter, r *http.Request) {
|
func handleWaveFile(w http.ResponseWriter, r *http.Request) {
|
||||||
zoneId := r.URL.Query().Get("zoneid")
|
zoneId := r.URL.Query().Get("zoneid")
|
||||||
name := r.URL.Query().Get("name")
|
name := r.URL.Query().Get("name")
|
||||||
|
offsetStr := r.URL.Query().Get("offset")
|
||||||
|
var offset int64 = 0
|
||||||
|
if offsetStr != "" {
|
||||||
|
var err error
|
||||||
|
offset, err = strconv.ParseInt(offsetStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("invalid offset: %v", err), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
if _, err := uuid.Parse(zoneId); err != nil {
|
if _, err := uuid.Parse(zoneId); err != nil {
|
||||||
http.Error(w, fmt.Sprintf("invalid zoneid: %v", err), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("invalid zoneid: %v", err), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@ -113,7 +123,7 @@ func handleWaveFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
file, err := filestore.WFS.Stat(r.Context(), zoneId, name)
|
file, err := filestore.WFS.Stat(r.Context(), zoneId, name)
|
||||||
if err == fs.ErrNotExist {
|
if err == fs.ErrNotExist {
|
||||||
http.NotFound(w, r)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -125,15 +135,19 @@ func handleWaveFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, fmt.Sprintf("error serializing file info: %v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("error serializing file info: %v", err), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
// can make more efficient by checking modtime + If-Modified-Since headers to allow caching
|
// can make more efficient by checking modtime + If-Modified-Since headers to allow caching
|
||||||
|
dataStartIdx := file.DataStartIdx()
|
||||||
|
if offset >= dataStartIdx {
|
||||||
|
dataStartIdx = offset
|
||||||
|
}
|
||||||
w.Header().Set(ContentTypeHeaderKey, ContentTypeBinary)
|
w.Header().Set(ContentTypeHeaderKey, ContentTypeBinary)
|
||||||
w.Header().Set(ContentLengthHeaderKey, fmt.Sprintf("%d", file.Size))
|
w.Header().Set(ContentLengthHeaderKey, fmt.Sprintf("%d", file.Size-dataStartIdx))
|
||||||
w.Header().Set(WaveZoneFileInfoHeaderKey, base64.StdEncoding.EncodeToString(jsonFileBArr))
|
w.Header().Set(WaveZoneFileInfoHeaderKey, base64.StdEncoding.EncodeToString(jsonFileBArr))
|
||||||
w.Header().Set(LastModifiedHeaderKey, time.UnixMilli(file.ModTs).UTC().Format(http.TimeFormat))
|
w.Header().Set(LastModifiedHeaderKey, time.UnixMilli(file.ModTs).UTC().Format(http.TimeFormat))
|
||||||
if file.Size == 0 {
|
if dataStartIdx >= file.Size {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for offset := file.DataStartIdx(); offset < file.Size; offset += filestore.DefaultPartDataSize {
|
for offset := dataStartIdx; offset < file.Size; offset += filestore.DefaultPartDataSize {
|
||||||
_, data, err := filestore.WFS.ReadAt(r.Context(), zoneId, name, offset, filestore.DefaultPartDataSize)
|
_, data, err := filestore.WFS.ReadAt(r.Context(), zoneId, name, offset, filestore.DefaultPartDataSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if offset == 0 {
|
if offset == 0 {
|
||||||
@ -177,6 +191,7 @@ func WebFnWrap(opts WebFnOpts, fn WebFnType) WebFnType {
|
|||||||
if !opts.AllowCaching {
|
if !opts.AllowCaching {
|
||||||
w.Header().Set(CacheControlHeaderKey, CacheControlHeaderNoCache)
|
w.Header().Set(CacheControlHeaderKey, CacheControlHeaderNoCache)
|
||||||
}
|
}
|
||||||
|
w.Header().Set("Access-Control-Expose-Headers", "X-ZoneFileInfo")
|
||||||
// reqAuthKey := r.Header.Get("X-AuthKey")
|
// reqAuthKey := r.Header.Get("X-AuthKey")
|
||||||
// if reqAuthKey == "" {
|
// if reqAuthKey == "" {
|
||||||
// w.WriteHeader(http.StatusInternalServerError)
|
// w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
WSCommand_SetBlockTermSize = "setblocktermsize"
|
WSCommand_SetBlockTermSize = "setblocktermsize"
|
||||||
|
WSCommand_BlockInput = "blockinput"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WSCommandType interface {
|
type WSCommandType interface {
|
||||||
@ -26,6 +27,7 @@ func WSCommandTypeUnionMeta() tsgenmeta.TypeUnionMeta {
|
|||||||
TypeFieldName: "wscommand",
|
TypeFieldName: "wscommand",
|
||||||
Types: []reflect.Type{
|
Types: []reflect.Type{
|
||||||
reflect.TypeOf(SetBlockTermSizeWSCommand{}),
|
reflect.TypeOf(SetBlockTermSizeWSCommand{}),
|
||||||
|
reflect.TypeOf(BlockInputWSCommand{}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,6 +42,16 @@ func (cmd *SetBlockTermSizeWSCommand) GetWSCommand() string {
|
|||||||
return cmd.WSCommand
|
return cmd.WSCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BlockInputWSCommand struct {
|
||||||
|
WSCommand string `json:"wscommand" tstype:"\"blockinput\""`
|
||||||
|
BlockId string `json:"blockid"`
|
||||||
|
InputData64 string `json:"inputdata64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *BlockInputWSCommand) GetWSCommand() string {
|
||||||
|
return cmd.WSCommand
|
||||||
|
}
|
||||||
|
|
||||||
func ParseWSCommandMap(cmdMap map[string]any) (WSCommandType, error) {
|
func ParseWSCommandMap(cmdMap map[string]any) (WSCommandType, error) {
|
||||||
cmdType, ok := cmdMap["wscommand"].(string)
|
cmdType, ok := cmdMap["wscommand"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -53,6 +65,13 @@ func ParseWSCommandMap(cmdMap map[string]any) (WSCommandType, error) {
|
|||||||
return nil, fmt.Errorf("error decoding SetBlockTermSizeWSCommand: %w", err)
|
return nil, fmt.Errorf("error decoding SetBlockTermSizeWSCommand: %w", err)
|
||||||
}
|
}
|
||||||
return &cmd, nil
|
return &cmd, nil
|
||||||
|
case WSCommand_BlockInput:
|
||||||
|
var cmd BlockInputWSCommand
|
||||||
|
err := utilfn.DoMapStucture(&cmd, cmdMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decoding BlockInputWSCommand: %w", err)
|
||||||
|
}
|
||||||
|
return &cmd, nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown wscommand type %q", cmdType)
|
return nil, fmt.Errorf("unknown wscommand type %q", cmdType)
|
||||||
}
|
}
|
||||||
|
@ -100,6 +100,12 @@ func processWSCommand(jmsg map[string]any, outputCh chan any) {
|
|||||||
TermSize: &cmd.TermSize,
|
TermSize: &cmd.TermSize,
|
||||||
}
|
}
|
||||||
blockservice.BlockServiceInstance.SendCommand(cmd.BlockId, blockCmd)
|
blockservice.BlockServiceInstance.SendCommand(cmd.BlockId, blockCmd)
|
||||||
|
case *webcmd.BlockInputWSCommand:
|
||||||
|
blockCmd := &wshutil.BlockInputCommand{
|
||||||
|
Command: wshutil.BlockCommand_Input,
|
||||||
|
InputData64: cmd.InputData64,
|
||||||
|
}
|
||||||
|
blockservice.BlockServiceInstance.SendCommand(cmd.BlockId, blockCmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,6 +201,24 @@ type BlockDef struct {
|
|||||||
Meta map[string]any `json:"meta,omitempty"`
|
Meta map[string]any `json:"meta,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StickerClickOptsType struct {
|
||||||
|
SendInput string `json:"sendinput,omitempty"`
|
||||||
|
CreateBlock *BlockDef `json:"createblock,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StickerDisplayOptsType struct {
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
ImgSrc string `json:"imgsrc"`
|
||||||
|
SvgBlob string `json:"svgblob,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StickerType struct {
|
||||||
|
StickerType string `json:"stickertype"`
|
||||||
|
Style map[string]any `json:"style"`
|
||||||
|
ClickOpts *StickerClickOptsType `json:"clickopts,omitempty"`
|
||||||
|
Display *StickerDisplayOptsType `json:"display"`
|
||||||
|
}
|
||||||
|
|
||||||
type RuntimeOpts struct {
|
type RuntimeOpts struct {
|
||||||
TermSize shellexec.TermSize `json:"termsize,omitempty"`
|
TermSize shellexec.TermSize `json:"termsize,omitempty"`
|
||||||
WinSize WinSize `json:"winsize,omitempty"`
|
WinSize WinSize `json:"winsize,omitempty"`
|
||||||
@ -223,6 +241,7 @@ type Block struct {
|
|||||||
Controller string `json:"controller"`
|
Controller string `json:"controller"`
|
||||||
View string `json:"view"`
|
View string `json:"view"`
|
||||||
RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"`
|
RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"`
|
||||||
|
Stickers []*StickerType `json:"stickers,omitempty"`
|
||||||
Meta map[string]any `json:"meta"`
|
Meta map[string]any `json:"meta"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
25
yarn.lock
25
yarn.lock
@ -4706,6 +4706,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@xterm/addon-serialize@npm:^0.13.0":
|
||||||
|
version: 0.13.0
|
||||||
|
resolution: "@xterm/addon-serialize@npm:0.13.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@xterm/xterm": ^5.0.0
|
||||||
|
checksum: 10c0/090f502867250cdceca9abdbe37aebe0c01eb5d708a09c51d3e0e9bb5d4d3b0d4d67af1c4c8bde9b78f108a5e4150420180de69e6228aac76596fdbf6c4c59dc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@xterm/xterm@npm:^5.5.0":
|
"@xterm/xterm@npm:^5.5.0":
|
||||||
version: 5.5.0
|
version: 5.5.0
|
||||||
resolution: "@xterm/xterm@npm:5.5.0"
|
resolution: "@xterm/xterm@npm:5.5.0"
|
||||||
@ -6051,7 +6060,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"d3@npm:^7.9.0":
|
"d3@npm:^7.6.1, d3@npm:^7.9.0":
|
||||||
version: 7.9.0
|
version: 7.9.0
|
||||||
resolution: "d3@npm:7.9.0"
|
resolution: "d3@npm:7.9.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -11026,6 +11035,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"react-gauge-chart@npm:^0.5.1":
|
||||||
|
version: 0.5.1
|
||||||
|
resolution: "react-gauge-chart@npm:0.5.1"
|
||||||
|
dependencies:
|
||||||
|
d3: "npm:^7.6.1"
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.2 || ^17.0 || ^18.x
|
||||||
|
react-dom: ^16.8.2 || ^17.0 || ^18.x
|
||||||
|
checksum: 10c0/16d0142130ed56c8c5cd710d596499b44ccf654ea8973a4ed973ff8dca6b01e0d03b9885c34276cf678cd0f6bae1eee836d921a3bcbdbbf4a21eadd9f1686b90
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"react-is@npm:18.1.0":
|
"react-is@npm:18.1.0":
|
||||||
version: 18.1.0
|
version: 18.1.0
|
||||||
resolution: "react-is@npm:18.1.0"
|
resolution: "react-is@npm:18.1.0"
|
||||||
@ -12268,6 +12289,7 @@ __metadata:
|
|||||||
"@vitejs/plugin-react": "npm:^4.3.0"
|
"@vitejs/plugin-react": "npm:^4.3.0"
|
||||||
"@vitest/coverage-istanbul": "npm:^1.6.0"
|
"@vitest/coverage-istanbul": "npm:^1.6.0"
|
||||||
"@xterm/addon-fit": "npm:^0.10.0"
|
"@xterm/addon-fit": "npm:^0.10.0"
|
||||||
|
"@xterm/addon-serialize": "npm:^0.13.0"
|
||||||
"@xterm/xterm": "npm:^5.5.0"
|
"@xterm/xterm": "npm:^5.5.0"
|
||||||
base64-js: "npm:^1.5.1"
|
base64-js: "npm:^1.5.1"
|
||||||
clsx: "npm:^2.1.1"
|
clsx: "npm:^2.1.1"
|
||||||
@ -12289,6 +12311,7 @@ __metadata:
|
|||||||
react-dnd-html5-backend: "npm:^16.0.1"
|
react-dnd-html5-backend: "npm:^16.0.1"
|
||||||
react-dom: "npm:^18.3.1"
|
react-dom: "npm:^18.3.1"
|
||||||
react-frame-component: "npm:^5.2.7"
|
react-frame-component: "npm:^5.2.7"
|
||||||
|
react-gauge-chart: "npm:^0.5.1"
|
||||||
react-markdown: "npm:^9.0.1"
|
react-markdown: "npm:^9.0.1"
|
||||||
remark-gfm: "npm:^4.0.0"
|
remark-gfm: "npm:^4.0.0"
|
||||||
rxjs: "npm:^7.8.1"
|
rxjs: "npm:^7.8.1"
|
||||||
|
Loading…
Reference in New Issue
Block a user