mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
Add a modal confirmation before installing WaveShell (#212)
* init * integrate showShellPrompt flag * renive debugging code * remove debugging code * run gofmt. add migration files. * remove debugging code * remove migrations and adjust code. show prompt on import ssh configs as well. * fix show/hide logic * reset mmap.go * use resolveBool and utilfn.ContainsStr * make AlertModal take a generic 'confirmkey' instead of hard coding hideShellPrompt * rename confirmkey to confirmflag (to be consistent). move confirmflag checking into the alertmodal. short circuit with Promise.resolve(true) if noConfirm checked. * disable buttons while status is 'connecting' * minor refactor
This commit is contained in:
parent
00e709d515
commit
8f39f0fc5e
@ -13,4 +13,6 @@ export const LineContainer_Main = "main";
|
||||
export const LineContainer_History = "history";
|
||||
export const LineContainer_Sidebar = "sidebar";
|
||||
|
||||
export const ConfirmKey_HideShellPrompt = "hideshellprompt";
|
||||
|
||||
export const NoStrPos = -1;
|
||||
|
@ -188,14 +188,14 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #9e9e9e;
|
||||
color: @term-bright-white;
|
||||
transition: color 250ms cubic-bezier(0.4, 0, 0.23, 1);
|
||||
}
|
||||
input[type="checkbox"] + label > span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 16px;
|
||||
margin-right: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: transparent;
|
||||
@ -205,10 +205,6 @@
|
||||
transition: all 250ms cubic-bezier(0.4, 0, 0.23, 1);
|
||||
}
|
||||
|
||||
input[type="checkbox"] + label:hover,
|
||||
input[type="checkbox"]:focus + label {
|
||||
color: #fff;
|
||||
}
|
||||
input[type="checkbox"] + label:hover > span,
|
||||
input[type="checkbox"]:focus + label > span {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
@ -99,23 +99,57 @@ class Toggle extends React.Component<{ checked: boolean; onChange: (value: boole
|
||||
}
|
||||
|
||||
class Checkbox extends React.Component<
|
||||
{ checked: boolean; onChange: (value: boolean) => void; label: React.ReactNode; id: string },
|
||||
{}
|
||||
{
|
||||
checked?: boolean;
|
||||
defaultChecked?: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
label: React.ReactNode;
|
||||
className?: string;
|
||||
id?: string;
|
||||
},
|
||||
{ checkedInternal: boolean }
|
||||
> {
|
||||
generatedId;
|
||||
static idCounter = 0;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
checkedInternal: this.props.checked !== undefined ? this.props.checked : Boolean(this.props.defaultChecked),
|
||||
};
|
||||
this.generatedId = `checkbox-${Checkbox.idCounter++}`;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.checked !== undefined && this.props.checked !== prevProps.checked) {
|
||||
this.setState({ checkedInternal: this.props.checked });
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
const newChecked = e.target.checked;
|
||||
if (this.props.checked === undefined) {
|
||||
this.setState({ checkedInternal: newChecked });
|
||||
}
|
||||
this.props.onChange(newChecked);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { checked, onChange, label, id } = this.props;
|
||||
const { label, className, id } = this.props;
|
||||
const { checkedInternal } = this.state;
|
||||
const checkboxId = id || this.generatedId;
|
||||
|
||||
return (
|
||||
<div className="checkbox">
|
||||
<div className={cn("checkbox", className)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
checked={checked}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
aria-checked={checked}
|
||||
id={checkboxId}
|
||||
checked={checkedInternal}
|
||||
onChange={this.handleChange}
|
||||
aria-checked={checkedInternal}
|
||||
role="checkbox"
|
||||
/>
|
||||
<label htmlFor={id}>
|
||||
<label htmlFor={checkboxId}>
|
||||
<span></span>
|
||||
{label}
|
||||
</label>
|
||||
|
@ -673,11 +673,15 @@
|
||||
}
|
||||
|
||||
.alert-modal {
|
||||
width: 500px;
|
||||
width: 510px;
|
||||
|
||||
.wave-modal-content {
|
||||
.wave-modal-body {
|
||||
padding: 40px 20px;
|
||||
|
||||
.dontshowagain-text {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,11 @@ import {
|
||||
Tooltip,
|
||||
Button,
|
||||
Status,
|
||||
Checkbox,
|
||||
} from "../common";
|
||||
import * as util from "../../../util/util";
|
||||
import * as textmeasure from "../../../util/textmeasure";
|
||||
import * as appconst from "../../appconst";
|
||||
import { ClientDataType } from "../../../types/types";
|
||||
import { Screen } from "../../../model/model";
|
||||
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
||||
@ -216,6 +218,15 @@ class AlertModal extends React.Component<{}, {}> {
|
||||
GlobalModel.confirmAlert();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleDontShowAgain(checked: boolean) {
|
||||
let message = GlobalModel.alertMessage.get();
|
||||
if (message.confirmflag == null) {
|
||||
return;
|
||||
}
|
||||
GlobalCommandRunner.clientSetConfirmFlag(message.confirmflag, checked);
|
||||
}
|
||||
|
||||
render() {
|
||||
let message = GlobalModel.alertMessage.get();
|
||||
let title = message?.title ?? (message?.confirm ? "Confirm" : "Alert");
|
||||
@ -229,16 +240,27 @@ class AlertModal extends React.Component<{}, {}> {
|
||||
<Markdown text={message?.message ?? ""} />
|
||||
</If>
|
||||
<If condition={!message?.markdown}>{message?.message}</If>
|
||||
<If condition={message.confirmflag}>
|
||||
<Checkbox
|
||||
onChange={this.handleDontShowAgain}
|
||||
label={"Don't show me this again"}
|
||||
className="dontshowagain-text"
|
||||
/>
|
||||
</If>
|
||||
</div>
|
||||
<div className="wave-modal-footer">
|
||||
<If condition={isConfirm}>
|
||||
<Button theme="secondary" onClick={this.closeModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button autoFocus={true} onClick={this.handleOK}>Ok</Button>
|
||||
<Button autoFocus={true} onClick={this.handleOK}>
|
||||
Ok
|
||||
</Button>
|
||||
</If>
|
||||
<If condition={!isConfirm}>
|
||||
<Button autoFocus={true} onClick={this.handleOK}>Ok</Button>
|
||||
<Button autoFocus={true} onClick={this.handleOK}>
|
||||
Ok
|
||||
</Button>
|
||||
</If>
|
||||
</div>
|
||||
</Modal>
|
||||
@ -503,6 +525,10 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
this.errorStr = mobx.observable.box(this.remoteEdit?.errorstr ?? null, { name: "CreateRemote-errorStr" });
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
GlobalModel.getClientData();
|
||||
}
|
||||
|
||||
remoteCName(): string {
|
||||
let hostName = this.tempHostName.get();
|
||||
if (hostName == "") {
|
||||
@ -521,6 +547,27 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
return this.remoteEdit?.errorstr ?? null;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleOk(): void {
|
||||
this.showShellPrompt(this.submitRemote);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
showShellPrompt(cb: () => void): void {
|
||||
let prtn = GlobalModel.showAlert({
|
||||
message:
|
||||
"You are about to install WaveShell on a remote machine. Please be aware that WaveShell will be executed on the remote system.",
|
||||
confirm: true,
|
||||
confirmflag: appconst.ConfirmKey_HideShellPrompt,
|
||||
});
|
||||
prtn.then((confirm) => {
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
submitRemote(): void {
|
||||
mobx.action(() => {
|
||||
@ -581,12 +628,6 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleClose(): void {
|
||||
this.model.closeModal();
|
||||
this.model.setRecentConnAdded(false);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeKeyFile(value: string): void {
|
||||
mobx.action(() => {
|
||||
@ -802,7 +843,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
<div className="settings-field settings-error">Error: {this.getErrorStr()}</div>
|
||||
</If>
|
||||
</div>
|
||||
<Modal.Footer onCancel={this.handleClose} onOk={this.submitRemote} okLabel="Connect" />
|
||||
<Modal.Footer onCancel={this.model.closeModal} onOk={this.handleOk} okLabel="Connect" />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -1097,6 +1138,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
let termFontSize = GlobalModel.termFontSize.get();
|
||||
let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize);
|
||||
let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias;
|
||||
let selectedRemoteStatus = this.getSelectedRemote().status;
|
||||
|
||||
return (
|
||||
<Modal className="rconndetail-modal">
|
||||
@ -1175,7 +1217,18 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal.Footer onOk={this.handleClose} onCancel={this.handleClose} okLabel="Done" />
|
||||
<div className="wave-modal-footer">
|
||||
<Button
|
||||
theme="secondary"
|
||||
disabled={selectedRemoteStatus == "connecting"}
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={selectedRemoteStatus == "connecting"} onClick={this.handleClose}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import { GlobalModel, RemotesModel, GlobalCommandRunner } from "../../model/mode
|
||||
import { Button, IconButton, Status } from "../common/common";
|
||||
import * as T from "../../types/types";
|
||||
import * as util from "../../util/util";
|
||||
import * as appconst from "../appconst";
|
||||
|
||||
import "./connections.less";
|
||||
|
||||
@ -74,10 +75,31 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleImportSshConfig(): void {
|
||||
importSshConfig(): void {
|
||||
GlobalCommandRunner.importSshConfig();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleImportSshConfig(): void {
|
||||
this.showShellPrompt(this.importSshConfig);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
showShellPrompt(cb: () => void): void {
|
||||
let prtn = GlobalModel.showAlert({
|
||||
message:
|
||||
"You are about to install WaveShell on a remote machine. Please be aware that WaveShell will be executed on the remote system.",
|
||||
confirm: true,
|
||||
confirmflag: appconst.ConfirmKey_HideShellPrompt,
|
||||
});
|
||||
prtn.then((confirm) => {
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleRead(remoteId: string): void {
|
||||
GlobalModel.remotesModel.openReadModal(remoteId);
|
||||
@ -163,8 +185,8 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered
|
||||
onClick={() => this.handleRead(item.remoteid)} // Moved onClick here
|
||||
>
|
||||
<td className="col-name">
|
||||
<Status status={this.getStatus(item.status)} text=""></Status>
|
||||
{this.getName(item)} {this.getImportSymbol(item)}
|
||||
<Status status={this.getStatus(item.status)} text=""></Status>
|
||||
{this.getName(item)} {this.getImportSymbol(item)}
|
||||
</td>
|
||||
<td className="col-type">
|
||||
<div>{item.remotetype}</div>
|
||||
|
@ -3287,6 +3287,13 @@ class Model {
|
||||
}
|
||||
|
||||
showAlert(alertMessage: AlertMessageType): Promise<boolean> {
|
||||
if (alertMessage.confirmflag != null) {
|
||||
let cdata = GlobalModel.clientData.get();
|
||||
let noConfirm = cdata.clientopts?.confirmflags?.[alertMessage.confirmflag];
|
||||
if (noConfirm) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
mobx.action(() => {
|
||||
this.alertMessage.set(alertMessage);
|
||||
GlobalModel.modalsModel.pushModal(appconst.ALERT);
|
||||
@ -4652,6 +4659,12 @@ class CommandRunner {
|
||||
GlobalModel.submitCommand("client", "accepttos", null, { nohist: "1" }, true);
|
||||
}
|
||||
|
||||
clientSetConfirmFlag(flag: string, value: boolean): Promise<CommandRtnType> {
|
||||
let kwargs = { nohist: "1" };
|
||||
let valueStr = value ? "1" : "0";
|
||||
return GlobalModel.submitCommand("client", "setconfirmflag", [flag, valueStr], kwargs, false);
|
||||
}
|
||||
|
||||
editBookmark(bookmarkId: string, desc: string, cmdstr: string) {
|
||||
let kwargs = {
|
||||
nohist: "1",
|
||||
|
@ -473,10 +473,15 @@ type FeOptsType = {
|
||||
termfontsize: number;
|
||||
};
|
||||
|
||||
type ConfirmFlagsType = {
|
||||
[k: string]: boolean;
|
||||
};
|
||||
|
||||
type ClientOptsType = {
|
||||
notelemetry: boolean;
|
||||
noreleasecheck: boolean;
|
||||
acceptedtos: number;
|
||||
confirmflags: ConfirmFlagsType;
|
||||
};
|
||||
|
||||
type ReleaseInfoType = {
|
||||
@ -525,6 +530,7 @@ type AlertMessageType = {
|
||||
message: string;
|
||||
confirm?: boolean;
|
||||
markdown?: boolean;
|
||||
confirmflag?: string;
|
||||
};
|
||||
|
||||
type HistorySearchParams = {
|
||||
|
@ -89,6 +89,7 @@ var ColorNames = []string{"yellow", "blue", "pink", "mint", "cyan", "violet", "o
|
||||
var TabIcons = []string{"square", "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"}
|
||||
var ConfirmFlags = []string{"hideshellprompt"}
|
||||
|
||||
var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"}
|
||||
var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"}
|
||||
@ -213,6 +214,7 @@ func init() {
|
||||
registerCmdFn("client:set", ClientSetCommand)
|
||||
registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand)
|
||||
registerCmdFn("client:accepttos", ClientAcceptTosCommand)
|
||||
registerCmdFn("client:setconfirmflag", ClientConfirmFlagCommand)
|
||||
|
||||
registerCmdFn("sidebar:open", SidebarOpenCommand)
|
||||
registerCmdFn("sidebar:close", SidebarCloseCommand)
|
||||
@ -4089,6 +4091,57 @@ func ClientAcceptTosCommand(ctx context.Context, pk *scpacket.FeCommandPacketTyp
|
||||
return update, nil
|
||||
}
|
||||
|
||||
var confirmKeyRe = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
|
||||
|
||||
// confirm flags must be all lowercase and only contain letters, numbers, and underscores (and start with letter)
|
||||
func ClientConfirmFlagCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
||||
// Check for valid arguments length
|
||||
if len(pk.Args) < 2 {
|
||||
return nil, fmt.Errorf("invalid arguments: expected at least 2, got %d", len(pk.Args))
|
||||
}
|
||||
|
||||
// Extract confirmKey and value from pk.Args
|
||||
confirmKey := pk.Args[0]
|
||||
if !confirmKeyRe.MatchString(confirmKey) {
|
||||
return nil, fmt.Errorf("invalid confirm flag key: %s", confirmKey)
|
||||
}
|
||||
value := resolveBool(pk.Args[1], true)
|
||||
validKey := utilfn.ContainsStr(ConfirmFlags, confirmKey)
|
||||
if !validKey {
|
||||
return nil, fmt.Errorf("invalid confirm flag key: %s", confirmKey)
|
||||
}
|
||||
|
||||
clientData, err := sstore.EnsureClientData(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
||||
}
|
||||
|
||||
// Initialize ConfirmFlags if it's nil
|
||||
if clientData.ClientOpts.ConfirmFlags == nil {
|
||||
clientData.ClientOpts.ConfirmFlags = make(map[string]bool)
|
||||
}
|
||||
|
||||
// Set the confirm flag
|
||||
clientData.ClientOpts.ConfirmFlags[confirmKey] = value
|
||||
|
||||
err = sstore.SetClientOpts(ctx, clientData.ClientOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error updating client data: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve updated client data
|
||||
clientData, err = sstore.EnsureClientData(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
|
||||
}
|
||||
|
||||
update := &sstore.ModelUpdate{
|
||||
ClientData: clientData,
|
||||
}
|
||||
|
||||
return update, nil
|
||||
}
|
||||
|
||||
func validateOpenAIAPIToken(key string) error {
|
||||
if len(key) > MaxOpenAIAPITokenLen {
|
||||
return fmt.Errorf("invalid openai token, too long")
|
||||
|
@ -11,10 +11,10 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -240,7 +240,8 @@ func (c *parseContext) tokenizeDQ() ([]*WordType, bool) {
|
||||
|
||||
// returns (words, eofexit)
|
||||
// backticks (WordTypeBQ) handle backslash in a special way, but that seems to mainly effect execution (not completion)
|
||||
// de_backslash => removes initial backslash in \`, \\, and \$ before execution
|
||||
//
|
||||
// de_backslash => removes initial backslash in \`, \\, and \$ before execution
|
||||
func (c *parseContext) tokenizeRaw() ([]*WordType, bool) {
|
||||
state := &tokenizeOutputState{}
|
||||
isExpSubShell := c.QC.cur() == WordTypeDP
|
||||
|
@ -267,9 +267,10 @@ func (tdata *TelemetryData) Scan(val interface{}) error {
|
||||
}
|
||||
|
||||
type ClientOptsType struct {
|
||||
NoTelemetry bool `json:"notelemetry,omitempty"`
|
||||
NoReleaseCheck bool `json:"noreleasecheck,omitempty"`
|
||||
AcceptedTos int64 `json:"acceptedtos,omitempty"`
|
||||
NoTelemetry bool `json:"notelemetry,omitempty"`
|
||||
NoReleaseCheck bool `json:"noreleasecheck,omitempty"`
|
||||
AcceptedTos int64 `json:"acceptedtos,omitempty"`
|
||||
ConfirmFlags map[string]bool `json:"confirmflags,omitempty"`
|
||||
}
|
||||
|
||||
type FeOptsType struct {
|
||||
|
Loading…
Reference in New Issue
Block a user