mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
connection handling / block controller handling (#326)
This commit is contained in:
@@ -276,6 +276,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
.connstatus-overlay {
|
||||
position: absolute;
|
||||
top: var(--header-height);
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
z-index: var(--zindex-block-mask-inner);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.connstatus-mainelem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
background-color: rgba(60, 60, 60, 0.65);
|
||||
backdrop-filter: blur(3px);
|
||||
font: var(--base-font);
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
.connstatus-error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.connstatus-actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.block-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -288,12 +327,12 @@
|
||||
border-radius: calc(var(--block-border-radius) + 2px);
|
||||
z-index: var(--zindex-block-mask-inner);
|
||||
|
||||
&.is-layoutmode {
|
||||
&.show-block-mask {
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&.is-layoutmode .block-mask-inner {
|
||||
&.show-block-mask .block-mask-inner {
|
||||
margin-top: var(--header-height); // TODO fix this magic
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
height: calc(100% - var(--header-height));
|
||||
|
||||
@@ -7,7 +7,12 @@ import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/
|
||||
import { ErrorBoundary } from "@/element/errorboundary";
|
||||
import { CenteredDiv } from "@/element/quickelems";
|
||||
import { NodeModel, useDebouncedNodeInnerRect } from "@/layout/index";
|
||||
import { counterInc, getViewModel, registerViewModel, unregisterViewModel } from "@/store/global";
|
||||
import {
|
||||
counterInc,
|
||||
getBlockComponentModel,
|
||||
registerBlockComponentModel,
|
||||
unregisterBlockComponentModel,
|
||||
} from "@/store/global";
|
||||
import * as WOS from "@/store/wos";
|
||||
import { getElemAsStr } from "@/util/focusutil";
|
||||
import * as util from "@/util/util";
|
||||
@@ -251,14 +256,15 @@ const Block = React.memo((props: BlockProps) => {
|
||||
counterInc("render-Block");
|
||||
counterInc("render-Block-" + props.nodeModel.blockId.substring(0, 8));
|
||||
const [blockData, loading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", props.nodeModel.blockId));
|
||||
let viewModel = getViewModel(props.nodeModel.blockId);
|
||||
const bcm = getBlockComponentModel(props.nodeModel.blockId);
|
||||
let viewModel = bcm?.viewModel;
|
||||
if (viewModel == null || viewModel.viewType != blockData?.meta?.view) {
|
||||
viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel);
|
||||
registerViewModel(props.nodeModel.blockId, viewModel);
|
||||
registerBlockComponentModel(props.nodeModel.blockId, { viewModel });
|
||||
}
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
unregisterViewModel(props.nodeModel.blockId);
|
||||
unregisterBlockComponentModel(props.nodeModel.blockId);
|
||||
};
|
||||
}, []);
|
||||
if (loading || util.isBlank(props.nodeModel.blockId) || blockData == null) {
|
||||
|
||||
@@ -13,7 +13,15 @@ import {
|
||||
import { Button } from "@/app/element/button";
|
||||
import { TypeAheadModal } from "@/app/modals/typeaheadmodal";
|
||||
import { ContextMenuModel } from "@/app/store/contextmenu";
|
||||
import { atoms, globalStore, useBlockAtom, useSettingsKeyAtom, WOS } from "@/app/store/global";
|
||||
import {
|
||||
atoms,
|
||||
getBlockComponentModel,
|
||||
getConnStatusAtom,
|
||||
globalStore,
|
||||
useBlockAtom,
|
||||
useSettingsKeyAtom,
|
||||
WOS,
|
||||
} from "@/app/store/global";
|
||||
import * as services from "@/app/store/services";
|
||||
import { WshServer } from "@/app/store/wshserver";
|
||||
import { MagnifyIcon } from "@/element/magnify";
|
||||
@@ -61,14 +69,6 @@ function handleHeaderContextMenu(
|
||||
},
|
||||
},
|
||||
];
|
||||
const blockController = blockData?.meta?.controller;
|
||||
if (!util.isBlank(blockController)) {
|
||||
menu.push({ type: "separator" });
|
||||
menu.push({
|
||||
label: "Restart Controller",
|
||||
click: () => WshServer.ControllerRestartCommand({ blockid: blockData.oid }),
|
||||
});
|
||||
}
|
||||
const extraItems = viewModel?.getSettingsMenuItems?.();
|
||||
if (extraItems && extraItems.length > 0) menu.push({ type: "separator" }, ...extraItems);
|
||||
menu.push(
|
||||
@@ -256,13 +256,70 @@ function renderHeaderElements(headerTextUnion: HeaderElem[], preview: boolean):
|
||||
return headerTextElems;
|
||||
}
|
||||
|
||||
const BlockMask = ({ nodeModel }: { nodeModel: NodeModel }) => {
|
||||
const ConnStatusOverlay = React.memo(
|
||||
({
|
||||
nodeModel,
|
||||
viewModel,
|
||||
changeConnModalAtom,
|
||||
}: {
|
||||
nodeModel: NodeModel;
|
||||
viewModel: ViewModel;
|
||||
changeConnModalAtom: jotai.PrimitiveAtom<boolean>;
|
||||
}) => {
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
|
||||
const [connModalOpen, setConnModalOpen] = jotai.useAtom(changeConnModalAtom);
|
||||
const connName = blockData.meta?.connection;
|
||||
const connStatus = jotai.useAtomValue(getConnStatusAtom(connName));
|
||||
const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom);
|
||||
const handleTryReconnect = React.useCallback(() => {
|
||||
const prtn = WshServer.ConnConnectCommand(connName, { timeout: 60000 });
|
||||
prtn.catch((e) => console.log("error reconnecting", connName, e));
|
||||
}, [connName]);
|
||||
const handleSwitchConnection = React.useCallback(() => {
|
||||
setConnModalOpen(true);
|
||||
}, [setConnModalOpen]);
|
||||
if (isLayoutMode || connStatus.status == "connected" || connModalOpen) {
|
||||
return null;
|
||||
}
|
||||
let statusText = `Disconnected from "${connName}"`;
|
||||
let showReconnect = true;
|
||||
if (connStatus.status == "connecting") {
|
||||
statusText = `Connecting to "${connName}"...`;
|
||||
showReconnect = false;
|
||||
}
|
||||
return (
|
||||
<div className="connstatus-overlay">
|
||||
<div className="connstatus-mainelem">
|
||||
<div style={{ marginBottom: 5 }}>{statusText}</div>
|
||||
{!util.isBlank(connStatus.error) ? (
|
||||
<div className="connstatus-error">error: {connStatus.error}</div>
|
||||
) : null}
|
||||
{showReconnect ? (
|
||||
<div className="connstatus-actions">
|
||||
<Button className="secondary" onClick={handleTryReconnect}>
|
||||
<i className="fa-sharp fa-solid fa-arrow-right-arrow-left" style={{ marginRight: 5 }} />
|
||||
Reconnect Now
|
||||
</Button>
|
||||
<Button className="secondary" onClick={handleSwitchConnection}>
|
||||
<i className="fa-sharp fa-solid fa-arrow-right-arrow-left" style={{ marginRight: 5 }} />
|
||||
Switch Connection
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => {
|
||||
const isFocused = jotai.useAtomValue(nodeModel.isFocused);
|
||||
const blockNum = jotai.useAtomValue(nodeModel.blockNum);
|
||||
const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom);
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
|
||||
|
||||
const style: React.CSSProperties = {};
|
||||
let showBlockMask = false;
|
||||
|
||||
if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
|
||||
style.borderColor = blockData.meta["frame:bordercolor"];
|
||||
}
|
||||
@@ -271,6 +328,7 @@ const BlockMask = ({ nodeModel }: { nodeModel: NodeModel }) => {
|
||||
}
|
||||
let innerElem = null;
|
||||
if (isLayoutMode) {
|
||||
showBlockMask = true;
|
||||
innerElem = (
|
||||
<div className="block-mask-inner">
|
||||
<div className="bignum">{blockNum}</div>
|
||||
@@ -278,11 +336,11 @@ const BlockMask = ({ nodeModel }: { nodeModel: NodeModel }) => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={clsx("block-mask", { "is-layoutmode": isLayoutMode })} style={style}>
|
||||
<div className={clsx("block-mask", { "show-block-mask": showBlockMask })} style={style}>
|
||||
{innerElem}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
||||
const { nodeModel, viewModel, blockModel, preview, numBlocksInTab, children } = props;
|
||||
@@ -290,10 +348,42 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
||||
const isFocused = jotai.useAtomValue(nodeModel.isFocused);
|
||||
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
|
||||
const customBg = util.useAtomValueSafe(viewModel.blockBg);
|
||||
const manageConnection = util.useAtomValueSafe(viewModel.manageConnection);
|
||||
const changeConnModalAtom = useBlockAtom(nodeModel.blockId, "changeConn", () => {
|
||||
return jotai.atom(false);
|
||||
}) as jotai.PrimitiveAtom<boolean>;
|
||||
const connBtnRef = React.useRef<HTMLDivElement>();
|
||||
React.useEffect(() => {
|
||||
if (!manageConnection) {
|
||||
return;
|
||||
}
|
||||
const bcm = getBlockComponentModel(nodeModel.blockId);
|
||||
if (bcm != null) {
|
||||
bcm.openSwitchConnection = () => {
|
||||
globalStore.set(changeConnModalAtom, true);
|
||||
};
|
||||
}
|
||||
return () => {
|
||||
const bcm = getBlockComponentModel(nodeModel.blockId);
|
||||
if (bcm != null) {
|
||||
bcm.openSwitchConnection = null;
|
||||
}
|
||||
};
|
||||
}, [manageConnection]);
|
||||
React.useEffect(() => {
|
||||
// on mount, if manageConnection, call ConnEnsure
|
||||
if (!manageConnection || blockData == null || preview) {
|
||||
return;
|
||||
}
|
||||
const connName = blockData?.meta?.connection;
|
||||
if (!util.isBlank(connName)) {
|
||||
console.log("ensure conn", nodeModel.blockId, connName);
|
||||
WshServer.ConnEnsureCommand(connName, { timeout: 60000 }).catch((e) => {
|
||||
console.log("error ensuring connection", nodeModel.blockId, connName, e);
|
||||
});
|
||||
}
|
||||
}, [manageConnection, blockData]);
|
||||
|
||||
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
|
||||
const innerStyle: React.CSSProperties = {};
|
||||
if (!preview && customBg?.bg != null) {
|
||||
@@ -319,6 +409,7 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
||||
ref={blockModel?.blockRef}
|
||||
>
|
||||
<BlockMask nodeModel={nodeModel} />
|
||||
<ConnStatusOverlay nodeModel={nodeModel} viewModel={viewModel} changeConnModalAtom={changeConnModalAtom} />
|
||||
<div className="block-frame-default-inner" style={innerStyle}>
|
||||
<BlockFrame_Header {...props} connBtnRef={connBtnRef} changeConnModalAtom={changeConnModalAtom} />
|
||||
{preview ? previewElem : children}
|
||||
@@ -359,6 +450,12 @@ const ChangeConnectionBlockModal = React.memo(
|
||||
const isNodeFocused = jotai.useAtomValue(nodeModel.isFocused);
|
||||
const changeConnection = React.useCallback(
|
||||
async (connName: string) => {
|
||||
if (connName == "") {
|
||||
connName = null;
|
||||
}
|
||||
if (connName == blockData?.meta?.connection) {
|
||||
return;
|
||||
}
|
||||
const oldCwd = blockData?.meta?.file ?? "";
|
||||
let newCwd: string;
|
||||
if (oldCwd == "") {
|
||||
@@ -370,10 +467,14 @@ const ChangeConnectionBlockModal = React.memo(
|
||||
oref: WOS.makeORef("block", blockId),
|
||||
meta: { connection: connName, file: newCwd },
|
||||
});
|
||||
await services.BlockService.EnsureConnection(blockId).catch((e) => console.log(e));
|
||||
await WshServer.ControllerRestartCommand({ blockid: blockId });
|
||||
const tabId = globalStore.get(atoms.activeTabId);
|
||||
try {
|
||||
await WshServer.ConnEnsureCommand(connName, { timeout: 60000 });
|
||||
} catch (e) {
|
||||
console.log("error connecting", blockId, connName, e);
|
||||
}
|
||||
},
|
||||
[blockId]
|
||||
[blockId, blockData]
|
||||
);
|
||||
const handleTypeAheadKeyDown = React.useCallback(
|
||||
(waveEvent: WaveKeyboardEvent): boolean => {
|
||||
|
||||
@@ -160,6 +160,7 @@ export const ControllerStatusIcon = React.memo(({ blockId }: { blockId: string }
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
const hasController = !util.isBlank(blockData?.meta?.controller);
|
||||
const [controllerStatus, setControllerStatus] = React.useState<BlockControllerRuntimeStatus>(null);
|
||||
const [gotInitialStatus, setGotInitialStatus] = React.useState(false);
|
||||
const connection = blockData?.meta?.connection ?? "local";
|
||||
const connStatusAtom = getConnStatusAtom(connection);
|
||||
const connStatus = jotai.useAtomValue(connStatusAtom);
|
||||
@@ -169,6 +170,7 @@ export const ControllerStatusIcon = React.memo(({ blockId }: { blockId: string }
|
||||
}
|
||||
const initialRTStatus = services.BlockService.GetControllerStatus(blockId);
|
||||
initialRTStatus.then((rts) => {
|
||||
setGotInitialStatus(true);
|
||||
setControllerStatus(rts);
|
||||
});
|
||||
const unsubFn = waveEventSubscribe("controllerstatus", makeORef("block", blockId), (event) => {
|
||||
@@ -179,25 +181,19 @@ export const ControllerStatusIcon = React.memo(({ blockId }: { blockId: string }
|
||||
unsubFn();
|
||||
};
|
||||
}, [hasController]);
|
||||
if (!hasController) {
|
||||
if (!hasController || !gotInitialStatus) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
controllerStatus == null ||
|
||||
(controllerStatus?.status == "running" && controllerStatus?.shellprocstatus == "running")
|
||||
) {
|
||||
if (controllerStatus?.shellprocstatus == "running") {
|
||||
return null;
|
||||
}
|
||||
if (connStatus?.status != "connected") {
|
||||
return null;
|
||||
}
|
||||
const controllerStatusElem = (
|
||||
<i
|
||||
key="controller-status"
|
||||
className="fa-sharp fa-solid fa-triangle-exclamation"
|
||||
title="Controller Is Not Running"
|
||||
style={{ color: "var(--error-color)" }}
|
||||
/>
|
||||
<div className="iconbutton disabled" key="controller-status">
|
||||
<i className="fa-sharp fa-solid fa-triangle-exclamation" title="Shell Process Is Not Running" />
|
||||
</div>
|
||||
);
|
||||
return controllerStatusElem;
|
||||
});
|
||||
@@ -206,7 +202,7 @@ export const ConnectionButton = React.memo(
|
||||
React.forwardRef<HTMLDivElement, ConnectionButtonProps>(
|
||||
({ connection, changeConnModalAtom }: ConnectionButtonProps, ref) => {
|
||||
const [connModalOpen, setConnModalOpen] = jotai.useAtom(changeConnModalAtom);
|
||||
const isLocal = util.isBlank(connection) || connection == "local";
|
||||
const isLocal = util.isBlank(connection);
|
||||
const connStatusAtom = getConnStatusAtom(connection);
|
||||
const connStatus = jotai.useAtomValue(connStatusAtom);
|
||||
let showDisconnectedSlash = false;
|
||||
|
||||
@@ -23,7 +23,7 @@ let PLATFORM: NodeJS.Platform = "darwin";
|
||||
const globalStore = jotai.createStore();
|
||||
let atoms: GlobalAtomsType;
|
||||
let globalEnvironment: "electron" | "renderer";
|
||||
const blockViewModelMap = new Map<string, ViewModel>();
|
||||
const blockComponentModelMap = new Map<string, BlockComponentModel>();
|
||||
const Counters = new Map<string, number>();
|
||||
const ConnStatusMap = new Map<string, jotai.PrimitiveAtom<ConnStatus>>();
|
||||
|
||||
@@ -526,16 +526,16 @@ async function openLink(uri: string, forceOpenInternally = false) {
|
||||
}
|
||||
}
|
||||
|
||||
function registerViewModel(blockId: string, viewModel: ViewModel) {
|
||||
blockViewModelMap.set(blockId, viewModel);
|
||||
function registerBlockComponentModel(blockId: string, bcm: BlockComponentModel) {
|
||||
blockComponentModelMap.set(blockId, bcm);
|
||||
}
|
||||
|
||||
function unregisterViewModel(blockId: string) {
|
||||
blockViewModelMap.delete(blockId);
|
||||
function unregisterBlockComponentModel(blockId: string) {
|
||||
blockComponentModelMap.delete(blockId);
|
||||
}
|
||||
|
||||
function getViewModel(blockId: string): ViewModel {
|
||||
return blockViewModelMap.get(blockId);
|
||||
function getBlockComponentModel(blockId: string): BlockComponentModel {
|
||||
return blockComponentModelMap.get(blockId);
|
||||
}
|
||||
|
||||
function refocusNode(blockId: string) {
|
||||
@@ -548,8 +548,8 @@ function refocusNode(blockId: string) {
|
||||
return;
|
||||
}
|
||||
layoutModel.focusNode(layoutNodeId.id);
|
||||
const viewModel = getViewModel(blockId);
|
||||
const ok = viewModel?.giveFocus?.();
|
||||
const bcm = getBlockComponentModel(blockId);
|
||||
const ok = bcm?.viewModel?.giveFocus?.();
|
||||
if (!ok) {
|
||||
const inputElem = document.getElementById(`${blockId}-dummy-focus`);
|
||||
inputElem?.focus();
|
||||
@@ -604,14 +604,26 @@ function subscribeToConnEvents() {
|
||||
function getConnStatusAtom(conn: string): jotai.PrimitiveAtom<ConnStatus> {
|
||||
let rtn = ConnStatusMap.get(conn);
|
||||
if (rtn == null) {
|
||||
const connStatus: ConnStatus = {
|
||||
connection: conn,
|
||||
connected: false,
|
||||
error: null,
|
||||
status: "disconnected",
|
||||
hasconnected: false,
|
||||
};
|
||||
rtn = jotai.atom(connStatus);
|
||||
if (util.isBlank(conn)) {
|
||||
// create a fake "local" status atom that's always connected
|
||||
const connStatus: ConnStatus = {
|
||||
connection: conn,
|
||||
connected: true,
|
||||
error: null,
|
||||
status: "connected",
|
||||
hasconnected: true,
|
||||
};
|
||||
rtn = jotai.atom(connStatus);
|
||||
} else {
|
||||
const connStatus: ConnStatus = {
|
||||
connection: conn,
|
||||
connected: false,
|
||||
error: null,
|
||||
status: "disconnected",
|
||||
hasconnected: false,
|
||||
};
|
||||
rtn = jotai.atom(connStatus);
|
||||
}
|
||||
ConnStatusMap.set(conn, rtn);
|
||||
}
|
||||
return rtn;
|
||||
@@ -625,13 +637,13 @@ export {
|
||||
createBlock,
|
||||
fetchWaveFile,
|
||||
getApi,
|
||||
getBlockComponentModel,
|
||||
getConnStatusAtom,
|
||||
getEventORefSubject,
|
||||
getEventSubject,
|
||||
getFileSubject,
|
||||
getObjectId,
|
||||
getUserName,
|
||||
getViewModel,
|
||||
globalStore,
|
||||
globalWS,
|
||||
initGlobal,
|
||||
@@ -641,12 +653,12 @@ export {
|
||||
openLink,
|
||||
PLATFORM,
|
||||
refocusNode,
|
||||
registerViewModel,
|
||||
registerBlockComponentModel,
|
||||
sendWSCommand,
|
||||
setNodeFocus,
|
||||
setPlatform,
|
||||
subscribeToConnEvents,
|
||||
unregisterViewModel,
|
||||
unregisterBlockComponentModel,
|
||||
useBlockAtom,
|
||||
useBlockCache,
|
||||
useBlockDataLoaded,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { atoms, createBlock, getApi, getViewModel, globalStore, refocusNode, WOS } from "@/app/store/global";
|
||||
import { atoms, createBlock, getApi, getBlockComponentModel, globalStore, refocusNode, WOS } from "@/app/store/global";
|
||||
import * as services from "@/app/store/services";
|
||||
import {
|
||||
deleteLayoutModelForTab,
|
||||
@@ -160,7 +160,14 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
|
||||
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
||||
const blockId = focusedNode?.data?.blockId;
|
||||
if (blockId != null && shouldDispatchToBlock(waveEvent)) {
|
||||
const viewModel = getViewModel(blockId);
|
||||
const bcm = getBlockComponentModel(blockId);
|
||||
if (bcm.openSwitchConnection != null) {
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:g")) {
|
||||
bcm.openSwitchConnection();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const viewModel = bcm?.viewModel;
|
||||
if (viewModel?.keyDownHandler) {
|
||||
const handledByBlock = viewModel.keyDownHandler(waveEvent);
|
||||
if (handledByBlock) {
|
||||
|
||||
@@ -7,9 +7,6 @@ import * as WOS from "./wos";
|
||||
|
||||
// blockservice.BlockService (block)
|
||||
class BlockServiceType {
|
||||
EnsureConnection(arg2: string): Promise<void> {
|
||||
return WOS.callBackendService("block", "EnsureConnection", Array.from(arguments))
|
||||
}
|
||||
GetControllerStatus(arg2: string): Promise<BlockControllerRuntimeStatus> {
|
||||
return WOS.callBackendService("block", "GetControllerStatus", Array.from(arguments))
|
||||
}
|
||||
|
||||
@@ -47,9 +47,14 @@ class WshServerType {
|
||||
return WOS.wshServerRpcHelper_call("controllerinput", data, opts);
|
||||
}
|
||||
|
||||
// command "controllerrestart" [call]
|
||||
ControllerRestartCommand(data: CommandBlockRestartData, opts?: RpcOpts): Promise<void> {
|
||||
return WOS.wshServerRpcHelper_call("controllerrestart", data, opts);
|
||||
// command "controllerresync" [call]
|
||||
ControllerResyncCommand(data: CommandControllerResyncData, opts?: RpcOpts): Promise<void> {
|
||||
return WOS.wshServerRpcHelper_call("controllerresync", data, opts);
|
||||
}
|
||||
|
||||
// command "controllerstop" [call]
|
||||
ControllerStopCommand(data: string, opts?: RpcOpts): Promise<void> {
|
||||
return WOS.wshServerRpcHelper_call("controllerstop", data, opts);
|
||||
}
|
||||
|
||||
// command "createblock" [call]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { useHeight } from "@/app/hook/useHeight";
|
||||
import { useWidth } from "@/app/hook/useWidth";
|
||||
import { globalStore, waveEventSubscribe, WOS } from "@/store/global";
|
||||
import { getConnStatusAtom, globalStore, waveEventSubscribe, WOS } from "@/store/global";
|
||||
import { WshServer } from "@/store/wshserver";
|
||||
import * as util from "@/util/util";
|
||||
import * as Plot from "@observablehq/plot";
|
||||
@@ -61,6 +61,7 @@ class CpuPlotViewModel {
|
||||
metrics: jotai.Atom<string[]>;
|
||||
connection: jotai.Atom<string>;
|
||||
manageConnection: jotai.Atom<boolean>;
|
||||
connStatus: jotai.Atom<ConnStatus>;
|
||||
|
||||
constructor(blockId: string) {
|
||||
this.viewType = "cpuplot";
|
||||
@@ -122,6 +123,12 @@ class CpuPlotViewModel {
|
||||
});
|
||||
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() {
|
||||
@@ -165,18 +172,24 @@ function makeCpuPlotViewModel(blockId: string): CpuPlotViewModel {
|
||||
|
||||
const plotColors = ["#58C142", "#FFC107", "#FF5722", "#2196F3", "#9C27B0", "#00BCD4", "#FFEB3B", "#795548"];
|
||||
|
||||
function CpuPlotView({ model }: { model: CpuPlotViewModel; blockId: string }) {
|
||||
const containerRef = React.useRef<HTMLInputElement>();
|
||||
const plotData = jotai.useAtomValue(model.dataAtom);
|
||||
const addPlotData = jotai.useSetAtom(model.addDataAtom);
|
||||
const parentHeight = useHeight(containerRef);
|
||||
const parentWidth = useWidth(containerRef);
|
||||
const yvals = jotai.useAtomValue(model.metrics);
|
||||
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();
|
||||
}
|
||||
const unsubFn = waveEventSubscribe("sysinfo", connName, (event: WaveEvent) => {
|
||||
@@ -191,6 +204,22 @@ function CpuPlotView({ model }: { model: CpuPlotViewModel; blockId: string }) {
|
||||
unsubFn();
|
||||
};
|
||||
}, [connName]);
|
||||
React.useEffect(() => {}, [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 parentHeight = useHeight(containerRef);
|
||||
const parentWidth = useWidth(containerRef);
|
||||
const yvals = jotai.useAtomValue(model.metrics);
|
||||
|
||||
React.useEffect(() => {
|
||||
const marks: Plot.Markish[] = [];
|
||||
@@ -254,6 +283,6 @@ function CpuPlotView({ model }: { model: CpuPlotViewModel; blockId: string }) {
|
||||
}, [plotData, parentHeight, parentWidth]);
|
||||
|
||||
return <div className="plot-view" ref={containerRef} />;
|
||||
}
|
||||
});
|
||||
|
||||
export { CpuPlotView, CpuPlotViewModel, makeCpuPlotViewModel };
|
||||
|
||||
@@ -7,7 +7,7 @@ import { tryReinjectKey } from "@/app/store/keymodel";
|
||||
import { WshServer } from "@/app/store/wshserver";
|
||||
import { Markdown } from "@/element/markdown";
|
||||
import { NodeModel } from "@/layout/index";
|
||||
import { createBlock, globalStore, refocusNode } from "@/store/global";
|
||||
import { createBlock, getConnStatusAtom, globalStore, refocusNode } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||
@@ -98,6 +98,7 @@ export class PreviewModel implements ViewModel {
|
||||
specializedView: jotai.Atom<Promise<{ specializedView?: string; errorStr?: string }>>;
|
||||
loadableSpecializedView: jotai.Atom<Loadable<{ specializedView?: string; errorStr?: string }>>;
|
||||
manageConnection: jotai.Atom<boolean>;
|
||||
connStatus: jotai.Atom<ConnStatus>;
|
||||
|
||||
metaFilePath: jotai.Atom<string>;
|
||||
statFilePath: jotai.Atom<Promise<string>>;
|
||||
@@ -146,10 +147,15 @@ export class PreviewModel implements ViewModel {
|
||||
this.monacoRef = createRef();
|
||||
this.viewIcon = jotai.atom((get) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
const mimeTypeLoadable = get(this.fileMimeTypeLoadable);
|
||||
if (blockData?.meta?.icon) {
|
||||
return blockData.meta.icon;
|
||||
}
|
||||
const connStatus = get(this.connStatus);
|
||||
if (connStatus?.status != "connected") {
|
||||
return null;
|
||||
}
|
||||
const fileName = get(this.metaFilePath);
|
||||
const mimeTypeLoadable = get(this.fileMimeTypeLoadable);
|
||||
const mimeType = util.jotaiLoadableValue(mimeTypeLoadable, "");
|
||||
if (mimeType == "directory") {
|
||||
return {
|
||||
@@ -189,9 +195,19 @@ export class PreviewModel implements ViewModel {
|
||||
});
|
||||
this.viewName = jotai.atom("Preview");
|
||||
this.viewText = jotai.atom((get) => {
|
||||
let headerPath = get(this.metaFilePath);
|
||||
const connStatus = get(this.connStatus);
|
||||
if (connStatus?.status != "connected") {
|
||||
return [
|
||||
{
|
||||
elemtype: "text",
|
||||
text: headerPath,
|
||||
className: "preview-filename",
|
||||
},
|
||||
];
|
||||
}
|
||||
const loadableSV = get(this.loadableSpecializedView);
|
||||
const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit";
|
||||
let headerPath = get(this.metaFilePath);
|
||||
const loadableFileInfo = get(this.loadableFileInfo);
|
||||
if (loadableFileInfo.state == "hasData") {
|
||||
headerPath = loadableFileInfo.data?.path;
|
||||
@@ -248,6 +264,10 @@ export class PreviewModel implements ViewModel {
|
||||
] as HeaderElem[];
|
||||
});
|
||||
this.preIconButton = jotai.atom((get) => {
|
||||
const connStatus = get(this.connStatus);
|
||||
if (connStatus?.status != "connected") {
|
||||
return null;
|
||||
}
|
||||
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
|
||||
if (mimeType == "directory") {
|
||||
return null;
|
||||
@@ -259,6 +279,10 @@ export class PreviewModel implements ViewModel {
|
||||
};
|
||||
});
|
||||
this.endIconButtons = jotai.atom((get) => {
|
||||
const connStatus = get(this.connStatus);
|
||||
if (connStatus?.status != "connected") {
|
||||
return null;
|
||||
}
|
||||
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
|
||||
const loadableSV = get(this.loadableSpecializedView);
|
||||
const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit";
|
||||
@@ -356,6 +380,12 @@ export class PreviewModel implements ViewModel {
|
||||
this.loadableSpecializedView = loadable(this.specializedView);
|
||||
this.canPreview = jotai.atom(false);
|
||||
this.loadableFileInfo = loadable(this.statFile);
|
||||
this.connStatus = jotai.atom((get) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
const connName = blockData?.meta?.connection;
|
||||
const connAtom = getConnStatusAtom(connName);
|
||||
return get(connAtom);
|
||||
});
|
||||
}
|
||||
|
||||
markdownShowTocToggle() {
|
||||
@@ -831,6 +861,10 @@ function PreviewView({
|
||||
contentRef: React.RefObject<HTMLDivElement>;
|
||||
model: PreviewModel;
|
||||
}) {
|
||||
const connStatus = jotai.useAtomValue(model.connStatus);
|
||||
if (connStatus?.status != "connected") {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<OpenFileModal blockId={blockId} model={model} blockRef={blockRef} />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { WshServer } from "@/app/store/wshserver";
|
||||
import { VDomView } from "@/app/view/term/vdom";
|
||||
import { WOS, atoms, getEventORefSubject, globalStore, useSettingsPrefixAtom } from "@/store/global";
|
||||
import { WOS, atoms, getConnStatusAtom, getEventORefSubject, globalStore, useSettingsPrefixAtom } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
import * as util from "@/util/util";
|
||||
@@ -108,6 +108,7 @@ class TermViewModel {
|
||||
viewName: jotai.Atom<string>;
|
||||
blockBg: jotai.Atom<MetaType>;
|
||||
manageConnection: jotai.Atom<boolean>;
|
||||
connStatus: jotai.Atom<ConnStatus>;
|
||||
|
||||
constructor(blockId: string) {
|
||||
this.viewType = "term";
|
||||
@@ -142,10 +143,12 @@ class TermViewModel {
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
resetConnection() {
|
||||
WshServer.ControllerRestartCommand({ blockid: this.blockId });
|
||||
this.connStatus = jotai.atom((get) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
const connName = blockData?.meta?.connection;
|
||||
const connAtom = getConnStatusAtom(connName);
|
||||
return get(connAtom);
|
||||
});
|
||||
}
|
||||
|
||||
giveFocus(): boolean {
|
||||
@@ -172,21 +175,39 @@ class TermViewModel {
|
||||
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
||||
const termThemes = fullConfig?.termthemes ?? {};
|
||||
const termThemeKeys = Object.keys(termThemes);
|
||||
|
||||
termThemeKeys.sort((a, b) => {
|
||||
return termThemes[a]["display:order"] - termThemes[b]["display:order"];
|
||||
});
|
||||
const fullMenu: ContextMenuItem[] = [];
|
||||
const submenu: ContextMenuItem[] = termThemeKeys.map((themeName) => {
|
||||
return {
|
||||
label: termThemes[themeName]["display:name"] ?? themeName,
|
||||
click: () => this.setTerminalTheme(themeName),
|
||||
};
|
||||
});
|
||||
return [
|
||||
{
|
||||
label: "Themes",
|
||||
submenu: submenu,
|
||||
fullMenu.push({
|
||||
label: "Themes",
|
||||
submenu: submenu,
|
||||
});
|
||||
fullMenu.push({ type: "separator" });
|
||||
fullMenu.push({
|
||||
label: "Force Restart Controller",
|
||||
click: () => {
|
||||
const termsize = {
|
||||
rows: this.termRef.current?.terminal?.rows,
|
||||
cols: this.termRef.current?.terminal?.cols,
|
||||
};
|
||||
const prtn = WshServer.ControllerResyncCommand({
|
||||
tabid: globalStore.get(atoms.activeTabId),
|
||||
blockid: this.blockId,
|
||||
forcerestart: true,
|
||||
rtopts: { termsize: termsize },
|
||||
});
|
||||
prtn.catch((e) => console.log("error controller resync (force restart)", e));
|
||||
},
|
||||
];
|
||||
});
|
||||
return fullMenu;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +220,28 @@ interface TerminalViewProps {
|
||||
model: TermViewModel;
|
||||
}
|
||||
|
||||
const TermResyncHandler = React.memo(({ blockId, model }: TerminalViewProps) => {
|
||||
const connStatus = jotai.useAtomValue(model.connStatus);
|
||||
const [lastConnStatus, setLastConnStatus] = React.useState<ConnStatus>(connStatus);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!model.termRef.current?.hasResized) {
|
||||
return;
|
||||
}
|
||||
const isConnected = connStatus?.status == "connected";
|
||||
const wasConnected = lastConnStatus?.status == "connected";
|
||||
const curConnName = connStatus?.connection;
|
||||
const lastConnName = lastConnStatus?.connection;
|
||||
if (isConnected == wasConnected && curConnName == lastConnName) {
|
||||
return;
|
||||
}
|
||||
model.termRef.current?.resyncController("resync handler");
|
||||
setLastConnStatus(connStatus);
|
||||
}, [connStatus]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
const viewRef = React.createRef<HTMLDivElement>();
|
||||
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||
@@ -257,7 +300,9 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
}
|
||||
if (shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
||||
// restart
|
||||
WshServer.ControllerRestartCommand({ blockid: blockId });
|
||||
const tabId = globalStore.get(atoms.activeTabId);
|
||||
const prtn = WshServer.ControllerResyncCommand({ tabid: tabId, blockid: blockId });
|
||||
prtn.catch((e) => console.log("error controller resync (enter)", blockId, e));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -352,6 +397,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||
|
||||
return (
|
||||
<div className={clsx("view-term", "term-mode-" + termMode)} ref={viewRef}>
|
||||
<TermResyncHandler blockId={blockId} model={model} />
|
||||
<TermThemeUpdater blockId={blockId} termRef={termRef} />
|
||||
<TermStickers config={stickerConfig} />
|
||||
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { WshServer } from "@/app/store/wshserver";
|
||||
import { PLATFORM, WOS, fetchWaveFile, getFileSubject, openLink, sendWSCommand } from "@/store/global";
|
||||
import {
|
||||
PLATFORM,
|
||||
WOS,
|
||||
atoms,
|
||||
fetchWaveFile,
|
||||
getFileSubject,
|
||||
globalStore,
|
||||
openLink,
|
||||
sendWSCommand,
|
||||
} from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as util from "@/util/util";
|
||||
import { base64ToArray, fireAndForget } from "@/util/util";
|
||||
@@ -11,9 +20,12 @@ import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||
import { WebglAddon } from "@xterm/addon-webgl";
|
||||
import * as TermTypes from "@xterm/xterm";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import debug from "debug";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import { FitAddon } from "./fitaddon";
|
||||
|
||||
const dlog = debug("wave:termwrap");
|
||||
|
||||
const TermFileName = "term";
|
||||
const TermCacheFileName = "cache:term:full";
|
||||
|
||||
@@ -49,6 +61,7 @@ export class TermWrap {
|
||||
heldData: Uint8Array[];
|
||||
handleResize_debounced: () => void;
|
||||
isRunning: boolean;
|
||||
hasResized: boolean;
|
||||
|
||||
constructor(
|
||||
blockId: string,
|
||||
@@ -60,13 +73,13 @@ export class TermWrap {
|
||||
this.blockId = blockId;
|
||||
this.ptyOffset = 0;
|
||||
this.dataBytesProcessed = 0;
|
||||
this.hasResized = false;
|
||||
this.terminal = new Terminal(options);
|
||||
this.fitAddon = new FitAddon();
|
||||
this.fitAddon.noScrollbar = PLATFORM == "darwin";
|
||||
this.serializeAddon = new SerializeAddon();
|
||||
this.terminal.loadAddon(this.fitAddon);
|
||||
this.terminal.loadAddon(this.serializeAddon);
|
||||
|
||||
this.terminal.loadAddon(
|
||||
new WebLinksAddon((e, uri) => {
|
||||
e.preventDefault();
|
||||
@@ -208,18 +221,35 @@ export class TermWrap {
|
||||
}
|
||||
}
|
||||
|
||||
async resyncController(reason: string) {
|
||||
dlog("resync controller", this.blockId, reason);
|
||||
const tabId = globalStore.get(atoms.activeTabId);
|
||||
const rtOpts: RuntimeOpts = { termsize: { rows: this.terminal.rows, cols: this.terminal.cols } };
|
||||
try {
|
||||
await WshServer.ControllerResyncCommand({ tabid: tabId, blockid: this.blockId, rtopts: rtOpts });
|
||||
} catch (e) {
|
||||
console.log(`error controller resync (${reason})`, this.blockId, e);
|
||||
}
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
const oldRows = this.terminal.rows;
|
||||
const oldCols = this.terminal.cols;
|
||||
this.fitAddon.fit();
|
||||
if (oldRows !== this.terminal.rows || oldCols !== this.terminal.cols) {
|
||||
const termSize: TermSize = { rows: this.terminal.rows, cols: this.terminal.cols };
|
||||
const wsCommand: SetBlockTermSizeWSCommand = {
|
||||
wscommand: "setblocktermsize",
|
||||
blockid: this.blockId,
|
||||
termsize: { rows: this.terminal.rows, cols: this.terminal.cols },
|
||||
termsize: termSize,
|
||||
};
|
||||
sendWSCommand(wsCommand);
|
||||
}
|
||||
dlog("resize", `${this.terminal.rows}x${this.terminal.cols}`, `${oldRows}x${oldCols}`, this.hasResized);
|
||||
if (!this.hasResized) {
|
||||
this.hasResized = true;
|
||||
this.resyncController("initial resize");
|
||||
}
|
||||
}
|
||||
|
||||
processAndCacheData() {
|
||||
|
||||
Vendored
+5
@@ -240,6 +240,11 @@ declare global {
|
||||
version: string;
|
||||
buildTime: number;
|
||||
}
|
||||
|
||||
type BlockComponentModel = {
|
||||
openSwitchConnection?: () => void;
|
||||
viewModel: ViewModel;
|
||||
};
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
Vendored
+9
-6
@@ -15,8 +15,8 @@ declare global {
|
||||
// blockcontroller.BlockControllerRuntimeStatus
|
||||
type BlockControllerRuntimeStatus = {
|
||||
blockid: string;
|
||||
status: string;
|
||||
shellprocstatus?: string;
|
||||
shellprocconnname?: string;
|
||||
};
|
||||
|
||||
// waveobj.BlockDef
|
||||
@@ -58,17 +58,20 @@ declare global {
|
||||
termsize?: TermSize;
|
||||
};
|
||||
|
||||
// wshrpc.CommandBlockRestartData
|
||||
type CommandBlockRestartData = {
|
||||
blockid: string;
|
||||
};
|
||||
|
||||
// wshrpc.CommandBlockSetViewData
|
||||
type CommandBlockSetViewData = {
|
||||
blockid: string;
|
||||
view: string;
|
||||
};
|
||||
|
||||
// wshrpc.CommandControllerResyncData
|
||||
type CommandControllerResyncData = {
|
||||
forcerestart?: boolean;
|
||||
tabid: string;
|
||||
blockid: string;
|
||||
rtopts?: RuntimeOpts;
|
||||
};
|
||||
|
||||
// wshrpc.CommandCreateBlockData
|
||||
type CommandCreateBlockData = {
|
||||
tabid: string;
|
||||
|
||||
+4
-1
@@ -72,7 +72,10 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const fullConfig = await services.FileService.GetFullConfig();
|
||||
console.log("fullconfig", fullConfig);
|
||||
globalStore.set(atoms.fullConfigAtom, fullConfig);
|
||||
services.ObjectService.SetActiveTab(waveWindow.activetabid); // no need to wait
|
||||
const prtn = services.ObjectService.SetActiveTab(waveWindow.activetabid); // no need to wait
|
||||
prtn.catch((e) => {
|
||||
console.log("error on initial SetActiveTab", e);
|
||||
});
|
||||
const reactElem = React.createElement(App, null, null);
|
||||
const elem = document.getElementById("main");
|
||||
const root = createRoot(elem);
|
||||
|
||||
Reference in New Issue
Block a user