@centreon/ui 24.4.1-MON-38194-widgets-display-optimizations.0 → 24.4.1-MON-38194-widgets-display-optimizations.2

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.1-MON-38194-widgets-display-optimizations.0",
3
+ "version": "24.4.1-MON-38194-widgets-display-optimizations.2",
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",
@@ -76,7 +76,7 @@ export const useDashboardItemStyles = makeStyles<{ hasHeader: boolean }>()(
76
76
  border: 'none',
77
77
  borderRadius: theme.spacing(1),
78
78
  height: '100%',
79
- paddingBottom: theme.spacing(2),
79
+ paddingBottom: theme.spacing(4),
80
80
  width: '100%'
81
81
  },
82
82
  widgetContent: {
@@ -92,17 +92,19 @@ export const useDashboardItemStyles = makeStyles<{ hasHeader: boolean }>()(
92
92
  '&[data-canMove="true"]': {
93
93
  cursor: 'move'
94
94
  },
95
- padding: theme.spacing(0.5, 2),
95
+ padding: theme.spacing(2.5, 2.5, 0.5),
96
96
  position: 'relative'
97
97
  },
98
98
  widgetHeaderDraggable: {
99
99
  height: '100%',
100
+ left: 0,
100
101
  position: 'absolute',
102
+ top: 0,
101
103
  width: '100%'
102
104
  },
103
105
  widgetPadding: {
104
106
  overflowX: 'auto',
105
- padding: hasHeader ? theme.spacing(1, 2, 0) : theme.spacing(1, 2)
107
+ padding: hasHeader ? theme.spacing(1, 2.5, 3) : theme.spacing(1, 2)
106
108
  }
107
109
  })
108
110
  );
@@ -6,7 +6,7 @@ interface MakeStylesProps {
6
6
  limitLegendRows?: boolean;
7
7
  }
8
8
 
9
- export const legendWidth = 21;
9
+ export const legendWidth = 25;
10
10
  const legendItemHeight = 5.25;
11
11
  const legendItemHeightCompact = 2;
12
12
 
@@ -62,12 +62,6 @@ interface StylesProps {
62
62
 
63
63
  export const useLegendHeaderStyles = makeStyles<StylesProps>()(
64
64
  (theme, { color }) => ({
65
- container: {
66
- display: 'flex',
67
- flexDirection: 'row',
68
- gap: theme.spacing(0.5),
69
- width: '100%'
70
- },
71
65
  disabled: {
72
66
  color: theme.palette.text.disabled
73
67
  },
@@ -78,12 +72,6 @@ export const useLegendHeaderStyles = makeStyles<StylesProps>()(
78
72
  minWidth: theme.spacing(1.5),
79
73
  width: theme.spacing(1.5)
80
74
  },
81
- legendName: {
82
- '&[data-mode="compact"]': {
83
- maxWidth: theme.spacing(legendWidth * 0.5)
84
- },
85
- maxWidth: theme.spacing(legendWidth * 0.75)
86
- },
87
75
  markerAndLegendName: {
88
76
  alignItems: 'center',
89
77
  display: 'flex',
@@ -97,7 +85,7 @@ export const useLegendHeaderStyles = makeStyles<StylesProps>()(
97
85
  whiteSpace: 'nowrap'
98
86
  },
99
87
  text: {
100
- fontWeight: theme.typography.fontWeightMedium,
88
+ fontWeight: theme.typography.fontWeightBold,
101
89
  lineHeight: 1
102
90
  }
103
91
  })
@@ -1,5 +1,3 @@
1
- import { isEmpty } from 'ramda';
2
-
3
1
  import { Typography } from '@mui/material';
4
2
 
5
3
  import {
@@ -36,60 +34,47 @@ const LegendHeader = ({
36
34
  const metricName = formatMetricName({ legend, name });
37
35
 
38
36
  const legendName = legend || name;
39
- const hasUnit = !isEmpty(unit);
40
37
  const unitName = `(${unit})`;
41
38
 
42
- const getEndText = (): string => {
43
- if (value) {
44
- return value;
45
- }
46
-
47
- return hasUnit ? ` ${unitName}` : '';
48
- };
49
-
50
39
  return (
51
- <div className={classes.container}>
52
- <Tooltip
53
- followCursor={false}
54
- label={
55
- minMaxAvg ? (
56
- <div>
57
- <Typography>{`${legendName} ${unitName}`}</Typography>
58
- <div className={classes.minMaxAvgContainer}>
59
- {minMaxAvg.map(({ label, value: subValue }) => (
60
- <LegendContent
61
- data={formatMetricValue({
62
- unit: line.unit,
63
- value: subValue
64
- })}
65
- key={label}
66
- label={label}
67
- />
68
- ))}
69
- </div>
40
+ <Tooltip
41
+ followCursor={false}
42
+ label={
43
+ minMaxAvg ? (
44
+ <div>
45
+ <Typography>{`${legendName} ${unitName}`}</Typography>
46
+ <div className={classes.minMaxAvgContainer}>
47
+ {minMaxAvg.map(({ label, value: subValue }) => (
48
+ <LegendContent
49
+ data={formatMetricValue({
50
+ unit: line.unit,
51
+ value: subValue
52
+ })}
53
+ key={label}
54
+ label={label}
55
+ />
56
+ ))}
70
57
  </div>
71
- ) : (
72
- `${legendName} ${unitName}`
73
- )
74
- }
75
- placement="top"
76
- >
77
- <div className={classes.markerAndLegendName}>
78
- <div className={cx(classes.icon, { [classes.disabled]: disabled })} />
79
- <EllipsisTypography
80
- className={cx(classes.text, classes.legendName)}
81
- data-mode={
82
- value ? LegendDisplayMode.Compact : LegendDisplayMode.Normal
83
- }
84
- >
85
- {metricName}
86
- </EllipsisTypography>
87
- </div>
88
- </Tooltip>
89
- {hasUnit && (
90
- <Typography className={classes.text}>{getEndText()}</Typography>
91
- )}
92
- </div>
58
+ </div>
59
+ ) : (
60
+ `${legendName} ${unitName}`
61
+ )
62
+ }
63
+ placement="top"
64
+ >
65
+ <div className={classes.markerAndLegendName}>
66
+ <div className={cx(classes.icon, { [classes.disabled]: disabled })} />
67
+ <EllipsisTypography
68
+ className={classes.text}
69
+ data-mode={
70
+ value ? LegendDisplayMode.Compact : LegendDisplayMode.Normal
71
+ }
72
+ variant="body2"
73
+ >
74
+ {metricName}
75
+ </EllipsisTypography>
76
+ </div>
77
+ </Tooltip>
93
78
  );
94
79
  };
95
80
 
@@ -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
+ });