Compare commits

..

8 Commits

Author SHA1 Message Date
Sylvia Crowe d1bffb5a0d merge branch 'main' into use-ssh-library 2024-01-29 13:01:50 -08:00
Sylvia Crowe df94b91e6c feat: create backend for user input requests
This is the first part of a change that allows the backend to request
user input from the frontend. Essentially, the backend will send a
request for the user to answer some query, and the frontend will send
that answer back. It is blocking, so it needs to be used within a
goroutine.

There is some placeholder code in the frontend that will be updated in
future commits. Similarly, there is some debug code in the backend
remote.go file.
2024-01-29 12:43:47 -08:00
Sylvia Crowe 1547fc856e fix: allow retry after ssh auth failure
Previously, the error status was not set when an ssh connection failed.
Because of this, an ssh connection failure would lock the failed remote
until waveterm was rebooted. This fix properly sets the error status so
this cannot happen.
2024-01-24 20:54:37 -08:00
Sylvia Crowe 37821738d8 allow switching between new and old ssh for dev
It is inconvenient to create milestones without being able to merge into
the main branch. But due to the experimental nature of the ssh changes,
it is not desired to use these changes in the main branch yet. This
change disables the new ssh launcher by default. It can be used by
changing the UseSshLibrary constant to true in remote.go. With this, it
becomes possible to merge these changes into the main branch without
them being used in production.
2024-01-24 20:18:27 -08:00
Sylvia Crowe 8e79eeccca merge branch 'main' into use-ssh-library 2024-01-24 18:29:20 -08:00
Sylvia Crowe 203f1f7505 clean up old mshell launch methods
In the debugging the addition of the ssh library, i had several versions
of the MShellProc Launch function. Since this seems mostly stable, I
have removed the old version and the experimental version in favor of
the combined version.
2024-01-24 17:19:52 -08:00
Sylvia Crowe 57a7641f82 add password and keyboard-interactive ssh auth
This adds several new ssh auth methods. In addition to the PublicKey
method used previously, this adds password authentication,
keyboard-interactive authentication, and PublicKey+Passphrase
authentication.

