Skip to content
This repository was archived by the owner on Jul 3, 2023. It is now read-only.

Commit 3ebf752

Browse files
authored
Topology view component PoC (#376)
* Topology view component PoC * Remove success status * Fix TS error * update version of package * Fix naming convention, cleanup * Update interface name
1 parent 0ea3fed commit 3ebf752

File tree

8 files changed

+2564
-8
lines changed

8 files changed

+2564
-8
lines changed

package-lock.json

Lines changed: 1575 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@patternfly/react-icons": "4.93.6",
3737
"@patternfly/react-styles": "4.92.6",
3838
"@patternfly/react-table": "4.113.0",
39+
"@patternfly/react-topology": "4.91.40",
3940
"@rhoas/app-services-ui-components": "2.30.0",
4041
"@rhoas/app-services-ui-shared": "0.16.6",
4142
"@rhoas/smart-events-management-sdk": "1.0.2",

public/mockServiceWorker.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/* tslint:disable */
33

44
/**
5-
* Mock Service Worker (1.0.0).
5+
* Mock Service Worker (1.2.1).
66
* @see https://github.com/mswjs/msw
77
* - Please do NOT modify this file.
88
* - Please do NOT serve this file on production.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
2+
import {
3+
DescriptionList,
4+
DescriptionListDescription,
5+
DescriptionListGroup,
6+
DescriptionListTerm,
7+
} from "@patternfly/react-core";
8+
import React from "react";
9+
import { NodeModel } from "@patternfly/react-topology";
10+
11+
interface NodeInformationProps {
12+
node: NodeModel | undefined;
13+
}
14+
15+
const NodeInformation = (props: NodeInformationProps): JSX.Element => {
16+
const { node } = props;
17+
18+
return (
19+
<DescriptionList
20+
isHorizontal
21+
isCompact
22+
style={{ marginLeft: "2rem", marginTop: "2rem" }}
23+
>
24+
<DescriptionListGroup>
25+
<DescriptionListTerm>{"Name"}</DescriptionListTerm>
26+
<DescriptionListDescription>{node?.label}</DescriptionListDescription>
27+
</DescriptionListGroup>
28+
<DescriptionListGroup>
29+
<DescriptionListTerm>{"Type"}</DescriptionListTerm>
30+
<DescriptionListDescription>
31+
{node?.data.type}
32+
</DescriptionListDescription>
33+
</DescriptionListGroup>
34+
<DescriptionListGroup>
35+
<DescriptionListTerm>{"Owner"}</DescriptionListTerm>
36+
<DescriptionListDescription>
37+
{node?.data.owner}
38+
</DescriptionListDescription>
39+
</DescriptionListGroup>
40+
<DescriptionListGroup>
41+
<DescriptionListTerm>{"Time created"}</DescriptionListTerm>
42+
<DescriptionListDescription>
43+
{node?.data.timeCreated}
44+
</DescriptionListDescription>
45+
</DescriptionListGroup>{" "}
46+
<DescriptionListGroup>
47+
<DescriptionListTerm>{"Time Updated"}</DescriptionListTerm>
48+
<DescriptionListDescription>
49+
{node?.data.timeUpdated}
50+
</DescriptionListDescription>
51+
</DescriptionListGroup>
52+
</DescriptionList>
53+
);
54+
};
55+
56+
export default NodeInformation;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from "react";
2+
import { ComponentMeta, ComponentStory } from "@storybook/react";
3+
import TopologyView from "./TopologyView";
4+
import {
5+
EDGES_12,
6+
EDGES_6,
7+
NODES_12,
8+
NODES_12_Without_Edges,
9+
NODES_6,
10+
} from "./topologyStoriesHelper";
11+
12+
export default {
13+
title: "PoCs/Topology View",
14+
component: TopologyView,
15+
} as ComponentMeta<typeof TopologyView>;
16+
17+
const Template: ComponentStory<typeof TopologyView> = (args) => (
18+
<TopologyView {...args} />
19+
);
20+
21+
export const DagreLayout_6_nodes = Template.bind({});
22+
DagreLayout_6_nodes.args = {
23+
layout: "Dagre_network-simplex",
24+
nodes: NODES_6,
25+
edges: EDGES_6,
26+
};
27+
28+
export const DagreLayout_12_nodes_with_tight_tree_algorithm = Template.bind({});
29+
DagreLayout_12_nodes_with_tight_tree_algorithm.args = {
30+
layout: "Dagre_tight-tree",
31+
nodes: NODES_12,
32+
edges: EDGES_12,
33+
};
34+
35+
export const DagreLayout_12_nodes_with_Network_simplex_algorithm =
36+
Template.bind({});
37+
DagreLayout_12_nodes_with_Network_simplex_algorithm.args = {
38+
layout: "Dagre_network-simplex",
39+
nodes: NODES_12,
40+
edges: EDGES_12,
41+
};
42+
43+
export const BreadthFirstLayout_6_nodes = Template.bind({});
44+
BreadthFirstLayout_6_nodes.args = {
45+
layout: "BreadthFirst",
46+
nodes: NODES_6,
47+
edges: EDGES_6,
48+
};
49+
50+
export const BreadthFirstLayout_12_nodes = Template.bind({});
51+
BreadthFirstLayout_12_nodes.args = {
52+
layout: "BreadthFirst",
53+
nodes: NODES_12,
54+
edges: EDGES_12,
55+
};
56+
57+
export const ColaLayout_Without_Edges = Template.bind({});
58+
ColaLayout_Without_Edges.args = {
59+
layout: "Cola",
60+
nodes: NODES_12_Without_Edges,
61+
edges: [],
62+
};
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import React from "react";
2+
import { RegionsIcon as Icon4 } from "@patternfly/react-icons";
3+
import { DataSourceIcon as Icon1 } from "@patternfly/react-icons";
4+
import { DataSinkIcon as Icon2 } from "@patternfly/react-icons";
5+
import { DataProcessorIcon as Icon3 } from "@patternfly/react-icons";
6+
import {
7+
action,
8+
BreadthFirstLayout,
9+
ColaLayout,
10+
ConcentricLayout,
11+
createTopologyControlButtons,
12+
DagreLayout,
13+
defaultControlButtonsOptions,
14+
DefaultEdge,
15+
DefaultGroup,
16+
DefaultNode,
17+
Edge,
18+
EdgeTerminalType,
19+
ForceLayout,
20+
GraphComponent,
21+
GridLayout,
22+
ModelKind,
23+
SELECTION_EVENT,
24+
TopologyControlBar,
25+
TopologySideBar,
26+
TopologyView as PFTopologyView,
27+
Visualization,
28+
VisualizationProvider,
29+
VisualizationSurface,
30+
withPanZoom,
31+
withSelection,
32+
WithSelectionProps,
33+
ComponentFactory,
34+
Graph,
35+
Layout,
36+
LayoutFactory,
37+
Model,
38+
Node,
39+
EdgeModel,
40+
NodeModel,
41+
} from "@patternfly/react-topology";
42+
43+
import NodeInformation from "./NodeSideBarDetails";
44+
45+
export interface TopologyViewProps {
46+
layout: string;
47+
nodes: NodeModel[];
48+
edges: EdgeModel[];
49+
truncateLength?: number;
50+
}
51+
52+
interface CustomNodeProps {
53+
element: Node;
54+
}
55+
interface DataEdgeProps {
56+
element: Edge;
57+
}
58+
59+
const DataEdge: React.FC<DataEdgeProps> = ({ element, ...rest }) => (
60+
<DefaultEdge
61+
element={element}
62+
startTerminalType={EdgeTerminalType.cross}
63+
endTerminalType={EdgeTerminalType.directionalAlt}
64+
{...rest}
65+
/>
66+
);
67+
68+
const CustomNode: React.FC<CustomNodeProps & WithSelectionProps> = ({
69+
element,
70+
onSelect,
71+
selected,
72+
...rest
73+
}) => {
74+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
75+
const data = element.getData();
76+
let Icon;
77+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
78+
if (data.type == "Source") {
79+
Icon = Icon1;
80+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
81+
} else if (data.type == "Sink") {
82+
Icon = Icon2;
83+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
84+
} else if (data.type == "Processor") {
85+
Icon = Icon3;
86+
} else {
87+
Icon = Icon4;
88+
}
89+
90+
return (
91+
<DefaultNode
92+
element={element}
93+
showStatusDecorator
94+
{...rest}
95+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
96+
truncateLength={data.length}
97+
onSelect={onSelect}
98+
selected={selected}
99+
>
100+
<g transform={`translate(25, 25)`}>
101+
<Icon style={{ color: "#393F44" }} width={25} height={25} />
102+
</g>
103+
</DefaultNode>
104+
);
105+
};
106+
107+
const customLayoutFactory: LayoutFactory = (
108+
type: string,
109+
graph: Graph
110+
): Layout | undefined => {
111+
switch (type) {
112+
case "Cola":
113+
return new ColaLayout(graph, {
114+
layoutOnDrag: false,
115+
linkDistance: 10,
116+
collideDistance: 30,
117+
maxTicks: 1,
118+
groupDistance: 150,
119+
});
120+
case "Dagre_network-simplex":
121+
return new DagreLayout(graph, {
122+
rankdir: "LR",
123+
nodesep: 10,
124+
linkDistance: 5,
125+
edgesep: 2,
126+
groupDistance: 150,
127+
ranker: "network-simplex",
128+
});
129+
case "Dagre_tight-tree":
130+
return new DagreLayout(graph, {
131+
rankdir: "LR",
132+
nodesep: 10,
133+
linkDistance: 5,
134+
edgesep: 2,
135+
groupDistance: 150,
136+
ranker: "tight-tree",
137+
});
138+
case "BreadthFirst":
139+
return new BreadthFirstLayout(graph, {
140+
nodeDistance: 80,
141+
});
142+
case "Concentric":
143+
return new ConcentricLayout(graph, {
144+
groupDistance: 150,
145+
});
146+
case "Grid":
147+
return new GridLayout(graph, {
148+
groupDistance: 150,
149+
nodeDistance: 75,
150+
});
151+
default:
152+
return new ForceLayout(graph, {
153+
groupDistance: 250,
154+
});
155+
}
156+
};
157+
158+
const customComponentFactory: ComponentFactory = (
159+
kind: ModelKind,
160+
type: string
161+
): any => {
162+
switch (type) {
163+
case "group":
164+
return DefaultGroup;
165+
case "data-edge":
166+
return DataEdge;
167+
default:
168+
switch (kind) {
169+
case ModelKind.graph:
170+
return withPanZoom()(GraphComponent);
171+
case ModelKind.node:
172+
return withSelection()(CustomNode as React.FC);
173+
case ModelKind.edge:
174+
return DefaultEdge;
175+
default:
176+
return undefined;
177+
}
178+
}
179+
};
180+
181+
const TopologyView = (props: TopologyViewProps): JSX.Element => {
182+
const { layout, nodes, edges, truncateLength } = props;
183+
184+
const [selectedIds, setSelectedIds] = React.useState<string[]>([]);
185+
186+
const controller = React.useMemo(() => {
187+
nodes.map((node) => {
188+
node.data && Object.assign(node?.data, { length: truncateLength });
189+
});
190+
191+
const model: Model = {
192+
nodes: nodes,
193+
edges: edges,
194+
graph: {
195+
id: "g1",
196+
type: "graph",
197+
layout,
198+
},
199+
};
200+
201+
const newController = new Visualization();
202+
newController.setFitToScreenOnLayout(true);
203+
newController.registerLayoutFactory(customLayoutFactory);
204+
newController.registerComponentFactory(customComponentFactory);
205+
206+
newController.addEventListener(SELECTION_EVENT, setSelectedIds);
207+
208+
newController.fromModel(model, false);
209+
210+
return newController;
211+
}, [edges, layout, nodes, truncateLength]);
212+
213+
const node = nodes.find((node) => node.id === selectedIds[0]);
214+
215+
const topologySideBar = (
216+
<TopologySideBar
217+
className="topology-example-sidebar"
218+
show={selectedIds.length > 0}
219+
onClose={(): void => setSelectedIds([])}
220+
>
221+
<NodeInformation node={node} />
222+
</TopologySideBar>
223+
);
224+
225+
return (
226+
<PFTopologyView
227+
sideBar={topologySideBar}
228+
controlBar={
229+
<TopologyControlBar
230+
controlButtons={createTopologyControlButtons({
231+
...defaultControlButtonsOptions,
232+
zoomInCallback: action(() => {
233+
controller.getGraph().scaleBy(4 / 3);
234+
}),
235+
zoomOutCallback: action(() => {
236+
controller.getGraph().scaleBy(0.75);
237+
}),
238+
fitToScreenCallback: action(() => {
239+
controller.getGraph().fit(80);
240+
}),
241+
resetViewCallback: action(() => {
242+
controller.getGraph().reset();
243+
controller.getGraph().layout();
244+
}),
245+
legend: false,
246+
})}
247+
/>
248+
}
249+
>
250+
<VisualizationProvider controller={controller}>
251+
<VisualizationSurface state={{ selectedIds }} />
252+
</VisualizationProvider>
253+
</PFTopologyView>
254+
);
255+
};
256+
257+
export default TopologyView;

0 commit comments

Comments
 (0)