@centreon/ui 24.4.38 → 24.4.40

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.38",
3
+ "version": "24.4.40",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "eslint": "eslint ./src --ext .js,.jsx,.ts,.tsx --max-warnings 0",
@@ -7,7 +7,6 @@ import { Box, alpha, useTheme } from '@mui/material';
7
7
 
8
8
  import { useMemoComponent } from '@centreon/ui';
9
9
 
10
- import { maxLinesDisplayedLegend } from '../common';
11
10
  import { formatMetricValue } from '../../common/timeSeries';
12
11
  import { Line, TimeValue } from '../../common/timeSeries/models';
13
12
  import { labelAvg, labelMax, labelMin } from '../translatedLabels';
@@ -24,7 +23,7 @@ import LegendContent from './LegendContent';
24
23
  interface Props {
25
24
  base: number;
26
25
  displayAnchor?: boolean;
27
- limitLegendRows?: boolean;
26
+ limitLegend?: false | number;
28
27
  lines: Array<Line>;
29
28
  renderExtraComponent?: ReactNode;
30
29
  setLinesGraph: Dispatch<SetStateAction<Array<Line> | null>>;
@@ -39,14 +38,14 @@ const MainLegend = ({
39
38
  timeSeries,
40
39
  base,
41
40
  toggable = true,
42
- limitLegendRows = true,
41
+ limitLegend = false,
43
42
  renderExtraComponent,
44
43
  displayAnchor = true,
45
44
  setLinesGraph,
46
45
  xScale,
47
46
  shouldDisplayLegendInCompactMode
48
47
  }: Props): JSX.Element => {
49
- const { classes, cx } = useStyles({ limitLegendRows });
48
+ const { classes, cx } = useStyles({ limitLegendRows: Boolean(limitLegend) });
50
49
  const theme = useTheme();
51
50
 
52
51
  const { selectMetricLine, clearHighlight, highlightLine, toggleMetricLine } =
@@ -61,8 +60,8 @@ const MainLegend = ({
61
60
 
62
61
  const sortedData = sortBy(prop('metric_id'), lines);
63
62
 
64
- const displayedLines = limitLegendRows
65
- ? slice(0, maxLinesDisplayedLegend, sortedData)
63
+ const displayedLines = limitLegend
64
+ ? slice(0, limitLegend, sortedData)
66
65
  : sortedData;
67
66
 
68
67
  const getMetricValue = ({ value, unit }: GetMetricValueProps): string =>
@@ -174,7 +173,7 @@ const MainLegend = ({
174
173
  const Legend = (props: Props): JSX.Element => {
175
174
  const {
176
175
  toggable,
177
- limitLegendRows,
176
+ limitLegend,
178
177
  timeSeries,
179
178
  lines,
180
179
  base,
@@ -191,7 +190,7 @@ const Legend = (props: Props): JSX.Element => {
191
190
  lines,
192
191
  base,
193
192
  toggable,
194
- limitLegendRows,
193
+ limitLegend,
195
194
  displayAnchor,
196
195
  shouldDisplayLegendInCompactMode
197
196
  ]
@@ -43,6 +43,7 @@ interface Props extends LineChartProps {
43
43
  graphInterval: GraphInterval;
44
44
  graphRef: MutableRefObject<HTMLDivElement | null>;
45
45
  legend?: LegendModel;
46
+ limitLegend?: false | number;
46
47
  marginBottom: number;
47
48
  shapeLines?: GlobalAreaLines;
48
49
  thresholdUnit?: string;
@@ -73,7 +74,8 @@ const LineChart = ({
73
74
  curve,
74
75
  marginBottom,
75
76
  thresholds,
76
- thresholdUnit
77
+ thresholdUnit,
78
+ limitLegend
77
79
  }: Props): JSX.Element => {
78
80
  const { classes } = useStyles();
79
81
 
@@ -301,6 +303,7 @@ const LineChart = ({
301
303
  <Legend
302
304
  base={baseAxis}
303
305
  displayAnchor={displayAnchor?.displayGuidingLines ?? true}
306
+ limitLegend={limitLegend}
304
307
  lines={newLines}
305
308
  renderExtraComponent={legend?.renderExtraComponent}
306
309
  setLinesGraph={setLinesGraph}
@@ -28,6 +28,7 @@ interface Props extends Partial<LineChartProps> {
28
28
  data?: LineChartData;
29
29
  end: string;
30
30
  legend: LegendModel;
31
+ limitLegend?: false | number;
31
32
  loading: boolean;
32
33
  marginBottom?: number;
33
34
  shapeLines?: GlobalAreaLines;
@@ -55,7 +56,8 @@ const WrapperLineChart = ({
55
56
  curve = Curve.curveLinear,
56
57
  marginBottom = 0,
57
58
  thresholds,
58
- thresholdUnit
59
+ thresholdUnit,
60
+ limitLegend
59
61
  }: Props): JSX.Element | null => {
60
62
  const { adjustedData } = useLineChartData({ data, end, start });
61
63
  const lineChartRef = useRef<HTMLDivElement | null>(null);
@@ -95,6 +97,7 @@ const WrapperLineChart = ({
95
97
  header={header}
96
98
  height={height || responsiveHeight}
97
99
  legend={legend}
100
+ limitLegend={limitLegend}
98
101
  loading={loading}
99
102
  marginBottom={marginBottom}
100
103
  shapeLines={shapeLines}
@@ -1,11 +1,20 @@
1
1
  import { useRef } from 'react';
2
2
 
3
- import { equals, has, isEmpty, pluck } from 'ramda';
3
+ import {
4
+ equals,
5
+ flatten,
6
+ has,
7
+ includes,
8
+ isEmpty,
9
+ not,
10
+ pipe,
11
+ pluck
12
+ } from 'ramda';
4
13
  import dayjs from 'dayjs';
5
14
 
6
15
  import { LineChartData, buildListingEndpoint, useFetchQuery } from '../..';
7
16
 
8
- import { Resource, WidgetResourceType } from './models';
17
+ import { Metric, Resource, WidgetResourceType } from './models';
9
18
 
10
19
  interface CustomTimePeriod {
11
20
  end: string;
@@ -14,7 +23,9 @@ interface CustomTimePeriod {
14
23
 
15
24
  interface UseMetricsQueryProps {
16
25
  baseEndpoint: string;
17
- metrics: Array<string>;
26
+ bypassMetricsExclusion?: boolean;
27
+ includeAllResources?: boolean;
28
+ metrics: Array<Metric>;
18
29
  refreshCount?: number;
19
30
  refreshInterval?: number | false;
20
31
  resources?: Array<Resource>;
@@ -71,6 +82,7 @@ const areResourcesFullfilled = (value: Array<Resource>): boolean =>
71
82
  );
72
83
 
73
84
  const useGraphQuery = ({
85
+ bypassMetricsExclusion,
74
86
  metrics,
75
87
  resources = [],
76
88
  baseEndpoint,
@@ -93,7 +105,7 @@ const useGraphQuery = ({
93
105
 
94
106
  const definedMetrics = metrics.filter((metric) => metric);
95
107
  const formattedDefinedMetrics = definedMetrics.map((metric) =>
96
- encodeURIComponent(metric)
108
+ encodeURIComponent(metric.name)
97
109
  );
98
110
 
99
111
  const {
@@ -145,7 +157,16 @@ const useGraphQuery = ({
145
157
  base: data.current.base,
146
158
  title: ''
147
159
  },
148
- metrics: data.current.metrics,
160
+ metrics: bypassMetricsExclusion
161
+ ? data.current.metrics
162
+ : data.current.metrics.filter(({ metric_id }) => {
163
+ return pipe(
164
+ pluck('excludedMetrics'),
165
+ flatten,
166
+ includes(metric_id),
167
+ not
168
+ )(metrics);
169
+ }),
149
170
  times: data.current.times
150
171
  }
151
172
  : undefined;
@@ -13,3 +13,8 @@ export enum WidgetResourceType {
13
13
  serviceCategory = 'service-category',
14
14
  serviceGroup = 'service-group'
15
15
  }
16
+
17
+ export interface Metric {
18
+ excludedMetrics: Array<number>;
19
+ name: string;
20
+ }
@@ -0,0 +1,76 @@
1
+ import { CollapsibleItem, Props } from './CollapsibleItem';
2
+
3
+ const title = 'Title';
4
+
5
+ const customizedTitle = <div>Customized title</div>;
6
+
7
+ const initialize = (props: Omit<Props, 'children'>): void => {
8
+ cy.mount({
9
+ Component: <CollapsibleItem {...props}>Content</CollapsibleItem>
10
+ });
11
+ };
12
+
13
+ describe('CollapsibleItem', () => {
14
+ it('displays the component collapsed by default', () => {
15
+ initialize({ title });
16
+
17
+ cy.contains(title).should('be.visible');
18
+ cy.contains('Content').should('not.be.visible');
19
+ cy.get('div[aria-expanded="false"]').should('exist');
20
+
21
+ cy.makeSnapshot();
22
+ });
23
+
24
+ it('displays the component expanded when the corresponding prop is set to true', () => {
25
+ initialize({ defaultExpanded: true, title });
26
+
27
+ cy.contains(title).should('be.visible');
28
+ cy.contains('Content').should('be.visible');
29
+ cy.get('div[aria-expanded="true"]').should('exist');
30
+
31
+ cy.makeSnapshot();
32
+ });
33
+
34
+ it('displays a customized title', () => {
35
+ initialize({ title: customizedTitle });
36
+
37
+ cy.contains('Customized title').should('be.visible');
38
+ cy.get('div[aria-expanded="false"]').should('exist');
39
+
40
+ cy.makeSnapshot();
41
+ });
42
+
43
+ it('displays the component as compact', () => {
44
+ initialize({ compact: true, title });
45
+
46
+ cy.contains(title).should('be.visible');
47
+ cy.get('div[aria-expanded="false"]').should('exist');
48
+ cy.get('div[data-compact="true"]').should('exist');
49
+
50
+ cy.makeSnapshot();
51
+ });
52
+
53
+ it('displays the component as compact and expanded when the icon is clicked', () => {
54
+ initialize({ compact: true, title });
55
+
56
+ cy.contains(title).should('be.visible');
57
+ cy.get('div[aria-expanded="false"]').should('exist');
58
+
59
+ cy.get('div[aria-expanded="false"]').click();
60
+
61
+ cy.get('div[aria-expanded="true"]').should('exist');
62
+ cy.contains('Content').should('be.visible');
63
+
64
+ cy.makeSnapshot();
65
+ });
66
+
67
+ it('displays the component as compact and a customized title', () => {
68
+ initialize({ compact: true, title: customizedTitle });
69
+
70
+ cy.contains('Customized title').should('be.visible');
71
+ cy.get('div[aria-expanded="false"]').should('exist');
72
+ cy.get('div[data-compact="true"]').should('exist');
73
+
74
+ cy.makeSnapshot();
75
+ });
76
+ });
@@ -1,5 +1,7 @@
1
1
  import { Meta, StoryObj } from '@storybook/react';
2
2
 
3
+ import { Checkbox, Typography } from '@mui/material';
4
+
3
5
  import { CollapsibleItem } from './CollapsibleItem';
4
6
 
5
7
  const meta: Meta<typeof CollapsibleItem> = {
@@ -23,3 +25,27 @@ export const ExpandedByDefault: Story = {
23
25
  title: 'Title'
24
26
  }
25
27
  };
28
+
29
+ export const customizedTitle: Story = {
30
+ args: {
31
+ children: 'Label',
32
+ defaultExpanded: false,
33
+ title: <Typography>Title</Typography>
34
+ }
35
+ };
36
+
37
+ export const customizedTitleAndCompact: Story = {
38
+ args: {
39
+ children: 'Label',
40
+ compact: true,
41
+ defaultExpanded: false,
42
+ title: (
43
+ <div
44
+ style={{ alignItems: 'center', display: 'flex', flexDirection: 'row' }}
45
+ >
46
+ <Checkbox size="small" />
47
+ <Typography>Title compact</Typography>
48
+ </div>
49
+ )
50
+ }
51
+ };
@@ -1,5 +1,7 @@
1
1
  import { ReactNode } from 'react';
2
2
 
3
+ import { equals, type } from 'ramda';
4
+
3
5
  import {
4
6
  AccordionDetails,
5
7
  AccordionSummary,
@@ -10,36 +12,63 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
10
12
 
11
13
  import { useCollapsibleItemStyles } from './useCollapsibleItemStyles';
12
14
 
13
- interface Props {
15
+ export interface Props {
14
16
  children: ReactNode;
17
+ compact?: boolean;
18
+ dataTestId?: string;
15
19
  defaultExpanded?: boolean;
16
- title: string;
20
+ title: string | JSX.Element;
17
21
  }
18
22
 
19
23
  export const CollapsibleItem = ({
20
24
  title,
21
25
  children,
22
- defaultExpanded
26
+ defaultExpanded,
27
+ compact = false,
28
+ dataTestId = ''
23
29
  }: Props): JSX.Element => {
24
- const { classes } = useCollapsibleItemStyles();
30
+ const { classes, cx } = useCollapsibleItemStyles();
31
+
32
+ const isStringTitle = equals(type(title), 'String');
25
33
 
26
34
  return (
27
35
  <Accordion
28
36
  disableGutters
29
37
  className={classes.accordion}
38
+ data-compact={compact}
39
+ data-testid={`${dataTestId}-accordion`}
30
40
  defaultExpanded={defaultExpanded}
31
41
  >
32
- <AccordionSummary
33
- classes={{
34
- content: classes.accordionSummary
35
- }}
36
- expandIcon={<ExpandMoreIcon color="primary" />}
42
+ <div className={classes.summaryContainer}>
43
+ <div className={classes.customTitle}>{!isStringTitle && title}</div>
44
+ <AccordionSummary
45
+ classes={{
46
+ content: cx(
47
+ compact
48
+ ? classes.accordionSummaryCompactContent
49
+ : classes.accordionSummary
50
+ ),
51
+ root: cx(
52
+ compact
53
+ ? classes.accordionSummaryCompactRoot
54
+ : classes.accordionSummaryRoot
55
+ )
56
+ }}
57
+ data-testid={`${dataTestId}-summary`}
58
+ expandIcon={<ExpandMoreIcon color="primary" />}
59
+ >
60
+ {isStringTitle && (
61
+ <Typography color="primary" variant="h6">
62
+ {title}
63
+ </Typography>
64
+ )}
65
+ </AccordionSummary>
66
+ </div>
67
+ <AccordionDetails
68
+ className={cx(
69
+ compact ? classes.accordionDetailsCompact : classes.accordionDetails
70
+ )}
37
71
  >
38
- <Typography color="primary" variant="h6">
39
- {title}
40
- </Typography>
41
- </AccordionSummary>
42
- <AccordionDetails className={classes.accordionDetails}>
43
72
  {children}
44
73
  </AccordionDetails>
45
74
  </Accordion>
@@ -4,12 +4,35 @@ export const useCollapsibleItemStyles = makeStyles()((theme) => ({
4
4
  accordion: {
5
5
  backgroundColor: 'transparent',
6
6
  border: 'none',
7
- borderBottom: `1px solid ${theme.palette.divider}`
7
+ borderBottom: `1px solid ${theme.palette.divider}`,
8
+ width: '100%'
8
9
  },
9
10
  accordionDetails: {
10
11
  padding: theme.spacing(0, 2, 2)
11
12
  },
13
+ accordionDetailsCompact: {
14
+ padding: theme.spacing(0)
15
+ },
12
16
  accordionSummary: {
13
17
  margin: theme.spacing(1.5, 0)
18
+ },
19
+ accordionSummaryCompactContent: {
20
+ margin: theme.spacing(0, 0, 0.5)
21
+ },
22
+ accordionSummaryCompactRoot: {
23
+ minHeight: theme.spacing(1),
24
+ width: '100%'
25
+ },
26
+ accordionSummaryRoot: {
27
+ width: '100%'
28
+ },
29
+ customTitle: {
30
+ whiteSpace: 'nowrap'
31
+ },
32
+ summaryContainer: {
33
+ alignItems: 'center',
34
+ display: 'flex',
35
+ flexDirection: 'row',
36
+ width: '100%'
14
37
  }
15
38
  }));
@@ -7,7 +7,7 @@ import { IconButton } from '..';
7
7
  import { useItemStyles } from './ItemComposition.styles';
8
8
 
9
9
  type Props = {
10
- children: Array<ReactElement>;
10
+ children: ReactElement | Array<ReactElement>;
11
11
  className?: string;
12
12
  deleteButtonHidden?: boolean;
13
13
  labelDelete: string;
@@ -0,0 +1,116 @@
1
+ import { ItemComposition } from '.';
2
+
3
+ interface Props {
4
+ addButtonHidden?: boolean;
5
+ addbuttonDisabled?: boolean;
6
+ deleteButtonHidden?: boolean;
7
+ secondaryLabel?: string;
8
+ }
9
+
10
+ const initialize = ({
11
+ addButtonHidden,
12
+ addbuttonDisabled,
13
+ secondaryLabel,
14
+ deleteButtonHidden
15
+ }: Props): unknown => {
16
+ const addItem = cy.stub();
17
+ const deleteItem = cy.stub();
18
+
19
+ cy.mount({
20
+ Component: (
21
+ <ItemComposition
22
+ addButtonHidden={addButtonHidden}
23
+ addbuttonDisabled={addbuttonDisabled}
24
+ labelAdd="Add"
25
+ secondaryLabel={secondaryLabel}
26
+ onAddItem={addItem}
27
+ >
28
+ <ItemComposition.Item
29
+ deleteButtonHidden={deleteButtonHidden}
30
+ labelDelete="Delete"
31
+ onDeleteItem={deleteItem}
32
+ >
33
+ <div>Item 1</div>
34
+ </ItemComposition.Item>
35
+ <ItemComposition.Item
36
+ deleteButtonHidden={deleteButtonHidden}
37
+ labelDelete="Delete"
38
+ onDeleteItem={deleteItem}
39
+ >
40
+ <div>Item 2</div>
41
+ </ItemComposition.Item>
42
+ </ItemComposition>
43
+ )
44
+ });
45
+
46
+ return {
47
+ addItem,
48
+ deleteItem
49
+ };
50
+ };
51
+
52
+ describe('ItemComposition', () => {
53
+ it('displays the component', () => {
54
+ initialize({});
55
+
56
+ cy.contains('Item 1').should('be.visible');
57
+ cy.contains('Item 2').should('be.visible');
58
+ cy.findAllByTestId('Delete').should('have.length', 2);
59
+ cy.findByTestId('Add').should('be.enabled');
60
+
61
+ cy.makeSnapshot();
62
+ });
63
+
64
+ it('displays the component with a secondary label', () => {
65
+ initialize({ secondaryLabel: 'Secondary label' });
66
+
67
+ cy.contains('Secondary label').should('be.visible');
68
+
69
+ cy.makeSnapshot();
70
+ });
71
+
72
+ it('displays add button as hidden when the prop is set to true', () => {
73
+ initialize({ addbuttonDisabled: true });
74
+
75
+ cy.findByTestId('Add').should('be.disabled');
76
+
77
+ cy.makeSnapshot();
78
+ });
79
+
80
+ it('does not display the add button when the prop is set to true', () => {
81
+ initialize({ addButtonHidden: true });
82
+
83
+ cy.findByTestId('Add').should('not.exist');
84
+
85
+ cy.makeSnapshot();
86
+ });
87
+
88
+ it('does not display the delete button when the prop is set to true', () => {
89
+ initialize({ deleteButtonHidden: true });
90
+
91
+ cy.findByTestId('Delete').should('not.exist');
92
+
93
+ cy.makeSnapshot();
94
+ });
95
+
96
+ it('calls the add function when the button is clicked', () => {
97
+ const { addItem } = initialize({});
98
+
99
+ cy.findByTestId('Add')
100
+ .click()
101
+ .then(() => {
102
+ expect(addItem).to.have.been.calledWith();
103
+ });
104
+ });
105
+
106
+ it('calls the delete function when the button is clicked', () => {
107
+ const { deleteItem } = initialize({});
108
+
109
+ cy.findAllByTestId('Delete')
110
+ .eq(1)
111
+ .click()
112
+ .then(() => {
113
+ expect(deleteItem).to.have.been.calledWith();
114
+ });
115
+ });
116
+ });
@@ -1,6 +1,13 @@
1
1
  import { makeStyles } from 'tss-react/mui';
2
2
 
3
3
  export const useItemCompositionStyles = makeStyles()((theme) => ({
4
+ buttonAndSecondaryLabel: {
5
+ alignItems: 'center',
6
+ display: 'flex',
7
+ flexDirection: 'row',
8
+ justifyContent: 'space-between',
9
+ width: '100%'
10
+ },
4
11
  itemCompositionContainer: {
5
12
  alignItems: 'flex-start',
6
13
  display: 'flex',
@@ -24,6 +31,6 @@ export const useItemStyles = makeStyles()((theme) => ({
24
31
  width: '100%'
25
32
  },
26
33
  visibilityHiden: {
27
- visibility: 'hidden'
34
+ display: 'none'
28
35
  }
29
36
  }));
@@ -1,18 +1,20 @@
1
1
  import { ReactElement } from 'react';
2
2
 
3
3
  import AddIcon from '@mui/icons-material/Add';
4
+ import { Typography } from '@mui/material';
4
5
 
5
6
  import { Button } from '..';
6
7
 
7
8
  import { useItemCompositionStyles } from './ItemComposition.styles';
8
9
 
9
- type Props = {
10
+ export type Props = {
10
11
  IconAdd?;
11
12
  addButtonHidden?: boolean;
12
13
  addbuttonDisabled?: boolean;
13
14
  children: Array<ReactElement>;
14
15
  labelAdd: string;
15
16
  onAddItem: () => void;
17
+ secondaryLabel?: string;
16
18
  };
17
19
 
18
20
  export const ItemComposition = ({
@@ -21,27 +23,35 @@ export const ItemComposition = ({
21
23
  labelAdd,
22
24
  addbuttonDisabled,
23
25
  addButtonHidden,
24
- IconAdd
26
+ IconAdd,
27
+ secondaryLabel
25
28
  }: Props): JSX.Element => {
26
29
  const { classes } = useItemCompositionStyles();
27
30
 
28
31
  return (
29
32
  <div className={classes.itemCompositionContainer}>
30
33
  {children}
31
- {!addButtonHidden && (
32
- <Button
33
- aria-label={labelAdd}
34
- data-testid={labelAdd}
35
- disabled={addbuttonDisabled}
36
- icon={IconAdd || <AddIcon />}
37
- iconVariant="start"
38
- size="small"
39
- variant="ghost"
40
- onClick={onAddItem}
41
- >
42
- {labelAdd}
43
- </Button>
44
- )}
34
+ <div className={classes.buttonAndSecondaryLabel}>
35
+ {!addButtonHidden && (
36
+ <Button
37
+ aria-label={labelAdd}
38
+ data-testid={labelAdd}
39
+ disabled={addbuttonDisabled}
40
+ icon={IconAdd || <AddIcon />}
41
+ iconVariant="start"
42
+ size="small"
43
+ variant="ghost"
44
+ onClick={onAddItem}
45
+ >
46
+ {labelAdd}
47
+ </Button>
48
+ )}
49
+ {secondaryLabel && (
50
+ <Typography sx={{ color: 'text.secondary' }}>
51
+ {secondaryLabel}
52
+ </Typography>
53
+ )}
54
+ </div>
45
55
  </div>
46
56
  );
47
57
  };