split settings modals (#272)

This commit is contained in:
Red J Adaya 2024-02-03 12:22:30 +08:00 committed by GitHub
parent 3a9f6dec6d
commit cedebe2196
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 921 additions and 1867 deletions

View File

@ -664,3 +664,161 @@ a.a-block {
}
}
}
.settings-field {
display: flex;
flex-direction: row;
align-items: center;
&.settings-field.sub-field {
.settings-label {
font-weight: normal;
text-align: right;
padding-right: 20px;
}
}
&.settings-error {
color: @term-red;
margin-top: 20px;
padding: 10px;
border-radius: 5px;
background-color: #200707;
border: 1px solid @term-red;
font-weight: bold;
.error-dismiss {
padding: 2px;
cursor: pointer;
}
}
.settings-label {
font-weight: bold;
width: 12em;
display: flex;
flex-direction: row;
align-items: center;
.info-message {
margin-left: 5px;
}
}
.settings-input {
display: flex;
flex-direction: row;
align-items: center;
color: @term-white;
&.settings-clickable {
cursor: pointer;
}
&.inline-edit.edit-active {
input.input {
padding: 0;
height: 20px;
}
.button {
height: 20px;
}
}
input {
padding: 4px;
border-radius: 3px;
}
.control {
.icon {
width: 1.5em;
height: 1.5em;
margin: 0;
}
}
.tab-color-icon.color-default path {
fill: @tab-green;
}
.tab-color-icon.color-green path {
fill: @tab-green;
}
.tab-color-icon.color-orange path {
fill: @tab-orange;
}
.tab-color-icon.color-red path {
fill: @tab-red;
}
.tab-color-icon.color-yellow path {
fill: @tab-yellow;
}
.tab-color-icon.color-blue path {
fill: @tab-blue;
}
.tab-color-icon.color-mint path {
fill: @tab-mint;
}
.tab-color-icon.color-cyan path {
fill: @tab-cyan;
}
.tab-color-icon.color-white path {
fill: @tab-white;
}
.tab-color-icon.color-violet path {
fill: @tab-violet;
}
.tab-color-icon.color-pink path {
fill: @tab-pink;
}
.tab-colors,
.tab-icons {
display: flex;
flex-direction: row;
align-items: center;
.tab-color-sep,
.tab-icon-sep {
padding-left: 10px;
padding-right: 10px;
font-weight: bold;
}
.tab-color-icon,
.tab-icon-icon {
width: 1.1em;
vertical-align: middle;
}
.tab-color-name,
.tab-icon-name {
margin-left: 1em;
}
.tab-color-select,
.tab-icon-select {
cursor: pointer;
margin: 5px;
&:hover {
outline: 2px solid white;
}
}
}
.action-text {
margin-left: 20px;
color: @term-red;
}
.settings-share-link {
width: 160px;
}
}
&:not(:first-child) {
margin-top: 10px;
}
}

View File

@ -6,3 +6,6 @@ export { CreateRemoteConnModal } from "./createremoteconn";
export { ViewRemoteConnDetailModal } from "./viewremoteconndetail";
export { EditRemoteConnModal } from "./editremoteconn";
export { TabSwitcherModal } from "./tabswitcher";
export { SessionSettingsModal } from "./sessionsettings";
export { ScreenSettingsModal } from "./screensettings";
export { LineSettingsModal } from "./linesettings";

View File

@ -0,0 +1,23 @@
@import "../../../app/common/themes/themes.less";
.line-settings-modal {
width: 640px;
.wave-modal-content {
gap: 24px;
.wave-modal-body {
display: flex;
padding: 0px 20px;
flex-direction: column;
align-items: flex-start;
gap: 4px;
align-self: stretch;
width: 100%;
.settings-input .hotkey {
color: @text-secondary;
}
}
}
}

View File

