mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
new tab flow (#60)
* init * init * error handling * use css * minor improvements * fix some issues and tabicon init * show error indicator when empty and is required * debounce input * fix decorator linting issue * icon system init * fix bugs * color custom icons and fix regression * remove debugging code * remove @tab-magenta. fix formatting. * swap magenta for mint * change tab color order
This commit is contained in:
@@ -58,6 +58,8 @@
|
||||
"@types/node": "^20.4.0",
|
||||
"@types/papaparse": "^5.3.10",
|
||||
"@types/react": "^18.0.12",
|
||||
"@types/sprintf-js": "^1.1.3",
|
||||
"@types/throttle-debounce": "^5.0.1",
|
||||
"@types/uuid": "9.0.6",
|
||||
"@types/webpack-env": "^1.18.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
|
||||
@@ -625,7 +625,81 @@
|
||||
}
|
||||
}
|
||||
|
||||
.textfield {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid @term-white;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
background-color: transparent;
|
||||
height: 44px;
|
||||
min-width: 412px;
|
||||
gap: 6px;
|
||||
|
||||
&.focused {
|
||||
border-color: @term-green;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: @term-red;
|
||||
}
|
||||
|
||||
.textfield-inner {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
|
||||
.textfield-label {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 16px;
|
||||
font-size: 12.5px;
|
||||
transition: all 0.3s;
|
||||
color: @term-white;
|
||||
line-height: 10px;
|
||||
|
||||
&.float {
|
||||
font-size: 10px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
&.start {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.textfield-input {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
border: none;
|
||||
padding: 5px 0 5px 16px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
color: @term-bright-white;
|
||||
line-height: 20px;
|
||||
|
||||
&.start {
|
||||
padding: 5px 16px 5px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-decoration {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.inline-edit {
|
||||
.icon {
|
||||
@@ -664,5 +738,4 @@
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import remarkGfm from "remark-gfm";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import type { RemoteType } from "../../types/types";
|
||||
import { debounce } from "throttle-debounce";
|
||||
|
||||
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
|
||||
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
|
||||
@@ -123,6 +124,152 @@ class Checkbox extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
interface InputDecorationProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class InputDecoration extends React.Component<InputDecorationProps, {}> {
|
||||
render() {
|
||||
const { children, onClick } = this.props;
|
||||
|
||||
return <div className="input-decoration">{children}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
interface TextFieldDecorationProps {
|
||||
startDecoration?: React.ReactNode;
|
||||
endDecoration?: React.ReactNode;
|
||||
}
|
||||
interface TextFieldProps {
|
||||
label: string;
|
||||
value?: string;
|
||||
className?: string;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
decoration?: TextFieldDecorationProps;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
interface TextFieldState {
|
||||
focused: boolean;
|
||||
internalValue: string;
|
||||
error: boolean;
|
||||
showHelpText: boolean;
|
||||
hasContent: boolean;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class TextField extends React.Component<TextFieldProps, TextFieldState> {
|
||||
inputRef: React.RefObject<HTMLInputElement>;
|
||||
state: TextFieldState;
|
||||
|
||||
constructor(props: TextFieldProps) {
|
||||
super(props);
|
||||
const hasInitialContent = Boolean(props.value || props.defaultValue);
|
||||
this.state = {
|
||||
focused: false,
|
||||
hasContent: hasInitialContent,
|
||||
internalValue: props.defaultValue || "",
|
||||
error: false,
|
||||
showHelpText: false,
|
||||
};
|
||||
this.inputRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: TextFieldProps) {
|
||||
// Only update the focus state if using as controlled
|
||||
if (this.props.value !== undefined && this.props.value !== prevProps.value) {
|
||||
this.setState({ focused: Boolean(this.props.value) });
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleFocus() {
|
||||
this.setState({ focused: true });
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleBlur() {
|
||||
const { required } = this.props;
|
||||
if (this.inputRef.current) {
|
||||
const value = this.inputRef.current.value;
|
||||
if (required && !value) {
|
||||
this.setState({ error: true, focused: false });
|
||||
} else {
|
||||
this.setState({ error: false, focused: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleHelpTextClick() {
|
||||
this.setState((prevState) => ({ showHelpText: !prevState.showHelpText }));
|
||||
}
|
||||
|
||||
debouncedOnChange = debounce(300, (value) => {
|
||||
const { onChange } = this.props;
|
||||
onChange?.(value);
|
||||
});
|
||||
|
||||
@boundMethod
|
||||
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { required } = this.props;
|
||||
const inputValue = e.target.value;
|
||||
|
||||
// Check if value is empty and the field is required
|
||||
if (required && !inputValue) {
|
||||
this.setState({ error: true, hasContent: false });
|
||||
} else {
|
||||
this.setState({ error: false, hasContent: Boolean(inputValue) });
|
||||
}
|
||||
|
||||
// Update the internal state for uncontrolled version
|
||||
if (this.props.value === undefined) {
|
||||
this.setState({ internalValue: inputValue });
|
||||
}
|
||||
|
||||
this.debouncedOnChange(inputValue);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, value, placeholder, decoration, className } = this.props;
|
||||
const { focused, internalValue, error } = this.state;
|
||||
|
||||
// Decide if the input should behave as controlled or uncontrolled
|
||||
const inputValue = value !== undefined ? value : internalValue;
|
||||
|
||||
return (
|
||||
<div className={cn(`textfield ${className || ""}`, { focused: focused, error: error })}>
|
||||
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||
<div className="textfield-inner">
|
||||
<label
|
||||
className={cn("textfield-label", {
|
||||
float: this.state.hasContent || this.state.focused || placeholder,
|
||||
start: decoration?.startDecoration,
|
||||
})}
|
||||
htmlFor={label}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
className={cn("textfield-input", { start: decoration?.startDecoration })}
|
||||
ref={this.inputRef}
|
||||
id={label}
|
||||
value={inputValue}
|
||||
onChange={this.handleInputChange}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
{decoration?.endDecoration && <div>{decoration.endDecoration}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class RemoteStatusLight extends React.Component<{ remote: RemoteType }, {}> {
|
||||
render() {
|
||||
@@ -362,4 +509,6 @@ export {
|
||||
InfoMessage,
|
||||
Markdown,
|
||||
SettingsError,
|
||||
TextField,
|
||||
InputDecoration,
|
||||
};
|
||||
|
||||
@@ -19,15 +19,15 @@
|
||||
@textarea-background: #2a2a2a;
|
||||
|
||||
@text-primary: #fff;
|
||||
@text-secondary: #C3C8C2;
|
||||
@text-secondary: #c3c8c2;
|
||||
@text-caption: #8b918a;
|
||||
|
||||
@accent-color: #3B3F3A;
|
||||
@accent-color: #3b3f3a;
|
||||
|
||||
@status-outline: #151715;
|
||||
@dropdown-menu: rgba(21, 23, 21, 1);
|
||||
|
||||
@status-connected: #46A758;
|
||||
@status-connected: #46a758;
|
||||
@status-connecting: #f5d90a;
|
||||
@status-error: #e54d2e;
|
||||
@status-disconnected: #c3c8c2;
|
||||
@@ -53,11 +53,11 @@
|
||||
@tab-orange: #ef713b;
|
||||
@tab-yellow: #e0b956;
|
||||
@tab-green: #58c142;
|
||||
@tab-mint: #4BFFA9;
|
||||
@tab-cyan: #4BDFFF;
|
||||
@tab-blue: #3971FF;
|
||||
@tab-violet: #BA76FF;
|
||||
@tab-pink: #E05677;
|
||||
@tab-mint: #4bffa9;
|
||||
@tab-cyan: #4bdfff;
|
||||
@tab-blue: #3971ff;
|
||||
@tab-violet: #ba76ff;
|
||||
@tab-pink: #e05677;
|
||||
@tab-white: #ffffff;
|
||||
|
||||
@tab-black-text: #333;
|
||||
|
||||
@@ -49,12 +49,12 @@
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
color: #ccc;
|
||||
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
color: #4e9a06;
|
||||
}
|
||||
|
||||
|
||||
&.should-fade {
|
||||
opacity: 1;
|
||||
animation: fade-in 2.5s;
|
||||
@@ -119,11 +119,12 @@
|
||||
margin: 16px;
|
||||
|
||||
.newtab-section {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
|
||||
&.conn-section {
|
||||
gap: 8px;
|
||||
@@ -145,13 +146,14 @@
|
||||
padding: 8px 0;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
|
||||
.icondiv {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@@ -169,6 +171,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
.status-div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 3px;
|
||||
|
||||
svg.status-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
svg.add-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
path {
|
||||
fill: @text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text-standard {
|
||||
color: @text-secondary;
|
||||
}
|
||||
|
||||
.text-caption {
|
||||
color: @text-caption;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(241, 246, 243, 0.08);
|
||||
}
|
||||
|
||||
.icon.color-white + .check-icon {
|
||||
path {
|
||||
fill: black;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { If, For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import dayjs from "dayjs";
|
||||
import { GlobalCommandRunner, TabColors } from "../../../model/model";
|
||||
import { GlobalCommandRunner, TabColors, TabIcons } from "../../../model/model";
|
||||
import type { LineType, RenderModeType, LineFactoryProps, CommandRtnType } from "../../../types/types";
|
||||
import * as T from "../../../types/types";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
@@ -20,7 +20,8 @@ import { GlobalModel, ScreenLines, Screen, Session } from "../../../model/model"
|
||||
import { Line } from "../../line/linecomps";
|
||||
import { LinesView } from "../../line/linesview";
|
||||
import { ConnectionDropdown } from "../../connections/connections";
|
||||
import * as util from "../../../util/util";
|
||||
import * as util from "../../../util/util";
|
||||
import { TextField, InputDecoration } from "../../common/common";
|
||||
import { ReactComponent as EllipseIcon } from "../../assets/icons/ellipse.svg";
|
||||
import { ReactComponent as Check12Icon } from "../../assets/icons/check12.svg";
|
||||
import { ReactComponent as GlobeIcon } from "../../assets/icons/globe.svg";
|
||||
@@ -37,7 +38,7 @@ dayjs.extend(localizedFormat);
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
@mobxReact.observer
|
||||
class ScreenView extends React.Component<{ session: Session, screen: Screen }, {}> {
|
||||
class ScreenView extends React.Component<{ session: Session; screen: Screen }, {}> {
|
||||
render() {
|
||||
let { session, screen } = this.props;
|
||||
if (screen == null) {
|
||||
@@ -54,8 +55,9 @@ class ScreenView extends React.Component<{ session: Session, screen: Screen }, {
|
||||
|
||||
@mobxReact.observer
|
||||
class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
|
||||
errorMessage: OV<string> = mobx.observable.box(null, { name: "NewTabSettings-errorMessage" });
|
||||
|
||||
connDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "NewTabSettings-connDropdownActive" });
|
||||
errorMessage: OV<string | null> = mobx.observable.box(null, { name: "NewTabSettings-errorMessage" });
|
||||
|
||||
@boundMethod
|
||||
selectTabColor(color: string): void {
|
||||
let { screen } = this.props;
|
||||
@@ -67,15 +69,29 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
inlineUpdateName(val: string): void {
|
||||
selectTabIcon(icon: string): void {
|
||||
let { screen } = this.props;
|
||||
if (util.isStrEq(val, screen.name.get())) {
|
||||
if (screen.getTabIcon() == icon) {
|
||||
return;
|
||||
}
|
||||
let prtn = GlobalCommandRunner.screenSetSettings(screen.screenId, { tabicon: icon }, false);
|
||||
util.commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
updateName(val: string): void {
|
||||
let { screen } = this.props;
|
||||
let prtn = GlobalCommandRunner.screenSetSettings(screen.screenId, { name: val }, false);
|
||||
util.commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
toggleConnDropdown(): void {
|
||||
mobx.action(() => {
|
||||
this.connDropdownActive.set(!this.connDropdownActive.get());
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
selectRemote(cname: string): void {
|
||||
let prtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
|
||||
@@ -84,62 +100,112 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
|
||||
|
||||
@boundMethod
|
||||
clickNewConnection(): void {
|
||||
GlobalModel.remotesModalModel.openModalForEdit({remoteedit: true}, true);
|
||||
GlobalModel.remotesModalModel.openModalForEdit({ remoteedit: true }, true);
|
||||
}
|
||||
|
||||
renderTabIconSelector(): React.ReactNode {
|
||||
let { screen } = this.props;
|
||||
let curIcon = screen.getTabIcon();
|
||||
if (util.isBlank(curIcon) || curIcon == "default") {
|
||||
curIcon = "square";
|
||||
}
|
||||
let icon: string | null = null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-s1 unselectable">Select the icon</div>
|
||||
<div className="control-iconlist">
|
||||
<For each="icon" of={TabIcons}>
|
||||
<div
|
||||
className="icondiv"
|
||||
key={icon}
|
||||
title={icon || ""}
|
||||
onClick={() => this.selectTabIcon(icon || "")}
|
||||
>
|
||||
<i className={`fa-sharp fa-solid fa-${icon}`}></i>
|
||||
</div>
|
||||
</For>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderTabColorSelector(): React.ReactNode {
|
||||
let { screen } = this.props;
|
||||
let curColor = screen.getTabColor();
|
||||
if (util.isBlank(curColor) || curColor == "default") {
|
||||
curColor = "green";
|
||||
}
|
||||
let color: string | null = null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-s1 unselectable">Select the color</div>
|
||||
<div className="control-iconlist">
|
||||
<For each="color" of={TabColors}>
|
||||
<div
|
||||
className="icondiv"
|
||||
key={color}
|
||||
title={color || ""}
|
||||
onClick={() => this.selectTabColor(color || "")}
|
||||
>
|
||||
<EllipseIcon className={cn("icon", "color-" + color)} />
|
||||
<If condition={color == curColor}>
|
||||
<Check12Icon className="check-icon" />
|
||||
</If>
|
||||
</div>
|
||||
</For>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let { screen } = this.props;
|
||||
let rptr = screen.curRemote.get();
|
||||
let curColor = screen.getTabColor();
|
||||
if (util.isBlank(curColor) || curColor == "default") {
|
||||
curColor = "green";
|
||||
}
|
||||
let color: string = null;
|
||||
let curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid);
|
||||
return (
|
||||
<div className="newtab-container">
|
||||
<div className="newtab-section name-section">
|
||||
<div className="text-standard">Name</div>
|
||||
<TextField
|
||||
label="Title"
|
||||
required={true}
|
||||
defaultValue={screen.name.get() ?? ""}
|
||||
onChange={this.updateName}
|
||||
decoration={{
|
||||
endDecoration: (
|
||||
<InputDecoration>
|
||||
<i className="fa-sharp fa-regular fa-circle-question"></i>
|
||||
</InputDecoration>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="newtab-spacer" />
|
||||
<div className="newtab-section conn-section">
|
||||
<div className="text-s1 unselectable">
|
||||
You're connected to [{getRemoteStr(rptr)}]. Do you want to change it?
|
||||
You're connected to [{getRemoteStr(rptr)}]. Do you want to change it?
|
||||
</div>
|
||||
<div>
|
||||
<ConnectionDropdown curRemote={curRemote} allowNewConn={true} onSelectRemote={this.selectRemote} onNewConn={this.clickNewConnection}/>
|
||||
<ConnectionDropdown
|
||||
curRemote={curRemote}
|
||||
allowNewConn={true}
|
||||
onSelectRemote={this.selectRemote}
|
||||
onNewConn={this.clickNewConnection}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-caption cr-help-text">
|
||||
To change connection from the command line use `cr [alias|user@host]`
|
||||
</div>
|
||||
</div>
|
||||
<div className="newtab-spacer"/>
|
||||
<div className="newtab-section settings-field">
|
||||
<div className="text-s1 unselectable">
|
||||
Name
|
||||
</div>
|
||||
<div className="settings-input">
|
||||
<InlineSettingsTextEdit
|
||||
placeholder="name"
|
||||
text={screen.name.get() ?? "(none)"}
|
||||
value={screen.name.get() ?? ""}
|
||||
onChange={this.inlineUpdateName}
|
||||
maxLength={50}
|
||||
showIcon={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="newtab-spacer"/>
|
||||
<div className="newtab-spacer" />
|
||||
<div className="newtab-section">
|
||||
<div className="text-s1 unselectable">
|
||||
Select the color
|
||||
</div>
|
||||
<div className="control-iconlist">
|
||||
<For each="color" of={TabColors}>
|
||||
<div className="icondiv" key={color} title={color} onClick={() => this.selectTabColor(color)}>
|
||||
<EllipseIcon className={cn("icon", "color-" + color)}/>
|
||||
<If condition={color == curColor}>
|
||||
<Check12Icon className="check-icon"/>
|
||||
</If>
|
||||
</div>
|
||||
</For>
|
||||
</div>
|
||||
<div>{this.renderTabIconSelector()}</div>
|
||||
</div>
|
||||
<div className="newtab-spacer" />
|
||||
<div className="newtab-section">
|
||||
<div>{this.renderTabColorSelector()}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -148,7 +214,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
|
||||
|
||||
// screen is not null
|
||||
@mobxReact.observer
|
||||
class ScreenWindowView extends React.Component<{ session: Session, screen: Screen }, {}> {
|
||||
class ScreenWindowView extends React.Component<{ session: Session; screen: Screen }, {}> {
|
||||
rszObs: any;
|
||||
windowViewRef: React.RefObject<any>;
|
||||
|
||||
@@ -309,13 +375,17 @@ class ScreenWindowView extends React.Component<{ session: Session, screen: Scree
|
||||
</div>
|
||||
<If condition={lines.length == 0}>
|
||||
<If condition={screen.nextLineNum.get() == 1}>
|
||||
<NewTabSettings screen={screen}/>
|
||||
<NewTabSettings screen={screen} />
|
||||
</If>
|
||||
<If condition={screen.nextLineNum.get() != 1}>
|
||||
<div className="window-view" ref={this.windowViewRef} data-screenid={screen.screenId}>
|
||||
<div key="lines" className="lines"></div>
|
||||
<div key="window-empty" className={cn("window-empty")}>
|
||||
<div><code className="text-standard">[workspace="{session.name.get()}" screen="{screen.name.get()}"]</code></div>
|
||||
<div>
|
||||
<code className="text-standard">
|
||||
[workspace="{session.name.get()}" screen="{screen.name.get()}"]
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
@@ -8,14 +8,28 @@
|
||||
border-radius: 12px 0px 0px 0px;
|
||||
}
|
||||
|
||||
&.color-green, &.color-default {
|
||||
&.color-green,
|
||||
&.color-default {
|
||||
svg.left-icon path {
|
||||
fill: @tab-green;
|
||||
}
|
||||
|
||||
.icon i {
|
||||
color: @tab-green;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-top: 1px solid @tab-green;
|
||||
background: linear-gradient(180deg, rgba(88, 193, 66, 0.20) 9.34%, rgba(88, 193, 66, 0.03) 44.16%, rgba(88, 193, 66, 0.00) 86.79%);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(88, 193, 66, 0.2) 9.34%,
|
||||
rgba(88, 193, 66, 0.03) 44.16%,
|
||||
rgba(88, 193, 66, 0) 86.79%
|
||||
);
|
||||
}
|
||||
|
||||
.icon i {
|
||||
color: @tab-green;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +38,18 @@
|
||||
fill: @tab-orange;
|
||||
}
|
||||
|
||||
.icon i {
|
||||
color: @tab-orange;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-top: 1px solid @tab-orange;
|
||||
background: linear-gradient(180deg, rgba(239, 113, 59, 0.20) 9.34%, rgba(239, 113, 59, 0.03) 44.16%, rgba(239, 113, 59, 0.00) 86.79%);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(239, 113, 59, 0.2) 9.34%,
|
||||
rgba(239, 113, 59, 0.03) 44.16%,
|
||||
rgba(239, 113, 59, 0) 86.79%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +58,18 @@
|
||||
fill: @tab-red;
|
||||
}
|
||||
|
||||
.icon i {
|
||||
color: @tab-red;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-top: 1px solid @tab-red;
|
||||
background: linear-gradient(180deg, rgba(229, 77, 46, 0.20) 9.34%, rgba(229, 77, 46, 0.03) 44.16%, rgba(229, 77, 46, 0.00) 86.79%);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(229, 77, 46, 0.2) 9.34%,
|
||||
rgba(229, 77, 46, 0.03) 44.16%,
|
||||
rgba(229, 77, 46, 0) 86.79%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +78,18 @@
|
||||
fill: @tab-yellow;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.icon i {
|
||||
color: @tab-yellow;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-top: 1px solid @tab-yellow;
|
||||
background: linear-gradient(180deg, rgba(224, 185, 86, 0.20) 9.34%, rgba(224, 185, 86, 0.03) 44.16%, rgba(224, 185, 86, 0.00) 86.79%);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(224, 185, 86, 0.2) 9.34%,
|
||||
rgba(224, 185, 86, 0.03) 44.16%,
|
||||
rgba(224, 185, 86, 0) 86.79%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,9 +98,18 @@
|
||||
fill: @tab-blue;
|
||||
}
|
||||
|
||||
.icon i {
|
||||
color: @tab-blue;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-top: 1px solid @tab-blue;
|
||||
background: linear-gradient(180deg, rgba(57, 113, 255, 0.20) 9.34%, rgba(57, 113, 255, 0.03) 44.16%, rgba(57, 113, 255, 0.00) 77.18%);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(57, 113, 255, 0.2) 9.34%,
|
||||
rgba(57, 113, 255, 0.03) 44.16%,
|
||||
rgba(57, 113, 255, 0) 77.18%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,9 +118,18 @@
|
||||
fill: @tab-mint;
|
||||
}
|
||||
|
||||
.icon i {
|
||||
color: @tab-mint;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-top: 1px solid @tab-mint;
|
||||
background: linear-gradient(180deg, rgba(75, 255, 169, 0.20) 9.34%, rgba(75, 255, 169, 0.03) 44.16%, rgba(75, 255, 169, 0.00) 77.18%);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(75, 255, 169, 0.2) 9.34%,
|
||||
rgba(75, 255, 169, 0.03) 44.16%,
|
||||
rgba(75, 255, 169, 0) 77.18%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +138,18 @@
|
||||
fill: @tab-cyan;
|
||||
}
|
||||
|
||||
.icon i {
|
||||
color: @tab-cyan;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-top: 1px solid @tab-cyan;
|
||||
background: linear-gradient(180deg, rgba(75, 223, 255, 0.20) 9.34%, rgba(75, 223, 255, 0.03) 44.16%, rgba(58, 186, 214, 0.00) 86.79%);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(75, 223, 255, 0.2) 9.34%,
|
||||
rgba(75, 223, 255, 0.03) 44.16%,
|
||||
rgba(58, 186, 214, 0) 86.79%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,9 +158,18 @@
|
||||
fill: @tab-white;
|
||||
}
|
||||
|
||||
.icon i {
|
||||
color: @tab-white;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-top: 1px solid @tab-white;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.20) 9.34%, rgba(255, 255, 255, 0.03) 44.16%, rgba(255, 255, 255, 0.00) 86.79%);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.2) 9.34%,
|
||||
rgba(255, 255, 255, 0.03) 44.16%,
|
||||
rgba(255, 255, 255, 0) 86.79%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,9 +178,18 @@
|
||||
fill: @tab-violet;
|
||||
}
|
||||
|
||||
.icon i {
|
||||
color: @tab-violet;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-top: 1px solid @tab-violet;
|
||||
background: linear-gradient(180deg, rgba(186, 118, 255, 0.20) 9.34%, rgba(186, 118, 255, 0.03) 44.16%, rgba(186, 118, 255, 0.00) 86.79%);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(186, 118, 255, 0.2) 9.34%,
|
||||
rgba(186, 118, 255, 0.03) 44.16%,
|
||||
rgba(186, 118, 255, 0) 86.79%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,9 +198,18 @@
|
||||
fill: @tab-pink;
|
||||
}
|
||||
|
||||
.icon i {
|
||||
color: @tab-pink;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-top: 1px solid @tab-pink;
|
||||
background: linear-gradient(180deg, rgba(255, 136, 165, 0.20) 9.34%, rgba(255, 136, 165, 0.03) 44.16%, rgba(255, 136, 165, 0.00) 86.79%);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 136, 165, 0.2) 9.34%,
|
||||
rgba(255, 136, 165, 0.03) 44.16%,
|
||||
rgba(255, 136, 165, 0) 86.79%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,19 @@ class ScreenTabs extends React.Component<{ session: Session }, {}> {
|
||||
})();
|
||||
}
|
||||
|
||||
renderTab(screen: Screen, activeScreenId: string, index: number): any {
|
||||
renderTabIcon = (screen: Screen): React.ReactNode => {
|
||||
const tabIcon = screen.getTabIcon();
|
||||
if (tabIcon === "default") {
|
||||
return <SquareIcon className="icon left-icon" />;
|
||||
}
|
||||
return (
|
||||
<div className="icon">
|
||||
<i className={`fa-sharp fa-solid fa-${tabIcon}`}></i>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderTab(screen: Screen, activeScreenId: string, index: number): JSX.Element {
|
||||
let tabIndex = null;
|
||||
if (index + 1 <= 9) {
|
||||
tabIndex = <div className="tab-index">{renderCmdText(String(index + 1))}</div>;
|
||||
@@ -132,7 +144,7 @@ class ScreenTabs extends React.Component<{ session: Session }, {}> {
|
||||
onClick={() => this.handleSwitchScreen(screen.screenId)}
|
||||
onContextMenu={(event) => this.openScreenSettings(event, screen)}
|
||||
>
|
||||
<SquareIcon className="icon left-icon" />
|
||||
{this.renderTabIcon(screen)}
|
||||
<div className="tab-name truncate">
|
||||
{archived}
|
||||
{webShared}
|
||||
@@ -149,7 +161,7 @@ class ScreenTabs extends React.Component<{ session: Session }, {}> {
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
let screen: Screen = null;
|
||||
let screen: Screen | null = null;
|
||||
let index = 0;
|
||||
let showingScreens = [];
|
||||
let activeScreenId = session.activeScreenId.get();
|
||||
|
||||
+27
-5
@@ -95,6 +95,18 @@ const MaxFontSize = 15;
|
||||
const InputChunkSize = 500;
|
||||
const RemoteColors = ["red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"];
|
||||
const TabColors = ["red", "orange", "yellow", "green", "mint", "cyan", "blue", "violet", "pink", "white"];
|
||||
const TabIcons = [
|
||||
"sparkle",
|
||||
"fire",
|
||||
"ghost",
|
||||
"cloud",
|
||||
"compass",
|
||||
"crown",
|
||||
"droplet",
|
||||
"graduation-cap",
|
||||
"heart",
|
||||
"file",
|
||||
];
|
||||
|
||||
// @ts-ignore
|
||||
const VERSION = __WAVETERM_VERSION__;
|
||||
@@ -469,6 +481,15 @@ class Screen {
|
||||
return tabColor;
|
||||
}
|
||||
|
||||
getTabIcon(): string {
|
||||
let tabIcon = "default";
|
||||
let screenOpts = this.opts.get();
|
||||
if (screenOpts != null && !isBlank(screenOpts.tabicon)) {
|
||||
tabIcon = screenOpts.tabicon;
|
||||
}
|
||||
return tabIcon;
|
||||
}
|
||||
|
||||
getCurRemoteInstance(): RemoteInstanceType {
|
||||
let session = GlobalModel.getSessionById(this.sessionId);
|
||||
let rptr = this.curRemote.get();
|
||||
@@ -3492,7 +3513,7 @@ class Model {
|
||||
submitCommand(
|
||||
metaCmd: string,
|
||||
metaSubCmd: string,
|
||||
args: string[],
|
||||
args: string[] | null,
|
||||
kwargs: Record<string, string>,
|
||||
interactive: boolean
|
||||
): Promise<CommandRtnType> {
|
||||
@@ -3505,7 +3526,7 @@ class Model {
|
||||
uicontext: this.getUIContext(),
|
||||
interactive: interactive,
|
||||
};
|
||||
/**
|
||||
/**
|
||||
console.log(
|
||||
"CMD",
|
||||
pk.metacmd + (pk.metasubcmd != null ? ":" + pk.metasubcmd : ""),
|
||||
@@ -3513,7 +3534,7 @@ class Model {
|
||||
pk.kwargs,
|
||||
pk.interactive
|
||||
);
|
||||
*/
|
||||
*/
|
||||
return this.submitCommandPacket(pk, interactive);
|
||||
}
|
||||
|
||||
@@ -3950,10 +3971,10 @@ class CommandRunner {
|
||||
|
||||
screenSetSettings(
|
||||
screenId: string,
|
||||
settings: { tabcolor?: string; name?: string; sharename?: string },
|
||||
settings: { tabcolor?: string; tabicon?: string; name?: string; sharename?: string },
|
||||
interactive: boolean
|
||||
): Promise<CommandRtnType> {
|
||||
let kwargs = Object.assign({}, settings);
|
||||
let kwargs: { [key: string]: any } = Object.assign({}, settings);
|
||||
kwargs["nohist"] = "1";
|
||||
kwargs["screen"] = screenId;
|
||||
return GlobalModel.submitCommand("screen", "set", null, kwargs, interactive);
|
||||
@@ -4169,6 +4190,7 @@ export {
|
||||
Screen,
|
||||
riToRPtr,
|
||||
TabColors,
|
||||
TabIcons,
|
||||
RemoteColors,
|
||||
getTermPtyData,
|
||||
RemotesModalModel,
|
||||
|
||||
+2
-1
@@ -50,6 +50,7 @@ type LineType = {
|
||||
|
||||
type ScreenOptsType = {
|
||||
tabcolor?: string;
|
||||
tabicon?: string;
|
||||
pterm?: string;
|
||||
};
|
||||
|
||||
@@ -167,7 +168,7 @@ type FeCmdPacketType = {
|
||||
type: string;
|
||||
metacmd: string;
|
||||
metasubcmd?: string;
|
||||
args: string[];
|
||||
args: string[] | null;
|
||||
kwargs: Record<string, string>;
|
||||
rawstr?: string;
|
||||
uicontext: UIContextType;
|
||||
|
||||
+1
-1
@@ -390,7 +390,7 @@ function getColorRGB(colorInput) {
|
||||
return computedColorStyle;
|
||||
}
|
||||
|
||||
function commandRtnHandler(prtn: Promise<CommandRtnType>, errorMessage: OV<string>) {
|
||||
function commandRtnHandler(prtn: Promise<CommandRtnType>, errorMessage: OV<string>) {
|
||||
prtn.then((crtn) => {
|
||||
if (crtn.success) {
|
||||
return;
|
||||
|
||||
+2
-1
@@ -11,6 +11,7 @@
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ const (
|
||||
)
|
||||
|
||||
var ColorNames = []string{"yellow", "blue", "pink", "mint", "cyan", "violet", "orange", "green", "red", "white"}
|
||||
var TabIcons = []string{"sparkle", "fire", "ghost", "cloud", "compass", "crown", "droplet", "graduation-cap", "heart", "file"}
|
||||
var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"}
|
||||
var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
|
||||
|
||||
@@ -81,6 +82,7 @@ var GlobalCmds = []string{"session", "screen", "remote", "set", "client", "telem
|
||||
|
||||
var SetVarNameMap map[string]string = map[string]string{
|
||||
"tabcolor": "screen.tabcolor",
|
||||
"tabicon": "screen.tabicon",
|
||||
"pterm": "screen.pterm",
|
||||
"anchor": "screen.anchor",
|
||||
"focus": "screen.focus",
|
||||
@@ -91,7 +93,7 @@ var SetVarScopes = []SetVarScope{
|
||||
SetVarScope{ScopeName: "global", VarNames: []string{}},
|
||||
SetVarScope{ScopeName: "client", VarNames: []string{"telemetry"}},
|
||||
SetVarScope{ScopeName: "session", VarNames: []string{"name", "pos"}},
|
||||
SetVarScope{ScopeName: "screen", VarNames: []string{"name", "tabcolor", "pos", "pterm", "anchor", "focus", "line"}},
|
||||
SetVarScope{ScopeName: "screen", VarNames: []string{"name", "tabcolor", "tabicon", "pos", "pterm", "anchor", "focus", "line"}},
|
||||
SetVarScope{ScopeName: "line", VarNames: []string{}},
|
||||
// connection = remote, remote = remoteinstance
|
||||
SetVarScope{ScopeName: "connection", VarNames: []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}},
|
||||
@@ -757,6 +759,16 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss
|
||||
varsUpdated = append(varsUpdated, "tabcolor")
|
||||
setNonAnchor = true
|
||||
}
|
||||
if pk.Kwargs["tabicon"] != "" {
|
||||
icon := pk.Kwargs["tabicon"]
|
||||
err = validateIcon(icon, "screen tabicon")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updateMap[sstore.ScreenField_TabIcon] = icon
|
||||
varsUpdated = append(varsUpdated, "tabicon")
|
||||
setNonAnchor = true
|
||||
}
|
||||
if pk.Kwargs["pos"] != "" {
|
||||
varsUpdated = append(varsUpdated, "pos")
|
||||
setNonAnchor = true
|
||||
@@ -806,7 +818,7 @@ func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss
|
||||
}
|
||||
}
|
||||
if len(varsUpdated) == 0 {
|
||||
return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos", "tabcolor", "focus", "anchor", "line", "sharename"}, "or", false))
|
||||
return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos", "tabcolor", "tabicon", "focus", "anchor", "line", "sharename"}, "or", false))
|
||||
}
|
||||
screen, err := sstore.UpdateScreen(ctx, ids.ScreenId, updateMap)
|
||||
if err != nil {
|
||||
@@ -1978,6 +1990,15 @@ func validateColor(color string, typeStr string) error {
|
||||
return fmt.Errorf("invalid %s, valid colors are: %s", typeStr, formatStrs(ColorNames, "or", false))
|
||||
}
|
||||
|
||||
func validateIcon(icon string, typeStr string) error {
|
||||
for _, c := range TabIcons {
|
||||
if icon == c {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("invalid %s, valid icons are: %s", typeStr, formatStrs(TabIcons, "or", false))
|
||||
}
|
||||
|
||||
func validateRemoteColor(color string, typeStr string) error {
|
||||
for _, c := range RemoteColorNames {
|
||||
if color == c {
|
||||
|
||||
@@ -1709,6 +1709,7 @@ const (
|
||||
ScreenField_SelectedLine = "selectedline" // int
|
||||
ScreenField_Focus = "focustype" // string
|
||||
ScreenField_TabColor = "tabcolor" // string
|
||||
ScreenField_TabIcon = "tabicon" // string
|
||||
ScreenField_PTerm = "pterm" // string
|
||||
ScreenField_Name = "name" // string
|
||||
ScreenField_ShareName = "sharename" // string
|
||||
@@ -1743,6 +1744,10 @@ func UpdateScreen(ctx context.Context, screenId string, editMap map[string]inter
|
||||
query = `UPDATE screen SET screenopts = json_set(screenopts, '$.tabcolor', ?) WHERE screenid = ?`
|
||||
tx.Exec(query, tabColor, screenId)
|
||||
}
|
||||
if tabIcon, found := editMap[ScreenField_TabIcon]; found {
|
||||
query = `UPDATE screen SET screenopts = json_set(screenopts, '$.tabicon', ?) WHERE screenid = ?`
|
||||
tx.Exec(query, tabIcon, screenId)
|
||||
}
|
||||
if pterm, found := editMap[ScreenField_PTerm]; found {
|
||||
query = `UPDATE screen SET screenopts = json_set(screenopts, '$.pterm', ?) WHERE screenid = ?`
|
||||
tx.Exec(query, pterm, screenId)
|
||||
|
||||
@@ -433,6 +433,7 @@ func (h *HistoryItemType) FromMap(m map[string]interface{}) bool {
|
||||
|
||||
type ScreenOptsType struct {
|
||||
TabColor string `json:"tabcolor,omitempty"`
|
||||
TabIcon string `json:"tabicon,omitempty"`
|
||||
PTerm string `json:"pterm,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -2083,6 +2083,16 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/sprintf-js@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/sprintf-js/-/sprintf-js-1.1.3.tgz#cdad9076d288921cea5b336ab07b915ab27f7921"
|
||||
integrity sha512-Z9z6EMpwsroPp4BivsuVi/LYWi7jnuMCz9gWD/tZYhlEOSV2MO3fawrwHUEl05mshHyzHejPwe6PM0ZBDQD9yw==
|
||||
|
||||
"@types/throttle-debounce@^5.0.1":
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-5.0.1.tgz#8ce917e41580b2cf16f8ee840e227947f4152b04"
|
||||
integrity sha512-/fifasjlhpz/r4YsH0r0ZXJvivXFB3F6bmezMnqgsn/NK/fYJn7vN84k7eYn/oALu/aenXo+t8Pv+QlkS6iYBg==
|
||||
|
||||
"@types/triple-beam@^1.3.2":
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.3.tgz#726ae98a5f6418c8f24f9b0f2a9f81a8664876ae"
|
||||
|
||||
Reference in New Issue
Block a user