@centreon/ui 24.4.64 → 24.4.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "24.4.64",
3
+ "version": "24.4.65",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -82,7 +82,7 @@
82
82
  "@vitejs/plugin-react": "^4.2.1",
83
83
  "@vitejs/plugin-react-swc": "^3.6.0",
84
84
  "axios-mock-adapter": "^1.22.0",
85
- "cypress": "^13.7.0",
85
+ "cypress": "^13.7.1",
86
86
  "identity-obj-proxy": "^3.0.0",
87
87
  "jest-transform-stub": "^2.0.0",
88
88
  "mochawesome": "^7.1.3",
@@ -114,15 +114,20 @@
114
114
  "@lexical/selection": "^0.12.6",
115
115
  "@lexical/utils": "^0.12.6",
116
116
  "@react-spring/web": "^9.7.3",
117
+ "@visx/clip-path": "^3.3.0",
117
118
  "@visx/curve": "^2.1.0",
119
+ "@visx/event": "^3.3.0",
118
120
  "@visx/group": "^3.3.0",
121
+ "@visx/hierarchy": "^3.3.0",
119
122
  "@visx/legend": "^3.5.0",
120
123
  "@visx/pattern": "^3.3.0",
124
+ "@visx/point": "^3.3.0",
121
125
  "@visx/scale": "^3.5.0",
122
- "@visx/shape": "^2.12.2",
126
+ "@visx/shape": "^2.18.0",
123
127
  "@visx/text": "^3.3.0",
124
128
  "@visx/threshold": "^2.12.2",
125
129
  "@visx/visx": "2.16.0",
130
+ "@visx/zoom": "^3.3.0",
126
131
  "anylogger": "^1.0.11",
127
132
  "d3-array": "3.2.4",
128
133
  "humanize-duration": "^3.31.0",