@ -0,0 +1,151 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
import { SettingsError, Modal, Dropdown } from "../common";
import { LineType, RendererPluginType } from "../../../types/types";
import { PluginModel } from "../../../plugins/plugins";
import { commandRtnHandler } from "../../../util/util";
import "./linesettings.less";
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class LineSettingsModal extends React.Component<{}, {}> {
rendererDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "lineSettings-rendererDropdownActive" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
linenum: number;
constructor(props: any) {
super(props);
this.linenum = GlobalModel.lineSettingsModal.get();
if (this.linenum == null) {
return;
}
}
@boundMethod
closeModal(): void {
mobx.action(() => {
GlobalModel.lineSettingsModal.set(null);
})();
GlobalModel.modalsModel.popModal();
}
@boundMethod
handleChangeArchived(val: boolean): void {
let line = this.getLine();
if (line == null) {
return;
}
let prtn = GlobalCommandRunner.lineArchive(line.lineid, val);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
toggleRendererDropdown(): void {
mobx.action(() => {
this.rendererDropdownActive.set(!this.rendererDropdownActive.get());
})();
}
getLine(): LineType {
let screen = GlobalModel.getActiveScreen();
if (screen == null) {
return;
}
return screen.getLineByNum(this.linenum);
}
@boundMethod
clickSetRenderer(renderer: string): void {
let line = this.getLine();
if (line == null) {
return;
}
let prtn = GlobalCommandRunner.lineSet(line.lineid, { renderer: renderer });
commandRtnHandler(prtn, this.errorMessage);
mobx.action(() => {
this.rendererDropdownActive.set(false);
})();
}
getOptions(plugins: RendererPluginType[]) {
// Add label and value to each object in the array
const options = plugins.map((item) => ({
...item,
label: item.name,
value: item.name,
}));
// Create an additional object with label "terminal" and value null
const terminalItem = {
label: "terminal",
value: null,
name: null,
rendererType: null,
heightType: null,
dataType: null,
collapseType: null,
globalCss: null,
mimeTypes: null,
};
// Create an additional object with label "none" and value none
const noneItem = {
label: "none",
value: "none",
name: null,
rendererType: null,
heightType: null,
dataType: null,
collapseType: null,
globalCss: null,
mimeTypes: null,
};
// Combine the options with the terminal item
return [terminalItem, ...options, noneItem];
}
render() {
let line = this.getLine();
if (line == null) {
setTimeout(() => {
this.closeModal();
}, 0);
return null;
}
let plugins = PluginModel.rendererPlugins;
let renderer = line.renderer ?? "terminal";
return (
<Modal className="line-settings-modal">
<Modal.Header onClose={this.closeModal} title={`line settings (${line.linenum})`} />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Renderer</div>
<div className="settings-input">
<Dropdown
className="renderer-dropdown"
options={this.getOptions(plugins)}
defaultValue={renderer}
onChange={this.clickSetRenderer}
/>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
<div style={{ height: 50 }} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
export { LineSettingsModal };

View File

@ -9,8 +9,10 @@ import {
ViewRemoteConnDetailModal,
EditRemoteConnModal,
TabSwitcherModal,
SessionSettingsModal,
ScreenSettingsModal,
LineSettingsModal,
} from "../modals";
import { ScreenSettingsModal, SessionSettingsModal, LineSettingsModal } from "./settings";
import * as constants from "../../appconst";
const modalsRegistry: { [key: string]: () => React.ReactElement } = {

View File

@ -0,0 +1,52 @@
@import "../../../app/common/themes/themes.less";
.screen-settings-modal {
width: 640px;
.wave-modal-content {
gap: 24px;
.wave-modal-body {
display: flex;
padding: 0px 20px;
flex-direction: column;
align-items: flex-start;
gap: 4px;
align-self: stretch;
width: 100%;
.screen-settings-dropdown {
width: 412px;
.lefticon {
position: absolute;
top: 50%;
left: 16px;
transform: translateY(-50%);
.globe-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.status-icon {
position: absolute;
left: 7px;
top: 8px;
}
}
}
.archived-label,
.actions-label {
div:first-child {
margin-right: 5px;
}
div:last-child i {
font-size: 13px;
}
}
}
}
}

View File

@ -0,0 +1,365 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, TabColors, TabIcons, Screen } from "../../../model/model";
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Dropdown, Tooltip } from "../common";
import { RemoteType } from "../../../types/types";
import * as util from "../../../util/util";
import { commandRtnHandler } from "../../../util/util";
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
import { ReactComponent as GlobeIcon } from "../../assets/icons/globe.svg";
import { ReactComponent as StatusCircleIcon } from "../../assets/icons/statuscircle.svg";
import "./screensettings.less";
type OV<V> = mobx.IObservableValue<V>;
const ScreenDeleteMessage = `
Are you sure you want to delete this tab?
All commands and output will be deleted. To hide the tab, and retain the commands and output, use 'archive'.
`.trim();
const WebShareConfirmMarkdown = `
You are about to share a terminal tab on the web. Please make sure that you do
NOT share any private information, keys, passwords, or other sensitive information.
You are responsible for what you are sharing, be smart.
`.trim();
const WebStopShareConfirmMarkdown = `
Are you sure you want to stop web-sharing this tab?
`.trim();
@mobxReact.observer
class ScreenSettingsModal extends React.Component<{}, {}> {
shareCopied: OV<boolean> = mobx.observable.box(false, { name: "ScreenSettings-shareCopied" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
screen: Screen;
sessionId: string;
screenId: string;
remotes: RemoteType[];
constructor(props) {
super(props);
let screenSettingsModal = GlobalModel.screenSettingsModal.get();
let { sessionId, screenId } = screenSettingsModal;
this.sessionId = sessionId;
this.screenId = screenId;
this.screen = GlobalModel.getScreenById(sessionId, screenId);
if (this.screen == null || sessionId == null || screenId == null) {
return;
}
this.remotes = GlobalModel.remotes;
}
@boundMethod
getOptions(): { label: string; value: string }[] {
return this.remotes
.filter((r) => !r.archived)
.map((remote) => ({
...remote,
label:
remote.remotealias && !util.isBlank(remote.remotealias)
? `${remote.remotecanonicalname}`
: remote.remotecanonicalname,
value: remote.remotecanonicalname,
}))
.sort((a, b) => {
let connValA = util.getRemoteConnVal(a);
let connValB = util.getRemoteConnVal(b);
if (connValA !== connValB) {
return connValA - connValB;
}
return a.remoteidx - b.remoteidx;
});
}
@boundMethod
closeModal(): void {
mobx.action(() => {
GlobalModel.screenSettingsModal.set(null);
})();
GlobalModel.modalsModel.popModal();
}
@boundMethod
selectTabColor(color: string): void {
if (this.screen == null) {
return;
}
if (this.screen.getTabColor() == color) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { tabcolor: color }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
selectTabIcon(icon: string): void {
if (this.screen.getTabIcon() == icon) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screen.screenId, { tabicon: icon }, false);
util.commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeArchived(val: boolean): void {
if (this.screen == null) {
return;
}
if (this.screen.archived.get() == val) {
return;
}
let prtn = GlobalCommandRunner.screenArchive(this.screenId, val);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeWebShare(val: boolean): void {
if (this.screen == null) {
return;
}
if (this.screen.isWebShared() == val) {
return;
}
let message = val ? WebShareConfirmMarkdown : WebStopShareConfirmMarkdown;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.screenWebShare(this.screen.screenId, val);
commandRtnHandler(prtn, this.errorMessage);
});
}
@boundMethod
copyShareLink(): void {
if (this.screen == null) {
return null;
}
let shareLink = this.screen.getWebShareUrl();
if (shareLink == null) {
return;
}
navigator.clipboard.writeText(shareLink);
mobx.action(() => {
this.shareCopied.set(true);
})();
setTimeout(() => {
mobx.action(() => {
this.shareCopied.set(false);
})();
}, 600);
}
@boundMethod
inlineUpdateName(val: string): void {
if (this.screen == null) {
return;
}
if (util.isStrEq(val, this.screen.name.get())) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { name: val }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateShareName(val: string): void {
if (this.screen == null) {
return;
}
if (util.isStrEq(val, this.screen.getShareName())) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { sharename: val }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
dismissError(): void {
mobx.action(() => {
this.errorMessage.set(null);
})();
}
@boundMethod
handleDeleteScreen(): void {
if (this.screen == null) {
return;
}
if (this.screen.getScreenLines().lines.length == 0) {
GlobalCommandRunner.screenDelete(this.screenId, false);
GlobalModel.modalsModel.popModal();
return;
}
let message = ScreenDeleteMessage;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.screenDelete(this.screenId, false);
commandRtnHandler(prtn, this.errorMessage);
GlobalModel.modalsModel.popModal();
});
}
@boundMethod
selectRemote(cname: string): void {
let prtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
util.commandRtnHandler(prtn, this.errorMessage);
}
render() {
let screen = this.screen;
if (screen == null) {
return null;
}
let color: string = null;
let icon: string = null;
let index: number = 0;
let curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid);
return (
<Modal className="screen-settings-modal">
<Modal.Header onClose={this.closeModal} title={`tab settings (${screen.name.get()})`} />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Tab Id</div>
<div className="settings-input">{screen.screenId}</div>
</div>
<div className="settings-field">
<div className="settings-label">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="settings-field">
<div className="settings-label">Connection</div>
<div className="settings-input">
<Dropdown
className="screen-settings-dropdown"
label={curRemote.remotealias}
options={this.getOptions()}
defaultValue={curRemote.remotecanonicalname}
onChange={this.selectRemote}
decoration={{
startDecoration: (
<div className="lefticon">
<GlobeIcon className="globe-icon" />
<StatusCircleIcon
className={cn("status-icon", "status-" + curRemote.status)}
/>
</div>
),
}}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Tab Color</div>
<div className="settings-input">
<div className="tab-colors">
<div className="tab-color-cur">
<SquareIcon className={cn("tab-color-icon", "color-" + screen.getTabColor())} />
<span className="tab-color-name">{screen.getTabColor()}</span>
</div>
<div className="tab-color-sep">|</div>
<For each="color" of={TabColors}>
<div
key={color}
className="tab-color-select"
onClick={() => this.selectTabColor(color)}
>
<SquareIcon className={cn("tab-color-icon", "color-" + color)} />
</div>
</For>
</div>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Tab Icon</div>
<div className="settings-input">
<div className="tab-icons">
<div className="tab-icon-cur">
<If condition={screen.getTabIcon() == "default"}>
<SquareIcon className={cn("tab-color-icon", "color-white")} />
</If>
<If condition={screen.getTabIcon() != "default"}>
<i className={`fa-sharp fa-solid fa-${screen.getTabIcon()}`}></i>
</If>
<span className="tab-icon-name">{screen.getTabIcon()}</span>
</div>
<div className="tab-icon-sep">|</div>
<For each="icon" index="index" of={TabIcons}>
<div
key={`${color}-${index}`}
className="tab-icon-select"
onClick={() => this.selectTabIcon(icon)}
>
<i className={`fa-sharp fa-solid fa-${icon}`}></i>
</div>
</For>
</div>
</div>
</div>
<div className="settings-field">
<div className="settings-label archived-label">
<div className="">Archived</div>
<Tooltip
message={`Archive will hide the tab. Commands and output will be retained in history.`}
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
className="screen-settings-tooltip"
>
<i className="fa-sharp fa-regular fa-circle-question" />
</Tooltip>
</div>
<div className="settings-input">
<Toggle checked={screen.archived.get()} onChange={this.handleChangeArchived} />
</div>
</div>
<div className="settings-field">
<div className="settings-label actions-label">
<div>Actions</div>
<Tooltip
message={`Delete will remove the tab, removing all commands and output from history.`}
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
className="screen-settings-tooltip"
>
<i className="fa-sharp fa-regular fa-circle-question" />
</Tooltip>
</div>
<div className="settings-input">
<div
onClick={this.handleDeleteScreen}
className="button is-prompt-danger is-outlined is-small"
>
Delete Tab
</div>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
export { ScreenSettingsModal };

View File

@ -0,0 +1,19 @@
@import "../../../app/common/themes/themes.less";
.session-settings-modal {
width: 640px;
.wave-modal-content {
gap: 24px;
.wave-modal-body {
display: flex;
padding: 0px 20px;
flex-direction: column;
align-items: flex-start;
gap: 4px;
align-self: stretch;
width: 100%;
}
}
}

View File

@ -0,0 +1,147 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { GlobalModel, GlobalCommandRunner, Session } from "../../../model/model";
import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage, Modal } from "../common";
import * as util from "../../../util/util";
import { commandRtnHandler } from "../../../util/util";
import "./sessionsettings.less";
type OV<V> = mobx.IObservableValue<V>;
const SessionDeleteMessage = `
Are you sure you want to delete this workspace?
All commands and output will be deleted. To hide the workspace, and retain the commands and output, use 'archive'.
`.trim();
@mobxReact.observer
class SessionSettingsModal extends React.Component<{}, {}> {
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
session: Session;
sessionId: string;
constructor(props: any) {
super(props);
this.sessionId = GlobalModel.sessionSettingsModal.get();
this.session = GlobalModel.getSessionById(this.sessionId);
if (this.session == null) {
return;
}
}
@boundMethod
closeModal(): void {
mobx.action(() => {
GlobalModel.sessionSettingsModal.set(null);
})();
GlobalModel.modalsModel.popModal();
}
@boundMethod
handleInlineChangeName(newVal: string): void {
if (this.session == null) {
return;
}
if (util.isStrEq(newVal, this.session.name.get())) {
return;
}
let prtn = GlobalCommandRunner.sessionSetSettings(this.sessionId, { name: newVal }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeArchived(val: boolean): void {
if (this.session == null) {
return;
}
if (this.session.archived.get() == val) {
return;
}
let prtn = GlobalCommandRunner.sessionArchive(this.sessionId, val);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleDeleteSession(): void {
let message = SessionDeleteMessage;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.sessionDelete(this.sessionId);
commandRtnHandler(prtn, this.errorMessage, () => GlobalModel.modalsModel.popModal());
});
}
@boundMethod
dismissError(): void {
mobx.action(() => {
this.errorMessage.set(null);
})();
}
render() {
if (this.session == null) {
return null;
}
return (
<Modal className="session-settings-modal">
<Modal.Header onClose={this.closeModal} title={`workspace settings (${this.session.name.get()})`} />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Name</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder="name"
text={this.session.name.get() ?? "(none)"}
value={this.session.name.get() ?? ""}
onChange={this.handleInlineChangeName}
maxLength={50}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">
<div>Archived</div>
<InfoMessage width={400}>
Archive will hide the workspace from the active menu. Commands and output will be
retained in history.
</InfoMessage>
</div>
<div className="settings-input">
<Toggle checked={this.session.archived.get()} onChange={this.handleChangeArchived} />
</div>
</div>
<div className="settings-field">
<div className="settings-label">
<div>Actions</div>
<InfoMessage width={400}>
Delete will remove the workspace, removing all commands and output from history.
</InfoMessage>
</div>
<div className="settings-input">
<div
onClick={this.handleDeleteSession}
className="button is-prompt-danger is-outlined is-small"
>
Delete Workspace
</div>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
export { SessionSettingsModal };

File diff suppressed because it is too large Load Diff

View File

@ -1,838 +0,0 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import {
GlobalModel,
GlobalCommandRunner,
TabColors,
MinFontSize,
MaxFontSize,
TabIcons,
Screen,
Session,
} from "../../../model/model";
import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage, Modal, Dropdown, Tooltip } from "../common";
import { LineType, RendererPluginType, ClientDataType, CommandRtnType, RemoteType } from "../../../types/types";
import { PluginModel } from "../../../plugins/plugins";
import * as util from "../../../util/util";
import { commandRtnHandler } from "../../../util/util";
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
import { ReactComponent as GlobeIcon } from "../../assets/icons/globe.svg";
import { ReactComponent as StatusCircleIcon } from "../../assets/icons/statuscircle.svg";
import "./settings.less";
type OV<V> = mobx.IObservableValue<V>;
// @ts-ignore
const VERSION = __WAVETERM_VERSION__;
// @ts-ignore
const BUILD = __WAVETERM_BUILD__;
const ScreenDeleteMessage = `
Are you sure you want to delete this tab?
All commands and output will be deleted. To hide the tab, and retain the commands and output, use 'archive'.
`.trim();
const SessionDeleteMessage = `
Are you sure you want to delete this workspace?
All commands and output will be deleted. To hide the workspace, and retain the commands and output, use 'archive'.
`.trim();
const WebShareConfirmMarkdown = `
You are about to share a terminal tab on the web. Please make sure that you do
NOT share any private information, keys, passwords, or other sensitive information.
You are responsible for what you are sharing, be smart.
`.trim();
const WebStopShareConfirmMarkdown = `
Are you sure you want to stop web-sharing this tab?
`.trim();
@mobxReact.observer
class ScreenSettingsModal extends React.Component<{}, {}> {
shareCopied: OV<boolean> = mobx.observable.box(false, { name: "ScreenSettings-shareCopied" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
screen: Screen;
sessionId: string;
screenId: string;
remotes: RemoteType[];
constructor(props) {
super(props);
let screenSettingsModal = GlobalModel.screenSettingsModal.get();
let { sessionId, screenId } = screenSettingsModal;
this.sessionId = sessionId;
this.screenId = screenId;
this.screen = GlobalModel.getScreenById(sessionId, screenId);
if (this.screen == null || sessionId == null || screenId == null) {
return;
}
this.remotes = GlobalModel.remotes;
}
@boundMethod
getOptions(): { label: string; value: string }[] {
return this.remotes
.filter((r) => !r.archived)
.map((remote) => ({
...remote,
label:
remote.remotealias && !util.isBlank(remote.remotealias)
? `${remote.remotecanonicalname}`
: remote.remotecanonicalname,
value: remote.remotecanonicalname,
}))
.sort((a, b) => {
let connValA = util.getRemoteConnVal(a);
let connValB = util.getRemoteConnVal(b);
if (connValA !== connValB) {
return connValA - connValB;
}
return a.remoteidx - b.remoteidx;
});
}
@boundMethod
closeModal(): void {
mobx.action(() => {
GlobalModel.screenSettingsModal.set(null);
})();
GlobalModel.modalsModel.popModal();
}
@boundMethod
selectTabColor(color: string): void {
if (this.screen == null) {
return;
}
if (this.screen.getTabColor() == color) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { tabcolor: color }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
selectTabIcon(icon: string): void {
if (this.screen.getTabIcon() == icon) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screen.screenId, { tabicon: icon }, false);
util.commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeArchived(val: boolean): void {
if (this.screen == null) {
return;
}
if (this.screen.archived.get() == val) {
return;
}
let prtn = GlobalCommandRunner.screenArchive(this.screenId, val);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeWebShare(val: boolean): void {
if (this.screen == null) {
return;
}
if (this.screen.isWebShared() == val) {
return;
}
let message = val ? WebShareConfirmMarkdown : WebStopShareConfirmMarkdown;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.screenWebShare(this.screen.screenId, val);
commandRtnHandler(prtn, this.errorMessage);
});
}
@boundMethod
copyShareLink(): void {
if (this.screen == null) {
return null;
}
let shareLink = this.screen.getWebShareUrl();
if (shareLink == null) {
return;
}
navigator.clipboard.writeText(shareLink);
mobx.action(() => {
this.shareCopied.set(true);
})();
setTimeout(() => {
mobx.action(() => {
this.shareCopied.set(false);
})();
}, 600);
}
@boundMethod
inlineUpdateName(val: string): void {
if (this.screen == null) {
return;
}
if (util.isStrEq(val, this.screen.name.get())) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { name: val }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateShareName(val: string): void {
if (this.screen == null) {
return;
}
if (util.isStrEq(val, this.screen.getShareName())) {
return;
}
let prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { sharename: val }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
dismissError(): void {
mobx.action(() => {
this.errorMessage.set(null);
})();
}
@boundMethod
handleDeleteScreen(): void {
if (this.screen == null) {
return;
}
if (this.screen.getScreenLines().lines.length == 0) {
GlobalCommandRunner.screenDelete(this.screenId, false);
GlobalModel.modalsModel.popModal();
return;
}
let message = ScreenDeleteMessage;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.screenDelete(this.screenId, false);
commandRtnHandler(prtn, this.errorMessage);
GlobalModel.modalsModel.popModal();
});
}
@boundMethod
selectRemote(cname: string): void {
let prtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
util.commandRtnHandler(prtn, this.errorMessage);
}
render() {
let screen = this.screen;
if (screen == null) {
return null;
}
let color: string = null;
let icon: string = null;
let index: number = 0;
let curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid);
return (
<Modal className="screen-settings-modal">
<Modal.Header onClose={this.closeModal} title={`tab settings (${screen.name.get()})`} />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Tab Id</div>
<div className="settings-input">{screen.screenId}</div>
</div>
<div className="settings-field">
<div className="settings-label">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="settings-field">
<div className="settings-label">Connection</div>
<div className="settings-input">
<Dropdown
className="screen-settings-dropdown"
label={curRemote.remotealias}
options={this.getOptions()}
defaultValue={curRemote.remotecanonicalname}
onChange={this.selectRemote}
decoration={{
startDecoration: (
<div className="lefticon">
<GlobeIcon className="globe-icon" />
<StatusCircleIcon
className={cn("status-icon", "status-" + curRemote.status)}
/>
</div>
),
}}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Tab Color</div>
<div className="settings-input">
<div className="tab-colors">
<div className="tab-color-cur">
<SquareIcon className={cn("tab-color-icon", "color-" + screen.getTabColor())} />
<span className="tab-color-name">{screen.getTabColor()}</span>
</div>
<div className="tab-color-sep">|</div>
<For each="color" of={TabColors}>
<div
key={color}
className="tab-color-select"
onClick={() => this.selectTabColor(color)}
>
<SquareIcon className={cn("tab-color-icon", "color-" + color)} />
</div>
</For>
</div>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Tab Icon</div>
<div className="settings-input">
<div className="tab-icons">
<div className="tab-icon-cur">
<If condition={screen.getTabIcon() == "default"}>
<SquareIcon className={cn("tab-color-icon", "color-white")} />
</If>
<If condition={screen.getTabIcon() != "default"}>
<i className={`fa-sharp fa-solid fa-${screen.getTabIcon()}`}></i>
</If>
<span className="tab-icon-name">{screen.getTabIcon()}</span>
</div>
<div className="tab-icon-sep">|</div>
<For each="icon" index="index" of={TabIcons}>
<div
key={`${color}-${index}`}
className="tab-icon-select"
onClick={() => this.selectTabIcon(icon)}
>
<i className={`fa-sharp fa-solid fa-${icon}`}></i>
</div>
</For>
</div>
</div>
</div>
<div className="settings-field">
<div className="settings-label archived-label">
<div className="">Archived</div>
<Tooltip
message={`Archive will hide the tab. Commands and output will be retained in history.`}
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
className="screen-settings-tooltip"
>
<i className="fa-sharp fa-regular fa-circle-question" />
</Tooltip>
</div>
<div className="settings-input">
<Toggle checked={screen.archived.get()} onChange={this.handleChangeArchived} />
</div>
</div>
<div className="settings-field">
<div className="settings-label actions-label">
<div>Actions</div>
<Tooltip
message={`Delete will remove the tab, removing all commands and output from history.`}
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
className="screen-settings-tooltip"
>
<i className="fa-sharp fa-regular fa-circle-question" />
</Tooltip>
</div>
<div className="settings-input">
<div
onClick={this.handleDeleteScreen}
className="button is-prompt-danger is-outlined is-small"
>
Delete Tab
</div>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
@mobxReact.observer
class SessionSettingsModal extends React.Component<{}, {}> {
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
session: Session;
sessionId: string;
constructor(props: any) {
super(props);
this.sessionId = GlobalModel.sessionSettingsModal.get();
this.session = GlobalModel.getSessionById(this.sessionId);
if (this.session == null) {
return;
}
}
@boundMethod
closeModal(): void {
mobx.action(() => {
GlobalModel.sessionSettingsModal.set(null);
})();
GlobalModel.modalsModel.popModal();
}
@boundMethod
handleInlineChangeName(newVal: string): void {
if (this.session == null) {
return;
}
if (util.isStrEq(newVal, this.session.name.get())) {
return;
}
let prtn = GlobalCommandRunner.sessionSetSettings(this.sessionId, { name: newVal }, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeArchived(val: boolean): void {
if (this.session == null) {
return;
}
if (this.session.archived.get() == val) {
return;
}
let prtn = GlobalCommandRunner.sessionArchive(this.sessionId, val);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleDeleteSession(): void {
let message = SessionDeleteMessage;
let alertRtn = GlobalModel.showAlert({ message: message, confirm: true, markdown: true });
alertRtn.then((result) => {
if (!result) {
return;
}
let prtn = GlobalCommandRunner.sessionDelete(this.sessionId);
commandRtnHandler(prtn, this.errorMessage, () => GlobalModel.modalsModel.popModal());
});
}
@boundMethod
dismissError(): void {
mobx.action(() => {
this.errorMessage.set(null);
})();
}
render() {
if (this.session == null) {
return null;
}
return (
<Modal className="session-settings-modal">
<Modal.Header onClose={this.closeModal} title={`workspace settings (${this.session.name.get()})`} />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Name</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder="name"
text={this.session.name.get() ?? "(none)"}
value={this.session.name.get() ?? ""}
onChange={this.handleInlineChangeName}
maxLength={50}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">
<div>Archived</div>
<InfoMessage width={400}>
Archive will hide the workspace from the active menu. Commands and output will be
retained in history.
</InfoMessage>
</div>
<div className="settings-input">
<Toggle checked={this.session.archived.get()} onChange={this.handleChangeArchived} />
</div>
</div>
<div className="settings-field">
<div className="settings-label">
<div>Actions</div>
<InfoMessage width={400}>
Delete will remove the workspace, removing all commands and output from history.
</InfoMessage>
</div>
<div className="settings-input">
<div
onClick={this.handleDeleteSession}
className="button is-prompt-danger is-outlined is-small"
>
Delete Workspace
</div>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
@mobxReact.observer
class LineSettingsModal extends React.Component<{}, {}> {
rendererDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "lineSettings-rendererDropdownActive" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ScreenSettings-errorMessage" });
linenum: number;
constructor(props: any) {
super(props);
this.linenum = GlobalModel.lineSettingsModal.get();
if (this.linenum == null) {
return;
}
}
@boundMethod
closeModal(): void {
mobx.action(() => {
GlobalModel.lineSettingsModal.set(null);
})();
GlobalModel.modalsModel.popModal();
}
@boundMethod
handleChangeArchived(val: boolean): void {
let line = this.getLine();
if (line == null) {
return;
}
let prtn = GlobalCommandRunner.lineArchive(line.lineid, val);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
toggleRendererDropdown(): void {
mobx.action(() => {
this.rendererDropdownActive.set(!this.rendererDropdownActive.get());
})();
}
getLine(): LineType {
let screen = GlobalModel.getActiveScreen();
if (screen == null) {
return;
}
return screen.getLineByNum(this.linenum);
}
@boundMethod
clickSetRenderer(renderer: string): void {
let line = this.getLine();
if (line == null) {
return;
}
let prtn = GlobalCommandRunner.lineSet(line.lineid, { renderer: renderer });
commandRtnHandler(prtn, this.errorMessage);
mobx.action(() => {
this.rendererDropdownActive.set(false);
})();
}
getOptions(plugins: RendererPluginType[]) {
// Add label and value to each object in the array
const options = plugins.map((item) => ({
...item,
label: item.name,
value: item.name,
}));
// Create an additional object with label "terminal" and value null
const terminalItem = {
label: "terminal",
value: null,
name: null,
rendererType: null,
heightType: null,
dataType: null,
collapseType: null,
globalCss: null,
mimeTypes: null,
};
// Create an additional object with label "none" and value none
const noneItem = {
label: "none",
value: "none",
name: null,
rendererType: null,
heightType: null,
dataType: null,
collapseType: null,
globalCss: null,
mimeTypes: null,
};
// Combine the options with the terminal item
return [terminalItem, ...options, noneItem];
}
render() {
let line = this.getLine();
if (line == null) {
setTimeout(() => {
this.closeModal();
}, 0);
return null;
}
let plugins = PluginModel.rendererPlugins;
let renderer = line.renderer ?? "terminal";
return (
<Modal className="line-settings-modal">
<Modal.Header onClose={this.closeModal} title={`line settings (${line.linenum})`} />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Renderer</div>
<div className="settings-input">
<Dropdown
className="renderer-dropdown"
options={this.getOptions(plugins)}
defaultValue={renderer}
onChange={this.clickSetRenderer}
/>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
<div style={{ height: 50 }} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
@mobxReact.observer
class ClientSettingsModal extends React.Component<{}, {}> {
fontSizeDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "clientSettings-fontSizeDropdownActive" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ClientSettings-errorMessage" });
@boundMethod
closeModal(): void {
GlobalModel.modalsModel.popModal();
}
@boundMethod
dismissError(): void {
mobx.action(() => {
this.errorMessage.set(null);
})();
}
@boundMethod
handleChangeFontSize(fontSize: string): void {
let newFontSize = Number(fontSize);
this.fontSizeDropdownActive.set(false);
if (GlobalModel.termFontSize.get() == newFontSize) {
return;
}
let prtn = GlobalCommandRunner.setTermFontSize(newFontSize, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
togglefontSizeDropdown(): void {
mobx.action(() => {
this.fontSizeDropdownActive.set(!this.fontSizeDropdownActive.get());
})();
}
@boundMethod
handleChangeTelemetry(val: boolean): void {
let prtn: Promise<CommandRtnType> = null;
if (val) {
prtn = GlobalCommandRunner.telemetryOn(false);
} else {
prtn = GlobalCommandRunner.telemetryOff(false);
}
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeReleaseCheck(val: boolean): void {
let prtn: Promise<CommandRtnType> = null;
if (val) {
prtn = GlobalCommandRunner.releaseCheckAutoOn(false);
} else {
prtn = GlobalCommandRunner.releaseCheckAutoOff(false);
}
commandRtnHandler(prtn, this.errorMessage);
}
getFontSizes(): any {
let availableFontSizes: { label: string; value: number }[] = [];
for (let s = MinFontSize; s <= MaxFontSize; s++) {
availableFontSizes.push({ label: s + "px", value: s });
}
return availableFontSizes;
}
@boundMethod
inlineUpdateOpenAIModel(newModel: string): void {
let prtn = GlobalCommandRunner.setClientOpenAISettings({ model: newModel });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateOpenAIToken(newToken: string): void {
let prtn = GlobalCommandRunner.setClientOpenAISettings({ apitoken: newToken });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateOpenAIMaxTokens(newMaxTokensStr: string): void {
let prtn = GlobalCommandRunner.setClientOpenAISettings({ maxtokens: newMaxTokensStr });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
setErrorMessage(msg: string): void {
mobx.action(() => {
this.errorMessage.set(msg);
})();
}
render() {
let cdata: ClientDataType = GlobalModel.clientData.get();
let openAIOpts = cdata.openaiopts ?? {};
let apiTokenStr = util.isBlank(openAIOpts.apitoken) ? "(not set)" : "********";
let maxTokensStr = String(
openAIOpts.maxtokens == null || openAIOpts.maxtokens == 0 ? 1000 : openAIOpts.maxtokens
);
let curFontSize = GlobalModel.termFontSize.get();
return (
<Modal className="client-settings-modal">
<Modal.Header onClose={this.closeModal} title="Client settings" />
<div className="wave-modal-body">
<div className="settings-field">
<div className="settings-label">Term Font Size</div>
<div className="settings-input">
<Dropdown
className="font-size-dropdown"
options={this.getFontSizes()}
defaultValue={`${curFontSize}px`}
onChange={this.handleChangeFontSize}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Client ID</div>
<div className="settings-input">{cdata.clientid}</div>
</div>
<div className="settings-field">
<div className="settings-label">Client Version</div>
<div className="settings-input">
{VERSION} {BUILD}
</div>
</div>
<div className="settings-field">
<div className="settings-label">DB Version</div>
<div className="settings-input">{cdata.dbversion}</div>
</div>
<div className="settings-field">
<div className="settings-label">Basic Telemetry</div>
<div className="settings-input">
<Toggle checked={!cdata.clientopts.notelemetry} onChange={this.handleChangeTelemetry} />
</div>
</div>
<div className="settings-field">
<div className="settings-label">Check for Updates</div>
<div className="settings-input">
<Toggle
checked={!cdata.clientopts.noreleasecheck}
onChange={this.handleChangeReleaseCheck}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">OpenAI Token</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder=""
text={apiTokenStr}
value={""}
onChange={this.inlineUpdateOpenAIToken}
maxLength={100}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">OpenAI Model</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder="gpt-3.5-turbo"
text={util.isBlank(openAIOpts.model) ? "gpt-3.5-turbo" : openAIOpts.model}
value={openAIOpts.model ?? ""}
onChange={this.inlineUpdateOpenAIModel}
maxLength={100}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">OpenAI MaxTokens</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder=""
text={maxTokensStr}
value={maxTokensStr}
onChange={this.inlineUpdateOpenAIMaxTokens}
maxLength={10}
showIcon={true}
/>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
<Modal.Footer cancelLabel="Close" onCancel={this.closeModal} />
</Modal>
);
}
}
export {
ScreenSettingsModal,
SessionSettingsModal,
LineSettingsModal,
ClientSettingsModal,
WebStopShareConfirmMarkdown,
};