Skip to content

Commit 0ae6977

Browse files
authored
Revamp the tree view (#613)
1 parent 4cf821b commit 0ae6977

File tree

12 files changed

+363
-245
lines changed

12 files changed

+363
-245
lines changed

.bablerc.js

-9
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,6 @@ const plugins = [
1616
camel2DashComponentName: false,
1717
},
1818
'icons',
19-
],
20-
[
21-
'babel-plugin-import',
22-
{
23-
libraryName: '@mui/lab',
24-
libraryDirectory: '',
25-
camel2DashComponentName: false,
26-
},
27-
'lab',
2819
]
2920
];
3021

package-lock.json

-42
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
"@babel/preset-typescript": "^7.21.0",
2828
"@babel/traverse": "^7.20.7",
2929
"@mui/icons-material": "5.10.9",
30-
"@mui/lab": "5.0.0-alpha.104",
3130
"@mui/material": "5.10.10",
3231
"@react-spring/web": "9.7.5",
3332
"babel-loader": "^9.1.2",
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,131 @@
1-
import React from "react";
2-
import TreeView from "@mui/lab/TreeView/";
3-
import TreeItem from "@mui/lab/TreeItem";
4-
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
5-
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
6-
import { StageInfo } from "../../../pipeline-graph-view/pipeline-graph/main/";
1+
import React, { useCallback, useState } from "react";
2+
import {
3+
Result,
4+
StageInfo,
5+
} from "../../../pipeline-graph-view/pipeline-graph/main/";
76
import StepStatus from "../../../step-status/StepStatus";
7+
import "./data-tree-view.scss";
88

