Add a button to filter out non-running commands (#113)

* add filter button to the command input box

This will become a button that temporarily filters out the non-running
commands from your screen. At the moment it is only a placeholder design
that will likely change with more feedback. It does not have any
functionality at the moment.

* add view indication of active filter

This will become a clickable notification to let users know that a
filter is being applied. It displays the number of lines that are being
filtered. The plan is for it to be clickable to remove the filter. The
current version is a placeholder that is likely to change. It has no
functionality at the moment.

* add basic state to the filtering buttons

The filtering buttons up until this point haven't done anything. Now
they can be clicked and unclick causing them to render differently
depending on if they're selected. They still have no functionality
outside of their own appearance.

* add filtering functionality to filter button

The filter button now hides all non-running commands. And pressing it
again or pressing the other filter button will bring
back the hidden commands.

There are currently some formatting issues with the second button as it
jumps to the top of the screen if the filter is on and no running
commands are present.

An additional change was made to remove a variable accidentally
introduced in the last commit.

* add count for number of lines filtered out

The secondary filter button now lists the number of non-running commands
that have been filtered out. This count is added to the screen model in
case it is needed elsewhere.

* fix the style on the secondary buttons

This fixes the margin an the button to bring it in line with the line
items. It also fixes empty window screen to use a different css class.
Previously, the window-view class being used would cover the button. It
is now using the window-empty class instead.

* change formatting for secondary filter button

The button is now yellow with a border style instead of red with a solid
style. The border-radius has been changed to give the button a pill
shape.

Additionally, a style tab has been added to the button component to
provide it with custom styling. It should be changed to a custom class
design in the future.

* update style on primary filter button

This is being changed to simpler hover text in line with other text in
the cmd box.

* add number display as text for first filter button

The main filter button originally displayed a somewhat vague message.
Now it displays the number of running tasks with the rotating arrow
symbol.

* remove numLinesHidden count from model

This numLineHidden count is no longer needed with the new button design.
Furthermore, it created several warnings in react due to its
implementation. For both of these reasons, it has been removed.

* update filter functionality to better utilize mobx

This consisted of a few changes. The first was to move the filter state
from the GlobalModel to ScreenLines in order to track state separately
for each screen. Then several of the functions had to be rewritten to
wrap setting variables in the mobx.action wrapper.

As is, there are still a few issues with this design:
- the filter is not remembered when switching tabs
- if all running tasks expire, the second filter button is still present

* move filtering observable to Screen model

The previous observable did not persist when changing tabs because
ScreenLines did not persist. By moving it to Screen, the ovservable now
persists after changing tabs.
This commit is contained in:
Sylvie Crowe 2023-12-01 20:54:49 -08:00 committed by GitHub
parent 23b6bb29e7
commit 7310481383
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 122 additions and 7 deletions

View File

@ -992,6 +992,26 @@
} }
} }
&.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 { &.color-red {
&.solid { &.solid {
border-color: @term-red; border-color: @term-red;

View File

@ -229,6 +229,7 @@ interface ButtonProps {
leftIcon?: React.ReactNode; leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode; rightIcon?: React.ReactNode;
color?: string; color?: string;
style?: React.CSSProperties;
} }
class Button extends React.Component<ButtonProps> { class Button extends React.Component<ButtonProps> {
@ -236,6 +237,7 @@ class Button extends React.Component<ButtonProps> {
theme: "primary", theme: "primary",
variant: "solid", variant: "solid",
color: "", color: "",
style: {},
}; };
@boundMethod @boundMethod
@ -246,13 +248,14 @@ class Button extends React.Component<ButtonProps> {
} }
render() { render() {
const { leftIcon, rightIcon, theme, children, disabled, variant, color } = this.props; const { leftIcon, rightIcon, theme, children, disabled, variant, color, style } = this.props;
return ( return (
<button <button
className={cn("wave-button", theme, variant, color, { disabled: disabled })} className={cn("wave-button", theme, variant, color, { disabled: disabled })}
onClick={this.handleClick} onClick={this.handleClick}
disabled={disabled} disabled={disabled}
style={style}
> >
{leftIcon && <span className="icon-left">{leftIcon}</span>} {leftIcon && <span className="icon-left">{leftIcon}</span>}
{children} {children}

View File

@ -73,6 +73,42 @@
.cmd-input-context { .cmd-input-context {
color: #fff; color: #fff;
white-space: nowrap; white-space: nowrap;
display: flex;
justify-content: space-between;
align-items: center;
}
.cmd-input-filter {
opacity: 0.5;
&:hover {
opacity: 1.0;
}
.avatar {
display: inline-block;
width: 1em;
height: 1em;
margin: 0 0.5em;
vertical-align: text-top;
fill: @base-color;
}
.warning {
fill: @warning-yellow;
}
@keyframes infiniteRotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.spin {
animation: infiniteRotate 2s linear infinite;
}
} }
.cmd-input-field { .cmd-input-field {

View File

@ -5,18 +5,19 @@ import * as React from "react";
import * as mobxReact from "mobx-react"; import * as mobxReact from "mobx-react";
import * as mobx from "mobx"; import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components"; import { If, Choose, When, Otherwise } from "tsx-control-statements/components";
import cn from "classnames"; import cn from "classnames";
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types"; import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner } from "../../../model/model"; import { GlobalModel, GlobalCommandRunner, Screen, ScreenLines } from "../../../model/model";
import { renderCmdText } from "../../common/common"; import { renderCmdText, Button } from "../../common/common";
import { TextAreaInput } from "./textareainput"; import { TextAreaInput } from "./textareainput";
import { InfoMsg } from "./infomsg"; import { InfoMsg } from "./infomsg";
import { HistoryInfo } from "./historyinfo"; import { HistoryInfo } from "./historyinfo";
import { Prompt } from "../../common/prompt/prompt"; import { Prompt } from "../../common/prompt/prompt";
import { ReactComponent as ExecIcon } from "../../assets/icons/exec.svg"; import { ReactComponent as ExecIcon } from "../../assets/icons/exec.svg";
import { ReactComponent as RotateIcon } from "../../assets/icons/line/rotate.svg";
import "./cmdinput.less"; import "./cmdinput.less";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
@ -90,6 +91,13 @@ class CmdInput extends React.Component<{}, {}> {
GlobalCommandRunner.connectRemote(remoteId); GlobalCommandRunner.connectRemote(remoteId);
} }
@boundMethod
toggleFilter(screen: Screen) {
mobx.action(() => {
screen.filterRunning.set(!screen.filterRunning.get());
})();
}
render() { render() {
let model = GlobalModel; let model = GlobalModel;
let inputModel = model.inputModel; let inputModel = model.inputModel;
@ -113,6 +121,8 @@ class CmdInput extends React.Component<{}, {}> {
let focusVal = inputModel.physicalInputFocused.get(); let focusVal = inputModel.physicalInputFocused.get();
let inputMode: string = inputModel.inputMode.get(); let inputMode: string = inputModel.inputMode.get();
let textAreaInputKey = screen == null ? "null" : screen.screenId; let textAreaInputKey = screen == null ? "null" : screen.screenId;
let win = GlobalModel.getScreenLinesById(screen.screenId) ?? GlobalModel.loadScreenLines(screen.screenId);
let numRunningLines = win.getRunningCmdLines().length;
return ( return (
<div <div
ref={this.cmdInputRef} ref={this.cmdInputRef}
@ -142,6 +152,14 @@ class CmdInput extends React.Component<{}, {}> {
<div className="has-text-white"> <div className="has-text-white">
<span ref={this.promptRef}><Prompt rptr={rptr} festate={feState} /></span> <span ref={this.promptRef}><Prompt rptr={rptr} festate={feState} /></span>
</div> </div>
<If condition={numRunningLines > 0}>
<div onClick={() => this.toggleFilter(screen)}className="cmd-input-filter">
{numRunningLines}
<div className="avatar">
<RotateIcon className="warning spin" />
</div>
</div>
</If>
</div> </div>
<div <div
key="input" key="input"

View File

@ -98,6 +98,11 @@
} }
} }
} }
.filter-running {
margin: auto 1rem 0 1rem;
align-self: center;
}
} }
.screen-settings-inline { .screen-settings-inline {

View File

@ -14,7 +14,7 @@ import { GlobalCommandRunner, TabColors, TabIcons } from "../../../model/model";
import type { LineType, RenderModeType, LineFactoryProps, CommandRtnType } from "../../../types/types"; import type { LineType, RenderModeType, LineFactoryProps, CommandRtnType } from "../../../types/types";
import * as T from "../../../types/types"; import * as T from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { InlineSettingsTextEdit, RemoteStatusLight } from "../../common/common"; import { InlineSettingsTextEdit, RemoteStatusLight, Button } from "../../common/common";
import { getRemoteStr } from "../../common/prompt/prompt"; import { getRemoteStr } from "../../common/prompt/prompt";
import { GlobalModel, ScreenLines, Screen, Session } from "../../../model/model"; import { GlobalModel, ScreenLines, Screen, Session } from "../../../model/model";
import { Line } from "../../line/linecomps"; import { Line } from "../../line/linecomps";
@ -334,6 +334,22 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
return <Line key={realLine.lineid} screen={screen} line={realLine} {...restProps} />; return <Line key={realLine.lineid} screen={screen} line={realLine} {...restProps} />;
} }
determineVisibleLines(win: ScreenLines): LineType[] {
let { screen } = this.props;
if (screen.filterRunning.get()) {
return win.getRunningCmdLines();
}
return win.getNonArchivedLines();
}
@boundMethod
disableFilter() {
let { screen } = this.props;
mobx.action(() => {
screen.filterRunning.set(false);
})();
}
render() { render() {
let { session, screen } = this.props; let { session, screen } = this.props;
let win = this.getScreenLines(); let win = this.getScreenLines();
@ -351,7 +367,7 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
return this.renderError("loading client data", true); return this.renderError("loading client data", true);
} }
let isActive = screen.isActive(); let isActive = screen.isActive();
let lines = win.getNonArchivedLines(); let lines = this.determineVisibleLines(win);
let renderMode = this.renderMode.get(); let renderMode = this.renderMode.get();
return ( return (
<div className="window-view" ref={this.windowViewRef}> <div className="window-view" ref={this.windowViewRef}>
@ -374,7 +390,7 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
<NewTabSettings screen={screen} /> <NewTabSettings screen={screen} />
</If> </If>
<If condition={screen.nextLineNum.get() != 1}> <If condition={screen.nextLineNum.get() != 1}>
<div className="window-view" ref={this.windowViewRef} data-screenid={screen.screenId}> <div className="window-empty" ref={this.windowViewRef} data-screenid={screen.screenId}>
<div key="lines" className="lines"></div> <div key="lines" className="lines"></div>
<div key="window-empty" className={cn("window-empty")}> <div key="window-empty" className={cn("window-empty")}>
<div> <div>
@ -422,6 +438,19 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
lineFactory={this.buildLineComponent} lineFactory={this.buildLineComponent}
/> />
</If> </If>
<If condition={screen.filterRunning.get()}>
<div className='filter-running'>
<Button
variant="outlined"
color="color-yellow"
style={{borderRadius: '999px'}}
onClick={this.disableFilter}
>
Showing Running Commands &nbsp;
<i className="fa-sharp fa-solid fa-xmark" />
</Button>
</div>
</If>
</div> </div>
); );
} }

View File

@ -357,6 +357,7 @@ class Screen {
renderers: Record<string, RendererModel> = {}; // lineid => RendererModel renderers: Record<string, RendererModel> = {}; // lineid => RendererModel
shareMode: OV<string>; shareMode: OV<string>;
webShareOpts: OV<WebShareOpts>; webShareOpts: OV<WebShareOpts>;
filterRunning: OV<boolean>;
constructor(sdata: ScreenDataType) { constructor(sdata: ScreenDataType) {
this.sessionId = sdata.sessionid; this.sessionId = sdata.sessionid;
@ -393,6 +394,9 @@ class Screen {
this.webShareOpts = mobx.observable.box(sdata.webshareopts, { this.webShareOpts = mobx.observable.box(sdata.webshareopts, {
name: "screen-webShareOpts", name: "screen-webShareOpts",
}); });
this.filterRunning = mobx.observable.box(false, {
name: "screen-filter-running",
})
} }
dispose() {} dispose() {}