mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
dynamic header updates (#102)
This commit is contained in:
parent
0cb9b8940b
commit
848a9af9a0
@ -69,8 +69,8 @@
|
||||
|
||||
.block-frame-default-header {
|
||||
display: flex;
|
||||
height: 26px;
|
||||
padding-left: 6px;
|
||||
height: 34px;
|
||||
padding: 4px 0 4px 10px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
@ -84,6 +84,10 @@
|
||||
gap: 8px;
|
||||
color: var(--main-text-color);
|
||||
|
||||
.block-frame-back-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.block-frame-view-icon {
|
||||
font-size: var(--header-icon-size);
|
||||
opacity: 0.5;
|
||||
@ -103,6 +107,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.block-frame-text {
|
||||
font: var(--fixed-font);
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.block-frame-end-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -139,109 +149,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.block-frame-tech {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 7px;
|
||||
margin: 8px 0 0 0;
|
||||
padding: 10px 2px 2px 2px;
|
||||
height: calc(100% - 8px);
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
|
||||
.block-header-icon {
|
||||
font-size: 14px;
|
||||
padding: 0 0;
|
||||
}
|
||||
|
||||
&.block-preview {
|
||||
background-color: var(--main-bg-color);
|
||||
|
||||
.block-frame-tech-close {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.block-focused {
|
||||
border: 2px solid var(--accent-color);
|
||||
|
||||
.block-frame-tech-header {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.block-frame-tech-header {
|
||||
position: absolute;
|
||||
max-width: 85%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
top: -11px;
|
||||
padding: 4px 6px 4px 6px;
|
||||
background-color: var(--main-bg-color);
|
||||
font: var(--fixed-font);
|
||||
color: var(--secondary-text-color);
|
||||
white-space: nowrap;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.block-frame-tech-close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -2px;
|
||||
padding: 0 0 1px 1px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.block-frame-preview {
|
||||
background-color: var(--main-bg-color);
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.block-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--panel-bg-color);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
border-radius: 0 0 4px 4px;
|
||||
cursor: grab;
|
||||
|
||||
.block-header-icon {
|
||||
font-size: 20px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.block-header-text {
|
||||
padding: 0 5px;
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
font-size: 12px;
|
||||
padding: 5px 5px 5px 5px;
|
||||
margin-right: 5px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--highlight-bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,6 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { CodeEdit } from "@/app/view/codeedit";
|
||||
import { PlotView } from "@/app/view/plotview";
|
||||
import { PreviewView } from "@/app/view/preview";
|
||||
import { TerminalView } from "@/app/view/term/term";
|
||||
import { WaveAi } from "@/app/view/waveai";
|
||||
import { WebView } from "@/app/view/webview";
|
||||
import { ErrorBoundary } from "@/element/errorboundary";
|
||||
import { CenteredDiv } from "@/element/quickelems";
|
||||
import { ContextMenuModel } from "@/store/contextmenu";
|
||||
@ -14,19 +8,43 @@ import { atoms, globalStore, setBlockFocus, useBlockAtom } from "@/store/global"
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
import * as util from "@/util/util";
|
||||
import { CodeEdit } from "@/view/codeedit";
|
||||
import { PlotView } from "@/view/plotview";
|
||||
import { PreviewView, makePreviewModel } from "@/view/preview";
|
||||
import { TerminalView } from "@/view/term/term";
|
||||
import { WaveAi } from "@/view/waveai";
|
||||
import { WebView } from "@/view/webview";
|
||||
import clsx from "clsx";
|
||||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
|
||||
import "./block.less";
|
||||
|
||||
const HoverPixels = 15;
|
||||
const HoverTimeoutMs = 100;
|
||||
interface LayoutComponentModel {
|
||||
onClose?: () => void;
|
||||
dragHandleRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
interface BlockProps {
|
||||
blockId: string;
|
||||
onClose?: () => void;
|
||||
dragHandleRef?: React.RefObject<HTMLDivElement>;
|
||||
preview: boolean;
|
||||
layoutModel: LayoutComponentModel;
|
||||
}
|
||||
|
||||
interface BlockComponentModel {
|
||||
onClick?: () => void;
|
||||
onFocusCapture?: React.FocusEventHandler<HTMLDivElement>;
|
||||
blockRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
interface BlockFrameProps {
|
||||
blockId: string;
|
||||
blockModel?: BlockComponentModel;
|
||||
layoutModel?: LayoutComponentModel;
|
||||
viewModel?: ViewModel;
|
||||
preview: boolean;
|
||||
numBlocksInTab?: number;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const colorRegex = /^((#[0-9a-f]{6,8})|([a-z]+))$/;
|
||||
@ -176,118 +194,13 @@ function handleHeaderContextMenu(e: React.MouseEvent<HTMLDivElement>, blockData:
|
||||
ContextMenuModel.showContextMenu(menu, e);
|
||||
}
|
||||
|
||||
interface FramelessBlockHeaderProps {
|
||||
blockId: string;
|
||||
onClose?: () => void;
|
||||
dragHandleRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const FramelessBlockHeader = React.memo(({ blockId, onClose, dragHandleRef }: FramelessBlockHeaderProps) => {
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
|
||||
|
||||
return (
|
||||
<div
|
||||
key="header"
|
||||
className="block-header"
|
||||
ref={dragHandleRef}
|
||||
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, onClose)}
|
||||
>
|
||||
<div className="block-header-text text-fixed">{getBlockHeaderText(null, blockData, settingsConfig)}</div>
|
||||
{onClose && (
|
||||
<div className="close-button" onClick={onClose}>
|
||||
<i className="fa fa-solid fa-xmark-large" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const hoverStateOff = "off";
|
||||
const hoverStatePending = "pending";
|
||||
const hoverStateOn = "on";
|
||||
|
||||
interface BlockFrameProps {
|
||||
blockId: string;
|
||||
onClose?: () => void;
|
||||
onClick?: () => void;
|
||||
onFocusCapture?: React.FocusEventHandler<HTMLDivElement>;
|
||||
preview: boolean;
|
||||
numBlocksInTab?: number;
|
||||
children?: React.ReactNode;
|
||||
blockRef?: React.RefObject<HTMLDivElement>;
|
||||
dragHandleRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const BlockFrame_Tech_Component = ({
|
||||
blockId,
|
||||
onClose,
|
||||
onClick,
|
||||
onFocusCapture,
|
||||
preview,
|
||||
blockRef,
|
||||
dragHandleRef,
|
||||
children,
|
||||
}: BlockFrameProps) => {
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
|
||||
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
|
||||
return jotai.atom((get) => {
|
||||
const winData = get(atoms.waveWindow);
|
||||
return winData?.activeblockid === blockId;
|
||||
});
|
||||
});
|
||||
let isFocused = jotai.useAtomValue(isFocusedAtom);
|
||||
const blockIcon = useBlockIcon(blockId);
|
||||
if (preview) {
|
||||
isFocused = true;
|
||||
}
|
||||
let style: React.CSSProperties = {};
|
||||
if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
|
||||
style.borderColor = blockData.meta["frame:bordercolor"];
|
||||
}
|
||||
if (isFocused && blockData?.meta?.["frame:bordercolor:focused"]) {
|
||||
style.borderColor = blockData.meta["frame:bordercolor:focused"];
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"block",
|
||||
"block-frame-tech",
|
||||
isFocused ? "block-focused" : null,
|
||||
preview ? "block-preview" : null
|
||||
)}
|
||||
onClick={onClick}
|
||||
onFocusCapture={onFocusCapture}
|
||||
ref={blockRef}
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
className="block-frame-tech-header"
|
||||
ref={dragHandleRef}
|
||||
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, onClose)}
|
||||
>
|
||||
{getBlockHeaderText(blockIcon, blockData, settingsConfig)}
|
||||
</div>
|
||||
<div className={clsx("block-frame-tech-close")} onClick={onClose}>
|
||||
<i className="fa fa-solid fa-xmark fa-fw" />
|
||||
</div>
|
||||
{preview ? <div className="block-frame-preview" /> : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockFrame_Tech = React.memo(BlockFrame_Tech_Component) as typeof BlockFrame_Tech_Component;
|
||||
|
||||
const BlockFrame_Default_Component = ({
|
||||
blockId,
|
||||
onClose,
|
||||
onClick,
|
||||
layoutModel,
|
||||
viewModel,
|
||||
blockModel,
|
||||
preview,
|
||||
blockRef,
|
||||
dragHandleRef,
|
||||
numBlocksInTab,
|
||||
onFocusCapture,
|
||||
children,
|
||||
}: BlockFrameProps) => {
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
@ -299,7 +212,10 @@ const BlockFrame_Default_Component = ({
|
||||
});
|
||||
});
|
||||
let isFocused = jotai.useAtomValue(isFocusedAtom);
|
||||
const blockIcon = useBlockIcon(blockId);
|
||||
const viewIcon = jotai.useAtomValue(viewModel.viewIcon);
|
||||
const viewText = jotai.useAtomValue(viewModel.viewText);
|
||||
const hasBackButton = jotai.useAtomValue(viewModel.hasBackButton);
|
||||
const hasForwardButton = jotai.useAtomValue(viewModel.hasForwardButton);
|
||||
if (preview) {
|
||||
isFocused = true;
|
||||
}
|
||||
@ -339,7 +255,7 @@ const BlockFrame_Default_Component = ({
|
||||
},
|
||||
});
|
||||
menuItems.push({ type: "separator" });
|
||||
menuItems.push({ label: "Close", click: onClose });
|
||||
menuItems.push({ label: "Close", click: layoutModel?.onClose });
|
||||
ContextMenuModel.showContextMenu(menuItems, e);
|
||||
};
|
||||
if (preview) {
|
||||
@ -355,29 +271,35 @@ const BlockFrame_Default_Component = ({
|
||||
preview ? "block-preview" : null,
|
||||
numBlocksInTab == 1 ? "block-no-highlight" : null
|
||||
)}
|
||||
onClick={onClick}
|
||||
onFocusCapture={onFocusCapture}
|
||||
ref={blockRef}
|
||||
onClick={blockModel?.onClick}
|
||||
onFocusCapture={blockModel?.onFocusCapture}
|
||||
ref={blockModel?.blockRef}
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
className="block-frame-default-header"
|
||||
ref={dragHandleRef}
|
||||
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, onClose)}
|
||||
ref={layoutModel?.dragHandleRef}
|
||||
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, layoutModel?.onClose)}
|
||||
>
|
||||
<div className="block-frame-default-header-iconview">
|
||||
<div className="block-frame-view-icon">{getBlockHeaderIcon(blockIcon, blockData)}</div>
|
||||
{hasBackButton && !hasForwardButton && (
|
||||
<div className="block-frame-back-button" onClick={viewModel.onBack}>
|
||||
<i className="fa fa-solid fa-chevron-left fa-fw" />
|
||||
</div>
|
||||
)}
|
||||
<div className="block-frame-view-icon">{getBlockHeaderIcon(viewIcon, blockData)}</div>
|
||||
<div className="block-frame-view-type">{blockViewToName(blockData?.view)}</div>
|
||||
{settingsConfig?.blockheader?.showblockids && (
|
||||
<div className="block-frame-blockid">[{blockId.substring(0, 8)}]</div>
|
||||
)}
|
||||
</div>
|
||||
{util.isBlank(viewText) ? null : <div className="block-frame-text">{viewText}</div>}
|
||||
<div className="flex-spacer"></div>
|
||||
<div className="block-frame-end-icons">
|
||||
<div className="block-frame-settings" onClick={handleSettings}>
|
||||
<i className="fa fa-solid fa-cog fa-fw" />
|
||||
</div>
|
||||
<div className={clsx("block-frame-default-close")} onClick={onClose}>
|
||||
<div className={clsx("block-frame-default-close")} onClick={layoutModel?.onClose}>
|
||||
<i className="fa fa-solid fa-xmark-large fa-fw" />
|
||||
</div>
|
||||
</div>
|
||||
@ -390,80 +312,6 @@ const BlockFrame_Default_Component = ({
|
||||
|
||||
const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component;
|
||||
|
||||
const BlockFrame_Frameless = ({
|
||||
blockId,
|
||||
onClose,
|
||||
onClick,
|
||||
preview,
|
||||
blockRef,
|
||||
dragHandleRef,
|
||||
children,
|
||||
}: BlockFrameProps) => {
|
||||
const localBlockRef = React.useRef<HTMLDivElement>(null);
|
||||
const [showHeader, setShowHeader] = React.useState(preview ? true : false);
|
||||
const hoverState = React.useRef(hoverStateOff);
|
||||
|
||||
// this forward the lcal ref to the blockRef
|
||||
React.useImperativeHandle(blockRef, () => localBlockRef.current);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (preview) {
|
||||
return;
|
||||
}
|
||||
const block = localBlockRef.current;
|
||||
let hoverTimeout: NodeJS.Timeout = null;
|
||||
const handleMouseMove = (event) => {
|
||||
const rect = block.getBoundingClientRect();
|
||||
if (event.clientY - rect.top <= HoverPixels) {
|
||||
if (hoverState.current == hoverStateOff) {
|
||||
hoverTimeout = setTimeout(() => {
|
||||
if (hoverState.current == hoverStatePending) {
|
||||
hoverState.current = hoverStateOn;
|
||||
setShowHeader(true);
|
||||
}
|
||||
}, HoverTimeoutMs);
|
||||
hoverState.current = hoverStatePending;
|
||||
}
|
||||
} else {
|
||||
if (hoverTimeout) {
|
||||
if (hoverState.current == hoverStatePending) {
|
||||
hoverState.current = hoverStateOff;
|
||||
}
|
||||
clearTimeout(hoverTimeout);
|
||||
hoverTimeout = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
block.addEventListener("mousemove", handleMouseMove);
|
||||
return () => {
|
||||
block.removeEventListener("mousemove", handleMouseMove);
|
||||
};
|
||||
});
|
||||
let mouseLeaveHandler = () => {
|
||||
if (preview) {
|
||||
return;
|
||||
}
|
||||
setShowHeader(false);
|
||||
hoverState.current = hoverStateOff;
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="block block-frame-frameless"
|
||||
ref={localBlockRef}
|
||||
onMouseLeave={mouseLeaveHandler}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
className={clsx("block-header-animation-wrap", showHeader ? "is-showing" : null)}
|
||||
onMouseLeave={mouseLeaveHandler}
|
||||
>
|
||||
<FramelessBlockHeader blockId={blockId} onClose={onClose} dragHandleRef={dragHandleRef} />
|
||||
</div>
|
||||
{preview ? <div className="block-frame-preview" /> : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockFrame = React.memo((props: BlockFrameProps) => {
|
||||
const blockId = props.blockId;
|
||||
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
@ -474,18 +322,6 @@ const BlockFrame = React.memo((props: BlockFrameProps) => {
|
||||
}
|
||||
let FrameElem = BlockFrame_Default;
|
||||
const numBlocks = tabData?.blockids?.length ?? 0;
|
||||
// Preview should always render as the full tech frame
|
||||
if (!props.preview) {
|
||||
// if 0 or 1 blocks, use frameless, otherwise use tech
|
||||
// if (numBlocks <= 1) {
|
||||
// FrameElem = BlockFrame_Frameless;
|
||||
// }
|
||||
if (blockData?.meta?.["frame"] === "tech") {
|
||||
FrameElem = BlockFrame_Tech;
|
||||
} else if (blockData?.meta?.["frame"] === "frameless") {
|
||||
FrameElem = BlockFrame_Frameless;
|
||||
}
|
||||
}
|
||||
return <FrameElem {...props} numBlocksInTab={numBlocks} />;
|
||||
});
|
||||
|
||||
@ -547,17 +383,78 @@ function useBlockIcon(blockId: string): string {
|
||||
return blockIcon;
|
||||
}
|
||||
|
||||
const wm = new WeakMap();
|
||||
let wmCounter = 0;
|
||||
function getObjectId(obj: any): number {
|
||||
if (!wm.has(obj)) {
|
||||
wm.set(obj, wmCounter++);
|
||||
function getViewElemAndModel(
|
||||
blockId: string,
|
||||
blockView: string,
|
||||
blockRef: React.RefObject<HTMLDivElement>
|
||||
): { viewModel: ViewModel; viewElem: JSX.Element } {
|
||||
if (blockView == null) {
|
||||
return { viewElem: null, viewModel: null };
|
||||
}
|
||||
return wm.get(obj);
|
||||
let viewElem: JSX.Element = null;
|
||||
let viewModel: ViewModel = null;
|
||||
if (blockView === "term") {
|
||||
viewElem = <TerminalView key={blockId} blockId={blockId} />;
|
||||
} else if (blockView === "preview") {
|
||||
const previewModel = makePreviewModel(blockId);
|
||||
viewElem = <PreviewView key={blockId} blockId={blockId} model={previewModel} />;
|
||||
viewModel = previewModel;
|
||||
} else if (blockView === "plot") {
|
||||
viewElem = <PlotView key={blockId} />;
|
||||
} else if (blockView === "codeedit") {
|
||||
viewElem = <CodeEdit key={blockId} text={null} filename={null} />;
|
||||
} else if (blockView === "web") {
|
||||
viewElem = <WebView key={blockId} blockId={blockId} parentRef={blockRef} />;
|
||||
} else if (blockView === "waveai") {
|
||||
viewElem = <WaveAi key={blockId} parentRef={blockRef} />;
|
||||
}
|
||||
if (viewModel == null) {
|
||||
viewModel = makeDefaultViewModel(blockId);
|
||||
}
|
||||
return { viewElem, viewModel };
|
||||
}
|
||||
|
||||
const Block = React.memo(({ blockId, onClose, dragHandleRef }: BlockProps) => {
|
||||
let blockElem: JSX.Element = null;
|
||||
function makeDefaultViewModel(blockId: string): ViewModel {
|
||||
const blockDataAtom = WOS.getWaveObjectAtom<Block>(WOS.makeORef("block", blockId));
|
||||
let viewModel: ViewModel = {
|
||||
viewIcon: jotai.atom((get) => {
|
||||
const blockData = get(blockDataAtom);
|
||||
return blockViewToIcon(blockData?.view);
|
||||
}),
|
||||
viewName: jotai.atom((get) => {
|
||||
const blockData = get(blockDataAtom);
|
||||
return blockViewToName(blockData?.view);
|
||||
}),
|
||||
viewText: jotai.atom((get) => {
|
||||
const blockData = get(blockDataAtom);
|
||||
return blockData?.meta?.title;
|
||||
}),
|
||||
hasBackButton: jotai.atom(false),
|
||||
hasForwardButton: jotai.atom(false),
|
||||
hasSearch: jotai.atom(false),
|
||||
};
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
const BlockPreview = React.memo(({ blockId, layoutModel }: BlockProps) => {
|
||||
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
if (!blockData) {
|
||||
return null;
|
||||
}
|
||||
let { viewModel } = getViewElemAndModel(blockId, blockData?.view, null);
|
||||
return (
|
||||
<BlockFrame
|
||||
key={blockId}
|
||||
blockId={blockId}
|
||||
layoutModel={layoutModel}
|
||||
preview={true}
|
||||
blockModel={null}
|
||||
viewModel={viewModel}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const BlockFull = React.memo(({ blockId, layoutModel }: BlockProps) => {
|
||||
const focusElemRef = React.useRef<HTMLInputElement>(null);
|
||||
const blockRef = React.useRef<HTMLDivElement>(null);
|
||||
const [blockClicked, setBlockClicked] = React.useState(false);
|
||||
@ -626,33 +523,30 @@ const Block = React.memo(({ blockId, onClose, dragHandleRef }: BlockProps) => {
|
||||
}
|
||||
}, [focusElemRef.current, getFocusableChildren]);
|
||||
|
||||
let { viewElem, viewModel } = React.useMemo(
|
||||
() => getViewElemAndModel(blockId, blockData?.view, blockRef),
|
||||
[blockId, blockData?.view, blockRef]
|
||||
);
|
||||
|
||||
if (!blockId || !blockData) return null;
|
||||
|
||||
if (blockDataLoading) {
|
||||
blockElem = <CenteredDiv>Loading...</CenteredDiv>;
|
||||
} else if (blockData.view === "term") {
|
||||
blockElem = <TerminalView key={blockId} blockId={blockId} />;
|
||||
} else if (blockData.view === "preview") {
|
||||
blockElem = <PreviewView key={blockId} blockId={blockId} />;
|
||||
} else if (blockData.view === "plot") {
|
||||
blockElem = <PlotView key={blockId} />;
|
||||
} else if (blockData.view === "codeedit") {
|
||||
blockElem = <CodeEdit key={blockId} text={null} filename={null} />;
|
||||
} else if (blockData.view === "web") {
|
||||
blockElem = <WebView key={blockId} parentRef={blockRef} initialUrl={blockData.meta.url} />;
|
||||
} else if (blockData.view === "waveai") {
|
||||
blockElem = <WaveAi key={blockId} parentRef={blockRef} />;
|
||||
viewElem = <CenteredDiv>Loading...</CenteredDiv>;
|
||||
}
|
||||
const blockModel: BlockComponentModel = {
|
||||
onClick: setBlockClickedTrue,
|
||||
onFocusCapture: determineFocusedChild,
|
||||
blockRef: blockRef,
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockFrame
|
||||
key={blockId}
|
||||
blockId={blockId}
|
||||
onClose={onClose}
|
||||
layoutModel={layoutModel}
|
||||
preview={false}
|
||||
onClick={setBlockClickedTrue}
|
||||
blockRef={blockRef}
|
||||
dragHandleRef={dragHandleRef}
|
||||
onFocusCapture={(e) => determineFocusedChild(e)}
|
||||
blockModel={blockModel}
|
||||
viewModel={viewModel}
|
||||
>
|
||||
<div key="focuselem" className="block-focuselem">
|
||||
<input
|
||||
@ -666,11 +560,18 @@ const Block = React.memo(({ blockId, onClose, dragHandleRef }: BlockProps) => {
|
||||
</div>
|
||||
<div key="content" className="block-content">
|
||||
<ErrorBoundary>
|
||||
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{blockElem}</React.Suspense>
|
||||
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</React.Suspense>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</BlockFrame>
|
||||
);
|
||||
});
|
||||
|
||||
export { Block, BlockFrame };
|
||||
const Block = React.memo((props: BlockProps) => {
|
||||
if (props.preview) {
|
||||
return <BlockPreview {...props} />;
|
||||
}
|
||||
return <BlockFull {...props} />;
|
||||
});
|
||||
|
||||
export { Block };
|
||||
|
@ -353,6 +353,15 @@ function setBlockFocus(blockId: string) {
|
||||
WOS.setObjectValue(winData, globalStore.set, true);
|
||||
}
|
||||
|
||||
const objectIdWeakMap = new WeakMap();
|
||||
let objectIdCounter = 0;
|
||||
function getObjectId(obj: any): number {
|
||||
if (!objectIdWeakMap.has(obj)) {
|
||||
objectIdWeakMap.set(obj, objectIdCounter++);
|
||||
}
|
||||
return objectIdWeakMap.get(obj);
|
||||
}
|
||||
|
||||
export {
|
||||
WOS,
|
||||
atoms,
|
||||
@ -363,6 +372,7 @@ export {
|
||||
getEventORefSubject,
|
||||
getEventSubject,
|
||||
getFileSubject,
|
||||
getObjectId,
|
||||
globalStore,
|
||||
globalWS,
|
||||
initWS,
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Block, BlockFrame } from "@/app/block/block";
|
||||
import { Block } from "@/app/block/block";
|
||||
import { getApi } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
@ -32,18 +32,15 @@ const TabContent = React.memo(({ tabId }: { tabId: string }) => {
|
||||
if (!tabData.blockId || !ready) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Block
|
||||
key={tabData.blockId}
|
||||
blockId={tabData.blockId}
|
||||
onClose={onClose}
|
||||
dragHandleRef={dragHandleRef}
|
||||
/>
|
||||
);
|
||||
const layoutModel = {
|
||||
onClose: onClose,
|
||||
dragHandleRef: dragHandleRef,
|
||||
};
|
||||
return <Block key={tabData.blockId} blockId={tabData.blockId} layoutModel={layoutModel} preview={false} />;
|
||||
}
|
||||
|
||||
function renderPreview(tabData: TabLayoutData) {
|
||||
return <BlockFrame key={tabData.blockId} blockId={tabData.blockId} preview={true} />;
|
||||
return <Block key={tabData.blockId} blockId={tabData.blockId} layoutModel={null} preview={true} />;
|
||||
}
|
||||
|
||||
function onNodeDelete(data: TabLayoutData) {
|
||||
|
@ -466,6 +466,7 @@ interface DirectoryPreviewProps {
|
||||
}
|
||||
|
||||
function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
|
||||
console.log("DirectoryPreview render");
|
||||
const [searchText, setSearchText] = React.useState("");
|
||||
const [focusIndex, setFocusIndex] = React.useState(0);
|
||||
const [content, setContent] = React.useState<FileInfo[]>([]);
|
||||
|
@ -2,12 +2,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Markdown } from "@/element/markdown";
|
||||
import { getBackendHostPort, globalStore, useBlockAtom, useBlockCache } from "@/store/global";
|
||||
import { getBackendHostPort, getObjectId, globalStore, useBlockAtom } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
import * as util from "@/util/util";
|
||||
import clsx from "clsx";
|
||||
import * as jotai from "jotai";
|
||||
import { loadable } from "jotai/utils";
|
||||
import { useRef } from "react";
|
||||
import { CenteredDiv } from "../element/quickelems";
|
||||
import { CodeEdit } from "./codeedit";
|
||||
@ -17,6 +18,113 @@ import { DirectoryPreview } from "./directorypreview";
|
||||
import "./view.less";
|
||||
|
||||
const MaxFileSize = 1024 * 1024 * 10; // 10MB
|
||||
const MaxCSVSize = 1024 * 1024 * 1; // 1MB
|
||||
|
||||
export class PreviewModel implements ViewModel {
|
||||
blockId: string;
|
||||
blockAtom: jotai.Atom<Block>;
|
||||
viewIcon: jotai.Atom<string>;
|
||||
viewName: jotai.Atom<string>;
|
||||
viewText: jotai.Atom<string>;
|
||||
hasBackButton: jotai.Atom<boolean>;
|
||||
hasForwardButton: jotai.Atom<boolean>;
|
||||
hasSearch: jotai.Atom<boolean>;
|
||||
|
||||
fileName: jotai.WritableAtom<string, [string], void>;
|
||||
statFile: jotai.Atom<Promise<FileInfo>>;
|
||||
fullFile: jotai.Atom<Promise<FullFile>>;
|
||||
fileMimeType: jotai.Atom<Promise<string>>;
|
||||
fileMimeTypeLoadable: jotai.Atom<Loadable<string>>;
|
||||
fileContent: jotai.Atom<Promise<string>>;
|
||||
|
||||
setPreviewFileName(fileName: string) {
|
||||
services.ObjectService.UpdateObjectMeta(`block:${this.blockId}`, { file: fileName });
|
||||
}
|
||||
|
||||
constructor(blockId: string) {
|
||||
this.blockId = blockId;
|
||||
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
||||
this.viewIcon = jotai.atom((get) => {
|
||||
let blockData = get(this.blockAtom);
|
||||
if (blockData?.meta?.icon) {
|
||||
return blockData.meta.icon;
|
||||
}
|
||||
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
|
||||
const fileName = get(this.fileName);
|
||||
return iconForFile(mimeType, fileName);
|
||||
});
|
||||
this.viewName = jotai.atom("Preview");
|
||||
this.viewText = jotai.atom((get) => {
|
||||
return get(this.fileName);
|
||||
});
|
||||
this.hasBackButton = jotai.atom(true);
|
||||
this.hasForwardButton = jotai.atom((get) => {
|
||||
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
|
||||
if (mimeType == "directory") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
this.hasSearch = jotai.atom(false);
|
||||
|
||||
this.fileName = jotai.atom<string, [string], void>(
|
||||
(get) => {
|
||||
return get(this.blockAtom)?.meta?.file;
|
||||
},
|
||||
(get, set, update) => {
|
||||
services.ObjectService.UpdateObjectMeta(`block:${blockId}`, { file: update });
|
||||
}
|
||||
);
|
||||
this.statFile = jotai.atom<Promise<FileInfo>>(async (get) => {
|
||||
const fileName = get(this.fileName);
|
||||
if (fileName == null) {
|
||||
return null;
|
||||
}
|
||||
// const statFile = await FileService.StatFile(fileName);
|
||||
console.log("PreviewModel calling StatFile", fileName);
|
||||
const statFile = await services.FileService.StatFile(fileName);
|
||||
return statFile;
|
||||
});
|
||||
this.fullFile = jotai.atom<Promise<FullFile>>(async (get) => {
|
||||
const fileName = get(this.fileName);
|
||||
if (fileName == null) {
|
||||
return null;
|
||||
}
|
||||
// const file = await FileService.ReadFile(fileName);
|
||||
const file = await services.FileService.ReadFile(fileName);
|
||||
return file;
|
||||
});
|
||||
this.fileMimeType = jotai.atom<Promise<string>>(async (get) => {
|
||||
const fileInfo = await get(this.statFile);
|
||||
return fileInfo?.mimetype;
|
||||
});
|
||||
this.fileMimeTypeLoadable = loadable(this.fileMimeType);
|
||||
this.fileContent = jotai.atom<Promise<string>>(async (get) => {
|
||||
const fullFile = await get(this.fullFile);
|
||||
return util.base64ToString(fullFile?.data64);
|
||||
});
|
||||
|
||||
this.onBack = this.onBack.bind(this);
|
||||
}
|
||||
|
||||
onBack() {
|
||||
const fileName = globalStore.get(this.fileName);
|
||||
if (fileName == null) {
|
||||
return;
|
||||
}
|
||||
const splitPath = fileName.split("/");
|
||||
console.log("splitPath-1", splitPath);
|
||||
splitPath.pop();
|
||||
console.log("splitPath-2", splitPath);
|
||||
const newPath = splitPath.join("/");
|
||||
globalStore.set(this.fileName, newPath);
|
||||
}
|
||||
}
|
||||
|
||||
function makePreviewModel(blockId: string): PreviewModel {
|
||||
const previewModel = new PreviewModel(blockId);
|
||||
return previewModel;
|
||||
}
|
||||
|
||||
function DirNav({ cwdAtom }: { cwdAtom: jotai.WritableAtom<string, [string], void> }) {
|
||||
const [cwd, setCwd] = jotai.useAtom(cwdAtom);
|
||||
@ -139,6 +247,9 @@ function CSVViewPreview({
|
||||
}
|
||||
|
||||
function iconForFile(mimeType: string, fileName: string): string {
|
||||
if (mimeType == null) {
|
||||
mimeType = "unknown";
|
||||
}
|
||||
if (mimeType == "application/pdf") {
|
||||
return "file-pdf";
|
||||
} else if (mimeType.startsWith("image/")) {
|
||||
@ -153,6 +264,7 @@ function iconForFile(mimeType: string, fileName: string): string {
|
||||
return "file-csv";
|
||||
} else if (
|
||||
mimeType.startsWith("text/") ||
|
||||
mimeType == "application/sql" ||
|
||||
(mimeType.startsWith("application/") &&
|
||||
(mimeType.includes("json") || mimeType.includes("yaml") || mimeType.includes("toml")))
|
||||
) {
|
||||
@ -161,60 +273,21 @@ function iconForFile(mimeType: string, fileName: string): string {
|
||||
if (fileName == "~" || fileName == "~/") {
|
||||
return "home";
|
||||
}
|
||||
return "folder";
|
||||
return "folder-open";
|
||||
} else {
|
||||
return "file";
|
||||
}
|
||||
}
|
||||
|
||||
function PreviewView({ blockId }: { blockId: string }) {
|
||||
function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel }) {
|
||||
console.log("render previewview", getObjectId(model));
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
||||
const fileNameAtom: jotai.WritableAtom<string, [string], void> = useBlockCache(blockId, "preview:filename", () =>
|
||||
jotai.atom<string, [string], void>(
|
||||
(get) => {
|
||||
return get(blockAtom)?.meta?.file;
|
||||
},
|
||||
(get, set, update) => {
|
||||
const blockId = get(blockAtom)?.oid;
|
||||
services.ObjectService.UpdateObjectMeta(`block:${blockId}`, { file: update });
|
||||
}
|
||||
)
|
||||
);
|
||||
const statFileAtom = useBlockAtom(blockId, "preview:statfile", () =>
|
||||
jotai.atom<Promise<FileInfo>>(async (get) => {
|
||||
const fileName = get(fileNameAtom);
|
||||
if (fileName == null) {
|
||||
return null;
|
||||
}
|
||||
// const statFile = await FileService.StatFile(fileName);
|
||||
const statFile = await services.FileService.StatFile(fileName);
|
||||
return statFile;
|
||||
})
|
||||
);
|
||||
const fullFileAtom = useBlockAtom(blockId, "preview:fullfile", () =>
|
||||
jotai.atom<Promise<FullFile>>(async (get) => {
|
||||
const fileName = get(fileNameAtom);
|
||||
if (fileName == null) {
|
||||
return null;
|
||||
}
|
||||
// const file = await FileService.ReadFile(fileName);
|
||||
const file = await services.FileService.ReadFile(fileName);
|
||||
return file;
|
||||
})
|
||||
);
|
||||
const fileMimeTypeAtom = useBlockAtom(blockId, "preview:mimetype", () =>
|
||||
jotai.atom<Promise<string>>(async (get) => {
|
||||
const fileInfo = await get(statFileAtom);
|
||||
return fileInfo?.mimetype;
|
||||
})
|
||||
);
|
||||
const fileContentAtom = useBlockAtom(blockId, "preview:filecontent", () =>
|
||||
jotai.atom<Promise<string>>(async (get) => {
|
||||
const fullFile = await get(fullFileAtom);
|
||||
return util.base64ToString(fullFile?.data64);
|
||||
})
|
||||
);
|
||||
const fileNameAtom = model.fileName;
|
||||
const statFileAtom = model.statFile;
|
||||
const fullFileAtom = model.fullFile;
|
||||
const fileMimeTypeAtom = model.fileMimeType;
|
||||
const fileContentAtom = model.fileContent;
|
||||
let mimeType = jotai.useAtomValue(fileMimeTypeAtom);
|
||||
if (mimeType == null) {
|
||||
mimeType = "";
|
||||
@ -241,11 +314,16 @@ function PreviewView({ blockId }: { blockId: string }) {
|
||||
} else if (mimeType === "text/markdown") {
|
||||
specializedView = <MarkdownPreview contentAtom={fileContentAtom} />;
|
||||
} else if (mimeType === "text/csv") {
|
||||
if (fileInfo.size > MaxCSVSize) {
|
||||
specializedView = <CenteredDiv>CSV File Too Large to Preview (1MB Max)</CenteredDiv>;
|
||||
} else {
|
||||
specializedView = (
|
||||
<CSVViewPreview parentRef={ref} contentAtom={fileContentAtom} filename={fileName} readonly={true} />
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
mimeType.startsWith("text/") ||
|
||||
mimeType == "application/sql" ||
|
||||
(mimeType.startsWith("application/") &&
|
||||
(mimeType.includes("json") || mimeType.includes("yaml") || mimeType.includes("toml")))
|
||||
) {
|
||||
@ -274,4 +352,4 @@ function PreviewView({ blockId }: { blockId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviewView };
|
||||
export { PreviewView, makePreviewModel };
|
||||
|
@ -4,17 +4,20 @@
|
||||
import { Button } from "@/app/element/button";
|
||||
import { useParentHeight } from "@/app/hook/useParentHeight";
|
||||
import { getApi } from "@/app/store/global";
|
||||
import { WOS } from "@/store/global";
|
||||
import { WebviewTag } from "electron";
|
||||
import React, { memo, useEffect, useRef, useState } from "react";
|
||||
import React, { memo, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import "./webview.less";
|
||||
|
||||
interface WebViewProps {
|
||||
blockId: string;
|
||||
parentRef: React.MutableRefObject<HTMLDivElement>;
|
||||
initialUrl: string;
|
||||
}
|
||||
|
||||
const WebView = memo(({ parentRef, initialUrl }: WebViewProps) => {
|
||||
const WebView = memo(({ blockId, parentRef }: WebViewProps) => {
|
||||
const blockData = WOS.useWaveObjectValueWithSuspense<Block>(WOS.makeORef("block", blockId));
|
||||
const initialUrl = useMemo(() => blockData?.meta?.url, []);
|
||||
const [url, setUrl] = useState(initialUrl);
|
||||
const [inputUrl, setInputUrl] = useState(initialUrl); // Separate state for the input field
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
19
frontend/types/custom.d.ts
vendored
19
frontend/types/custom.d.ts
vendored
@ -1,6 +1,7 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type * as jotai from "jotai";
|
||||
import type * as rxjs from "rxjs";
|
||||
|
||||
declare global {
|
||||
@ -114,6 +115,24 @@ declare global {
|
||||
}
|
||||
|
||||
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
||||
|
||||
interface ViewModel {
|
||||
viewIcon: jotai.Atom<string>;
|
||||
viewName: jotai.Atom<string>;
|
||||
viewText: jotai.Atom<string>;
|
||||
hasBackButton: jotai.Atom<boolean>;
|
||||
hasForwardButton: jotai.Atom<boolean>;
|
||||
hasSearch: jotai.Atom<boolean>;
|
||||
|
||||
onBack?: () => void;
|
||||
onForward?: () => void;
|
||||
onSearchChange?: (text: string) => void;
|
||||
onSearch?: (text: string) => void;
|
||||
getSettingsMenuItems?: () => ContextMenuItem[];
|
||||
}
|
||||
|
||||
// jotai doesn't export this type :/
|
||||
type Loadable<T> = { state: "loading" } | { state: "hasData"; data: T } | { state: "hasError"; error: unknown };
|
||||
}
|
||||
|
||||
export {};
|
||||
|
@ -99,4 +99,68 @@ function fireAndForget(f: () => Promise<any>) {
|
||||
});
|
||||
}
|
||||
|
||||
export { base64ToArray, base64ToString, fireAndForget, isBlank, jsonDeepEqual, makeIconClass, stringToBase64 };
|
||||
const promiseWeakMap = new WeakMap<Promise<any>, ResolvedValue<any>>();
|
||||
|
||||
type ResolvedValue<T> = {
|
||||
pending: boolean;
|
||||
error: any;
|
||||
value: T;
|
||||
};
|
||||
|
||||
// returns the value, pending state, and error of a promise
|
||||
function getPromiseState<T>(promise: Promise<T>): [T, boolean, any] {
|
||||
if (promise == null) {
|
||||
return [null, false, null];
|
||||
}
|
||||
if (promiseWeakMap.has(promise)) {
|
||||
const value = promiseWeakMap.get(promise);
|
||||
return [value.value, value.pending, value.error];
|
||||
}
|
||||
const value: ResolvedValue<T> = {
|
||||
pending: true,
|
||||
error: null,
|
||||
value: null,
|
||||
};
|
||||
promise.then(
|
||||
(result) => {
|
||||
value.pending = false;
|
||||
value.error = null;
|
||||
value.value = result;
|
||||
},
|
||||
(error) => {
|
||||
value.pending = false;
|
||||
value.error = error;
|
||||
}
|
||||
);
|
||||
promiseWeakMap.set(promise, value);
|
||||
return [value.value, value.pending, value.error];
|
||||
}
|
||||
|
||||
// returns the value of a promise, or a default value if the promise is still pending (or had an error)
|
||||
function getPromiseValue<T>(promise: Promise<T>, def: T): T {
|
||||
const [value, pending, error] = getPromiseState(promise);
|
||||
if (pending || error) {
|
||||
return def;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function jotaiLoadableValue<T>(value: Loadable<T>, def: T): T {
|
||||
if (value.state === "hasData") {
|
||||
return value.data;
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
export {
|
||||
base64ToArray,
|
||||
base64ToString,
|
||||
fireAndForget,
|
||||
getPromiseState,
|
||||
getPromiseValue,
|
||||
isBlank,
|
||||
jotaiLoadableValue,
|
||||
jsonDeepEqual,
|
||||
makeIconClass,
|
||||
stringToBase64,
|
||||
};
|
||||
|
@ -56,7 +56,7 @@ func getSettingsConfigDefaults() SettingsConfigType {
|
||||
"text/css": {Icon: "css3-alt fa-brands"},
|
||||
"text/javascript": {Icon: "js fa-brands"},
|
||||
"text/typescript": {Icon: "js fa-brands"},
|
||||
"text/golang": {Icon: "go fa-brands"},
|
||||
"text/golang": {Icon: "golang fa-brands"},
|
||||
"text/html": {Icon: "html5 fa-brands"},
|
||||
"text/less": {Icon: "less fa-brands"},
|
||||
"text/markdown": {Icon: "markdown fa-brands"},
|
||||
|
@ -19,6 +19,7 @@
|
||||
"@/util/*": ["frontend/util/*"],
|
||||
"@/faraday/*": ["frontend/faraday/*"],
|
||||
"@/store/*": ["frontend/app/store/*"],
|
||||
"@/view/*": ["frontend/app/view/*"],
|
||||
"@/element/*": ["frontend/app/element/*"],
|
||||
"@/bindings/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/service/*"],
|
||||
"@/gopkg/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/*"],
|
||||
|
Loading…
Reference in New Issue
Block a user