Rght sidebar (#461)

* init

* model

* pass model

* increase snap threshold

* move saving of state to model. backend implementation

* fix wrong clientdata prop

* right header space for right sidebar toggles

* button component refactor

* set default classname to primary

* remove debugging code

* hide trigger for now

* disable right sidebar in production (only show on dev for now)
This commit is contained in:
Red J Adaya
2024-03-14 22:59:55 -07:00
committed by GitHub
parent 61c9d21014
commit 07e0b53b17
32 changed files with 414 additions and 142 deletions
+4
View File
@@ -44,6 +44,10 @@
--floating-logo-width: 40px;
--floating-logo-height: var(--screentabs-height);
/* right sidebar triggers */
--floating-right-sidebar-triggers-width-darwin: 50px;
--floating-right-sidebar-triggers-width: 40px;
/* global colors */
--app-bg-color: black;
--app-accent-color: rgb(88, 193, 66);
+19
View File
@@ -302,6 +302,25 @@ a.a-block {
}
}
.right-sidebar-triggers {
position: absolute;
z-index: 25;
right: 0px;
display: flex;
flex-direction: row;
align-items: center;
pointer-events: none;
height: 38px;
padding: 0 5px;
.right-sidebar-trigger {
cursor: pointer;
user-select: none;
-webkit-app-region: no-drag;
pointer-events: all;
}
}
.copied-indicator {
position: absolute;
top: 0;
+35 -11
View File
@@ -16,10 +16,12 @@ import { BookmarksView } from "./bookmarks/bookmarks";
import { HistoryView } from "./history/history";
import { ConnectionsView } from "./connections/connections";
import { ClientSettingsView } from "./clientsettings/clientsettings";
import { MainSideBar } from "./sidebar/sidebar";
import { DisconnectedModal, ClientStopModal } from "./common/modals";
import { ModalsProvider } from "./common/modals/provider";
import { ErrorBoundary } from "./common/error/errorboundary";
import { MainSideBar } from "./sidebar/main";
import { RightSideBar } from "./sidebar/right";
import { DisconnectedModal, ClientStopModal } from "@/modals";
import { ModalsProvider } from "@/modals/provider";
import { Button } from "@/elements";
import { ErrorBoundary } from "@/common/error/errorboundary";
import cn from "classnames";
import "./app.less";
@@ -65,9 +67,17 @@ class App extends React.Component<{}, {}> {
}
@boundMethod
openSidebar() {
const width = GlobalModel.mainSidebarModel.getWidth(true);
GlobalCommandRunner.clientSetSidebar(width, false);
openMainSidebar() {
const mainSidebarModel = GlobalModel.mainSidebarModel;
const width = mainSidebarModel.getWidth(true);
mainSidebarModel.saveState(width, false);
}
@boundMethod
openRightSidebar() {
const rightSidebarModel = GlobalModel.rightSidebarModel;
const width = rightSidebarModel.getWidth(true);
rightSidebarModel.saveState(width, false);
}
render() {
@@ -110,23 +120,36 @@ class App extends React.Component<{}, {}> {
}
// used to force a full reload of the application
const renderVersion = GlobalModel.renderVersion.get();
const sidebarCollapsed = GlobalModel.mainSidebarModel.getCollapsed();
const mainSidebarCollapsed = GlobalModel.mainSidebarModel.getCollapsed();
const rightSidebarCollapsed = GlobalModel.rightSidebarModel.getCollapsed();
const activeMainView = GlobalModel.activeMainView.get();
const lightDarkClass = GlobalModel.isThemeDark() ? "is-dark" : "is-light";
return (
<div
key={"version-" + renderVersion}
id="main"
className={cn("platform-" + platform, { "sidebar-collapsed": sidebarCollapsed }, lightDarkClass)}
className={cn(
"platform-" + platform,
{ "mainsidebar-collapsed": mainSidebarCollapsed, "rightsidebar-collapsed": rightSidebarCollapsed },
lightDarkClass
)}
onContextMenu={this.handleContextMenu}
>
<If condition={sidebarCollapsed}>
<If condition={mainSidebarCollapsed}>
<div key="logo-button" className="logo-button-container">
<div className="logo-button-spacer" />
<div className="logo-button" onClick={this.openSidebar}>
<div className="logo-button" onClick={this.openMainSidebar}>
<img src="public/logos/wave-logo.png" alt="logo" />
</div>
</div>
</If>
<If condition={GlobalModel.isDev && rightSidebarCollapsed && activeMainView == "session"}>
<div className="right-sidebar-triggers">
<Button className="secondary ghost right-sidebar-trigger" onClick={this.openRightSidebar}>
<i className="fa-sharp fa-regular fa-lightbulb"></i>
</Button>
</div>
</If>
<div ref={this.mainContentRef} className="main-content">
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<ErrorBoundary>
@@ -137,6 +160,7 @@ class App extends React.Component<{}, {}> {
<ConnectionsView model={remotesModel} />
<ClientSettingsView model={remotesModel} />
</ErrorBoundary>
<RightSideBar parentRef={this.mainContentRef} clientData={clientData} />
</div>
<ModalsProvider />
</div>
+9 -3
View File
@@ -5,7 +5,6 @@
font: inherit;
cursor: pointer;
outline: inherit;
display: flex;
padding: 6px 16px;
align-items: center;
@@ -15,7 +14,8 @@
line-height: 1.5;
&.primary {
background: none;
color: var(--form-element-text-color);
background: var(--form-element-primary-color);
i {
fill: var(--form-element-primary-color);
@@ -31,11 +31,12 @@
}
&.outlined {
background: none;
border: 1px solid var(--form-element-primary-color);
}
&.ghost {
// Styles for .ghost are already defined above
background: none;
}
&:hover {
@@ -46,6 +47,9 @@
&.secondary {
color: var(--form-element-text-color);
background: none;
color: var(--form-element-text-color);
background: var(--form-element-secondary-color);
box-shadow: none;
&.solid {
color: var(--form-element-text-color);
@@ -54,10 +58,12 @@
}
&.outlined {
background: none;
border: 1px solid var(--form-element-secondary-color);
}
&.ghost {
background: none;
padding: 6px 10px;
i {
+3 -34
View File
@@ -1,24 +1,15 @@
// 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;
@@ -27,10 +18,8 @@ interface ButtonProps {
class Button extends React.Component<ButtonProps> {
static defaultProps = {
theme: "primary",
variant: "solid",
color: "",
style: {},
className: "primary",
};
@boundMethod
@@ -41,31 +30,11 @@ class Button extends React.Component<ButtonProps> {
}
render() {
const {
leftIcon,
rightIcon,
theme,
children,
disabled,
variant,
color,
style,
autoFocus,
termInline,
className,
} = this.props;
const { leftIcon, rightIcon, children, disabled, style, autoFocus, termInline, className } = this.props;
return (
<button
className={cn(
"wave-button",
theme,
variant,
color,
{ disabled: disabled },
{ "term-inline": termInline },
className
)}
className={cn("wave-button", { disabled }, { "term-inline": termInline }, className)}
onClick={this.handleClick}
disabled={disabled}
style={style}
-21
View File
@@ -1,21 +0,0 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import { Button } from "./button";
class IconButton extends Button {
render() {
const { children, theme, variant = "solid", ...rest } = this.props;
const className = `wave-button icon-button ${theme} ${variant}`;
return (
<button {...rest} className={className}>
{children}
</button>
);
}
}
export default IconButton;
export { IconButton };
-1
View File
@@ -3,7 +3,6 @@ export { Checkbox } from "./checkbox";
export { CmdStrCode } from "./cmdstrcode";
export { renderCmdText } from "./cmdtext";
export { Dropdown } from "./dropdown";
export { IconButton } from "./iconbutton";
export { InlineSettingsTextEdit } from "./inlinesettingstextedit";
export { InputDecoration } from "./inputdecoration";
export { LinkButton } from "./linkbutton";
+8 -5
View File
@@ -6,7 +6,6 @@ import * as mobx from "mobx";
import { If } from "tsx-control-statements/components";
import ReactDOM from "react-dom";
import { Button } from "./button";
import { IconButton } from "./iconbutton";
import "./modal.less";
@@ -19,9 +18,9 @@ const ModalHeader: React.FC<ModalHeaderProps> = ({ onClose, title }) => (
<div className="wave-modal-header">
{<div className="wave-modal-title">{title}</div>}
<If condition={onClose}>
<IconButton theme="secondary" variant="ghost" onClick={onClose}>
<Button className="secondary ghost" onClick={onClose}>
<i className="fa-sharp fa-solid fa-xmark"></i>
</IconButton>
</Button>
</If>
</div>
);
@@ -36,11 +35,15 @@ interface ModalFooterProps {
const ModalFooter: React.FC<ModalFooterProps> = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }) => (
<div className="wave-modal-footer">
{onCancel && (
<Button theme="secondary" onClick={onCancel}>
<Button className="secondary" onClick={onCancel}>
{cancelLabel}
</Button>
)}
{onOk && <Button onClick={onOk}>{okLabel}</Button>}
{onOk && (
<Button className="primary" onClick={onOk}>
{okLabel}
</Button>
)}
</div>
);
+24 -33
View File
@@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner } from "@/models";
import { GlobalCommandRunner, SidebarModel } from "@/models";
import { MagicLayout } from "@/app/magiclayout";
import "./resizablesidebar.less";
@@ -14,6 +14,7 @@ import "./resizablesidebar.less";
interface ResizableSidebarProps {
parentRef: React.RefObject<HTMLElement>;
position: "left" | "right";
model: SidebarModel;
enableSnap?: boolean;
className?: string;
children?: (toggleCollapsed: () => void) => React.ReactNode;
@@ -32,7 +33,7 @@ class ResizableSidebar extends React.Component<ResizableSidebarProps> {
startResizing(event: React.MouseEvent<HTMLDivElement>) {
event.preventDefault();
const { parentRef, position } = this.props;
const { parentRef, position, model } = this.props;
const parentRect = parentRef.current?.getBoundingClientRect();
if (!parentRect) return;
@@ -43,17 +44,16 @@ class ResizableSidebar extends React.Component<ResizableSidebarProps> {
this.startX = event.clientX - parentRect.left;
}
const mainSidebarModel = GlobalModel.mainSidebarModel;
const collapsed = mainSidebarModel.getCollapsed();
const collapsed = model.getCollapsed();
this.resizeStartWidth = mainSidebarModel.getWidth();
this.resizeStartWidth = model.getWidth();
document.addEventListener("mousemove", this.onMouseMove);
document.addEventListener("mouseup", this.stopResizing);
document.body.style.cursor = "col-resize";
mobx.action(() => {
mainSidebarModel.setTempWidthAndTempCollapsed(this.resizeStartWidth, collapsed);
mainSidebarModel.isDragging.set(true);
model.setTempWidthAndTempCollapsed(this.resizeStartWidth, collapsed);
model.isDragging.set(true);
})();
}
@@ -61,11 +61,10 @@ class ResizableSidebar extends React.Component<ResizableSidebarProps> {
onMouseMove(event: MouseEvent) {
event.preventDefault();
const { parentRef, enableSnap, position } = this.props;
const { parentRef, enableSnap, position, model } = this.props;
const parentRect = parentRef.current?.getBoundingClientRect();
const mainSidebarModel = GlobalModel.mainSidebarModel;
if (!mainSidebarModel.isDragging.get() || !parentRect) return;
if (!model.isDragging.get() || !parentRect) return;
let delta: number, newWidth: number;
@@ -100,34 +99,27 @@ class ResizableSidebar extends React.Component<ResizableSidebarProps> {
if (newWidth - dragResistance > minWidth && newWidth < snapPoint && dragDirection == "+") {
newWidth = snapPoint;
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
model.setTempWidthAndTempCollapsed(newWidth, false);
} else if (newWidth + dragResistance < snapPoint && dragDirection == "-") {
newWidth = minWidth;
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
model.setTempWidthAndTempCollapsed(newWidth, true);
} else if (newWidth > snapPoint) {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
model.setTempWidthAndTempCollapsed(newWidth, false);
}
} else {
if (newWidth <= MagicLayout.MainSidebarMinWidth) {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
model.setTempWidthAndTempCollapsed(newWidth, true);
} else {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
model.setTempWidthAndTempCollapsed(newWidth, false);
}
}
}
@boundMethod
stopResizing() {
let mainSidebarModel = GlobalModel.mainSidebarModel;
const { model } = this.props;
GlobalCommandRunner.clientSetSidebar(
mainSidebarModel.tempWidth.get(),
mainSidebarModel.tempCollapsed.get()
).finally(() => {
mobx.action(() => {
mainSidebarModel.isDragging.set(false);
})();
});
model.saveState(model.tempWidth.get(), model.tempCollapsed.get());
document.removeEventListener("mousemove", this.onMouseMove);
document.removeEventListener("mouseup", this.stopResizing);
@@ -136,19 +128,18 @@ class ResizableSidebar extends React.Component<ResizableSidebarProps> {
@boundMethod
toggleCollapsed() {
const mainSidebarModel = GlobalModel.mainSidebarModel;
const { model } = this.props;
const tempCollapsed = mainSidebarModel.getCollapsed();
const width = mainSidebarModel.getWidth(true);
mainSidebarModel.setTempWidthAndTempCollapsed(width, !tempCollapsed);
GlobalCommandRunner.clientSetSidebar(width, !tempCollapsed);
const tempCollapsed = model.getCollapsed();
const width = model.getWidth(true);
model.setTempWidthAndTempCollapsed(width, !tempCollapsed);
model.saveState(width, !tempCollapsed);
}
render() {
const { className, children } = this.props;
const mainSidebarModel = GlobalModel.mainSidebarModel;
const width = mainSidebarModel.getWidth();
const isCollapsed = mainSidebarModel.getCollapsed();
const { className, children, model } = this.props;
const width = model.getWidth();
const isCollapsed = model.getCollapsed();
return (
<div className={cn("sidebar", className, { collapsed: isCollapsed })} style={{ width, minWidth: width }}>
+1 -1
View File
@@ -54,7 +54,7 @@ class AlertModal extends React.Component<{}, {}> {
</div>
<div className="wave-modal-footer">
<If condition={isConfirm}>
<Button theme="secondary" onClick={this.closeModal}>
<Button className="secondary" onClick={this.closeModal}>
Cancel
</Button>
<Button autoFocus={true} onClick={this.handleOK}>
+1 -1
View File
@@ -31,7 +31,7 @@ class ClientStopModal extends React.Component<{}, {}> {
</If>
<div>
<Button
theme="secondary"
className="secondary"
onClick={this.refreshClient}
leftIcon={<i className="fa-sharp fa-solid fa-rotate"></i>}
>
+2 -2
View File
@@ -75,7 +75,7 @@ class DisconnectedModal extends React.Component<{}, {}> {
</div>
<div className="wave-modal-footer">
<Button
theme="secondary"
className="secondary"
onClick={this.tryReconnect}
leftIcon={
<span className="icon">
@@ -86,7 +86,7 @@ class DisconnectedModal extends React.Component<{}, {}> {
Try Reconnect
</Button>
<Button
theme="secondary"
className="secondary"
onClick={this.restartServer}
leftIcon={<i className="fa-sharp fa-solid fa-triangle-exclamation"></i>}
>
+10 -10
View File
@@ -162,42 +162,42 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
renderHeaderBtns(remote: RemoteType): React.ReactNode {
let buttons: React.ReactNode[] = [];
const disconnectButton = (
<Button theme="secondary" onClick={() => this.disconnectRemote(remote.remoteid)}>
<Button className="secondary" onClick={() => this.disconnectRemote(remote.remoteid)}>
Disconnect Now
</Button>
);
const connectButton = (
<Button theme="secondary" onClick={() => this.connectRemote(remote.remoteid)}>
<Button className="secondary" onClick={() => this.connectRemote(remote.remoteid)}>
Connect Now
</Button>
);
const tryReconnectButton = (
<Button theme="secondary" onClick={() => this.connectRemote(remote.remoteid)}>
<Button className="secondary" onClick={() => this.connectRemote(remote.remoteid)}>
Try Reconnect
</Button>
);
let updateAuthButton = (
<Button theme="secondary" onClick={() => this.openEditModal()}>
<Button className="secondary" onClick={() => this.openEditModal()}>
Edit
</Button>
);
let cancelInstallButton = (
<Button theme="secondary" onClick={() => this.cancelInstall(remote.remoteid)}>
<Button className="secondary" onClick={() => this.cancelInstall(remote.remoteid)}>
Cancel Install
</Button>
);
let installNowButton = (
<Button theme="secondary" onClick={() => this.installRemote(remote.remoteid)}>
<Button className="secondary" onClick={() => this.installRemote(remote.remoteid)}>
Install Now
</Button>
);
let archiveButton = (
<Button theme="secondary" onClick={() => this.clickArchive()}>
<Button className="secondary" onClick={() => this.clickArchive()}>
Delete
</Button>
);
const reinstallButton = (
<Button theme="secondary" onClick={this.clickReinstall}>
<Button className="secondary" onClick={this.clickReinstall}>
Reinstall
</Button>
);
@@ -207,7 +207,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
}
if (remote.sshconfigsrc == "sshconfig-import") {
archiveButton = (
<Button theme="secondary" onClick={() => this.clickArchive()}>
<Button className="secondary" onClick={() => this.clickArchive()}>
Delete
<Tooltip
message={
@@ -383,7 +383,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
</div>
<div className="wave-modal-footer">
<Button
theme="secondary"
className="secondary"
disabled={selectedRemoteStatus == "connecting"}
onClick={this.handleClose}
>
+2 -2
View File
@@ -180,14 +180,14 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered
</table>
<footer>
<Button
theme="secondary"
className="secondary"
leftIcon={<i className="fa-sharp fa-solid fa-plus"></i>}
onClick={this.handleAddConnection}
>
New Connection
</Button>
<Button
theme="secondary"
className="secondary"
leftIcon={<i className="fa-sharp fa-solid fa-fw fa-file-import"></i>}
onClick={this.handleImportSshConfig}
>
+8 -2
View File
@@ -16,11 +16,17 @@ let MagicLayout = {
ScreenSidebarMinWidth: 200,
ScreenSidebarHeaderHeight: 26,
MainSidebarMinWidth: 75,
MainSidebarMinWidth: 0,
MainSidebarMaxWidth: 300,
MainSidebarSnapThreshold: 90,
MainSidebarSnapThreshold: 165,
MainSidebarDragResistance: 50,
MainSidebarDefaultWidth: 240,
RightSidebarMinWidth: 0,
RightSidebarMaxWidth: 300,
RightSidebarSnapThreshold: 90,
RightSidebarDragResistance: 50,
RightSidebarDefaultWidth: 240,
};
let m = MagicLayout;
@@ -21,7 +21,7 @@ import { isBlank, openLink } from "@/util/util";
import { ResizableSidebar } from "@/common/elements";
import * as appconst from "@/app/appconst";
import "./sidebar.less";
import "./main.less";
import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "@/common/icons/icons";
dayjs.extend(localizedFormat);
@@ -251,6 +251,7 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
render() {
return (
<ResizableSidebar
model={GlobalModel.mainSidebarModel}
className="main-sidebar"
position="left"
enableSnap={true}
+28
View File
@@ -0,0 +1,28 @@
@import "@/common/icons/icons.less";
.right-sidebar {
padding: 0;
display: flex;
flex-direction: column;
position: relative;
line-height: 20px;
backdrop-filter: blur(4px);
z-index: 20;
font-size: var(--sidebar-font-size);
font-family: var(--base-font-family);
font-weight: var(--sidebar-font-weight);
border-left: 1px solid var(--app-border-color);
.header {
height: 39px;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 5px;
border-bottom: 1px solid var(--app-border-color);
}
&.collapsed {
display: none;
}
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobxReact from "mobx-react";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "@/models";
import { ResizableSidebar, Button } from "@/elements";
import "./right.less";
dayjs.extend(localizedFormat);
interface RightSideBarProps {
parentRef: React.RefObject<HTMLElement>;
clientData: ClientDataType;
}
@mobxReact.observer
class RightSideBar extends React.Component<RightSideBarProps, {}> {
render() {
return (
<ResizableSidebar
model={GlobalModel.rightSidebarModel}
className="right-sidebar"
position="right"
enableSnap={true}
parentRef={this.props.parentRef}
>
{(toggleCollapse) => (
<React.Fragment>
<div className="header">
<Button className="secondary ghost" onClick={toggleCollapse}>
<i className="fa-sharp fa-regular fa-xmark"></i>
</Button>
</div>
</React.Fragment>
)}
</ResizableSidebar>
);
}
}
export { RightSideBar };
+1 -1
View File
@@ -325,7 +325,7 @@ class ScreenSidebar extends React.Component<{ screen: Screen; width: string }, {
<br />
</div>
<div onClick={this.sidebarClose} className="close-button-container">
<Button theme="secondary" onClick={this.sidebarClose}>
<Button className="secondary" onClick={this.sidebarClose}>
Close Sidebar
</Button>
</div>

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