From 14af17675c54083983674d073ed8b1dff0db1e29 Mon Sep 17 00:00:00 2001 From: Nikolai Peram Date: Fri, 17 Sep 2021 20:34:26 -0400 Subject: [PATCH] Allow editing trace set labels via Opened Traces widget. Fixes issue #384. Made each menu item into its own react component. Signed-off-by: Nikolai Peram --- packages/base/src/signals/signal-manager.ts | 4 + .../src/trace-explorer/menu-item-trace.tsx | 148 ++++++++++++++++++ .../trace-explorer-opened-traces-widget.tsx | 52 +++--- .../react-components/style/trace-explorer.css | 29 +++- .../src/browser/trace-viewer/trace-viewer.tsx | 8 + 5 files changed, 218 insertions(+), 23 deletions(-) create mode 100644 packages/react-components/src/trace-explorer/menu-item-trace.tsx diff --git a/packages/base/src/signals/signal-manager.ts b/packages/base/src/signals/signal-manager.ts index 194280c5d..795bbe2e5 100644 --- a/packages/base/src/signals/signal-manager.ts +++ b/packages/base/src/signals/signal-manager.ts @@ -23,6 +23,7 @@ export declare interface SignalManager { } export const Signals = { + EXPERIMENT_CHANGED: 'tab changed', TRACE_OPENED: 'trace opened', TRACE_CLOSED: 'trace closed', EXPERIMENT_OPENED: 'experiment opened', @@ -43,6 +44,9 @@ export const Signals = { }; export class SignalManager extends EventEmitter implements SignalManager { + fireExperimentChangedSignal(tabName: string, experimentUUID: string): void { + this.emit(Signals.EXPERIMENT_CHANGED, { tabName, experimentUUID }); + } fireTraceOpenedSignal(trace: Trace): void { this.emit(Signals.TRACE_OPENED, trace); } diff --git a/packages/react-components/src/trace-explorer/menu-item-trace.tsx b/packages/react-components/src/trace-explorer/menu-item-trace.tsx new file mode 100644 index 000000000..6580b3ed9 --- /dev/null +++ b/packages/react-components/src/trace-explorer/menu-item-trace.tsx @@ -0,0 +1,148 @@ +import * as React from 'react'; +import { Trace } from 'tsp-typescript-client'; +import { signalManager, Signals } from '@trace-viewer/base/lib/signals/signal-manager'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faPencilAlt } from '@fortawesome/free-solid-svg-icons'; + +interface MenuItemProps { + index: number; + experimentName: string; + experimentUUID: string; + traces: Trace[]; + onExperimentNameChange: (newExperimentName: string, index: number) => void; + menuItemTraceContainerClassName: string; + handleClickEvent: (event: React.MouseEvent, experimentName: string) => void; + handleContextMenuEvent: (event: React.MouseEvent, experimentUUID: string) => void; +} +interface MenuItemState { + editingTab: boolean; + oldExperimentName: string; + experimentNameState: string; +} + +export class MenuItemTrace extends React.Component { + + private wrapper: React.RefObject; + + constructor(menuItemProps: MenuItemProps) { + super(menuItemProps); + this.state = { + oldExperimentName: this.props.experimentName, + experimentNameState: this.props.experimentName, + editingTab: false + }; + this.wrapper = React.createRef(); + this.handleClickOutside = this.handleClickOutside.bind(this); + } + + submitNewExperimentName(): void { + if (this.state.experimentNameState.length > 0) { + this.setState({ + editingTab: false, + oldExperimentName: this.state.experimentNameState + }); + this.props.onExperimentNameChange(this.state.experimentNameState, this.props.index); + } else { + this.setState({ + editingTab: false, + experimentNameState: this.state.oldExperimentName + }); + const tabName = 'Trace: ' + this.state.oldExperimentName; + signalManager().fireExperimentChangedSignal(tabName, this.props.experimentUUID); + } + document.removeEventListener('click', this.handleClickOutside); + } + + handleClickOutside = (event: Event): void => { + const node = this.wrapper.current; + if ((!node || !node.contains(event.target as Node)) && this.state.editingTab === true) { + this.submitNewExperimentName(); + } + }; + + protected handleEnterPress(event: React.KeyboardEvent): void{ + if (event.key === 'Enter' && this.state.editingTab === true){ + this.submitNewExperimentName(); + } + } + + protected changeText(event: React.ChangeEvent): void{ + let newName = event.target.value.toString(); + this.setState({ + experimentNameState : newName + }); + newName = 'Trace: ' + newName; + signalManager().fireExperimentChangedSignal(newName, this.props.experimentUUID); + } + protected inputTab(): React.ReactNode { + if (!this.state.editingTab) { + return ( +
+ {this.state.experimentNameState} +
{this.renderEditTraceName(e);}}> + +
+
+ ); + } + return ( (this.changeText(e))} + onClick = {e => e.stopPropagation()} + onKeyPress = {e => (this.handleEnterPress(e))} + maxLength = {50} + />); + } + protected renderEditTraceName(event: React.MouseEvent): void { + document.addEventListener('click', this.handleClickOutside); + this.setState(() => ({ + editingTab: true + })); + event.stopPropagation(); + event.preventDefault(); + } + protected renderTracesForExperiment = (): React.ReactNode => this.doRenderTracesForExperiment(); + protected doRenderTracesForExperiment(): React.ReactNode { + const tracePaths = this.props.traces; + return ( +
+ {tracePaths.map(trace => ( +
+ {` > ${trace.name}`} +
+ ))} +
+ ); + } + protected subscribeToExplorerEvents(): void { + signalManager().on(Signals.OUTPUT_ADDED, this.changeText); + } + + render(): JSX.Element { + return ( +
{ + this.props.handleClickEvent(event, this.props.experimentUUID); + } + } + onContextMenu={event => { this.props.handleContextMenuEvent(event, this.props.experimentUUID); }} + data-id={`${this.props.index}`} + ref={this.wrapper}> +
+
+

+ {this.inputTab()} +

+ { this.renderTracesForExperiment() } +
+ {/*
+ +
*/} +
+
+ ); + } +} diff --git a/packages/react-components/src/trace-explorer/trace-explorer-opened-traces-widget.tsx b/packages/react-components/src/trace-explorer/trace-explorer-opened-traces-widget.tsx index 76e0e84d4..d0adda26b 100644 --- a/packages/react-components/src/trace-explorer/trace-explorer-opened-traces-widget.tsx +++ b/packages/react-components/src/trace-explorer/trace-explorer-opened-traces-widget.tsx @@ -8,6 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCopy } from '@fortawesome/free-solid-svg-icons'; import { OpenedTracesUpdatedSignalPayload } from '@trace-viewer/base/lib/signals/opened-traces-updated-signal-payload'; import { ITspClientProvider } from '@trace-viewer/base/lib/tsp-client-provider'; +import { MenuItemTrace } from './menu-item-trace'; export interface ReactOpenTracesWidgetProps { id: string, @@ -44,6 +45,7 @@ export class ReactOpenTracesWidget extends React.Component { this._experimentManager = this.props.tspClientProvider.getExperimentManager(); @@ -109,7 +111,6 @@ export class ReactOpenTracesWidget extends React.Component {({ width }) => { this.handleClickEvent(event, traceUUID); }} - onContextMenu={event => { this.handleContextMenuEvent(event, traceUUID); }} - data-id={`${props.index}`}> -
-
-

{traceName}

- {this.renderTracesForExperiment(props.index)} -
- {/*
- -
*/} -
- ; + return ; } protected renderTracesForExperiment(index: number): React.ReactNode { @@ -235,15 +227,33 @@ export class ReactOpenTracesWidget extends React.Component { const remoteExperiments = await this._experimentManager.getOpenedExperiments(); + remoteExperiments.forEach(experiment => { this._experimentManager.addExperiment(experiment); }); + + remoteExperiments.forEach(newExp => { + this.state.openedExperiments.forEach(oldExp =>{ + if (newExp.UUID === oldExp.UUID) { + newExp.name = oldExp.name; + } + }); + }); + const selectedIndex = remoteExperiments.findIndex(experiment => this._selectedExperiment && experiment.UUID === this._selectedExperiment.UUID); this.setState({ openedExperiments: remoteExperiments, selectedExperimentIndex: selectedIndex !== -1 ? selectedIndex : 0 }); signalManager().fireOpenedTracesChangedSignal(new OpenedTracesUpdatedSignalPayload(remoteExperiments ? remoteExperiments.length : 0)); } + protected handleExperimentNameUpdate(newExperimentName: string, index: number): void { + const modifiedOpenedExperiments = this.state.openedExperiments.slice(); + modifiedOpenedExperiments[index].name = newExperimentName; + this.setState({ + openedExperiments: modifiedOpenedExperiments + }); + } + protected handleShareButtonClick = (index: number): void => this.doHandleShareButtonClick(index); protected doHandleShareButtonClick(index: number): void { diff --git a/packages/react-components/style/trace-explorer.css b/packages/react-components/style/trace-explorer.css index 516eb10cd..c51eda321 100644 --- a/packages/react-components/style/trace-explorer.css +++ b/packages/react-components/style/trace-explorer.css @@ -53,7 +53,29 @@ .trace-list-container.theia-mod-selected, .outputs-list-container.theia-mod-selected { - background-color: var(--theia-selection-background); + background-color: #3b3a4a; +} + +.trace-list-container:hover .edit-trace-name { + visibility: visible; + float: right; +} + +.trace-list-container:hover .wrapper { + background-color: #151515; +} + +.trace-list-container .edit-trace-name { + visibility: hidden; +} + +.name-input-box { + background-color: #151515; + margin-bottom: 5px; + width: 100%; + height: 18px; + -webkit-box-sizing: border-box; + box-sizing: border-box; } /* Share options have been commented out, grid is disabled to optimize horizontal space */ @@ -75,7 +97,10 @@ .trace-element-name, .outputs-element-name { font-weight: bold; - margin: unset; + margin-top: unset; + margin-left: unset; + margin-right: unset; + margin-bottom: 2px; height: var(--trace-extension-list-line-height); } diff --git a/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer.tsx b/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer.tsx index c909e814d..53d34ffa9 100644 --- a/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer.tsx +++ b/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer.tsx @@ -54,6 +54,7 @@ export class TraceViewerWidget extends ReactWidget { private onOutputAdded = (payload: OutputAddedSignalPayload): void => this.doHandleOutputAddedSignal(payload); private onExperimentSelected = (experiment: Experiment): void => this.doHandleExperimentSelectedSignal(experiment); private onCloseExperiment = (UUID: string): void => this.doHandleCloseExperimentSignal(UUID); + private onExperimentNameChange = (payload: { tabName: string, experimentUUID: string; }): void => this.doHandleExperimentNameChange(payload); @inject(TraceViewerWidgetOptions) protected readonly options: TraceViewerWidgetOptions; @inject(TspClientProvider) protected tspClientProvider: TspClientProvider; @@ -111,6 +112,7 @@ export class TraceViewerWidget extends ReactWidget { signalManager().on(Signals.OUTPUT_ADDED, this.onOutputAdded); signalManager().on(Signals.EXPERIMENT_SELECTED, this.onExperimentSelected); signalManager().on(Signals.CLOSE_TRACEVIEWERTAB, this.onCloseExperiment); + signalManager().on(Signals.EXPERIMENT_CHANGED, this.onExperimentNameChange); } protected updateBackgroundTheme(): void { @@ -118,6 +120,12 @@ export class TraceViewerWidget extends ReactWidget { signalManager().fireThemeChangedSignal(currentThemeType); } + protected doHandleExperimentNameChange(payload: { tabName: string, experimentUUID: string; }): void { + if (payload.experimentUUID === this.options.traceUUID) { + this.title.label = payload.tabName; + } + } + dispose(): void { super.dispose(); signalManager().off(Signals.OUTPUT_ADDED, this.onOutputAdded);