Furthermore, it refactores the ssh connection code into its own wavesrv
file rather than storing int in waveshell's shexec file.
2024-01-24 17:16:22 -08:00
Sylvia Crowe 03ea444030 create proof of concept ssh library integration
This is a first attempt to integrate the golang crypto/ssh library for
handling remote connections. As it stands, this features is limited to
identity files without passphrases. It needs to be expanded to include
key+passphrase and password verifications as well.
2024-01-22 23:18:49 -08:00
178 changed files with 10317 additions and 12741 deletions
+2 -19
View File
@@ -1,21 +1,4 @@
{
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
+1 -3
View File
@@ -10,7 +10,7 @@
# Wave Terminal
A open-source, cross-platform, AI-integrated, modern terminal for seamless workflows.
A open-source, cross-platform, modern terminal for seamless workflows.
Wave isn't just another terminal emulator; it's a rethink on how terminals are built. Wave combines command line with the power of the open web to help veteran CLI users and new developers alike.
@@ -18,7 +18,6 @@ Wave isn't just another terminal emulator; it's a rethink on how terminals are b
* Persistent sessions that can restore state across network disconnections and reboots
* Searchable contextual command history across all remote sessions (saved locally)
* Workspaces, tabs, and command blocks to keep you organized
* AI Integration with ChatGPT (or ChatGPT compatible APIs) to help write commands and get answers inline
## Installation
@@ -36,7 +35,6 @@ brew install --cask wave
* Homepage — https://www.waveterm.dev
* Download Page — https://www.waveterm.dev/download
* Documentation — https://docs.waveterm.dev/
* Blog — https://blog.waveterm.dev/
* Quick Start Guide — https://docs.waveterm.dev/quickstart/
* Discord Community — https://discord.gg/XfvZ334gwU
-158
View File
@@ -664,161 +664,3 @@ 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;
}
}
+10 -15
View File
@@ -7,9 +7,10 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import dayjs from "dayjs";
import type { ContextMenuOpts } from "../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "@/models";
import { isBlank } from "@/util/util";
import { GlobalModel } from "../model/model";
import { isBlank } from "../util/util";
import { WorkspaceView } from "./workspace/workspaceview";
import { PluginsView } from "./pluginsview/pluginsview";
import { BookmarksView } from "./bookmarks/bookmarks";
@@ -24,12 +25,13 @@ import "./app.less";
dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class App extends React.Component<{}, {}> {
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
mainContentRef: React.RefObject<HTMLDivElement> = React.createRef();
constructor(props: {}) {
constructor(props: any) {
super(props);
if (GlobalModel.isDev) document.body.className = "is-dev";
}
@@ -73,13 +75,6 @@ class App extends React.Component<{}, {}> {
let hasClientStop = GlobalModel.getHasClientStop();
let dcWait = this.dcWait.get();
let platform = GlobalModel.getPlatform();
let clientData = GlobalModel.clientData.get();
// Previously, this is done in sidebar.tsx but it causes flicker when clientData is null cos screen-view shifts around.
// Doing it here fixes the flicker cos app is not rendered until clientData is populated.
if (clientData == null) {
return null;
}
if (disconnected || hasClientStop) {
if (!dcWait) {
@@ -87,8 +82,8 @@ class App extends React.Component<{}, {}> {
}
return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div ref={this.mainContentRef} className="main-content">
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<div className="main-content">
<MainSideBar />
<div className="session-view" />
</div>
<If condition={dcWait}>
@@ -107,8 +102,8 @@ class App extends React.Component<{}, {}> {
}
return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div ref={this.mainContentRef} className="main-content">
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<div className="main-content">
<MainSideBar />
<ErrorBoundary>
<PluginsView />
<WorkspaceView />
-41
View File
@@ -8,7 +8,6 @@ export const SESSION_SETTINGS = "sessionSettings";
export const LINE_SETTINGS = "lineSettings";
export const CLIENT_SETTINGS = "clientSettings";
export const TAB_SWITCHER = "tabSwitcher";
export const USER_INPUT = "userInput";
export const LineContainer_Main = "main";
export const LineContainer_History = "history";
@@ -17,43 +16,3 @@ export const LineContainer_Sidebar = "sidebar";
export const ConfirmKey_HideShellPrompt = "hideshellprompt";
export const NoStrPos = -1;
export const RemotePtyRows = 8; // also in main.tsx
export const RemotePtyCols = 80;
export const ProdServerEndpoint = "http://127.0.0.1:1619";
export const ProdServerWsEndpoint = "ws://127.0.0.1:1623";
export const DevServerEndpoint = "http://127.0.0.1:8090";
export const DevServerWsEndpoint = "ws://127.0.0.1:8091";
export const DefaultTermFontSize = 12;
export const MinFontSize = 8;
export const MaxFontSize = 24;
export const InputChunkSize = 500;
export const RemoteColors = ["red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"];
export const TabColors = ["red", "orange", "yellow", "green", "mint", "cyan", "blue", "violet", "pink", "white"];
export const TabIcons = [
"sparkle",
"fire",
"ghost",
"cloud",
"compass",
"crown",
"droplet",
"graduation-cap",
"heart",
"file",
];
// @ts-ignore
export const VERSION = __WAVETERM_VERSION__;
// @ts-ignore
export const BUILD = __WAVETERM_BUILD__;
/**
* Levels for the screen status indicator
*/
export enum StatusIndicatorLevel {
None = 0,
Output = 1,
Success = 2,
Error = 3,
}
+1 -1
View File
@@ -1,4 +1,4 @@
@import "@/common/themes/themes.less";
@import "../../app/common/themes/themes.less";
.bookmarks-view {
background-color: @background-session;
+9 -20
View File
@@ -7,27 +7,20 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { GlobalModel } from "@/models";
import { CmdStrCode, Markdown } from "@/common/elements";
import type { BookmarkType } from "../../types/types";
import { GlobalModel } from "../../model/model";
import { CmdStrCode, Markdown } from "../common/common";
import { ReactComponent as XmarkIcon } from "@/assets/icons/line/xmark.svg";
import { ReactComponent as CopyIcon } from "@/assets/icons/favourites/copy.svg";
import { ReactComponent as PenIcon } from "@/assets/icons/favourites/pen.svg";
import { ReactComponent as TrashIcon } from "@/assets/icons/favourites/trash.svg";
import { ReactComponent as FavoritesIcon } from "@/assets/icons/favourites.svg";
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
import { ReactComponent as CopyIcon } from "../assets/icons/favourites/copy.svg";
import { ReactComponent as PenIcon } from "../assets/icons/favourites/pen.svg";
import { ReactComponent as TrashIcon } from "../assets/icons/favourites/trash.svg";
import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg";
import "./bookmarks.less";
type BookmarkProps = {
bookmark: BookmarkType;
};
@mobxReact.observer
class Bookmark extends React.Component<BookmarkProps, {}> {
constructor(props: BookmarkProps) {
super(props);
}
class Bookmark extends React.Component<{ bookmark: BookmarkType }, {}> {
@boundMethod
handleDeleteClick(): void {
let { bookmark } = this.props;
@@ -186,10 +179,6 @@ class Bookmark extends React.Component<BookmarkProps, {}> {
@mobxReact.observer
class BookmarksView extends React.Component<{}, {}> {
constructor(props: {}) {
super(props);
}
@boundMethod
closeView(): void {
GlobalModel.bookmarksModel.closeView();
+1 -1
View File
@@ -1,4 +1,4 @@
@import "@/common/themes/themes.less";
@import "../../app/common/themes/themes.less";
.clientsettings-view {
background-color: @background-session;
+28 -56
View File
@@ -6,18 +6,23 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "@/models";
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "@/common/elements";
import { commandRtnHandler, isBlank } from "@/util/util";
import * as appconst from "@/app/appconst";
import { GlobalModel, GlobalCommandRunner, MinFontSize, MaxFontSize, RemotesModel } from "../../model/model";
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "../common/common";
import { CommandRtnType, ClientDataType } from "../../types/types";
import { commandRtnHandler, isBlank } from "../../util/util";
import "./clientsettings.less";
type OV<V> = mobx.IObservableValue<V>;
// @ts-ignore
const VERSION = __WAVETERM_VERSION__;
// @ts-ignore
const BUILD = __WAVETERM_BUILD__;
@mobxReact.observer
class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hoveredItemId: string }> {
fontSizeDropdownActive: OV<boolean> = mobx.observable.box(false, {
name: "clientSettings-fontSizeDropdownActive",
});
fontSizeDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "clientSettings-fontSizeDropdownActive" });
errorMessage: OV<string> = mobx.observable.box(null, { name: "ClientSettings-errorMessage" });
@boundMethod
@@ -29,12 +34,12 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
@boundMethod
handleChangeFontSize(fontSize: string): void {
const newFontSize = Number(fontSize);
let newFontSize = Number(fontSize);
this.fontSizeDropdownActive.set(false);
if (GlobalModel.termFontSize.get() == newFontSize) {
return;
}
const prtn = GlobalCommandRunner.setTermFontSize(newFontSize, false);
let prtn = GlobalCommandRunner.setTermFontSize(newFontSize, false);
commandRtnHandler(prtn, this.errorMessage);
}
@@ -67,29 +72,29 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
commandRtnHandler(prtn, this.errorMessage);
}
getFontSizes(): DropdownItem[] {
const availableFontSizes: DropdownItem[] = [];
for (let s = appconst.MinFontSize; s <= appconst.MaxFontSize; s++) {
availableFontSizes.push({ label: s + "px", value: String(s) });
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 {
const prtn = GlobalCommandRunner.setClientOpenAISettings({ model: newModel });
let prtn = GlobalCommandRunner.setClientOpenAISettings({ model: newModel });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateOpenAIToken(newToken: string): void {
const prtn = GlobalCommandRunner.setClientOpenAISettings({ apitoken: newToken });
let prtn = GlobalCommandRunner.setClientOpenAISettings({ apitoken: newToken });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateOpenAIMaxTokens(newMaxTokensStr: string): void {
const prtn = GlobalCommandRunner.setClientOpenAISettings({ maxtokens: newMaxTokensStr });
let prtn = GlobalCommandRunner.setClientOpenAISettings({ maxtokens: newMaxTokensStr });
commandRtnHandler(prtn, this.errorMessage);
}
@@ -105,41 +110,19 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
GlobalModel.clientSettingsViewModel.closeView();
}
@boundMethod
handleChangeShortcut(newShortcut: string): void {
const prtn = GlobalCommandRunner.setGlobalShortcut(newShortcut);
commandRtnHandler(prtn, this.errorMessage);
}
getFKeys(): DropdownItem[] {
const opts: DropdownItem[] = [];
opts.push({ label: "Disabled", value: "" });
const platform = GlobalModel.getPlatform();
for (let i = 1; i <= 12; i++) {
const shortcut = (platform == "darwin" ? "Cmd" : "Alt") + "+F" + String(i);
opts.push({ label: shortcut, value: shortcut });
}
return opts;
}
getCurrentShortcut(): string {
const clientData = GlobalModel.clientData.get();
return clientData?.clientopts?.globalshortcut ?? "";
}
render() {
const isHidden = GlobalModel.activeMainView.get() != "clientsettings";
let isHidden = GlobalModel.activeMainView.get() != "clientsettings";
if (isHidden) {
return null;
}
const cdata: ClientDataType = GlobalModel.clientData.get();
const openAIOpts = cdata.openaiopts ?? {};
const apiTokenStr = isBlank(openAIOpts.apitoken) ? "(not set)" : "********";
const maxTokensStr = String(
let cdata: ClientDataType = GlobalModel.clientData.get();
let openAIOpts = cdata.openaiopts ?? {};
let apiTokenStr = isBlank(openAIOpts.apitoken) ? "(not set)" : "********";
let maxTokensStr = String(
openAIOpts.maxtokens == null || openAIOpts.maxtokens == 0 ? 1000 : openAIOpts.maxtokens
);
const curFontSize = GlobalModel.termFontSize.get();
let curFontSize = GlobalModel.termFontSize.get();
return (
<div className={cn("view clientsettings-view")}>
@@ -168,7 +151,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
<div className="settings-field">
<div className="settings-label">Client Version</div>
<div className="settings-input">
{appconst.VERSION} {appconst.BUILD}
{VERSION} {BUILD}
</div>
</div>
<div className="settings-field">
@@ -229,17 +212,6 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Global Hotkey</div>
<div className="settings-input">
<Dropdown
className="hotkey-dropdown"
options={this.getFKeys()}
defaultValue={this.getCurrentShortcut()}
onChange={this.handleChangeShortcut}
/>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
</div>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-123
View File
@@ -1,123 +0,0 @@
@import "@/common/themes/themes.less";
.wave-button {
background: none;
color: inherit;
border: none;
font: inherit;
cursor: pointer;
outline: inherit;
display: flex;
padding: 6px 16px;
align-items: center;
gap: 4px;
border-radius: 6px;
height: auto;
&:hover {
color: @term-white;
}
i {
fill: rgba(255, 255, 255, 0.12);
}
&.primary {
color: @term-green;
background: none;
i {
fill: @term-green;
}
&.solid {
color: @term-bright-white;
background: @term-green;
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset;
i {
fill: @term-white;
}
}
&.outlined {
border: 1px solid @term-green;
}
&.ghost {
// Styles for .ghost are already defined above
}
&:hover {
color: @term-bright-white;
}
}
&.secondary {
color: @term-white;
background: none;
&.solid {
background: rgba(255, 255, 255, 0.09);
box-shadow: none;
}
&.outlined {
border: 1px solid rgba(255, 255, 255, 0.09);
}
&.ghost {
padding: 6px 10px;
i {
fill: @term-green;
}
}
}
&.color-yellow {
&.solid {
border-color: @warning-yellow;
background-color: mix(@warning-yellow, @term-white, 50%);
box-shadow: none;
}
&.outlined {
color: @warning-yellow;
border-color: @warning-yellow;
&:hover {
color: @term-white;
border-color: @term-white;
}
}
&.ghost {
}
}
&.color-red {
&.solid {
border-color: @term-red;
background-color: mix(@term-red, @term-white, 50%);
box-shadow: none;
}
&.outlined {
color: @term-red;
border-color: @term-red;
}
&.ghost {
}
}
&.disabled {
opacity: 0.5;
}
&.link-button {
cursor: pointer;
}
}
-63
View File
@@ -1,63 +0,0 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import "./button.less";
type ButtonVariantType = "outlined" | "solid" | "ghost";
type ButtonThemeType = "primary" | "secondary";
interface ButtonProps {
theme?: ButtonThemeType;
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
variant?: ButtonVariantType;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
color?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
className?: string;
}
class Button extends React.Component<ButtonProps> {
static defaultProps = {
theme: "primary",
variant: "solid",
color: "",
style: {},
};
@boundMethod
handleClick() {
if (this.props.onClick && !this.props.disabled) {
this.props.onClick();
}
}
render() {
const { leftIcon, rightIcon, theme, children, disabled, variant, color, style, autoFocus, className } =
this.props;
return (
<button
className={cn("wave-button", theme, variant, color, { disabled: disabled }, className)}
onClick={this.handleClick}
disabled={disabled}
style={style}
autoFocus={autoFocus}
>
{leftIcon && <span className="icon-left">{leftIcon}</span>}
{children}
{rightIcon && <span className="icon-right">{rightIcon}</span>}
</button>
);
}
}
export { Button };
export type { ButtonProps };
-68
View File
@@ -1,68 +0,0 @@
@import "@/common/themes/themes.less";
.checkbox {
display: flex;
input[type="checkbox"] {
height: 0;
width: 0;
}
input[type="checkbox"] + label {
position: relative;
display: flex;
align-items: center;
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: 10px;
width: 20px;
height: 20px;
background: transparent;
border: 2px solid #9e9e9e;
border-radius: 2px;
cursor: pointer;
transition: all 250ms cubic-bezier(0.4, 0, 0.23, 1);
}
input[type="checkbox"] + label:hover > span,
input[type="checkbox"]:focus + label > span {
background: rgba(255, 255, 255, 0.1);
}
input[type="checkbox"]:checked + label > ins {
height: 100%;
}
input[type="checkbox"]:checked + label > span {
border: 10px solid @term-green;
}
input[type="checkbox"]:checked + label > span:before {
content: "";
position: absolute;
top: -2px;
left: 3px;
width: 7px;
height: 12px;
border-right: 2px solid #fff;
border-bottom: 2px solid #fff;
transform: rotate(45deg);
transform-origin: 0% 100%;
animation: checkbox-check 500ms cubic-bezier(0.4, 0, 0.23, 1);
}
@keyframes checkbox-check {
0% {
opacity: 0;
}
33% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
}
-70
View File
@@ -1,70 +0,0 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobx from "mobx";
import cn from "classnames";
import "./checkbox.less";
class Checkbox extends React.Component<
{
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 ?? 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 { label, className, id } = this.props;
const { checkedInternal } = this.state;
const checkboxId = id || this.generatedId;
return (
<div className={cn("checkbox", className)}>
<input
type="checkbox"
id={checkboxId}
checked={checkedInternal}
onChange={this.handleChange}
aria-checked={checkedInternal}
role="checkbox"
/>
<label htmlFor={checkboxId}>
<span></span>
{label}
</label>
</div>
);
}
}
export { Checkbox };
-102
View File
@@ -1,102 +0,0 @@
@import "@/common/themes/themes.less";
.cmdstr-code {
position: relative;
display: flex;
flex-direction: row;
padding: 0px 10px 0px 0;
&.is-large {
.use-button {
height: 28px;
width: 28px;
}
.code-div code {
}
}
&.limit-height .code-div {
max-height: 58px;
}
&.limit-height.is-large .code-div {
max-height: 68px;
}
.use-button {
flex-grow: 0;
padding: 3px;
border-radius: 3px 0 0 3px;
height: 22px;
width: 22px;
display: flex;
align-items: center;
justify-content: center;
align-self: flex-start;
cursor: pointer;
}
.code-div {
background-color: @term-black;
display: flex;
flex-direction: row;
min-width: 100px;
overflow: auto;
border-left: 1px solid #777;
code {
flex-shrink: 0;
min-width: 100px;
color: @term-white;
white-space: pre;
padding: 2px 8px 2px 8px;
background-color: @term-black;
font-size: 1em;
font-family: @fixed-font;
}
}
.copy-control {
width: 0;
position: relative;
display: block;
visibility: hidden;
.inner-copy {
position: absolute;
bottom: -1px;
right: -20px;
padding: 2px;
padding-left: 4px;
cursor: pointer;
width: 20px;
&:hover {
color: @term-white;
}
}
}
&:hover .copy-control {
visibility: visible !important;
}
}
.copied-indicator {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: @term-white;
opacity: 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
pointer-events: none;
animation-name: fade-in-out;
animation-duration: 0.3s;
}
-66
View File
@@ -1,66 +0,0 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { If } from "tsx-control-statements/components";
import { ReactComponent as CheckIcon } from "@/assets/icons/line/check.svg";
import { ReactComponent as CopyIcon } from "@/assets/icons/history/copy.svg";
import "./cmdstrcode.less";
class CmdStrCode extends React.Component<
{
cmdstr: string;
onUse: () => void;
onCopy: () => void;
isCopied: boolean;
fontSize: "normal" | "large";
limitHeight: boolean;
},
{}
> {
@boundMethod
handleUse(e: any) {
e.stopPropagation();
if (this.props.onUse != null) {
this.props.onUse();
}
}
@boundMethod
handleCopy(e: any) {
e.stopPropagation();
if (this.props.onCopy != null) {
this.props.onCopy();
}
}
render() {
let { isCopied, cmdstr, fontSize, limitHeight } = this.props;
return (
<div className={cn("cmdstr-code", { "is-large": fontSize == "large" }, { "limit-height": limitHeight })}>
<If condition={isCopied}>
<div key="copied" className="copied-indicator">
<div>copied</div>
</div>
</If>
<div key="use" className="use-button hoverEffect" title="Use Command" onClick={this.handleUse}>
<CheckIcon className="icon" />
</div>
<div key="code" className="code-div">
<code>{cmdstr}</code>
</div>
<div key="copy" className="copy-control hoverEffect">
<div className="inner-copy" onClick={this.handleCopy} title="copy">
<CopyIcon className="icon" />
</div>
</div>
</div>
);
}
}
export { CmdStrCode };
-10
View File
@@ -1,10 +0,0 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
function renderCmdText(text: string): any {
return <span>&#x2318;{text}</span>;
}
export { renderCmdText };
-127
View File
@@ -1,127 +0,0 @@
@import "@/common/themes/themes.less";
.wave-dropdown {
position: relative;
height: 44px;
min-width: 150px;
width: 100%;
border: 1px solid rgba(241, 246, 243, 0.15);
border-radius: 6px;
background: rgba(255, 255, 255, 0.06);
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
&.no-label {
height: 34px;
}
&-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;
}
&.offset-left {
left: 42px;
}
}
&-display {
position: absolute;
left: 16px;
bottom: 5px;
&.offset-left {
left: 42px;
}
}
&-arrow {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
transition: transform 0.3s;
pointer-events: none;
i {
font-size: 14px;
}
}
&-arrow-rotate {
transform: translateY(-50%) rotate(180deg); // Rotate the arrow when dropdown is open
}
&-item {
display: flex;
min-width: 120px;
padding: 5px 8px;
justify-content: space-between;
align-items: center;
align-self: stretch;
border-radius: 6px;
&-highlighted,
&:hover {
background: rgba(241, 246, 243, 0.08);
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
}
}
.wave-input-decoration {
position: absolute;
top: 0;
height: 100%;
}
.wave-input-decoration.end-position {
margin-right: 44px;
right: 0;
}
.wave-input-decoration.start-position {
left: 0;
}
&-error {
border-color: @term-red;
}
&:focus {
border-color: @term-green;
}
}
.wave-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 2px;
max-height: 200px;
overflow-y: auto;
padding: 6px;
flex-direction: column;
align-items: flex-start;
gap: 4px;
border-radius: 6px;
background: #151715;
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5), 0px 3px 8px 0px rgba(0, 0, 0, 0.35), 0px 0px 0.5px 0px #fff inset,
0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
animation-fill-mode: forwards;
z-index: 1000;
}
.wave-dropdown-menu-close {
z-index: 0;
}
-259
View File
@@ -1,259 +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 { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { If } from "tsx-control-statements/components";
import ReactDOM from "react-dom";
import "./dropdown.less";
interface DropdownDecorationProps {
startDecoration?: React.ReactNode;
endDecoration?: React.ReactNode;
}
interface DropdownProps {
label?: string;
options: DropdownItem[];
value?: string;
className?: string;
onChange: (value: string) => void;
placeholder?: string;
decoration?: DropdownDecorationProps;
defaultValue?: string;
required?: boolean;
}
interface DropdownState {
isOpen: boolean;
internalValue: string;
highlightedIndex: number;
isTouched: boolean;
}
@mobxReact.observer
class Dropdown extends React.Component<DropdownProps, DropdownState> {
wrapperRef: React.RefObject<HTMLDivElement>;
menuRef: React.RefObject<HTMLDivElement>;
timeoutId: any;
constructor(props: DropdownProps) {
super(props);
this.state = {
isOpen: false,
internalValue: props.defaultValue || "",
highlightedIndex: -1,
isTouched: false,
};
this.wrapperRef = React.createRef();
this.menuRef = React.createRef();
}
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
componentDidUpdate(prevProps: Readonly<DropdownProps>, prevState: Readonly<DropdownState>, snapshot?: any): void {
// If the dropdown was open but now is closed, start the timeout
if (prevState.isOpen && !this.state.isOpen) {
this.timeoutId = setTimeout(() => {
if (this.menuRef.current) {
this.menuRef.current.style.display = "none";
}
}, 300); // Time is equal to the animation duration
}
// If the dropdown is now open, cancel any existing timeout and show the menu
else if (!prevState.isOpen && this.state.isOpen) {
if (this.timeoutId !== null) {
clearTimeout(this.timeoutId); // Cancel any existing timeout
this.timeoutId = null;
}
if (this.menuRef.current) {
this.menuRef.current.style.display = "inline-flex";
}
}
}
@boundMethod
handleClickOutside(event: MouseEvent) {
// Check if the click is outside both the wrapper and the menu
if (
this.wrapperRef.current &&
!this.wrapperRef.current.contains(event.target as Node) &&
this.menuRef.current &&
!this.menuRef.current.contains(event.target as Node)
) {
this.setState({ isOpen: false });
}
}
@boundMethod
handleClick() {
this.toggleDropdown();
}
@boundMethod
handleFocus() {
this.setState({ isTouched: true });
}
@boundMethod
handleKeyDown(event: React.KeyboardEvent) {
const { options } = this.props;
const { isOpen, highlightedIndex } = this.state;
switch (event.key) {
case "Enter":
case " ":
if (isOpen) {
const option = options[highlightedIndex];
if (option) {
this.handleSelect(option.value, undefined);
}
} else {
this.toggleDropdown();
}
break;
case "Escape":
this.setState({ isOpen: false });
break;
case "ArrowUp":
if (isOpen) {
this.setState((prevState) => ({
highlightedIndex:
prevState.highlightedIndex > 0 ? prevState.highlightedIndex - 1 : options.length - 1,
}));
}
break;
case "ArrowDown":
if (isOpen) {
this.setState((prevState) => ({
highlightedIndex:
prevState.highlightedIndex < options.length - 1 ? prevState.highlightedIndex + 1 : 0,
}));
}
break;
case "Tab":
this.setState({ isOpen: false });
break;
}
}
@boundMethod
handleSelect(value: string, event?: React.MouseEvent | React.KeyboardEvent) {
const { onChange } = this.props;
if (event) {
event.stopPropagation(); // This stops the event from bubbling up to the wrapper
}
if (!("value" in this.props)) {
this.setState({ internalValue: value });
}
onChange(value);
this.setState({ isOpen: false, isTouched: true });
}
@boundMethod
toggleDropdown() {
this.setState((prevState) => ({ isOpen: !prevState.isOpen, isTouched: true }));
}
@boundMethod
calculatePosition(): React.CSSProperties {
if (this.wrapperRef.current) {
const rect = this.wrapperRef.current.getBoundingClientRect();
return {
position: "absolute",
top: `${rect.bottom + window.scrollY}px`,
left: `${rect.left + window.scrollX}px`,
width: `${rect.width}px`,
};
}
return {};
}
render() {
const { label, options, value, placeholder, decoration, className, required } = this.props;
const { isOpen, internalValue, highlightedIndex, isTouched } = this.state;
const currentValue = value ?? internalValue;
const selectedOptionLabel =
options.find((option) => option.value === currentValue)?.label || placeholder || internalValue;
// Determine if the dropdown should be marked as having an error
const isError =
required &&
(value === undefined || value === "") &&
(internalValue === undefined || internalValue === "") &&
isTouched;
// Determine if the label should float
const shouldLabelFloat = !!value || !!internalValue || !!placeholder || isOpen;
const dropdownMenu = isOpen
? ReactDOM.createPortal(
<div className={cn("wave-dropdown-menu")} ref={this.menuRef} style={this.calculatePosition()}>
{options.map((option, index) => (
<div
key={option.value}
className={cn("wave-dropdown-item unselectable", {
"wave-dropdown-item-highlighted": index === highlightedIndex,
})}
onClick={(e) => this.handleSelect(option.value, e)}
onMouseEnter={() => this.setState({ highlightedIndex: index })}
onMouseLeave={() => this.setState({ highlightedIndex: -1 })}
>
{option.label}
</div>
))}
</div>,
document.getElementById("app")!
)
: null;
return (
<div
className={cn("wave-dropdown", className, {
"wave-dropdown-error": isError,
"no-label": !label,
})}
ref={this.wrapperRef}
tabIndex={0}
onKeyDown={this.handleKeyDown}
onClick={this.handleClick}
onFocus={this.handleFocus}
>
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
<If condition={label}>
<div
className={cn("wave-dropdown-label unselectable", {
float: shouldLabelFloat,
"offset-left": decoration?.startDecoration,
})}
>
{label}
</div>
</If>
<div
className={cn("wave-dropdown-display unselectable", { "offset-left": decoration?.startDecoration })}
>
{selectedOptionLabel}
</div>
<div className={cn("wave-dropdown-arrow", { "wave-dropdown-arrow-rotate": isOpen })}>
<i className="fa-sharp fa-solid fa-chevron-down"></i>
</div>
{dropdownMenu}
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
</div>
);
}
}
export { Dropdown };

Some files were not shown because too many files have changed in this diff Show More