Skip to content

Commit 33d7ea8

Browse files
committed
Added ability for users to change tab names from menu items in trace explorer widget as outlined in issue #384
Signed-off-by: Nikolai Peram <[email protected]>
1 parent 820176f commit 33d7ea8

File tree

5 files changed

+193
-19
lines changed

5 files changed

+193
-19
lines changed

packages/base/src/signals/signal-manager.ts

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export declare interface SignalManager {
2323
}
2424

2525
export const Signals = {
26+
TABNAME_CHANGED: 'tab changed',
2627
TRACE_OPENED: 'trace opened',
2728
TRACE_CLOSED: 'trace closed',
2829
EXPERIMENT_OPENED: 'experiment opened',
@@ -43,6 +44,9 @@ export const Signals = {
4344
};
4445

4546
export class SignalManager extends EventEmitter implements SignalManager {
47+
fireTabChangedSignal(tabName: string, expirementUUID: string): void {
48+
this.emit(Signals.TABNAME_CHANGED, {tabName, expirementUUID});
49+
}
4650
fireTraceOpenedSignal(trace: Trace): void {
4751
this.emit(Signals.TRACE_OPENED, trace);
4852
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import * as React from 'react';
2+
import { Trace } from 'tsp-typescript-client';
3+
import { signalManager, Signals } from '@trace-viewer/base/lib/signals/signal-manager';
4+
5+
interface MenuItemProps {
6+
index: number;
7+
experimentName: string;
8+
experimentUUID: string;
9+
traces: Trace[];
10+
onTabNameChange: (tabEditingOpen: string, index: number) => void;
11+
menuItemTraceContainerClassName: string;
12+
handleClickEvent: (event: React.MouseEvent<HTMLDivElement>, experimentName: string) => void;
13+
handleContextMenuEvent: (event: React.MouseEvent<HTMLDivElement>, experimentUUID: string) => void;
14+
}
15+
interface MenuItemState {
16+
editingTab: boolean;
17+
oldexpirementName: string;
18+
expirementNameState: string;
19+
}
20+
21+
export class MenuItemTrace extends React.Component<MenuItemProps, MenuItemState> {
22+
23+
private wrapper: React.RefObject<HTMLDivElement>;
24+
25+
constructor(menuItemProps: MenuItemProps) {
26+
super(menuItemProps);
27+
this.state = {
28+
oldexpirementName: this.props.experimentName,
29+
expirementNameState: this.props.experimentName,
30+
editingTab: false
31+
};
32+
this.wrapper = React.createRef();
33+
}
34+
35+
componentDidMount(): void {
36+
document.addEventListener('click', this.handleClickOutside);
37+
}
38+
39+
componentWillUnmount(): void {
40+
document.removeEventListener('click', this.handleClickOutside);
41+
}
42+
43+
submitNewTab(): void {
44+
if (this.state.expirementNameState.length > 0) {
45+
this.setState({
46+
editingTab: false,
47+
oldexpirementName: this.state.expirementNameState
48+
});
49+
this.props.onTabNameChange(this.state.expirementNameState, this.props.index);
50+
} else {
51+
this.setState({
52+
editingTab: false,
53+
expirementNameState: this.state.oldexpirementName
54+
});
55+
signalManager().fireTabChangedSignal(this.state.oldexpirementName, this.props.experimentUUID);
56+
}
57+
}
58+
59+
handleClickOutside = (event: Event): void => {
60+
const node = this.wrapper.current;
61+
if ((!node || !node.contains(event.target as Node)) && this.state.editingTab === true) {
62+
this.submitNewTab();
63+
}
64+
};
65+
66+
protected handleEnterPress(event: React.KeyboardEvent<HTMLInputElement>): void{
67+
if (event.key === 'Enter' && this.state.editingTab === true){
68+
this.submitNewTab();
69+
}
70+
}
71+
72+
protected changeText(event: React.ChangeEvent<HTMLInputElement>): void{
73+
const newName = event.target.value.toString();
74+
this.setState({
75+
expirementNameState : newName
76+
});
77+
signalManager().fireTabChangedSignal(newName, this.props.experimentUUID);
78+
}
79+
protected inputTab(): React.ReactNode {
80+
return <input name="tab-name" className="theia-input"
81+
defaultValue = {this.state.expirementNameState}
82+
onChange = {e => (this.changeText(e))}
83+
onClick = {e => e.stopPropagation()}
84+
onKeyPress = {e => (this.handleEnterPress(e))}
85+
maxLength = {50}
86+
/>;
87+
}
88+
protected renderEditTraceName(event: React.MouseEvent<HTMLDivElement>): void {
89+
this.setState(() => ({
90+
editingTab: true
91+
}));
92+
event.stopPropagation();
93+
}
94+
protected renderTracesForExperiment = (): React.ReactNode => this.doRenderTracesForExperiment();
95+
protected doRenderTracesForExperiment(): React.ReactNode {
96+
const tracePaths = this.props.traces;
97+
return (
98+
<div className='trace-element-path-container'>
99+
{tracePaths.map(trace => (
100+
<div className='trace-element-path child-element' id={trace.UUID} key={trace.UUID}>
101+
{` > ${trace.name}`}
102+
</div>
103+
))}
104+
</div>
105+
);
106+
}
107+
protected subscribeToExplorerEvents(): void {
108+
signalManager().on(Signals.OUTPUT_ADDED, this.changeText);
109+
}
110+
111+
render(): JSX.Element {
112+
return (
113+
<div className={this.props.menuItemTraceContainerClassName}
114+
id={`${this.props.menuItemTraceContainerClassName}-${this.props.index}`}
115+
onClick={event => { this.props.handleClickEvent(event, this.props.experimentUUID); }}
116+
onContextMenu={event => { this.props.handleContextMenuEvent(event, this.props.experimentUUID); }}
117+
data-id={`${this.props.index}`}
118+
ref={this.wrapper}>
119+
<div className='trace-element-container'>
120+
<div className='trace-element-info' >
121+
<h4 className='trace-element-name'>
122+
{this.state.editingTab ? this.inputTab() : this.state.expirementNameState}
123+
<div className='edit-trace-name' onClick={e => {this.renderEditTraceName(e);}}>
124+
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
125+
width="16px" height="16px" viewBox="0 0 512 512" enableBackground="new 0 0 512 512" xmlSpace="preserve">
126+
<g>
127+
<path fill="#020202" d="M422.953,176.019c0.549-0.48,1.09-0.975,1.612-1.498l21.772-21.772c12.883-12.883,12.883-33.771,0-46.654
128+
l-40.434-40.434c-12.883-12.883-33.771-12.883-46.653,0l-21.772,21.772c-0.523,0.523-1.018,1.064-1.498,1.613L422.953,176.019z"/>
129+
<polygon fill="#020202" points="114.317,397.684 157.317,440.684 106.658,448.342 56,456 63.658,405.341 71.316,354.683 "/>
130+
<polygon fill="#020202" points="349.143,125.535 118.982,355.694 106.541,343.253 336.701,113.094 324.26,100.653 81.659,343.253
131+
168.747,430.341 411.348,187.74 "/>
132+
</g>
133+
</svg>
134+
</div>
135+
</h4>
136+
{ this.renderTracesForExperiment() }
137+
</div>
138+
{/* <div className='trace-element-options'>
139+
<button className='share-context-button' onClick={this.handleShareButtonClick.bind(this, props.index)}>
140+
<FontAwesomeIcon icon={faShareSquare} />
141+
</button>
142+
</div> */}
143+
</div>
144+
</div>
145+
);
146+
}
147+
}

packages/react-components/src/trace-explorer/trace-explorer-opened-traces-widget.tsx

+30-19
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
88
import { faCopy } from '@fortawesome/free-solid-svg-icons';
99
import { OpenedTracesUpdatedSignalPayload } from '@trace-viewer/base/lib/signals/opened-traces-updated-signal-payload';
1010
import { ITspClientProvider } from '@trace-viewer/base/lib/tsp-client-provider';
11+
import { MenuItemTrace } from './menu-item-trace';
1112

1213
export interface ReactOpenTracesWidgetProps {
1314
id: string,
@@ -44,6 +45,7 @@ export class ReactOpenTracesWidget extends React.Component<ReactOpenTracesWidget
4445
signalManager().on(Signals.EXPERIMENT_CLOSED, this._onExperimentClosed);
4546
signalManager().on(Signals.TRACEVIEWERTAB_ACTIVATED, this._onOpenedTracesWidgetActivated);
4647

48+
this.handleTabNameUpdate = this.handleTabNameUpdate.bind(this);
4749
this._experimentManager = this.props.tspClientProvider.getExperimentManager();
4850
this.props.tspClientProvider.addTspClientChangeListener(() => {
4951
this._experimentManager = this.props.tspClientProvider.getExperimentManager();
@@ -151,25 +153,16 @@ export class ReactOpenTracesWidget extends React.Component<ReactOpenTracesWidget
151153
if (props.index === this.state.selectedExperimentIndex) {
152154
traceContainerClassName = traceContainerClassName + ' theia-mod-selected';
153155
}
154-
return <div className={traceContainerClassName}
155-
id={`${traceContainerClassName}-${props.index}`}
156-
key={props.key}
157-
style={props.style}
158-
onClick={event => { this.handleClickEvent(event, traceUUID); }}
159-
onContextMenu={event => { this.handleContextMenuEvent(event, traceUUID); }}
160-
data-id={`${props.index}`}>
161-
<div className='trace-element-container'>
162-
<div className='trace-element-info' >
163-
<h4 className='trace-element-name'>{traceName}</h4>
164-
{this.renderTracesForExperiment(props.index)}
165-
</div>
166-
{/* <div className='trace-element-options'>
167-
<button className='share-context-button' onClick={this.handleShareButtonClick.bind(this, props.index)}>
168-
<FontAwesomeIcon icon={faShareSquare} />
169-
</button>
170-
</div> */}
171-
</div>
172-
</div>;
156+
return <MenuItemTrace
157+
onTabNameChange = {this.handleTabNameUpdate}
158+
menuItemTraceContainerClassName = {traceContainerClassName}
159+
traces={this.state.openedExperiments[props.index].traces}
160+
experimentName={traceName}
161+
experimentUUID={traceUUID}
162+
index={props.index}
163+
handleContextMenuEvent={this.handleContextMenuEvent}
164+
handleClickEvent={this.handleClickEvent}
165+
/>;
173166
}
174167

175168
protected renderTracesForExperiment(index: number): React.ReactNode {
@@ -235,15 +228,33 @@ export class ReactOpenTracesWidget extends React.Component<ReactOpenTracesWidget
235228

236229
protected async doUpdateOpenedExperiments(): Promise<void> {
237230
const remoteExperiments = await this._experimentManager.getOpenedExperiments();
231+
238232
remoteExperiments.forEach(experiment => {
239233
this._experimentManager.addExperiment(experiment);
240234
});
235+
236+
remoteExperiments.forEach(newExp => {
237+
this.state.openedExperiments.forEach(oldExp =>{
238+
if (newExp.UUID === oldExp.UUID) {
239+
newExp.name = oldExp.name;
240+
}
241+
});
242+
});
243+
241244
const selectedIndex = remoteExperiments.findIndex(experiment => this._selectedExperiment &&
242245
experiment.UUID === this._selectedExperiment.UUID);
243246
this.setState({ openedExperiments: remoteExperiments, selectedExperimentIndex: selectedIndex !== -1 ? selectedIndex : 0 });
244247
signalManager().fireOpenedTracesChangedSignal(new OpenedTracesUpdatedSignalPayload(remoteExperiments ? remoteExperiments.length : 0));
245248
}
246249

250+
protected handleTabNameUpdate(tabEditingOpen: string, index: number): void {
251+
const exp = this.state.openedExperiments[index];
252+
exp.name = tabEditingOpen;
253+
const experimentOpenNew = this.state.openedExperiments;
254+
experimentOpenNew[index] = exp;
255+
this.setState({openedExperiments: experimentOpenNew});
256+
}
257+
247258
protected handleShareButtonClick = (index: number): void => this.doHandleShareButtonClick(index);
248259

249260
protected doHandleShareButtonClick(index: number): void {

packages/react-components/style/trace-explorer.css

+4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@
5656
background-color: var(--theia-selection-background);
5757
}
5858

59+
.edit-trace-name {
60+
display: inline-block;
61+
}
62+
5963
/* Share options have been commented out, grid is disabled to optimize horizontal space */
6064
/* .trace-element-container {
6165
display: grid;

theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export class TraceViewerWidget extends ReactWidget {
5454
private onOutputAdded = (payload: OutputAddedSignalPayload): void => this.doHandleOutputAddedSignal(payload);
5555
private onExperimentSelected = (experiment: Experiment): void => this.doHandleExperimentSelectedSignal(experiment);
5656
private onCloseExperiment = (UUID: string): void => this.doHandleCloseExperimentSignal(UUID);
57+
private onTabNameChange = (payload: { tabName: string, expirementUUID: string; }): void => this.doHandleTabeNameChange(payload);
5758

5859
@inject(TraceViewerWidgetOptions) protected readonly options: TraceViewerWidgetOptions;
5960
@inject(TspClientProvider) protected tspClientProvider: TspClientProvider;
@@ -111,13 +112,20 @@ export class TraceViewerWidget extends ReactWidget {
111112
signalManager().on(Signals.OUTPUT_ADDED, this.onOutputAdded);
112113
signalManager().on(Signals.EXPERIMENT_SELECTED, this.onExperimentSelected);
113114
signalManager().on(Signals.CLOSE_TRACEVIEWERTAB, this.onCloseExperiment);
115+
signalManager().on(Signals.TABNAME_CHANGED, this.onTabNameChange);
114116
}
115117

116118
protected updateBackgroundTheme(): void {
117119
const currentThemeType = ThemeService.get().getCurrentTheme().type;
118120
signalManager().fireThemeChangedSignal(currentThemeType);
119121
}
120122

123+
protected doHandleTabeNameChange(payload: { tabName: string, expirementUUID: string; }): void {
124+
if (payload.expirementUUID === this.options.traceUUID) {
125+
this.title.label = payload.tabName;
126+
}
127+
}
128+
121129
dispose(): void {
122130
super.dispose();
123131
signalManager().off(Signals.OUTPUT_ADDED, this.onOutputAdded);

0 commit comments

Comments
 (0)