@centreon/ui 25.3.2 → 25.3.4

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.
Files changed (33) hide show
  1. package/package.json +49 -46
  2. package/public/mockServiceWorker.js +1 -1
  3. package/src/ActionsList/index.tsx +1 -0
  4. package/src/Button/Icon/index.tsx +3 -1
  5. package/src/Button/Save/index.tsx +17 -35
  6. package/src/Button/Save/useSave.tsx +89 -0
  7. package/src/Form/Inputs/LoadingSkeleton.tsx +1 -1
  8. package/src/Form/Inputs/Radio.tsx +7 -5
  9. package/src/Form/Inputs/Switch.tsx +4 -2
  10. package/src/Graph/Chart/Chart.cypress.spec.tsx +2 -2
  11. package/src/Graph/Chart/InteractiveComponents/Annotations/Annotation/index.tsx +1 -1
  12. package/src/Graph/Chart/helpers/index.ts +4 -3
  13. package/src/Graph/SingleBar/ThresholdLine.tsx +2 -2
  14. package/src/Graph/common/Thresholds/ThresholdLine.tsx +2 -2
  15. package/src/InputField/Select/index.tsx +11 -9
  16. package/src/Listing/Cell/index.tsx +26 -19
  17. package/src/Listing/Listing.cypress.spec.tsx +9 -11
  18. package/src/RichTextEditor/RichTextEditor.tsx +1 -1
  19. package/src/ThemeProvider/palettes.ts +2 -2
  20. package/src/api/customFetch.ts +0 -9
  21. package/src/api/useBulkResponse.ts +58 -0
  22. package/src/api/useMutationQuery/useMutationQuery.cypress.spec.tsx +0 -18
  23. package/src/components/Button/Button.tsx +19 -15
  24. package/src/components/CollapsibleItem/CollapsibleItem.cypress.spec.tsx +8 -8
  25. package/src/components/CopyCommand/CopyCommand.cypress.spec.tsx +11 -10
  26. package/src/components/CrudPage/Actions/Actions.styles.ts +15 -1
  27. package/src/components/CrudPage/Actions/Actions.tsx +7 -4
  28. package/src/components/CrudPage/Actions/Search.tsx +15 -14
  29. package/src/components/Header/PageHeader/PageHeader.styles.ts +5 -5
  30. package/src/components/Layout/PageLayout/PageLayout.styles.ts +1 -1
  31. package/src/index.ts +1 -0
  32. package/src/Button/Save/Content.tsx +0 -34
  33. package/src/Button/Save/StartIcon.tsx +0 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "25.3.2",
3
+ "version": "25.3.4",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -16,7 +16,10 @@
16
16
  "cypress:run:updateSnapshots": "cypress run --component --browser=chrome --env updateSnapshots=true",
17
17
  "cypress:run:coverage": "cypress run --component --browser=chrome --env codeCoverageTasksRegistered=true",
18
18
  "cypress:run": "cypress run --component --browser=chrome",
19
- "tokens:transform": "TS_NODE_PROJECT=tsconfig.node.json ts-node style-dictionary.transform.ts"
19
+ "cypress:install": "cypress install",
20
+ "cypress:cli": "./cypress/scripts/cypress-cli.sh",
21
+ "tokens:transform": "TS_NODE_PROJECT=tsconfig.node.json ts-node style-dictionary.transform.ts",
22
+ "install:arm:binaries": "pnpm i -D @swc/core-linux-arm64-gnu@1.6.6 @rspack/binding-linux-arm64-gnu@1.1.6 -w && pnpm remove @swc/core-linux-arm64-gnu @rspack/binding-linux-arm64-gnu"
20
23
  },
21
24
  "type": "module",
22
25
  "sideEffects": false,
@@ -50,71 +53,71 @@
50
53
  "test/*"
51
54
  ],
