mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
merge branch 'main' into sylvie/plotting
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.wave-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
display: flex;
|
||||
padding: 6px 16px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 6px;
|
||||
height: auto;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
|
||||
color: var(--main-text-color);
|
||||
background: var(--accent-color);
|
||||
i {
|
||||
fill: var(--main-text-color);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: var(--main-text-color);
|
||||
background: var(--accent-color);
|
||||
i {
|
||||
fill: var(--main-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.primary.danger {
|
||||
background: var(--error-color);
|
||||
}
|
||||
|
||||
&.primary.outlined {
|
||||
background: none;
|
||||
border: 1px solid var(--accent-color);
|
||||
|
||||
i {
|
||||
fill: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.primary.greyoutlined {
|
||||
background: none;
|
||||
border: 1px solid var(--secondary-text-color);
|
||||
|
||||
i {
|
||||
fill: var(--secondary-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.primary.outlined,
|
||||
&.primary.greyoutlined {
|
||||
&.hover-danger:hover {
|
||||
color: var(--main-text-color);
|
||||
border: 1px solid var(--error-color);
|
||||
background: var(--error-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.primary.outlined.danger {
|
||||
background: none;
|
||||
border: 1px solid var(--error-color);
|
||||
|
||||
i {
|
||||
fill: var(--error-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.greytext {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
&.primary.ghost {
|
||||
background: none;
|
||||
i {
|
||||
fill: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.primary.ghost.danger {
|
||||
background: none;
|
||||
i {
|
||||
fill: var(--app-error-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: var(--main-text-color);
|
||||
background: var(--highlight-bg-color);
|
||||
i {
|
||||
fill: var(--main-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary.outlined {
|
||||
background: none;
|
||||
border: 1px solid var(--main-text-color);
|
||||
}
|
||||
|
||||
&.secondary.outlined.danger {
|
||||
background: none;
|
||||
border: 1px solid var(--error-color);
|
||||
}
|
||||
|
||||
&.secondary.ghost {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&.secondary.danger {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
&.small {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 3.6px;
|
||||
}
|
||||
|
||||
&.term-inline {
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.link-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { clsx } from "clsx";
|
||||
|
||||
import "./button.less";
|
||||
|
||||
interface ButtonProps {
|
||||
children: React.ReactNode;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
disabled?: boolean;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
autoFocus?: boolean;
|
||||
className?: string;
|
||||
termInline?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
class Button extends React.Component<ButtonProps> {
|
||||
static defaultProps = {
|
||||
style: {},
|
||||
className: "primary",
|
||||
};
|
||||
|
||||
handleClick(e) {
|
||||
if (this.props.onClick && !this.props.disabled) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { leftIcon, rightIcon, children, disabled, style, autoFocus, termInline, className, title } = this.props;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={clsx("wave-button", { disabled }, { "term-inline": termInline }, className)}
|
||||
onClick={this.handleClick.bind(this)}
|
||||
disabled={disabled}
|
||||
style={style}
|
||||
autoFocus={autoFocus}
|
||||
title={title}
|
||||
>
|
||||
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
||||
{children}
|
||||
{rightIcon && <span className="icon-right">{rightIcon}</span>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Button };
|
||||
export type { ButtonProps };
|
||||
@@ -1,5 +1,8 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.markdown {
|
||||
color: var(--main-color);
|
||||
color: var(--main-text-color);
|
||||
font-family: var(--markdown-font);
|
||||
font-size: 14px;
|
||||
overflow-wrap: break-word;
|
||||
@@ -9,13 +12,13 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--main-color);
|
||||
color: var(--main-text-color);
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--main-color);
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -24,7 +27,7 @@
|
||||
|
||||
table {
|
||||
tr th {
|
||||
color: var(--main-color);
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +64,7 @@
|
||||
|
||||
code {
|
||||
font: var(--fixed-font);
|
||||
color: var(--main-color);
|
||||
color: var(--main-text-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--panel-bg-color);
|
||||
padding: 0.15em 0.5em;
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
import * as jotai from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as rxjs from "rxjs";
|
||||
import type { WailsEvent } from "@wailsio/runtime/types/events";
|
||||
import { Events } from "@wailsio/runtime";
|
||||
|
||||
const globalStore = jotai.createStore();
|
||||
|
||||
@@ -42,4 +45,40 @@ const atoms = {
|
||||
blockAtomFamily,
|
||||
};
|
||||
|
||||
export { globalStore, atoms };
|
||||
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
||||
|
||||
const blockSubjects = new Map<string, SubjectWithRef<any>>();
|
||||
|
||||
function getBlockSubject(blockId: string): SubjectWithRef<any> {
|
||||
let subject = blockSubjects.get(blockId);
|
||||
if (subject == null) {
|
||||
subject = new rxjs.Subject<any>() as any;
|
||||
subject.refCount = 0;
|
||||
subject.release = () => {
|
||||
subject.refCount--;
|
||||
if (subject.refCount === 0) {
|
||||
subject.complete();
|
||||
blockSubjects.delete(blockId);
|
||||
}
|
||||
};
|
||||
blockSubjects.set(blockId, subject);
|
||||
}
|
||||
subject.refCount++;
|
||||
return subject;
|
||||
}
|
||||
|
||||
Events.On("block:ptydata", (event: any) => {
|
||||
const data = event?.data;
|
||||
if (data?.blockid == null) {
|
||||
console.log("block:ptydata with null blockid");
|
||||
return;
|
||||
}
|
||||
// we don't use getBlockSubject here because we don't want to create a new subject
|
||||
const subject = blockSubjects.get(data.blockid);
|
||||
if (subject == null) {
|
||||
return;
|
||||
}
|
||||
subject.next(data);
|
||||
});
|
||||
|
||||
export { globalStore, atoms, getBlockSubject };
|
||||
|
||||
@@ -6,6 +6,10 @@ import * as jotai from "jotai";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import type { ITheme } from "@xterm/xterm";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { Button } from "@/element/button";
|
||||
import * as BlockService from "@/bindings/pkg/service/blockservice/BlockService";
|
||||
import { getBlockSubject } from "@/store/global";
|
||||
import { base64ToArray } from "@/util/util";
|
||||
|
||||
import "./view.less";
|
||||
import "/public/xterm.css";
|
||||
@@ -40,6 +44,8 @@ function getThemeFromCSSVars(el: Element): ITheme {
|
||||
|
||||
const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||
const [term, setTerm] = React.useState<Terminal | null>(null);
|
||||
const [blockStarted, setBlockStarted] = React.useState<boolean>(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!connectElemRef.current) {
|
||||
@@ -53,17 +59,85 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
fontWeight: "normal",
|
||||
fontWeightBold: "bold",
|
||||
});
|
||||
setTerm(term);
|
||||
const fitAddon = new FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(connectElemRef.current);
|
||||
fitAddon.fit();
|
||||
term.write("Hello, world!");
|
||||
term.write("Hello, world!\r\n");
|
||||
console.log(term);
|
||||
term.onData((data) => {
|
||||
const b64data = btoa(data);
|
||||
const inputCmd = { command: "input", blockid: blockId, inputdata64: b64data };
|
||||
BlockService.SendCommand(blockId, inputCmd);
|
||||
});
|
||||
|
||||
// resize observer
|
||||
const rszObs = new ResizeObserver(() => {
|
||||
const oldRows = term.rows;
|
||||
const oldCols = term.cols;
|
||||
fitAddon.fit();
|
||||
if (oldRows !== term.rows || oldCols !== term.cols) {
|
||||
BlockService.SendCommand(blockId, { command: "input", termsize: { rows: term.rows, cols: term.cols } });
|
||||
}
|
||||
});
|
||||
rszObs.observe(connectElemRef.current);
|
||||
|
||||
// block subject
|
||||
const blockSubject = getBlockSubject(blockId);
|
||||
blockSubject.subscribe((data) => {
|
||||
// base64 decode
|
||||
const decodedData = base64ToArray(data.ptydata);
|
||||
term.write(decodedData);
|
||||
});
|
||||
|
||||
return () => {
|
||||
term.dispose();
|
||||
rszObs.disconnect();
|
||||
blockSubject.release();
|
||||
};
|
||||
}, [connectElemRef.current]);
|
||||
|
||||
return <div key="conntectElem" className="view-term term-connectelem" ref={connectElemRef}></div>;
|
||||
async function handleRunClick() {
|
||||
try {
|
||||
if (!blockStarted) {
|
||||
await BlockService.StartBlock(blockId);
|
||||
setBlockStarted(true);
|
||||
}
|
||||
let termSize = { rows: term.rows, cols: term.cols };
|
||||
await BlockService.SendCommand(blockId, { command: "run", cmdstr: "ls -l", termsize: termSize });
|
||||
} catch (e) {
|
||||
console.log("run click error: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStartTerminalClick() {
|
||||
try {
|
||||
if (!blockStarted) {
|
||||
await BlockService.StartBlock(blockId);
|
||||
setBlockStarted(true);
|
||||
}
|
||||
let termSize = { rows: term.rows, cols: term.cols };
|
||||
await BlockService.SendCommand(blockId, { command: "runshell", termsize: termSize });
|
||||
} catch (e) {
|
||||
console.log("start terminal click error: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="view-term">
|
||||
<div className="term-header">
|
||||
<div>Terminal</div>
|
||||
<Button className="term-inline" onClick={() => handleRunClick()}>
|
||||
Run `ls`
|
||||
</Button>
|
||||
<Button className="term-inline" onClick={() => handleStartTerminalClick()}>
|
||||
Start Terminal
|
||||
</Button>
|
||||
</div>
|
||||
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { TerminalView };
|
||||
|
||||
@@ -7,6 +7,23 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.term-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 4px 10px;
|
||||
height: 35px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.term-connectelem {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.view-preview {
|
||||
|
||||
Reference in New Issue
Block a user