9-
const getTreeItemsFromStage = (
10-
stageItems: StageInfo[],
11-
selectedStage: string,
12-
) => {
13-
return stageItems.map((stageItemData) => {
14-
let children: JSX.Element[] = [];
15-
if (stageItemData.children && stageItemData.children.length > 0) {
16-
children = getTreeItemsFromStage(stageItemData.children, selectedStage);
17-
}
18-
return (
19-
<TreeItem
20-
className={
21-
String(stageItemData.id) == selectedStage
22-
? "stage-tree-item-selected"
23-
: "stage-tree-item"
24-
}
25-
key={stageItemData.id}
26-
nodeId={String(stageItemData.id)}
27-
label={
28-
<div
29-
id={`stage-tree-icon-${stageItemData.id}`}
30-
key={`stage-tree-icon-${stageItemData.id}`}
31-
>
9+
export default function DataTreeView({
10+
stages,
11+
selected,
12+
onNodeSelect,
13+
}: DataTreeViewProps) {
14+
const handleSelect = useCallback(
15+
(event: React.MouseEvent, nodeId: string) => {
16+
onNodeSelect(event, nodeId);
17+
},
18+
[onNodeSelect],
19+
);
20+
21+
return (
22+
<div id="tasks">
23+
{stages.map((stage) => (
24+
<TreeNode
25+
key={stage.id}
26+
stage={stage}
27+
selected={selected}
28+
onSelect={handleSelect}
29+
/>
30+
))}
31+
</div>
32+
);
33+
}
34+
35+
function TreeNode({ stage, selected, onSelect }: TreeNodeProps) {
36+
const hasChildren = stage.children && stage.children.length > 0;
37+
const isSelected = String(stage.id) === selected;
38+
const [isExpanded, setIsExpanded] = useState<boolean>(
39+
hasSelectedDescendant(stage),
40+
);
41+
42+
function hasSelectedDescendant(stage: StageInfo): boolean {
43+
return stage.children?.some(
44+
(child) => String(child.id) === selected || hasSelectedDescendant(child),
45+
);
46+
}
47+
48+
const handleToggleClick = (e: React.MouseEvent) => {
49+
e.stopPropagation();
50+
setIsExpanded(!isExpanded);
51+
};
52+
53+
return (
54+
<div className="task">
55+
<div className="pgv-tree-node-header">
56+
<button
57+
onClick={(e) => {
58+
if (!isSelected) {
59+
onSelect(e, String(stage.id));
60+
}
61+
setIsExpanded(!isExpanded);
62+
}}
63+
className={`pgv-tree-item task-link ${
64+
isSelected ? "task-link--active" : ""
65+
}`}
66+
>
67+
<div>
3268
<StepStatus
33-
status={stageItemData.state}
34-
text={stageItemData.name}
35-
key={`status-${stageItemData.id}`}
36-
percent={stageItemData.completePercent}
69+
status={stage.state}
70+
text={stage.name}
71+
key={`status-${stage.id}`}
72+
percent={stage.completePercent}
3773
radius={10}
3874
/>
75+
{stage.state === Result.running && (
76+
<span className="pgv-tree-item__description">
77+
{stage.totalDurationMillis}
78+
</span>
79+
)}
3980
</div>
40-
}
41-
children={children}
42-
classes={{
43-
label: stageItemData.synthetic
44-
? "pgv-graph-node--synthetic"
45-
: undefined,
46-
}}
47-
/>
48-
);
49-
});
50-
};
81+
</button>
5182

52-
export interface DataTreeViewProps {
53-
stages: Array<StageInfo>;
54-
onNodeToggle: (event: React.ChangeEvent<any>, nodeIds: string[]) => void;
55-
onNodeSelect: (event: React.ChangeEvent<any>, nodeIds: string) => void;
56-
selected: string;
57-
expanded: string[];
58-
}
59-
60-
export default class DataTreeView extends React.Component {
61-
props!: DataTreeViewProps;
83+
{hasChildren && (
84+
<button
85+
className={`pgv-toggle-icon ${
86+
isExpanded ? "pgv-toggle-icon--active" : ""
87+
}`}
88+
onClick={handleToggleClick}
89+
aria-label={isExpanded ? "Collapse" : "Expand"}
90+
>
91+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
92+
<path
93+
fill="none"
94+
stroke="currentColor"
95+
strokeLinecap="round"
96+
strokeLinejoin="round"
97+
strokeWidth="48"
98+
d="M184 112l144 144-144 144"
99+
/>
100+
</svg>
101+
</button>
102+
)}
103+
</div>
62104

63-
constructor(props: DataTreeViewProps) {
64-
super(props);
65-
this.state = {
66-
stages: [],
67-
steps: new Map(),
68-
expanded: [],
69-
};
70-
this.handleToggle = this.handleToggle.bind(this);
71-
}
105+
{hasChildren && isExpanded && (
106+
<div className="pgv-tree-children">
107+
{stage.children.map((child) => (
108+
<TreeNode
109+
key={child.id}
110+
stage={child}
111+
selected={selected}
112+
onSelect={onSelect}
113+
/>
114+
))}
115+
</div>
116+
)}
117+
</div>
118+
);
119+
}
72120

73-
handleToggle(event: React.ChangeEvent<{}>, nodeIds: string[]): void {
74-
this.setState({
75-
expanded: nodeIds,
76-
});
77-
}
121+
interface DataTreeViewProps {
122+
stages: StageInfo[];
123+
selected: string;
124+
onNodeSelect: (event: React.MouseEvent, nodeId: string) => void;
125+
}
78126

79-
render() {
80-
return (
81-
<TreeView
82-
defaultCollapseIcon={<ExpandMoreIcon />}
83-
defaultExpandIcon={<ChevronRightIcon />}
84-
expanded={this.props.expanded}
85-
selected={this.props.selected}
86-
onNodeToggle={this.props.onNodeToggle}
87-
onNodeSelect={this.props.onNodeSelect}
88-
onNodeFocus={(event: React.SyntheticEvent, nodeId: string) => {
89-
console.debug(`node '${nodeId}' focused.`);
90-
}}
91-
key="console-tree-view"
92-
>
93-
{getTreeItemsFromStage(this.props.stages, this.props.selected)}
94-
</TreeView>
95-
);
96-
}
127+
interface TreeNodeProps {
128+
stage: StageInfo;
129+
selected: string;
130+
onSelect: (event: React.MouseEvent, id: string) => void;
97131
}

src/main/frontend/pipeline-console-view/pipeline-console/main/PipelineConsole.spec.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
getDefaultSelectedStep,
88
updateStepBuffer,
99
} from "./PipelineConsole";
10-
import DataTreeView, { DataTreeViewProps } from "./DataTreeView";
10+
import DataTreeView from "./DataTreeView";
1111
import StageView, { StageViewProps } from "./StageView";
1212
import { StepInfo, StepLogBufferInfo } from "./PipelineConsoleModel";
1313
import { render } from "@testing-library/react";
@@ -64,7 +64,7 @@ jest.mock("../../../common/RestClient", () => {
6464
});
6565

6666
jest.mock("./DataTreeView", () => {
67-
return jest.fn((props: DataTreeViewProps) => {
67+
return jest.fn((props) => {
6868
return (
6969
<div>
7070
SimpleDataTreeView...<div>{JSON.stringify(props)}</div>
@@ -179,9 +179,7 @@ describe("PipelineConsole", () => {
179179
await findByText("SimpleDataTreeView...");
180180
expect(DataTreeView).toHaveBeenLastCalledWith(
181181
{
182-
expanded: ["3", "2"],
183182
onNodeSelect: expect.anything(),
184-
onNodeToggle: expect.anything(),
185183
selected: "3",
186184
stages: defaultStagesList,
187185
},

0 commit comments

Comments
 (0)