Skip to content

Commit 6e923bc

Browse files
Add UI components for trace output customization
This commit enhances the react-components package to support the trace output customization feature by adding UI elements that allow users to: - Trigger customization of outputs directly from the Available Views panel - Remove custom outputs they no longer need - Visually identify customizable and removable outputs The implementation extends the filter tree components to support interactive elements and provides handlers for customization and deletion operations. These UI enhancements complete the customization workflow by connecting the JSON editor to the trace explorer interface. Signed-off-by: Will Yang <[email protected]>
1 parent db365be commit 6e923bc

File tree

9 files changed

+150
-33
lines changed

9 files changed

+150
-33
lines changed

packages/react-components/src/components/utils/filter-tree/table-cell.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class TableCell extends React.Component<TableCellProps> {
1818
if (node.elementIndex && node.elementIndex === index && node.getElement) {
1919
content = node.getElement();
2020
} else {
21-
content = node.labels[index];
21+
content = node.getEnrichedContent ? node.getEnrichedContent() : node.labels[index];
2222
}
2323

2424
let title = undefined;
@@ -30,6 +30,7 @@ export class TableCell extends React.Component<TableCellProps> {
3030
title = node.labels[index];
3131
}
3232
}
33+
3334
return (
3435
<td key={this.props.index + '-td-' + this.props.node.id}>
3536
<span title={title}>

packages/react-components/src/components/utils/filter-tree/table-row.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export class TableRow extends React.Component<TableRowProps> {
2929

3030
isCollapsed = (): boolean => this.props.collapsedNodes.includes(this.props.node.id);
3131

32-
private handleCollapse = (): void => {
32+
private handleCollapse = (e: React.MouseEvent<HTMLDivElement>): void => {
33+
e.stopPropagation();
3334
this.props.onToggleCollapse(this.props.node.id);
3435
};
3536

@@ -40,10 +41,10 @@ export class TableRow extends React.Component<TableRowProps> {
4041
renderToggleCollapse = (): React.ReactNode => {
4142
const width = (this.props.level + 1) * 12;
4243
return this.props.node.children.length === 0 ? (
43-
<div style={{ width, paddingRight: 5, display: 'inline-block' }} />
44+
<div style={{ width, paddingRight: 5, display: 'inline-block', flexShrink: 0 }} />
4445
) : (
4546
<div
46-
style={{ width, paddingRight: 5, textAlign: 'right', display: 'inline-block' }}
47+
style={{ width, paddingRight: 5, textAlign: 'right', display: 'inline-block', flexShrink: 0 }}
4748
onClick={this.handleCollapse}
4849
>
4950
{this.isCollapsed() ? icons.expand : icons.collapse}

packages/react-components/src/components/utils/filter-tree/tree-node.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,15 @@ export interface TreeNode {
88
showTooltip?: boolean;
99
elementIndex?: number;
1010
getElement?: () => JSX.Element;
11+
/**
12+
* TODO - Remove or fix this comment.
13+
*
14+
* This lets you add dynamic HTML where the content goes in a Tree Node
15+
* Instead of just a string.
16+
* I'm not sure what the use case of getElement()? is based on the logic in TableCell.jsx
17+
* So I didn't use it. But we may be able to just use that instead.
18+
*
19+
* -WY
20+
*/
21+
getEnrichedContent?: () => JSX.Element;
1122
}

packages/react-components/src/trace-explorer/trace-explorer-views-widget.tsx

Lines changed: 90 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,20 @@ import { ExperimentManager } from 'traceviewer-base/lib/experiment-manager';
99
import { FilterTree } from '../components/utils/filter-tree/tree';
1010
import { TreeNode } from '../components/utils/filter-tree/tree-node';
1111
import { getAllExpandedNodeIds } from '../components/utils/filter-tree/utils';
12+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
13+
import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons';
1214

1315
export interface ReactAvailableViewsProps {
1416
id: string;
1517
title: string;
1618
tspClientProvider: ITspClientProvider;
1719
contextMenuRenderer?: (event: React.MouseEvent<HTMLDivElement>, output: OutputDescriptor) => void;
20+
/**
21+
* This is a placeholder for the customization implementation.
22+
* TODO - Make sure this comment an accurate reflection before PR.
23+
* @returns
24+
*/
25+
onCustomizationClick?: (entry: OutputDescriptor, experiment: Experiment) => void;
1826
}
1927

2028
export interface ReactAvailableViewsState {
@@ -179,8 +187,9 @@ export class ReactAvailableViewsWidget extends React.Component<ReactAvailableVie
179187
const idStringToNodeId: { [key: string]: number } = {};
180188

181189
// Fill-in the lookup table
182-
list.forEach(output => {
190+
list.forEach((output, index) => {
183191
const node: TreeNode = this.entryToTreeNode(output, idStringToNodeId);
192+
node.elementIndex = index;
184193
lookup[output.id] = node;
185194
this._nodeIdToOutput[node.id] = output;
186195
});
@@ -207,34 +216,90 @@ export class ReactAvailableViewsWidget extends React.Component<ReactAvailableVie
207216
}
208217

209218
private entryToTreeNode(entry: OutputDescriptor, idStringToNodeId: { [key: string]: number }): TreeNode {
210-
const labels = [entry.name];
211-
let tooltips = undefined;
212-
if (entry.description) {
213-
tooltips = [entry.description];
214-
}
215-
let id = idStringToNodeId[entry.id];
216-
if (id === undefined) {
217-
id = this._idGenerator++;
218-
idStringToNodeId[entry.id] = id;
219-
}
219+
let id = idStringToNodeId[entry.id] ?? (idStringToNodeId[entry.id] = this._idGenerator++);
220+
220221
let parentId = -1;
221222
if (entry.parentId) {
222-
const existingId = idStringToNodeId[entry.parentId];
223-
if (existingId === undefined) {
224-
parentId = this._idGenerator++;
225-
idStringToNodeId[entry.parentId] = parentId;
226-
} else {
227-
parentId = existingId;
228-
}
223+
parentId = idStringToNodeId[entry.parentId] ?? (idStringToNodeId[entry.parentId] = this._idGenerator++);
229224
}
230-
return {
231-
labels: labels,
232-
tooltips: tooltips,
225+
226+
const treeNode: TreeNode = {
227+
labels: [entry.name],
228+
tooltips: entry.description ? [entry.description] : undefined,
233229
showTooltip: true,
234230
isRoot: false,
235-
id: id,
236-
parentId: parentId,
237-
children: []
238-
} as TreeNode;
231+
id,
232+
parentId,
233+
children: [],
234+
getEnrichedContent: this.createEnrichedContent(entry)
235+
};
236+
237+
return treeNode;
239238
}
239+
240+
private createEnrichedContent(entry: OutputDescriptor): (() => JSX.Element) | undefined {
241+
const isCustomizable = entry.capabilities?.canCreate === true;
242+
const isDeletable = entry.capabilities?.canDelete === true;
243+
244+
if (!isCustomizable && !isDeletable) {
245+
return undefined;
246+
}
247+
248+
const nameSpanStyle = {
249+
overflow: 'hidden',
250+
textOverflow: 'ellipsis',
251+
whiteSpace: 'nowrap',
252+
minWidth: 0,
253+
flexShrink: 1
254+
};
255+
256+
if (isCustomizable) {
257+
return (): JSX.Element => (
258+
<>
259+
<span style={nameSpanStyle}>{entry.name}</span>
260+
<div className="remove-output-button-container" title={`Add custom analysis to ${entry.name}`}>
261+
<button className="remove-output-button" onClick={e => this.handleCustomizeClick(entry, e)}>
262+
<FontAwesomeIcon icon={faPlus} />
263+
</button>
264+
</div>
265+
</>
266+
);
267+
} else {
268+
// Must be deletable based on our conditions
269+
return (): JSX.Element => (
270+
<>
271+
<span style={nameSpanStyle}>{entry.configuration?.name}</span>
272+
<div className="remove-output-button-container" title={`Remove "${entry.configuration?.name}"`}>
273+
<button className="remove-output-button" onClick={e => this.handleDeleteClick(entry, e)}>
274+
<FontAwesomeIcon icon={faTimes} />
275+
</button>
276+
</div>
277+
</>
278+
);
279+
}
280+
}
281+
282+
private handleCustomizeClick = async (entry: OutputDescriptor, e: React.MouseEvent) => {
283+
e.stopPropagation();
284+
if (this.props.onCustomizationClick && this._selectedExperiment) {
285+
await this.props.onCustomizationClick(entry, this._selectedExperiment);
286+
this.updateAvailableViews();
287+
}
288+
};
289+
290+
private handleDeleteClick = async (entry: OutputDescriptor, e: React.MouseEvent) => {
291+
e.stopPropagation();
292+
if (this._selectedExperiment?.UUID) {
293+
console.dir(entry);
294+
const res = await this.props.tspClientProvider
295+
.getTspClient()
296+
.deleteDerivedOutput(this._selectedExperiment.UUID, entry.parentId as string, entry.id);
297+
if (!res.isOk()) {
298+
// request is failing for some reason...
299+
// But the output is removed when we update available views regardless
300+
console.error(`${res.getStatusCode()} - ${res.getStatusMessage()}`);
301+
}
302+
this.updateAvailableViews();
303+
}
304+
};
240305
}

packages/react-components/style/output-components-style.css

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,8 @@ canvas {
246246
text-overflow: ellipsis;
247247
overflow: hidden;
248248
white-space: nowrap;
249-
display: block;
249+
display: flex;
250+
align-items: center;
250251
}
251252

252253
.item-properties-table .table-tree td span {
@@ -557,3 +558,40 @@ canvas {
557558
margin-left: 4px;
558559
cursor: pointer;
559560
}
561+
562+
input[type="button"].input-button,
563+
input[type="submit"].input-button,
564+
input[type="reset"].input-button {
565+
border: none;
566+
color: var(--trace-viewer-button-foreground);
567+
background-color: var(--trace-viewer-button-background);
568+
min-width: 65px;
569+
outline: none;
570+
cursor: pointer;
571+
padding: 4px 9px;
572+
font-size-adjust: 0.45;
573+
margin-left: auto;
574+
margin-right: 10px;
575+
flex-shrink: 0;
576+
}
577+
578+
.remove-output-button-container {
579+
margin-right: 2px;
580+
display: flex;
581+
justify-content: center;
582+
align-items: flex-start;
583+
margin-left: auto;
584+
}
585+
586+
.remove-output-button {
587+
background: none;
588+
border: none;
589+
visibility: visible;
590+
margin-top: 2px;
591+
color: var(--trace-viewer-ui-font-color0);
592+
cursor: pointer;
593+
}
594+
595+
.remove-output-button :hover {
596+
background: none;
597+
}

theia-extensions/viewer-prototype/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@theia/editor": "1.55.0",
2525
"@theia/filesystem": "1.55.0",
2626
"@theia/messages": "1.55.0",
27+
"ajv": "^8.17.1",
2728
"animate.css": "^4.1.1",
2829
"traceviewer-base": "0.7.2",
2930
"traceviewer-react-components": "0.7.2",

theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-sub-widgets/theia-trace-explorer-views-widget.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export class TraceExplorerViewsWidget extends ReactWidget {
2626
id={this.id}
2727
title={this.title.label}
2828
tspClientProvider={this.tspClientProvider}
29+
onCustomizationClick={console.log}
2930
></ReactAvailableViewsWidget>
3031
</div>
3132
);

theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer-frontend-module.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ export default new ContainerModule(bind => {
4646
bind(TabBarToolbarContribution).toService(TraceViewerToolbarContribution);
4747
bind(CommandContribution).toService(TraceViewerToolbarContribution);
4848
bind(TraceServerConnectionStatusClient).to(TraceServerConnectionStatusClientImpl).inSingletonScope();
49-
5049
bind(TraceViewerWidget).toSelf();
5150
bind<WidgetFactory>(WidgetFactory)
5251
.toDynamicValue(context => ({

yarn.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4452,7 +4452,7 @@ ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.3:
44524452
json-schema-traverse "^0.4.1"
44534453
uri-js "^4.2.2"
44544454

4455-
ajv@^8.0.0, ajv@^8.0.1, ajv@^8.6.3, ajv@^8.9.0:
4455+
ajv@^8.0.0, ajv@^8.0.1, ajv@^8.17.1, ajv@^8.6.3, ajv@^8.9.0:
44564456
version "8.17.1"
44574457
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
44584458
integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
@@ -14099,7 +14099,7 @@ tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.6
1409914099

1410014100
tsp-typescript-client@^0.6.0:
1410114101
version "0.6.0"
14102-
resolved "https://registry.npmjs.org/tsp-typescript-client/-/tsp-typescript-client-0.6.0.tgz#59d53a76dcb7759f8f16eb9e798320a9a790b7b1"
14102+
resolved "https://registry.yarnpkg.com/tsp-typescript-client/-/tsp-typescript-client-0.6.0.tgz#59d53a76dcb7759f8f16eb9e798320a9a790b7b1"
1410314103
integrity sha512-K6tl773Nq7lo2XAexHBtVDKiFGUlrwFbzKL6aZkf33iHRyAM80xBc0cAoXXTgsSLb3pBodLQRVzw8sBTGWGwOA==
1410414104
dependencies:
1410514105
json-bigint sidorares/json-bigint#2c0a5f896d7888e68e5f4ae3c7ea5cd42fd54473

0 commit comments

Comments
 (0)