52
55
  "devDependencies": {
53
- "@cypress/react": "^8.0.2",
54
- "@cypress/webpack-dev-server": "^3.11.0",
55
- "@faker-js/faker": "^9.3.0",
56
+ "@cypress/react": "^9.0.0",
57
+ "@cypress/webpack-dev-server": "^4.0.1",
58
+ "@faker-js/faker": "^9.5.1",
56
59
  "@mdx-js/react": "^3.1.0",
57
60
  "@simonsmith/cypress-image-snapshot": "^9.1.0",
58
- "@storybook/addon-a11y": "^8.4.7",
59
- "@storybook/addon-docs": "^8.4.7",
60
- "@storybook/addon-essentials": "^8.4.7",
61
- "@storybook/addon-interactions": "^8.4.7",
62
- "@storybook/addon-themes": "^8.4.7",
63
- "@storybook/blocks": "^8.4.7",
64
- "@storybook/manager-api": "^8.4.7",
61
+ "@storybook/addon-a11y": "^8.6.3",
62
+ "@storybook/addon-docs": "^8.6.3",
63
+ "@storybook/addon-essentials": "^8.6.3",
64
+ "@storybook/addon-interactions": "^8.6.3",
65
+ "@storybook/addon-themes": "^8.6.3",
66
+ "@storybook/blocks": "^8.6.3",
67
+ "@storybook/manager-api": "^8.6.3",
65
68
  "@storybook/mdx2-csf": "^1.1.0",
66
- "@storybook/preview-api": "^8.4.7",
67
- "@storybook/react": "^8.4.7",
68
- "@storybook/react-vite": "^8.4.7",
69
- "@storybook/test": "^8.4.7",
70
- "@storybook/test-runner": "^0.20.1",
71
- "@storybook/theming": "^8.4.7",
72
- "@testing-library/cypress": "^10.0.2",
69
+ "@storybook/preview-api": "^8.6.3",
70
+ "@storybook/react": "^8.6.3",
71
+ "@storybook/react-vite": "^8.6.3",
72
+ "@storybook/test": "^8.6.3",
73
+ "@storybook/test-runner": "^0.22.0",
74
+ "@storybook/theming": "^8.6.3",
75
+ "@testing-library/cypress": "^10.0.3",
73
76
  "@testing-library/jest-dom": "^6.6.3",
74
- "@testing-library/react": "^16.1.0",
77
+ "@testing-library/react": "^16.2.0",
75
78
  "@testing-library/react-hooks": "^8.0.1",
76
79
  "@types/jest": "^29.5.14",
77
80
  "@types/mocha": "^10.0.10",
78
81
  "@types/ramda": "^0.30.2",
79
- "@types/react": "^18.3.3",
82
+ "@types/react": "^19.0.10",
80
83
  "@types/testing-library__jest-dom": "^6.0.0",
81
84
  "@vitejs/plugin-react": "^4.3.4",
82
- "@vitejs/plugin-react-swc": "^3.7.2",
83
- "chai": "^5.1.2",
84
- "cypress": "^13.16.1",
85
+ "@vitejs/plugin-react-swc": "^3.8.0",
86
+ "chai": "^5.2.0",
87
+ "cypress": "^14.1.0",
85
88
  "identity-obj-proxy": "^3.0.0",
86
89
  "jest-transform-stub": "^2.0.0",
87
90
  "mochawesome": "^7.1.3",
88
- "msw": "2.6.8",
91
+ "msw": "2.7.3",
89
92
  "msw-storybook-addon": "^2.0.4",
90
- "react": "^18.3.1",
91
- "react-dom": "^18.3.1",
93
+ "react": "^19.0.0",
94
+ "react-dom": "^19.0.0",
92
95
  "react-test-renderer": "^19.0.0",
93
- "remark-gfm": "^4.0.0",
96
+ "remark-gfm": "^4.0.1",
94
97
  "speed-measure-vite-plugin": "^1.1.0",
95
- "storybook": "^8.4.7",
98
+ "storybook": "^8.6.3",
96
99
  "storybook-addon-mock": "^5.0.0",
97
100
  "storybook-dark-mode": "^4.0.2",
98
- "style-dictionary": "^4.3.0",
101
+ "style-dictionary": "^4.3.3",
99
102
  "ts-node": "^10.9.2",
100
103
  "use-resize-observer": "^9.1.0",
101
- "vite": "^6.0.3",
102
- "vite-plugin-istanbul": "^6.0.2",
104
+ "vite": "^6.2.0",
105
+ "vite-plugin-istanbul": "^7.0.0",
103
106
  "vite-plugin-svgr": "^4.3.0",
104
107
  "vite-plugin-turbosnap": "^1.0.3"
105
108
  },
106
109
  "peerDependencies": {
107
- "@centreon/ui-context": "file:../ui-context"
110
+ "@centreon/ui-context": "link:../ui-context"
108
111
  },
109
112
  "dependencies": {
110
- "@lexical/html": "^0.21.0",
111
- "@lexical/link": "^0.21.0",
112
- "@lexical/list": "^0.21.0",
113
- "@lexical/react": "^0.21.0",
114
- "@lexical/rich-text": "^0.21.0",
115
- "@lexical/selection": "^0.21.0",
116
- "@lexical/utils": "^0.21.0",
117
- "@mui/material": "^6.2.0",
113
+ "@lexical/html": "^0.27.0",
114
+ "@lexical/link": "^0.27.0",
115
+ "@lexical/list": "^0.27.0",
116
+ "@lexical/react": "^0.27.0",
117
+ "@lexical/rich-text": "^0.27.0",
118
+ "@lexical/selection": "^0.27.0",
119
+ "@lexical/utils": "^0.27.0",
120
+ "@mui/material": "^6.4.6",
118
121
  "@react-spring/web": "^9.7.5",
119
122
  "@visx/clip-path": "^3.12.0",
120
123
  "@visx/curve": "^3.12.0",
@@ -133,18 +136,18 @@
133
136
  "anylogger": "^1.0.11",
134
137
  "d3-array": "3.2.4",
135
138
  "dayjs": "^1.11.13",
136
- "highlight.js": "^11.10.0",
137
- "html-react-parser": "^5.2.0",
139
+ "highlight.js": "^11.11.1",
140
+ "html-react-parser": "^5.2.2",
138
141
  "humanize-duration": "^3.32.1",
139
- "lexical": "^0.21.0",
140
- "notistack": "^3.0.1",
142
+ "lexical": "^0.27.0",
143
+ "notistack": "^3.0.2",
141
144
  "numeral": "^2.0.6",
142
145
  "ramda": "0.30.1",
143
146
  "react-grid-layout": "^1.5.0",
144
147
  "react-resizable": "^3.0.5",
145
148
  "react-router": "7",
146
149
  "react-transition-group": "^4.4.5",
147
- "sanitize-html": "^2.13.1",
150
+ "sanitize-html": "^2.14.0",
148
151
  "ulog": "^2.0.0-beta.19"
149
152
  },