@@ -0,0 +1,88 @@
1
+ import { useState } from 'react';
2
+
3
+ import { HierarchyPointNode } from '@visx/hierarchy/lib/types';
4
+ import { Group } from '@visx/group';
5
+ import { gt, isNil, pluck } from 'ramda';
6
+
7
+ import { BaseProp, Node, TreeProps } from './models';
8
+
9
+ interface Props<TData> extends Pick<TreeProps<TData>, 'children'> {
10
+ descendants: Array<HierarchyPointNode<Node<TData>>>;
11
+ expandCollapseNode: (targetNode: Node<TData>) => void;
12
+ getExpanded: (d: Node<TData>) => Array<Node<TData>> | undefined;
13
+ nodeSize: {
14
+ height: number;
15
+ width: number;
16
+ };
17
+ }
18
+
19
+ const DescendantNodes = <TData extends BaseProp>({
20
+ descendants,
21
+ children,
22
+ expandCollapseNode,
23
+ getExpanded,
24
+ nodeSize
25
+ }: Props<TData>): Array<JSX.Element> => {
26
+ const [pressEventTimeStamp, setPressEventTimeStamp] = useState<number | null>(
27
+ null
28
+ );
29
+
30
+ const mouseDown = (e: MouseEvent): void => {
31
+ setPressEventTimeStamp(e.timeStamp);
32
+ };
33
+
34
+ const mouseUp =
35
+ (callback) =>
36
+ (e: MouseEvent): void => {
37
+ if (isNil(pressEventTimeStamp)) {
38
+ callback();
39
+
40
+ return;
41
+ }
42
+
43
+ const diffTimeStamp = e.timeStamp - pressEventTimeStamp;
44
+
45
+ if (gt(diffTimeStamp, 120)) {
46
+ return;
47
+ }
48
+
49
+ callback();
50
+ };
51
+
52
+ return descendants.map((node) => {
53
+ const top = node.x;
54
+ const left = node.y;
55
+ const ancestorIds = node
56
+ .ancestors()
57
+ .map((ancestor) => ancestor.data.data.id);
58
+ const descendantIds = node
59
+ .descendants()
60
+ .map((ancestor) => ancestor.data.data.id);
61
+
62
+ const key = `${node.data.data.id}-${node.data.data.name}-${ancestorIds.toString()}-${descendantIds.toString()}`;
63
+
64
+ return (
65
+ <Group key={key} left={left} top={top}>
66
+ <foreignObject
67
+ height={nodeSize.height}
68
+ width={nodeSize.width}
69
+ x={-nodeSize.width / 2}
70
+ y={-nodeSize.height / 2}
71
+ >
72
+ {children({
73
+ ancestors: pluck('data', node.ancestors()),
74
+ depth: node.depth,
75
+ expandCollapseNode,
76
+ isExpanded: !!getExpanded(node.data),
77
+ node: node.data,
78
+ nodeSize,
79
+ onMouseDown: mouseDown,
80
+ onMouseUp: mouseUp
81
+ })}
82
+ </foreignObject>
83
+ </Group>
84
+ );
85
+ });
86
+ };
87
+
88
+ export default DescendantNodes;
@@ -0,0 +1,64 @@
1
+ import { LinkHorizontal } from '@visx/shape';
2
+ import { HierarchyPointLink } from '@visx/hierarchy/lib/types';
3
+
4
+ import { useTheme } from '@mui/material';
5
+
6
+ import { TreeProps, Node, BaseProp } from './models';
7
+
8
+ interface Props<TData> extends Pick<TreeProps<TData>, 'treeLink'> {
9
+ links: Array<HierarchyPointLink<Node<TData>>>;
10
+ }
11
+
12
+ const Links = <TData extends BaseProp>({
13
+ links,
14
+ treeLink
15
+ }: Props<TData>): Array<JSX.Element> => {
16
+ const theme = useTheme();
17
+
18
+ return links.map((link) => {
19
+ const ancestorIds = link.target
20
+ .ancestors()
21
+ .map((ancestor) => ancestor.data.data.id);
22
+
23
+ const descendantIds = link.target
24
+ .descendants()
25
+ .map((ancestor) => ancestor.data.data.id);
26
+
27
+ const key = `${link.source.data.data.id}-${link.source.data.data.name}-${ancestorIds}_${link.target.data.data.id}-${link.target.data.data.name}-${descendantIds}`;
28
+
29
+ return (
30
+ <LinkHorizontal
31
+ data={link}
32
+ data-testid={`${link.source.data.data.id}_to_${link.target.data.data.id}`}
33
+ fill="none"
34
+ key={key}
35
+ stroke={
36
+ treeLink?.getStroke?.({
37
+ source: link.source.data.data,
38
+ target: link.target.data.data
39
+ }) || theme.palette.text.primary
40
+ }
41
+ strokeDasharray={
42
+ treeLink?.getStrokeDasharray?.({
43
+ source: link.source.data.data,
44
+ target: link.target.data.data
45
+ }) || '0'
46
+ }
47
+ strokeOpacity={
48
+ treeLink?.getStrokeOpacity?.({
49
+ source: link.source.data.data,
50
+ target: link.target.data.data
51
+ }) || 1
52
+ }
53
+ strokeWidth={
54
+ treeLink?.getStrokeWidth?.({
55
+ source: link.source.data.data,
56
+ target: link.target.data.data
57
+ }) || '2'
58
+ }
59
+ />
60
+ );
61
+ });
62
+ };
63
+
64
+ export default Links;
@@ -0,0 +1,32 @@
1
+ import { useState } from 'react';
2
+
3
+ import { ParentSize } from '../..';
4
+
5
+ import { BaseProp, TreeProps } from './models';
6
+ import { Tree } from './Tree';
7
+
8
+ export const StandaloneTree = <TData extends BaseProp>({
9
+ tree,
10
+ ...props
11
+ }: Omit<
12
+ TreeProps<TData>,
13
+ 'containerHeight' | 'containerWidth'
14
+ >): JSX.Element => {
15
+ const [currentTree, setTree] = useState(tree);
16
+
17
+ return (
18
+ <ParentSize>
19
+ {({ width, height }) => (
20
+ <svg height={height} width={width}>
21
+ <Tree
22
+ {...props}
23
+ changeTree={setTree}
24
+ containerHeight={height}
25
+ containerWidth={width}
26
+ tree={currentTree}
27
+ />
28
+ </svg>
29
+ )}
30
+ </ParentSize>
31
+ );
32
+ };
@@ -0,0 +1,171 @@
1
+ import { equals } from 'ramda';
2
+
3
+ import {
4
+ ComplexData,
5
+ complexData,
6
+ SimpleData,
7
+ simpleData
8
+ } from './stories/datas';
9
+ import { ComplexContent, SimpleContent } from './stories/contents';
10
+
11
+ import { Node, StandaloneTree, TreeProps } from '.';
12
+
13
+ const validateTree = (tree): void => {
14
+ if (!tree.children) {
15
+ cy.contains(tree.data.name).should('be.visible');
16
+
17
+ return;
18
+ }
19
+
20
+ cy.contains(tree.data.name).should('be.visible');
21
+ tree.children.forEach((child) => {
22
+ validateTree(child);
23
+ });
24
+ };
25
+
26
+ interface InitializeProps
27
+ extends Pick<TreeProps<SimpleData | ComplexData>, 'treeLink' | 'children'> {
28
+ data?: Node<SimpleData | ComplexData>;
29
+ isDefaultExpanded?: (data: SimpleData | ComplexData) => boolean;
30
+ }
31
+
32
+ const initializeStandaloneTree = ({
33
+ data = simpleData,
34
+ isDefaultExpanded = undefined,
35
+ treeLink,
36
+ children = SimpleContent
37
+ }: InitializeProps): void => {
38
+ cy.mount({
39
+ Component: (
40
+ <div style={{ height: '99vh' }}>
41
+ <StandaloneTree
42
+ node={{ height: 70, isDefaultExpanded, width: 70 }}
43
+ tree={data}
44
+ treeLink={treeLink}
45
+ >
46
+ {children}
47
+ </StandaloneTree>
48
+ </div>
49
+ )
50
+ });
51
+ };
52
+
53
+ describe('Simple data tree', () => {
54
+ it('displays the whole tree', () => {
55
+ initializeStandaloneTree({});
56
+
57
+ validateTree(simpleData);
58
+
59
+ cy.makeSnapshot();
60
+ });
61
+
62
+ it("collapses a node's childrens when a node is clicked", () => {
63
+ initializeStandaloneTree({});
64
+
65
+ cy.contains(/^E$/).should('be.visible');
66
+ cy.contains(/^E1$/).should('be.visible');
67
+
68
+ cy.contains(/^C$/).click();
69
+
70
+ cy.contains(/^E$/).should('not.exist');
71
+ cy.contains(/^E1$/).should('not.exist');
72
+
73
+ cy.makeSnapshot();
74
+ });
75
+
76
+ it("expands a node's childrens when a node is clicked", () => {
77
+ initializeStandaloneTree({});
78
+
79
+ cy.contains(/^A$/).click();
80
+
81
+ cy.contains(/^A1$/).should('not.exist');
82
+ cy.contains(/^A2$/).should('not.exist');
83
+ cy.contains(/^A3$/).should('not.exist');
84
+ cy.contains(/^C$/).should('not.exist');
85
+
86
+ cy.contains(/^A$/).click();
87
+
88
+ cy.contains(/^A1$/).should('be.visible');
89
+ cy.contains(/^A2$/).should('be.visible');
90
+ cy.contains(/^A3$/).should('be.visible');
91
+ cy.contains(/^C$/).should('be.visible');
92
+
93
+ cy.makeSnapshot();
94
+ });
95
+
96
+ it('cannot collapses a node when a leaf is clicked', () => {
97
+ initializeStandaloneTree({});
98
+
99
+ cy.contains(/^Z$/).click();
100
+
101
+ cy.contains(/^Z$/).should('be.visible');
102
+
103
+ cy.makeSnapshot();
104
+ });
105
+
106
+ it('expands nodes by default when a prop is set', () => {
107
+ initializeStandaloneTree({
108
+ isDefaultExpanded: (data: SimpleData) => equals('critical', data.status)
109
+ });
110
+
111
+ cy.contains(/^T$/).should('be.visible');
112
+ cy.contains(/^A$/).should('be.visible');
113
+ cy.contains(/^A3$/).should('be.visible');
114
+ cy.contains(/^C$/).should('be.visible');
115
+ cy.contains(/^E$/).should('be.visible');
116
+ cy.contains(/^E1$/).should('be.visible');
117
+
118
+ cy.contains(/^B1$/).should('not.exist');
119
+ cy.contains(/^D1$/).should('not.exist');
120
+
121
+ cy.makeSnapshot();
122
+ });
123
+
124
+ it('displays customized links when a prop is set', () => {
125
+ initializeStandaloneTree({
126
+ treeLink: {
127
+ getStroke: ({ target }) => (target.status === 'ok' ? 'grey' : 'black'),
128
+ getStrokeDasharray: ({ target }) =>
129
+ target.status === 'ok' ? '5,5' : '0',
130
+ getStrokeOpacity: ({ target }) => (target.status === 'ok' ? 0.8 : 1),
131
+ getStrokeWidth: ({ target }) => (target.status === 'ok' ? 1 : 2)
132
+ }
133
+ });
134
+
135
+ cy.contains(/^Z$/).should('be.visible');
136
+
137
+ cy.makeSnapshot();
138
+ });
139
+ });
140
+
141
+ describe('Complex data tree', () => {
142
+ it('cannot collapse a node when a node is not clickable', () => {
143
+ initializeStandaloneTree({
144
+ children: ComplexContent,
145
+ data: complexData
146
+ });
147
+
148
+ cy.contains('BA 3').should('be.visible');
149
+
150
+ cy.contains('BA 2').click();
151
+
152
+ cy.contains('BA 3').should('be.visible');
153
+
154
+ cy.makeSnapshot();
155
+ });
156
+
157
+ it('collapses a node when a node is clickable', () => {
158
+ initializeStandaloneTree({
159
+ children: ComplexContent,
160
+ data: complexData
161
+ });
162
+
163
+ cy.contains('BA 3').should('be.visible');
164
+
165
+ cy.contains('2').click();
166
+
167
+ cy.contains('BA 3').should('not.exist');
168
+
169
+ cy.makeSnapshot();
170
+ });
171
+ });
@@ -0,0 +1,144 @@
1
+ import { useState } from 'react';
2
+
3
+ import { Meta, StoryObj } from '@storybook/react';
4
+ import { equals, has } from 'ramda';
5
+
6
+ import { Zoom } from '../../components';
7
+
8
+ import {
9
+ ComplexData,
10
+ complexData,
11
+ moreComplexData,
12
+ SimpleData,
13
+ simpleData
14
+ } from './stories/datas';
15
+ import { ComplexContent, SimpleContent } from './stories/contents';
16
+
17
+ import { StandaloneTree, Tree, TreeProps } from '.';
18
+
19
+ const meta: Meta<typeof StandaloneTree> = {
20
+ component: StandaloneTree
21
+ };
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof StandaloneTree>;
25
+
26
+ const StandaloneTreeTemplate = (args): JSX.Element => (
27
+ <div style={{ height: '90vh', width: '100%' }}>
28
+ <StandaloneTree<SimpleData> {...args} />
29
+ </div>
30
+ );
31
+
32
+ const TreeWithZoom = <TData,>({
33
+ tree,
34
+ ...args
35
+ }: TreeProps<TData>): JSX.Element => {
36
+ const [currentTree, setTree] = useState(tree);
37
+
38
+ return (
39
+ <div style={{ height: '90vh', width: '100%' }}>
40
+ <Zoom
41
+ showMinimap
42
+ labels={{
43
+ clear: 'Clear'
44
+ }}
45
+ >
46
+ {({ width, height }) => (
47
+ <Tree<ComplexData>
48
+ {...args}
49
+ changeTree={setTree}
50
+ containerHeight={height}
51
+ containerWidth={width}
52
+ tree={currentTree}
53
+ />
54
+ )}
55
+ </Zoom>
56
+ </div>
57
+ );
58
+ };
59
+
60
+ export const DefaultStandaloneTree: Story = {
61
+ args: {
62
+ children: SimpleContent,
63
+ node: {
64
+ height: 90,
65
+ width: 90
66
+ },
67
+ tree: simpleData
68
+ },
69
+ render: StandaloneTreeTemplate
70
+ };
71
+
72
+ export const WithDefaultExpandFilter: Story = {
73
+ args: {
74
+ children: SimpleContent,
75
+ node: {
76
+ height: 90,
77
+ isDefaultExpanded: (data: SimpleData) => equals('critical', data.status),
78
+ width: 90
79
+ },
80
+ tree: simpleData
81
+ },
82
+ render: StandaloneTreeTemplate
83
+ };
84
+
85
+ export const WithCustomLinks: Story = {
86
+ args: {
87
+ children: SimpleContent,
88
+ node: {
89
+ height: 90,
90
+ width: 90
91
+ },
92
+ tree: simpleData,
93
+ treeLink: {
94
+ getStroke: ({ target }) => (target.status === 'ok' ? 'grey' : 'black'),
95
+ getStrokeDasharray: ({ target }) =>
96
+ target.status === 'ok' ? '5,5' : '0',
97
+ getStrokeOpacity: ({ target }) => (target.status === 'ok' ? 0.8 : 1),
98
+ getStrokeWidth: ({ target }) => (target.status === 'ok' ? 1 : 2)
99
+ }
100
+ },
101
+ render: StandaloneTreeTemplate
102
+ };
103
+
104
+ export const WithComplexData: Story = {
105
+ args: {
106
+ children: ComplexContent,
107
+ node: {
108
+ height: 90,
109
+ isDefaultExpanded: (data: SimpleData) =>
110
+ equals('critical', data.status) || !has('count', data),
111
+ width: 90
112
+ },
113
+ tree: complexData,
114
+ treeLink: {
115
+ getStroke: ({ target }) => (target.status === 'ok' ? 'grey' : 'black'),
116
+ getStrokeDasharray: ({ target }) =>
117
+ target.status === 'ok' ? '5,5' : '0',
118
+ getStrokeOpacity: ({ target }) => (target.status === 'ok' ? 0.8 : 1),
119
+ getStrokeWidth: ({ target }) => (target.status === 'ok' ? 1 : 2)
120
+ }
121
+ },
122
+ render: StandaloneTreeTemplate
123
+ };
124
+
125
+ export const treeWithZoom: Story = {
126
+ args: {
127
+ children: ComplexContent,
128
+ node: {
129
+ height: 90,
130
+ isDefaultExpanded: (data: SimpleData) =>
131
+ equals('critical', data.status) || !has('count', data),
132
+ width: 90
133
+ },
134
+ tree: moreComplexData,
135
+ treeLink: {
136
+ getStroke: ({ target }) => (target.status === 'ok' ? 'grey' : 'black'),
137
+ getStrokeDasharray: ({ target }) =>
138
+ target.status === 'ok' ? '5,5' : '0',
139
+ getStrokeOpacity: ({ target }) => (target.status === 'ok' ? 0.8 : 1),
140
+ getStrokeWidth: ({ target }) => (target.status === 'ok' ? 1 : 2)
141
+ }
142
+ },
143
+ render: TreeWithZoom
144
+ };
@@ -0,0 +1,116 @@
1
+ import { useCallback, useMemo } from 'react';
2
+
3
+ import { Group } from '@visx/group';
4
+ import { hierarchy, Tree as VisxTree } from '@visx/hierarchy';
5
+ import { isNil } from 'ramda';
6
+
7
+ import { useDeepCompare } from '../../utils';
8
+
9
+ import { nodeMargins } from './constants';
10
+ import { BaseProp, Node, TreeProps } from './models';
11
+ import { updateNodeFromTree } from './utils';
12
+ import Links from './Links';
13
+ import DescendantNodes from './DescendantNodes';
14
+
15
+ export const Tree = <TData extends BaseProp>({
16
+ containerHeight,
17
+ containerWidth,
18
+ tree,
19
+ node,
20
+ treeLink = {},
21
+ changeTree,
22
+ children
23
+ }: TreeProps<TData>): JSX.Element => {
24
+ const formattedTree: Node<TData> = useMemo(
25
+ () => ({
26
+ ...tree,
27
+ isExpanded: true
28
+ }),
29
+ useDeepCompare([tree])
30
+ );
31
+
32
+ const toggleTreeNodesExpanded = useCallback(
33
+ ({ currentTree, targetNode }): Node<TData> => {
34
+ return updateNodeFromTree({
35
+ callback: (subTree) => {
36
+ if (isNil(subTree.isExpanded) && isNil(node.isDefaultExpanded)) {
37
+ return {
38
+ isExpanded: false
39
+ };
40
+ }
41
+
42
+ return {
43
+ isExpanded: isNil(subTree.isExpanded)
44
+ ? !node.isDefaultExpanded?.(subTree.data)
45
+ : !subTree.isExpanded || false
46
+ };
47
+ },
48
+ targetNode,
49
+ tree: currentTree
50
+ });
51
+ },
52
+ [node.isDefaultExpanded]
53
+ );
54
+
55
+ const expandCollapseNode = useCallback(
56
+ (targetNode: Node<TData>): void => {
57
+ changeTree?.(
58
+ toggleTreeNodesExpanded({ currentTree: formattedTree, targetNode })
59
+ );
60
+ },
61
+ [formattedTree]
62
+ );
63
+
64
+ const getExpanded = useCallback(
65
+ (d: Node<TData>): Array<Node<TData>> | undefined | null => {
66
+ if (isNil(d.isExpanded) && isNil(node.isDefaultExpanded)) {
67
+ return d.children;
68
+ }
69
+ if (isNil(d.isExpanded)) {
70
+ return node.isDefaultExpanded?.(d.data) ? d.children : null;
71
+ }
72
+
73
+ return d.isExpanded ? d.children : null;
74
+ },
75
+ [node.isDefaultExpanded]
76
+ );
77
+
78
+ const origin = useMemo(
79
+ () => ({
80
+ x: 0,
81
+ y: containerHeight / 2
82
+ }),
83
+ [containerHeight]
84
+ );
85
+
86
+ return (
87
+ <Group left={node.width}>
88
+ <VisxTree
89
+ left={0}
90
+ nodeSize={[node.width + nodeMargins.y, node.height + nodeMargins.x]}
91
+ root={hierarchy(formattedTree, getExpanded)}
92
+ separation={() => 1}
93
+ size={[containerWidth, containerHeight]}
94
+ top={0}
95
+ >
96
+ {(subTree) => (
97
+ <Group left={origin.x} top={origin.y}>
98
+ <Links links={subTree.links()} treeLink={treeLink} />
99
+
100
+ <DescendantNodes
101
+ descendants={subTree.descendants()}
102
+ expandCollapseNode={expandCollapseNode}
103
+ getExpanded={getExpanded}
104
+ nodeSize={{
105
+ height: node.height,
106
+ width: node.width
107
+ }}
108
+ >
109
+ {children}
110
+ </DescendantNodes>
111
+ </Group>
112
+ )}
113
+ </VisxTree>
114
+ </Group>
115
+ );
116
+ };
@@ -0,0 +1,2 @@
1
+ export const margins = { bottom: 30, left: 30, right: 30, top: 30 };
2
+ export const nodeMargins = { x: 90, y: 16 };
@@ -0,0 +1,4 @@
1
+ export { Tree } from './Tree';
2
+ export { StandaloneTree } from './StandaloneTree';
3
+ export * from './utils';
4
+ export type { Node, TreeProps, ChildrenProps } from './models';