mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
User Input (#119)
Port the User Input feature from the previous version of the app. This is currently being used to verify a few different prompts for ssh.
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
background-color: rgba(21, 23, 21, 0.7);
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
flex-direction: column;
|
||||
border-radius: 10px;
|
||||
padding: 0;
|
||||
width: 80vw;
|
||||
height: 80vh;
|
||||
width: 80%;
|
||||
margin-top: 25vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@@ -70,7 +70,7 @@ interface WaveModalProps {
|
||||
|
||||
function WaveModal({ title, description, onSubmit, onCancel, buttonLabel = "Ok", children }: WaveModalProps) {
|
||||
return (
|
||||
<Modal id="text-box-modal" onClickOut={onCancel}>
|
||||
<Modal onClickOut={onCancel}>
|
||||
<ModalHeader title={title} description={description} />
|
||||
<ModalContent>{children}</ModalContent>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.userinput-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
|
||||
font: var(--fixed-font);
|
||||
color: var(--main-text-color);
|
||||
|
||||
.userinput-markdown {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.userinput-text {
|
||||
}
|
||||
|
||||
.userinput-inputbox {
|
||||
resize: none;
|
||||
background-color: var(--panel-bg-color);
|
||||
border-radius: 6px;
|
||||
margin: 0;
|
||||
border: var(--border-color);
|
||||
padding: 5px 0 5px 16px;
|
||||
min-height: 30px;
|
||||
|
||||
&:hover {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline-color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Markdown } from "@/element/markdown";
|
||||
import { WaveModal } from "@/element/modal";
|
||||
import { atoms } from "@/store/global";
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
import { UserInputService } from "../store/services";
|
||||
|
||||
import "./userinputmodal.less";
|
||||
|
||||
export const UserInputModal = (userInputRequest: UserInputRequest) => {
|
||||
const setModals = jotai.useSetAtom(atoms.userInput);
|
||||
const [responseText, setResponseText] = React.useState("");
|
||||
const [countdown, setCountdown] = React.useState(Math.floor(userInputRequest.timeoutms / 1000));
|
||||
const checkboxStatus = React.useRef(false);
|
||||
|
||||
const handleSendCancel = React.useCallback(() => {
|
||||
UserInputService.SendUserInputResponse({
|
||||
type: "userinputresp",
|
||||
requestid: userInputRequest.requestid,
|
||||
errormsg: "Canceled by the user",
|
||||
});
|
||||
setModals((prev) => {
|
||||
prev.pop();
|
||||
return [...prev];
|
||||
});
|
||||
}, [responseText, userInputRequest]);
|
||||
|
||||
const handleSendText = React.useCallback(() => {
|
||||
UserInputService.SendUserInputResponse({
|
||||
type: "userinputresp",
|
||||
requestid: userInputRequest.requestid,
|
||||
text: responseText,
|
||||
checkboxstat: checkboxStatus.current,
|
||||
});
|
||||
setModals((prev) => {
|
||||
prev.pop();
|
||||
return [...prev];
|
||||
});
|
||||
}, [responseText, userInputRequest]);
|
||||
|
||||
const handleSendConfirm = React.useCallback(() => {
|
||||
UserInputService.SendUserInputResponse({
|
||||
type: "userinputresp",
|
||||
requestid: userInputRequest.requestid,
|
||||
confirm: true,
|
||||
checkboxstat: checkboxStatus.current,
|
||||
});
|
||||
setModals((prev) => {
|
||||
prev.pop();
|
||||
return [...prev];
|
||||
});
|
||||
}, [userInputRequest]);
|
||||
|
||||
const handleSubmit = React.useCallback(() => {
|
||||
switch (userInputRequest.responsetype) {
|
||||
case "text":
|
||||
handleSendText();
|
||||
break;
|
||||
case "confirm":
|
||||
handleSendConfirm();
|
||||
break;
|
||||
}
|
||||
}, [handleSendConfirm, handleSendText, userInputRequest.responsetype]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(waveEvent: WaveKeyboardEvent): boolean => {
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Escape")) {
|
||||
handleSendCancel();
|
||||
return;
|
||||
}
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
||||
handleSubmit();
|
||||
return true;
|
||||
}
|
||||
},
|
||||
[handleSendCancel, handleSubmit]
|
||||
);
|
||||
|
||||
const queryText = React.useMemo(() => {
|
||||
if (userInputRequest.markdown) {
|
||||
return <Markdown text={userInputRequest.querytext} className="userinput-markdown" />;
|
||||
}
|
||||
return <span className="userinput-text">{userInputRequest.querytext}</span>;
|
||||
}, [userInputRequest.markdown, userInputRequest.querytext]);
|
||||
|
||||
const inputBox = React.useMemo(() => {
|
||||
if (userInputRequest.responsetype === "confirm") {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<input
|
||||
type={userInputRequest.publictext ? "text" : "password"}
|
||||
onChange={(e) => setResponseText(e.target.value)}
|
||||
value={responseText}
|
||||
maxLength={400}
|
||||
className="userinput-inputbox"
|
||||
autoFocus={true}
|
||||
onKeyDown={(e) => keyutil.keydownWrapper(handleKeyDown)(e)}
|
||||
/>
|
||||
);
|
||||
}, [userInputRequest.responsetype, userInputRequest.publictext, responseText, handleKeyDown, setResponseText]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
if (countdown == 0) {
|
||||
timeout = setTimeout(() => {
|
||||
handleSendCancel();
|
||||
}, 300);
|
||||
} else {
|
||||
timeout = setTimeout(() => {
|
||||
setCountdown(countdown - 1);
|
||||
}, 1000);
|
||||
}
|
||||
return () => clearTimeout(timeout);
|
||||
}, [countdown]);
|
||||
|
||||
return (
|
||||
<WaveModal
|
||||
title={userInputRequest.title + ` (${countdown}s)`}
|
||||
onSubmit={() => handleSubmit()}
|
||||
onCancel={() => handleSendCancel()}
|
||||
>
|
||||
<div className="userinput-body">
|
||||
{queryText}
|
||||
{inputBox}
|
||||
</div>
|
||||
</WaveModal>
|
||||
);
|
||||
};
|
||||
@@ -81,6 +81,7 @@ const activeTabIdAtom: jotai.Atom<string> = jotai.atom((get) => {
|
||||
}
|
||||
return windowData.activetabid;
|
||||
});
|
||||
const userInputAtom = jotai.atom([]) as jotai.PrimitiveAtom<Array<UserInputRequest>>;
|
||||
|
||||
const atoms = {
|
||||
// initialized in wave.ts (will not be null inside of application)
|
||||
@@ -93,6 +94,7 @@ const atoms = {
|
||||
settingsConfigAtom: settingsConfigAtom,
|
||||
tabAtom: tabAtom,
|
||||
activeTabId: activeTabIdAtom,
|
||||
userInput: userInputAtom,
|
||||
};
|
||||
|
||||
// key is "eventType" or "eventType|oref"
|
||||
@@ -208,6 +210,13 @@ function handleWSEventMessage(msg: WSEventType) {
|
||||
console.log("config", data);
|
||||
return;
|
||||
}
|
||||
if (msg.eventtype == "userinput") {
|
||||
// handle user input
|
||||
const data: UserInputRequest = msg.data;
|
||||
console.log(data);
|
||||
globalStore.set(userInputAtom, (prev) => [...prev, data]);
|
||||
return;
|
||||
}
|
||||
if (msg.eventtype == "blockfile") {
|
||||
const fileData: WSFileEventData = msg.data;
|
||||
const fileSubject = getFileSubject(fileData.zoneid, fileData.filename);
|
||||
|
||||
@@ -126,6 +126,15 @@ class ObjectServiceType {
|
||||
|
||||
export const ObjectService = new ObjectServiceType();
|
||||
|
||||
// userinputservice.UserInputService (userinput)
|
||||
class UserInputServiceType {
|
||||
SendUserInputResponse(arg1: UserInputResponse): Promise<void> {
|
||||
return WOS.callBackendService("userinput", "SendUserInputResponse", Array.from(arguments))
|
||||
}
|
||||
}
|
||||
|
||||
export const UserInputService = new UserInputServiceType();
|
||||
|
||||
// windowservice.WindowService (window)
|
||||
class WindowServiceType {
|
||||
// @returns object updates
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import { TabBar } from "@/app/tab/tabbar";
|
||||
import { TabContent } from "@/app/tab/tabcontent";
|
||||
import { UserInputModal } from "@/element/userinputmodal";
|
||||
import { atoms, createBlock } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as util from "@/util/util";
|
||||
@@ -106,9 +107,11 @@ const Widgets = React.memo(() => {
|
||||
const WorkspaceElem = React.memo(() => {
|
||||
const windowData = jotai.useAtomValue(atoms.waveWindow);
|
||||
const activeTabId = windowData?.activetabid;
|
||||
const modals = jotai.useAtomValue(atoms.userInput);
|
||||
const ws = jotai.useAtomValue(atoms.workspace);
|
||||
return (
|
||||
<div className="workspace">
|
||||
{modals.length > 0 && <UserInputModal {...modals[modals.length - 1]} />}
|
||||
<TabBar key={ws.oid} workspace={ws} />
|
||||
<div className="workspace-tabcontent">
|
||||
{activeTabId == "" ? (
|
||||
|
||||
Vendored
+22
@@ -262,6 +262,28 @@ declare global {
|
||||
activetabid: string;
|
||||
};
|
||||
|
||||
// userinput.UserInputRequest
|
||||
type UserInputRequest = {
|
||||
requestid: string;
|
||||
querytext: string;
|
||||
responsetype: string;
|
||||
title: string;
|
||||
markdown: boolean;
|
||||
timeoutms: number;
|
||||
checkboxmsg: string;
|
||||
publictext: boolean;
|
||||
};
|
||||
|
||||
// userinput.UserInputResponse
|
||||
type UserInputResponse = {
|
||||
type: string;
|
||||
requestid: string;
|
||||
text?: string;
|
||||
confirm?: boolean;
|
||||
errormsg?: string;
|
||||
checkboxstat?: boolean;
|
||||
};
|
||||
|
||||
type WSCommandType = {
|
||||
wscommand: string;
|
||||
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand | WSRpcCommand );
|
||||
|
||||
Reference in New Issue
Block a user