150
153
  "jest-junit": {
@@ -8,7 +8,7 @@
8
8
  * - Please do NOT serve this file on production.
9
9
  */
10
10
 
11
- const PACKAGE_VERSION = '2.6.8'
11
+ const PACKAGE_VERSION = '2.7.3'
12
12
  const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
13
13
  const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
14
14
  const activeClientIds = new Set()
@@ -59,6 +59,7 @@ const ActionsList = ({
59
59
  return (
60
60
  <MenuItem
61
61
  aria-label={label}
62
+ data-testid={label}
62
63
  className={classes.item}
63
64
  data-variant={variant}
64
65
  id={label}
@@ -19,6 +19,7 @@ const useStyles = makeStyles()((theme) => ({
19
19
 
20
20
  type Props = {
21
21
  ariaLabel?: string;
22
+ dataTestid?: string;
22
23
  className?: string;
23
24
  onClick: (event) => void;
24
25
  title?: string | JSX.Element;
@@ -42,6 +43,7 @@ export const IconButton = ({
42
43
  title = '',
43
44
  ariaLabel,
44
45
  className,
46
+ dataTestid,
45
47
  tooltipPlacement,
46
48
  tooltipClassName,
47
49
  ...props
@@ -59,7 +61,7 @@ export const IconButton = ({
59
61
  aria-label={ariaLabel}
60
62
  className={cx(classes.button, className)}
61
63
  color="primary"
62
- data-testid={ariaLabel}
64
+ data-testid={dataTestid || ariaLabel}
63
65
  id={getNormalizedId(ariaLabel || '')}
64
66
  {...props}
65
67
  />
@@ -1,13 +1,11 @@
1
- import { any, isEmpty, isNil, not, or, pipe } from 'ramda';
2
1
  import { makeStyles } from 'tss-react/mui';
3
2
 
4
- import { LoadingButton, LoadingButtonProps } from '@mui/lab';
5
3
  import { Theme, Tooltip } from '@mui/material';
6
4
 
7
5
  import { getNormalizedId } from '../../utils';
8
6
 
9
- import Content from './Content';
10
- import StartIcon from './StartIcon';
7
+ import { Button, ButtonProps } from '../../components';
8
+ import { useSave } from './useSave';
11
9
 
12
10
  const useStyles = makeStyles()((theme: Theme) => ({
13
11
  loadingButton: {
@@ -15,7 +13,7 @@ const useStyles = makeStyles()((theme: Theme) => ({
15
13
  }
16
14
  }));
17
15
 
18
- interface Props {
16
+ export interface Props {
19
17
  className?: string;
20
18
  labelLoading?: string;
21
19
  labelSave?: string;
@@ -28,15 +26,6 @@ interface Props {
28
26
  tooltipLabel?: string;
29
27
  }
30
28
 
31
- interface StartIconConfigProps {
32
- hasLabel: boolean;
33
- loading: boolean;
34
- succeeded: boolean;
35
- }
36
-
37
- const isNilOrEmpty = (value): boolean => or(isNil(value), isEmpty(value));
38
- const hasValue = any(pipe(isNilOrEmpty, not));
39
-
40
29
  const SaveButton = ({
41
30
  succeeded = false,
42
31
  loading = false,
@@ -48,20 +37,22 @@ const SaveButton = ({
48
37
  className,
49
38
  startIcon = true,
50
39
  ...rest
51
- }: Props & LoadingButtonProps): JSX.Element => {
40
+ }: Props & Omit<ButtonProps, 'children'>): JSX.Element => {
52
41
  const { classes, cx } = useStyles();
53
- const hasLabel = hasValue([labelLoading, labelSave, labelSucceeded]);
54
42
 
55
- const startIconConfig = {
56
- hasLabel,
43
+ const { content, startIconToDisplay, hasLabel } = useSave({
44
+ labelLoading,
45
+ labelSave,
46
+ labelSucceeded,
57
47
  loading,
58
- succeeded
59
- } as StartIconConfigProps;
48
+ succeeded,
49
+ startIcon
50
+ });
60
51
 
61
52
  return (
62
53
  <Tooltip placement="bottom" title={tooltipLabel}>
63
54
  <div>
64
- <LoadingButton
55
+ <Button
65
56
  aria-label="save button"
66
57
  className={cx(
67
58
  {
@@ -69,26 +60,17 @@ const SaveButton = ({
69
60
  },
70
61
  className
71
62
  )}
72
- color="primary"
73
63
  data-testid={labelSave}
74
64
  id={getNormalizedId(labelSave)}
75
65
  loading={loading}
76
- loadingPosition={labelLoading ? 'start' : 'center'}
66
+ loadingPosition={labelLoading ? 'start' : undefined}
77
67
  size={size}
78
- startIcon={
79
- startIcon && <StartIcon startIconConfig={startIconConfig} />
80
- }
81
- variant="contained"
68
+ startIcon={startIconToDisplay}
69
+ variant="primary"
82
70
  {...rest}
83
71
  >
84
- {Content({
85
- labelLoading,
86
- labelSave,
87
- labelSucceeded,
88
- loading,
89
- succeeded
90
- })}
91
- </LoadingButton>
72
+ {content}
73
+ </Button>
92
74
  </div>
93
75
  </Tooltip>
94
76
  );
@@ -0,0 +1,89 @@
1
+ import CheckIcon from '@mui/icons-material/Check';
2
+ import SaveIcon from '@mui/icons-material/Save';
3
+ import {
4
+ T,
5
+ always,
6
+ any,
7
+ cond,
8
+ isEmpty,
9
+ isNil,
10
+ not,
11
+ or,
12
+ pipe,
13
+ propEq
14
+ } from 'ramda';
15
+ import { useMemo } from 'react';
16
+ import { useTranslation } from 'react-i18next';
17
+ import { Props } from '.';
18
+
19
+ interface StartIconConfigProps {
20
+ hasLabel: boolean;
21
+ loading: boolean;
22
+ succeeded: boolean;
23
+ enabled: boolean;
24
+ }
25
+
26
+ const isNilOrEmpty = (value): boolean => or(isNil(value), isEmpty(value));
27
+ const hasValue = any(pipe(isNilOrEmpty, not));
28
+
29
+ interface UseSaveState {
30
+ content: string | JSX.Element;
31
+ startIconToDisplay: null | JSX.Element;
32
+ hasLabel: boolean;
33
+ }
34
+
35
+ export const useSave = ({
36
+ labelLoading,
37
+ labelSave,
38
+ labelSucceeded,
39
+ loading,
40
+ succeeded,
41
+ startIcon
42
+ }: Pick<
43
+ Props,
44
+ | 'startIcon'
45
+ | 'succeeded'
46
+ | 'loading'
47
+ | 'labelSave'
48
+ | 'labelSucceeded'
49
+ | 'labelLoading'
50
+ >): UseSaveState => {
51
+ const { t } = useTranslation();
52
+
53
+ const hasLabel = hasValue([labelLoading, labelSave, labelSucceeded]);
54
+
55
+ const startIconConfig = {
56
+ hasLabel,
57
+ loading,
58
+ succeeded,
59
+ enabled: startIcon
60
+ } as StartIconConfigProps;
61
+
62
+ const content = useMemo(() => {
63
+ if (loading) {
64
+ return t(labelLoading || 'loading');
65
+ }
66
+
67
+ if (succeeded) {
68
+ return labelSucceeded ? t(labelSucceeded) : <CheckIcon />;
69
+ }
70
+
71
+ return labelSave ? t(labelSave) : <SaveIcon />;
72
+ }, [labelLoading, labelSucceeded, labelSave, loading, succeeded]);
73
+
74
+ const startIconToDisplay = useMemo(() => {
75
+ return cond<Array<StartIconConfigProps>, JSX.Element | null>([
76
+ [propEq(true, 'enabled'), always(null)],
77
+ [pipe(propEq(true, 'hasLabel'), not), always(null)],
78
+ [propEq(true, 'succeeded'), always(<CheckIcon />)],
79
+ [propEq(true, 'loading'), always(<SaveIcon />)],
80
+ [T, always(<SaveIcon />)]
81
+ ])(startIconConfig);
82
+ }, [startIconConfig]);
83
+
84
+ return {
85
+ content,
86
+ startIconToDisplay,
87
+ hasLabel
88
+ };
89
+ };
@@ -1,6 +1,6 @@
1
1
  import { always, cond, equals } from 'ramda';
2
2
 
3
- import { LoadingSkeleton } from '../..';
3
+ import LoadingSkeleton from '../../LoadingSkeleton';
4
4
 
5
5
  import { InputProps, InputType } from './models';
6
6
 
@@ -80,11 +80,13 @@ const Radio = ({
80
80
  data-testid={`${dataTestId} ${optionLabel}`}
81
81
  disabled={disabled}
82
82
  id={getNormalizedId(`${dataTestId}${optionLabel}`)}
83
- inputProps={{
84
- 'aria-label':
85
- (equals(type(optionLabel), 'String') &&
86
- t(optionLabel as string)) ||
87
- ''
83
+ slotProps={{
84
+ input: {
85
+ 'aria-label':
86
+ (equals(type(optionLabel), 'String') &&
87
+ t(optionLabel as string)) ||
88
+ ''
89
+ }
88
90
  }}
89
91
  />
90
92
  }
@@ -58,8 +58,10 @@ const Switch = ({
58
58
  data-testid={dataTestId}
59
59
  disabled={disabled}
60
60
  id={getNormalizedId(dataTestId || '')}
61
- inputProps={{
62
- 'aria-label': t(label) || ''
61
+ slotProps={{
62
+ input: {
63
+ 'aria-label': t(label) || ''
64
+ }
63
65
  }}
64
66
  onChange={changeSwitchValue}
65
67
  />
@@ -758,8 +758,8 @@ describe('Lines and bars', () => {
758
758
  cy.get('path[data-metric="3"]').should('be.visible');
759
759
 
760
760
  cy.contains('some text').should('be.visible');
761
- cy.findByTestId('pink-3').should('be.visible');
762
- cy.findByTestId('red-0.15').should('be.visible');
761
+ cy.findByTestId('pink-3').should('exist');
762
+ cy.findByTestId('red-0.15').should('exist');
763
763
 
764
764
  cy.makeSnapshot();
765
765
  });
@@ -43,7 +43,7 @@ const Annotation = ({
43
43
 
44
44
  const setAnnotationHovered = useSetAtom(annotationHoveredAtom);
45
45
 
46
- const content = `${truncate(event.content)} (${t(labelBy)} ${
46
+ const content = `${truncate({ content: event.content })} (${t(labelBy)} ${
47
47
  event.contact?.name
48
48
  })`;
49
49
 
@@ -38,9 +38,10 @@ export const getXAxisTickFormat = (graphInterval: GraphInterval): string => {
38
38
  return gte(numberDays, 2) ? dateFormat : timeFormat;
39
39
  };
40
40
 
41
- export const truncate = (content?: string): string => {
42
- const maxLength = 180;
43
-
41
+ export const truncate = ({
42
+ content,
43
+ maxLength = 180
44
+ }: { content?: string; maxLength?: number }): string => {
44
45
  if (isNil(content)) {
45
46
  return '';
46
47
  }
@@ -63,7 +63,7 @@ export const ThresholdLine = ({
63
63
  strokeDasharray="6, 6"
64
64
  strokeWidth={2}
65
65
  x1={scaledValue}
66
- x2={scaledValue}
66
+ x2={scaledValue + 1}
67
67
  y1={
68
68
  isSmall
69
69
  ? groupMargin - lineMargin + 6
@@ -80,7 +80,7 @@ export const ThresholdLine = ({
80
80
  stroke="transparent"
81
81
  strokeWidth={5}
82
82
  x1={scaledValue}
83
- x2={scaledValue}
83
+ x2={scaledValue + 1}
84
84
  y1={
85
85
  isSmall
86
86
  ? groupMargin - lineMargin + 5
@@ -45,11 +45,11 @@ export const ThresholdLine = ({
45
45
  x1: 0,
46
46
  x2: width,
47
47
  y1: scaledValue,
48
- y2: scaledValue
48
+ y2: scaledValue + 1
49
49
  }
50
50
  : {
51
51
  x1: scaledValue,
52
- x2: scaledValue,
52
+ x2: scaledValue + 1,
53
53
  y1: 0,
54
54
  y2: width
55
55
  };
@@ -97,15 +97,17 @@ const SelectField = ({
97
97
  <Select
98
98
  displayEmpty
99
99
  fullWidth={fullWidth}
100
- inputProps={{
101
- 'aria-label': ariaLabel,
102
- className: cx(classes.input, {
103
- [classes.noLabelInput]: !label && !compact,
104
- [classes.compact]: compact
105
- }),
106
- 'data-testid': dataTestId,
107
- id: getNormalizedId(dataTestId || ''),
108
- ...inputProps
100
+ slotProps={{
101
+ input: {
102
+ 'aria-label': ariaLabel,
103
+ className: cx(classes.input, {
104
+ [classes.noLabelInput]: !label && !compact,
105
+ [classes.compact]: compact
106
+ }),
107
+ 'data-testid': dataTestId,
108
+ id: getNormalizedId(dataTestId || ''),
109
+ ...inputProps
110
+ }
109
111
  }}
110
112
  label={label}
111
113
  renderValue={(id): string => {
@@ -25,6 +25,18 @@ interface GetBackgroundColorProps extends Omit<Props, 'isRowHighlighted'> {
25
25
  theme: Theme;
26
26
  }
27
27
 
28
+ interface StylesProps extends Props {
29
+ isRowHighlighted?: boolean;
30
+ listingVariant?: ListingVariant;
31
+ }
32
+
33
+ interface GetRowHighlightStyleProps {
34
+ isRowHighlighted?: boolean;
35
+ theme: Theme;
36
+ disableRowCondition;
37
+ row;
38
+ }
39
+
28
40
  const getBackgroundColor = ({
29
41
  isRowHovered,
30
42
  row,
@@ -51,25 +63,20 @@ const getBackgroundColor = ({
51
63
  return 'unset';
52
64
  };
53
65
 
54
- interface StylesProps extends Props {
55
- isRowHighlighted?: boolean;
56
- listingVariant?: ListingVariant;
57
- }
58
-
59
- interface GetRowHighlightStyleProps {
60
- isRowHighlighted?: boolean;
61
- theme: Theme;
62
- }
63
-
64
- const getRowHighlightStyle = ({
66
+ const getRowTextColor = ({
65
67
  isRowHighlighted,
66
- theme
67
- }: GetRowHighlightStyleProps): CSSObject | undefined =>
68
- isRowHighlighted
69
- ? {
70
- color: theme.palette.text.primary
71
- }
72
- : undefined;
68
+ theme,
69
+ disableRowCondition,
70
+ row
71
+ }: GetRowHighlightStyleProps): CSSObject | undefined => {
72
+ if (isRowHighlighted) {
73
+ return { color: theme.palette.text.primary };
74
+ }
75
+
76
+ if (disableRowCondition(row)) {
77
+ return { color: alpha(theme.palette.text.secondary, 0.5) };
78
+ }
79
+ };
73
80
 
74
81
  const useStyles = makeStyles<StylesProps>()(
75
82
  (
@@ -112,7 +119,7 @@ const useStyles = makeStyles<StylesProps>()(
112
119
  height: '100%',
113
120
  overflow: 'hidden',
114
121
  ...getTextStyleByViewMode({ listingVariant, theme }),
115
- p: getRowHighlightStyle({ isRowHighlighted, theme }),
122
+ p: getRowTextColor({ isRowHighlighted, disableRowCondition, row, theme }),
116
123
  padding: theme.spacing(0, 1),
117
124
  whiteSpace: 'nowrap'
118
125
  }
@@ -237,35 +237,33 @@ describe('Listing', () => {
237
237
  it('selects displayed rows when a row is selected and another row is selected with the shift key', () => {
238
238
  mountListingForSubItems({ canCheckSubItems: true, isSmallListing: true });
239
239
 
240
- cy.findByLabelText('Expand 0').click();
240
+ cy.findByLabelText('Expand 2').click();
241
241
 
242
- cy.findByLabelText('Collapse 0').should('be.visible');
242
+ cy.findByLabelText('Collapse 2').should('be.visible');
243
243
 
244
- cy.findAllByLabelText('Select row 0').eq(0).click();
244
+ cy.findAllByLabelText('Select row 2').eq(0).click();
245
245
  cy.get('body').type('{shift}', { release: false });
246
246
  cy.findByLabelText('Select row 50').eq(0).click();
247
247
 
248
- cy.findAllByLabelText('Select row 0').eq(0).should('be.checked');
249
- cy.findAllByLabelText('Select row 0').eq(1).should('be.checked');
248
+ cy.findAllByLabelText('Select row 2').eq(0).should('be.checked');
250
249
  cy.findByLabelText('Select row 10').should('be.checked');
251
250
  cy.findByLabelText('Select row 20').should('be.checked');
252
251
  cy.findByLabelText('Select row 30').should('be.checked');
253
252
  cy.findByLabelText('Select row 40').should('be.checked');
254
- cy.findByLabelText('Select row 50').should('be.checked');
255
253
 
256
- cy.findByLabelText('Collapse 0').click();
254
+ cy.findByLabelText('Collapse 2').click();
257
255
  });
258
256
 
259
257
  it('selects displayed rows when the corresponding checkbox is clicked', () => {
260
258
  mountListingForSubItems({ canCheckSubItems: true, isSmallListing: true });
261
259
 
262
- cy.findByLabelText('Expand 0').click();
260
+ cy.findByLabelText('Expand 2').click();
263
261
 
264
- cy.findByLabelText('Collapse 0').should('be.visible');
262
+ cy.findByLabelText('Collapse 2').should('be.visible');
265
263
 
266
264
  cy.findAllByLabelText('Select all').eq(0).click();
267
265
 
268
- cy.findAllByLabelText('Select row 0').eq(0).should('be.checked');
266
+ cy.findAllByLabelText('Select row 2').eq(0).should('be.checked');
269
267
  tenElements.forEach((_, index) => {
270
268
  if (index === 0) {
271
269
  cy.findAllByLabelText('Select row 0').eq(1).should('be.checked');
@@ -277,7 +275,7 @@ describe('Listing', () => {
277
275
  cy.findByLabelText('Select row 1').should('be.checked');
278
276
  cy.findByLabelText('Select row 2').should('be.checked');
279
277
 
280
- cy.findByLabelText('Collapse 0').click();
278
+ cy.findByLabelText('Collapse 2').click();
281
279
  });
282
280
  });
283
281
 
@@ -2,7 +2,7 @@ import { $generateHtmlFromNodes } from '@lexical/html';
2
2
  import { AutoLinkNode, LinkNode } from '@lexical/link';
3
3
  import { ListItemNode, ListNode } from '@lexical/list';
4
4
  import { LexicalComposer } from '@lexical/react/LexicalComposer';
5
- import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
5
+ import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
6
6
  import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
7
7
  import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
8
8
  import { ListPlugin } from '@lexical/react/LexicalListPlugin';
@@ -211,7 +211,7 @@ export const lightPalette: PaletteOptions = {
211
211
  default: '#696969'
212
212
  }
213
213
  },
214
- border: '#4A4A4A',
214
+ border: '#EDEDED',
215
215
  description: '#4A4A4A',
216
216
  title: '#000000'
217
217
  }
@@ -353,7 +353,7 @@ export const darkPalette: PaletteOptions = {
353
353
  default: '#696969'
354
354
  }
355
355
  },
356
- border: '#bdbdbd',
356
+ border: '#666666',
357
357
  description: '#bdbdbd',
358
358
  title: '#fff'
359
359
  }
@@ -95,15 +95,6 @@ export const customFetch = <T>({
95
95
  };
96
96
  }
97
97
 
98
- if (equals(response.status, 207)) {
99
- return {
100
- data: data.results,
101
- isError: false,
102
- message: '',
103
- statusCode: response.status
104
- };
105
- }
106
-
107
98
  if (decoder) {
108
99
  return decoder.decodeToPromise(data).catch((error: string) => {
109
100
  catchError({
@@ -0,0 +1,58 @@
1
+ import {
2
+ complement,
3
+ includes,
4
+ isEmpty,
5
+ isNil,
6
+ last,
7
+ length,
8
+ prop,
9
+ propEq,
10
+ split
11
+ } from 'ramda';
12
+ import useSnackbar from '../Snackbar/useSnackbar';
13
+
14
+ const useBulkResponse = () => {
15
+ const { showSuccessMessage, showErrorMessage, showWarningMessage } =
16
+ useSnackbar();
17
+
18
+ const handleBulkResponse = ({
19
+ data,
20
+ labelSuccess,
21
+ labelWarning,
22
+ labelFailed,
23
+ items
24
+ }) => {
25
+ const successfullResponses =
26
+ data?.filter(propEq(204, 'status')) || isNil(data);
27
+
28
+ const failedResponses = data?.filter(complement(propEq(204, 'status')));
29
+
30
+ const failedResponsesIds = failedResponses
31
+ ?.map(prop('href'))
32
+ ?.map((item: string) =>
33
+ Number.parseInt(last(split('/', item || '')) as string, 10)
34
+ );
35
+
36
+ if (isEmpty(successfullResponses)) {
37
+ showErrorMessage(labelFailed);
38
+
39
+ return;
40
+ }
41
+
42
+ if (length(successfullResponses) < length(data)) {
43
+ const failedResponsesNames = items
44
+ ?.filter((item) => includes(item.id, failedResponsesIds))
45
+ .map((item) => item.name);
46
+
47
+ showWarningMessage(`${labelWarning}: ${failedResponsesNames.join(', ')}`);
48
+
49
+ return;
50
+ }
51
+
52
+ showSuccessMessage(labelSuccess);
53
+ };
54
+
55
+ return handleBulkResponse;
56
+ };
57
+
58
+ export default useBulkResponse;
@@ -2,17 +2,12 @@ import useMutationQuery, { Method } from '.';
2
2
  import SnackbarProvider from '../../Snackbar/SnackbarProvider';
3
3
  import TestQueryProvider from '../TestQueryProvider';
4
4
 
5
- // biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
6
- let spyMutation;
7
-
8
5
  const TestComponent = (props) => {
9
6
  const mutation = useMutationQuery({
10
7
  ...props,
11
8
  getEndpoint: () => '/endpoint'
12
9
  });
13
10
 
14
- spyMutation = mutation;
15
-
16
11
  return (
17
12
  <button
18
13
  type="button"
@@ -46,8 +41,6 @@ const initialize = ({ mutationProps, isError = false }) => {
46
41
  </TestQueryProvider>
47
42
  </SnackbarProvider>
48
43
  )
49
- }).then(() => {
50
- cy.spy(spyMutation, 'mutateAsync').as('mutateAsync');
51
44
  });
52
45
  };
53
46
 
@@ -67,13 +60,6 @@ describe('useMutationQuery', () => {
67
60
  expect(request.body).to.deep.equal({ a: 'a', b: 2, c: ['arr', 'ay'] });
68
61
  expect(request.headers.get('content-type')).to.equal('application/json');
69
62
  });
70
- cy.get('@mutateAsync').should('be.calledWith', {
71
- payload: {
72
- a: 'a',
73
- b: 2,
74
- c: ['arr', 'ay']
75
- }
76
- });
77
63
  });
78
64
 
79
65
  it("shows an error from the API via the Snackbar and inside the browser's console when posting data to an endpoint", () => {
@@ -87,8 +73,6 @@ describe('useMutationQuery', () => {
87
73
 
88
74
  cy.get('button').click();
89
75
 
90
- cy.get('@mutateAsync').should('be.called');
91
-
92
76
  cy.contains('custom error message').should('be.visible');
93
77
  });
94
78
 
@@ -104,8 +88,6 @@ describe('useMutationQuery', () => {
104
88
 
105
89
  cy.get('button').click();
106
90
 
107
- cy.get('@mutateAsync').should('be.called');
108
-
109
91
  cy.contains('custom error message').should('not.exist');
110
92
  });
111
93
  });
@@ -1,6 +1,9 @@
1
1
  import { ReactElement, ReactNode, useMemo } from 'react';
2
2
 
3
- import { Button as MuiButton } from '@mui/material';
3
+ import {
4
+ Button as MuiButton,
5
+ ButtonProps as MuiButtonProps
6
+ } from '@mui/material';
4
7
 
5
8
  import { AriaLabelingAttributes } from '../../@types/aria-attributes';
6
9
  import { DataTestAttributes } from '../../@types/data-attributes';
@@ -16,20 +19,21 @@ const muiVariantMap: Record<
16
19
  secondary: 'outlined'
17
20
  };
18
21
 
19
- export type ButtonProps = {
20
- children: ReactNode;
21
- className?: string;
22
- disabled?: boolean;
23
- icon?: string | ReactNode;
24
- iconVariant?: 'none' | 'start' | 'end';
25
- isDanger?: boolean;
26
- onClick?: (e) => void;
27
- ref?: React.Ref<HTMLButtonElement>;
28
- size?: 'small' | 'medium' | 'large';
29
- type?: 'button' | 'submit' | 'reset';
30
- variant?: 'primary' | 'secondary' | 'ghost';
31
- } & AriaLabelingAttributes &
32
- DataTestAttributes;
22
+ export type ButtonProps = AriaLabelingAttributes &
23
+ DataTestAttributes &
24
+ Omit<MuiButtonProps, 'variant'> & {
25
+ children: ReactNode;
26
+ className?: string;
27
+ disabled?: boolean;
28
+ icon?: string | ReactNode;
29
+ iconVariant?: 'none' | 'start' | 'end';
30
+ isDanger?: boolean;
31
+ onClick?: (e) => void;
32
+ ref?: React.Ref<HTMLButtonElement>;
33
+ size?: 'small' | 'medium' | 'large';
34
+ type?: 'button' | 'submit' | 'reset';
35
+ variant?: 'primary' | 'secondary' | 'ghost';
36
+ };
33
37
 
34
38
  const Button = ({
35
39
  children,
@@ -16,7 +16,7 @@ describe('CollapsibleItem', () => {
16
16
 
17
17
  cy.contains(title).should('be.visible');
18
18
  cy.contains('Content').should('not.be.visible');
19
- cy.get('div[aria-expanded="false"]').should('exist');
19
+ cy.get('button[aria-expanded="false"]').should('exist');
20
20
 
21
21
  cy.makeSnapshot();
22
22
  });
@@ -26,7 +26,7 @@ describe('CollapsibleItem', () => {
26
26
 
27
27
  cy.contains(title).should('be.visible');
28
28
  cy.contains('Content').should('be.visible');
29
- cy.get('div[aria-expanded="true"]').should('exist');
29
+ cy.get('button[aria-expanded="true"]').should('exist');
30
30
 
31
31
  cy.makeSnapshot();
32
32
  });
@@ -35,7 +35,7 @@ describe('CollapsibleItem', () => {
35
35
  initialize({ title: customizedTitle });
36
36
 
37
37
  cy.contains('Customized title').should('be.visible');
38
- cy.get('div[aria-expanded="false"]').should('exist');
38
+ cy.get('button[aria-expanded="false"]').should('exist');
39
39
 
40
40
  cy.makeSnapshot();
41
41
  });
@@ -44,7 +44,7 @@ describe('CollapsibleItem', () => {
44
44
  initialize({ compact: true, title });
45
45
 
46
46
  cy.contains(title).should('be.visible');
47
- cy.get('div[aria-expanded="false"]').should('exist');
47
+ cy.get('button[aria-expanded="false"]').should('exist');
48
48
  cy.get('div[data-compact="true"]').should('exist');
49
49
 
50
50
  cy.makeSnapshot();
@@ -54,11 +54,11 @@ describe('CollapsibleItem', () => {
54
54
  initialize({ compact: true, title });
55
55
 
56
56
  cy.contains(title).should('be.visible');
57
- cy.get('div[aria-expanded="false"]').should('exist');
57
+ cy.get('button[aria-expanded="false"]').should('exist');
58
58
 
59
- cy.get('div[aria-expanded="false"]').click();
59
+ cy.get('button[aria-expanded="false"]').click();
60
60
 
61
- cy.get('div[aria-expanded="true"]').should('exist');
61
+ cy.get('button[aria-expanded="true"]').should('exist');
62
62
  cy.contains('Content').should('be.visible');
63
63
 
64
64
  cy.makeSnapshot();
@@ -68,7 +68,7 @@ describe('CollapsibleItem', () => {
68
68
  initialize({ compact: true, title: customizedTitle });
69
69
 
70
70
  cy.contains('Customized title').should('be.visible');
71
- cy.get('div[aria-expanded="false"]').should('exist');
71
+ cy.get('button[aria-expanded="false"]').should('exist');
72
72
  cy.get('div[data-compact="true"]').should('exist');
73
73
 
74
74
  cy.makeSnapshot();
@@ -1,5 +1,4 @@
1
1
  import { ThemeMode, userAtom } from '@centreon/ui-context';
2
- import { mount } from 'cypress/react18';
3
2
  import { Provider, createStore } from 'jotai';
4
3
  import SnackbarProvider from '../../Snackbar/SnackbarProvider';
5
4
  import ThemeProvider from '../../ThemeProvider';
@@ -8,15 +7,17 @@ import CopyCommand, { CopyCommandProps } from './CopyCommand';
8
7
  const initialize = (props: CopyCommandProps & { theme?: ThemeMode }): void => {
9
8
  const store = createStore();
10
9
  store.set(userAtom, { themeMode: props.theme || ThemeMode.light });
11
- mount(
12
- <Provider store={store}>
13
- <ThemeProvider>
14
- <SnackbarProvider>
15
- <CopyCommand {...props} />
16
- </SnackbarProvider>
17
- </ThemeProvider>
18
- </Provider>
19
- );
10
+ cy.mount({
11
+ Component: (
12
+ <Provider store={store}>
13
+ <ThemeProvider>
14
+ <SnackbarProvider>
15
+ <CopyCommand {...props} />
16
+ </SnackbarProvider>
17
+ </ThemeProvider>
18
+ </Provider>
19
+ )
20
+ });
20
21
  };
21
22
 
22
23
  describe('CopyCommand', () => {
@@ -2,7 +2,21 @@ import { makeStyles } from 'tss-react/mui';
2
2
 
3
3
  export const useActionsStyles = makeStyles()((theme) => ({
4
4
  search: {
5
- maxWidth: theme.spacing(50)
5
+ maxWidth: theme.spacing(60),
6
+ minWidth: theme.spacing(20),
7
+ width: '100%'
8
+ },
9
+ filters: {
10
+ width: '100%',
11
+ paddingInline: theme.spacing(1),
12
+ display: 'flex',
13
+ alignItems: 'center',
14
+ justifyContent: 'center'
15
+ },
16
+ actions: {
17
+ display: 'grid',
18
+ gridTemplateColumns: 'min-content auto',
19
+ gap: theme.spacing(1)
6
20
  },
7
21
  clearButton: {
8
22
  alignSelf: 'flex-start'
@@ -1,4 +1,5 @@
1
1
  import { Box } from '@mui/material';
2
+ import { useActionsStyles } from './Actions.styles';
2
3
  import AddButton from './AddButton';
3
4
  import Search from './Search';
4
5
 
@@ -11,12 +12,14 @@ interface Props {
11
12
  }
12
13
 
13
14
  const Actions = ({ labels, filters }: Props): JSX.Element => {
15
+ const { classes } = useActionsStyles();
16
+
14
17
  return (
15
- <Box
16
- sx={{ display: 'grid', gridTemplateColumns: 'min-content auto', gap: 2 }}
17
- >
18
+ <Box className={classes.actions}>
18
19
  <AddButton label={labels.add} />
19
- <Search label={labels.search} filters={filters} />
20
+ <div className={classes.filters}>
21
+ <Search label={labels.search} filters={filters} />
22
+ </div>
20
23
  </Box>
21
24
  );
22
25
  };
@@ -14,21 +14,22 @@ const Search = ({ label, filters }: Props): JSX.Element => {
14
14
  const { change } = useSearch();
15
15
 
16
16
  return (
17
- <SearchField
18
- className={classes.search}
19
- debounced
20
- fullWidth
21
- dataTestId={label}
22
- placeholder={label}
23
- onChange={change}
24
- textFieldSlotsAndSlotProps={{
25
- slotProps: {
26
- input: {
27
- endAdornment: <Filters label="filters" filters={filters} />
17
+ <div className={classes.search}>
18
+ <SearchField
19
+ debounced
20
+ fullWidth
21
+ dataTestId={label}
22
+ placeholder={label}
23
+ onChange={change}
24
+ textFieldSlotsAndSlotProps={{
25
+ slotProps: {
26
+ input: {
27
+ endAdornment: <Filters label="filters" filters={filters} />
28
+ }
28
29
  }
29
- }
30
- }}
31
- />
30
+ }}
31
+ />
32
+ </div>
32
33
  );
33
34
  };
34
35
 
@@ -25,9 +25,10 @@ const useStyles = makeStyles()((theme) => ({
25
25
  },
26
26
  pageHeader: {
27
27
  alignItems: 'center',
28
- borderBottom: `1px solid ${theme.palette.header.page.border}`,
28
+ borderBottom: `2px solid ${theme.palette.header.page.border}`,
29
29
  display: 'flex',
30
- gap: theme.spacing(4)
30
+ gap: theme.spacing(4),
31
+ paddingBottom: theme.spacing(0.5)
31
32
  },
32
33
  pageHeaderActions: {
33
34
  '& > button': {
@@ -46,8 +47,7 @@ const useStyles = makeStyles()((theme) => ({
46
47
  pageHeaderMain: {
47
48
  display: 'flex',
48
49
  flexGrow: 1,
49
- gap: theme.spacing(1),
50
- minHeight: theme.spacing(4.5)
50
+ gap: theme.spacing(1)
51
51
  },
52
52
  pageHeaderMenu: {
53
53
  alignItems: 'flex-start',
@@ -96,7 +96,7 @@ const useStyles = makeStyles()((theme) => ({
96
96
  alignSelf: 'flex-start',
97
97
  h1: {
98
98
  ...theme.typography.h5,
99
- fontWeight: theme.typography.fontWeightMedium,
99
+ fontWeight: theme.typography.fontWeightBold,
100
100
  margin: theme.spacing(0),
101
101
  lineHeight: '1'
102
102
  }
@@ -29,7 +29,7 @@ export const useStyles = makeStyles()((theme) => ({
29
29
  display: 'grid',
30
30
  gridTemplateRows: 'auto',
31
31
  overflow: 'hidden',
32
- padding: theme.spacing(1.5, 3, 1.5)
32
+ padding: theme.spacing(0, 3, 1.5)
33
33
  },
34
34
  pageLayoutHeader: {
35
35
  '[data-variant="fixed-header"] &': {
package/src/index.ts CHANGED
@@ -133,6 +133,7 @@ export {
133
133
  } from './api/useGraphQuery';
134
134
  export { WidgetResourceType as ResourceType } from './api/useGraphQuery/models';
135
135
  export { default as QueryProvider, client } from './api/QueryProvider';
136
+ export { default as useBulkResponse } from './api/useBulkResponse';
136
137
  export {
137
138
  default as FileDropZone,
138
139
  transformFileListToArray
@@ -1,34 +0,0 @@
1
- import { useTranslation } from 'react-i18next';
2
-
3
- import CheckIcon from '@mui/icons-material/Check';
4
- import SaveIcon from '@mui/icons-material/Save';
5
-
6
- interface Props {
7
- labelLoading: string;
8
- labelSave: string;
9
- labelSucceeded: string;
10
- loading: boolean;
11
- succeeded: boolean;
12
- }
13
-
14
- const Content = ({
15
- succeeded,
16
- labelSucceeded,
17
- labelSave,
18
- loading,
19
- labelLoading
20
- }: Props): JSX.Element | string | null => {
21
- const { t } = useTranslation();
22
-
23
- if (loading) {
24
- return t(labelLoading);
25
- }
26
-
27
- if (succeeded) {
28
- return labelSucceeded ? t(labelSucceeded) : <CheckIcon />;
29
- }
30
-
31
- return labelSave ? t(labelSave) : <SaveIcon />;
32
- };
33
-
34
- export default Content;
@@ -1,24 +0,0 @@
1
- import { T, always, cond, not, pipe, propEq } from 'ramda';
2
-
3
- import CheckIcon from '@mui/icons-material/Check';
4
- import SaveIcon from '@mui/icons-material/Save';
5
-
6
- interface StartIconConfigProps {
7
- hasLabel: boolean;
8
- loading: boolean;
9
- succeeded: boolean;
10
- }
11
-
12
- interface Props {
13
- startIconConfig: StartIconConfigProps;
14
- }
15
-
16
- const StartIcon = ({ startIconConfig }: Props): JSX.Element | null =>
17
- cond<Array<StartIconConfigProps>, JSX.Element | null>([
18
- [pipe(propEq(true, 'hasLabel'), not), always(null)],
19
- [propEq(true, 'succeeded'), always(<CheckIcon />)],
20
- [propEq(true, 'loading'), always(<SaveIcon />)],
21
- [T, always(<SaveIcon />)]
22
- ])(startIconConfig);
23
-
24
- export default StartIcon;