@centreon/ui 23.10.53 → 23.10.55

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": "23.10.53",
3
+ "version": "23.10.55",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "eslint": "eslint ./src --ext .js,.jsx,.ts,.tsx --max-warnings 0",
@@ -135,6 +135,10 @@ const ContentEditable = ({
135
135
  const isEditable = editor.isEditable();
136
136
 
137
137
  useEffect(() => {
138
+ if (isNil(disabled)) {
139
+ return;
140
+ }
141
+
138
142
  editor.setEditable(!disabled);
139
143
  }, [disabled]);
140
144
 
@@ -0,0 +1,23 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import Avatar from './Avatar';
4
+
5
+ const meta: Meta<typeof Avatar> = {
6
+ component: Avatar
7
+ };
8
+
9
+ export default meta;
10
+ type Story = StoryObj<typeof Avatar>;
11
+
12
+ export const Default: Story = {
13
+ args: {
14
+ children: 'Label'
15
+ }
16
+ };
17
+
18
+ export const compact: Story = {
19
+ args: {
20
+ ...Default.args,
21
+ compact: true
22
+ }
23
+ };
@@ -0,0 +1,11 @@
1
+ import { makeStyles } from 'tss-react/mui';
2
+
3
+ export const useAvatarStyles = makeStyles()((theme) => ({
4
+ avatar: {
5
+ '&[data-compact="true"]': {
6
+ fontSize: theme.typography.body1.fontSize,
7
+ height: theme.spacing(2),
8
+ width: theme.spacing(2)
9
+ }
10
+ }
11
+ }));
@@ -0,0 +1,32 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ import { AvatarProps, Avatar as MUIAvatar } from '@mui/material';
4
+
5
+ import { useAvatarStyles } from './Avatar.styles';
6
+
7
+ interface Props extends AvatarProps {
8
+ children: ReactNode;
9
+ className?: string;
10
+ compact?: boolean;
11
+ }
12
+
13
+ const Avatar = ({
14
+ compact = false,
15
+ children,
16
+ className,
17
+ ...attr
18
+ }: Props): JSX.Element => {
19
+ const { classes, cx } = useAvatarStyles();
20
+
21
+ return (
22
+ <MUIAvatar
23
+ {...attr}
24
+ className={cx(classes.avatar, className)}
25
+ data-compact={`${compact}`}
26
+ >
27
+ {children}
28
+ </MUIAvatar>
29
+ );
30
+ };
31
+
32
+ export default Avatar;
@@ -0,0 +1 @@
1
+ export { default as Avatar } from './Avatar';
@@ -0,0 +1,25 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { CollapsibleItem } from './CollapsibleItem';
4
+
5
+ const meta: Meta<typeof CollapsibleItem> = {
6
+ component: CollapsibleItem
7
+ };
8
+
9
+ export default meta;
10
+ type Story = StoryObj<typeof CollapsibleItem>;
11
+
12
+ export const Default: Story = {
13
+ args: {
14
+ children: 'Label',
15
+ title: 'Title'
16
+ }
17
+ };
18
+
19
+ export const ExpandedByDefault: Story = {
20
+ args: {
21
+ children: 'Label',
22
+ defaultExpanded: true,
23
+ title: 'Title'
24
+ }
25
+ };
@@ -0,0 +1,47 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ import {
4
+ AccordionDetails,
5
+ AccordionSummary,
6
+ Accordion,
7
+ Typography
8
+ } from '@mui/material';
9
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
10
+
11
+ import { useCollapsibleItemStyles } from './useCollapsibleItemStyles';
12
+
13
+ interface Props {
14
+ children: ReactNode;
15
+ defaultExpanded?: boolean;
16
+ title: string;
17
+ }
18
+
19
+ export const CollapsibleItem = ({
20
+ title,
21
+ children,
22
+ defaultExpanded
23
+ }: Props): JSX.Element => {
24
+ const { classes } = useCollapsibleItemStyles();
25
+
26
+ return (
27
+ <Accordion
28
+ disableGutters
29
+ className={classes.accordion}
30
+ defaultExpanded={defaultExpanded}
31
+ >
32
+ <AccordionSummary
33
+ classes={{
34
+ content: classes.accordionSummary
35
+ }}
36
+ expandIcon={<ExpandMoreIcon color="primary" />}
37
+ >
38
+ <Typography color="primary" variant="h6">
39
+ {title}
40
+ </Typography>
41
+ </AccordionSummary>
42
+ <AccordionDetails className={classes.accordionDetails}>
43
+ {children}
44
+ </AccordionDetails>
45
+ </Accordion>
46
+ );
47
+ };
@@ -0,0 +1 @@
1
+ export { CollapsibleItem } from './CollapsibleItem';
@@ -0,0 +1,14 @@
1
+ import { makeStyles } from 'tss-react/mui';
2
+
3
+ export const useCollapsibleItemStyles = makeStyles()((theme) => ({
4
+ accordion: {
5
+ border: 'none',
6
+ borderBottom: `1px solid ${theme.palette.divider}`
7
+ },
8
+ accordionDetails: {
9
+ padding: theme.spacing(0, 2, 2)
10
+ },
11
+ accordionSummary: {
12
+ margin: theme.spacing(1.5, 0)
13
+ }
14
+ }));
@@ -9,6 +9,7 @@ import { useItemStyles } from './ItemComposition.styles';
9
9
  type Props = {
10
10
  children: Array<ReactElement>;
11
11
  className?: string;
12
+ deleteButtonHidden?: boolean;
12
13
  labelDelete: string;
13
14
  onDeleteItem: () => void;
14
15
  };
@@ -17,21 +18,24 @@ export const Item = ({
17
18
  onDeleteItem,
18
19
  children,
19
20
  labelDelete,
20
- className
21
+ className,
22
+ deleteButtonHidden
21
23
  }: Props): JSX.Element => {
22
24
  const { classes, cx } = useItemStyles();
23
25
 
24
26
  return (
25
27
  <div className={classes.itemContainer}>
26
28
  <div className={cx(classes.itemContent, className)}>{children}</div>
27
- <IconButton
28
- aria-label={labelDelete}
29
- data-testid={labelDelete}
30
- icon={<CloseIcon />}
31
- size="small"
32
- variant="ghost"
33
- onClick={onDeleteItem}
34
- />
29
+ {!deleteButtonHidden && (
30
+ <IconButton
31
+ aria-label={labelDelete}
32
+ data-testid={labelDelete}
33
+ icon={<CloseIcon />}
34
+ size="small"
35
+ variant="ghost"
36
+ onClick={onDeleteItem}
37
+ />
38
+ )}
35
39
  </div>
36
40
  );
37
41
  };
@@ -7,6 +7,7 @@ import { Button } from '..';
7
7
  import { useItemCompositionStyles } from './ItemComposition.styles';
8
8
 
9
9
  type Props = {
10
+ addButtonHidden?: boolean;
10
11
  addbuttonDisabled?: boolean;
11
12
  children: Array<ReactElement>;
12
13
  labelAdd: string;
@@ -17,25 +18,28 @@ export const ItemComposition = ({
17
18
  onAddItem,
18
19
  children,
19
20
  labelAdd,
20
- addbuttonDisabled
21
+ addbuttonDisabled,
22
+ addButtonHidden
21
23
  }: Props): JSX.Element => {
22
24
  const { classes } = useItemCompositionStyles();
23
25
 
24
26
  return (
25
27
  <div className={classes.itemCompositionContainer}>
26
28
  {children}
27
- <Button
28
- aria-label={labelAdd}
29
- data-testid={labelAdd}
30
- disabled={addbuttonDisabled}
31
- icon={<AddIcon />}
32
- iconVariant="start"
33
- size="small"
34
- variant="ghost"
35
- onClick={onAddItem}
36
- >
37
- {labelAdd}
38
- </Button>
29
+ {!addButtonHidden && (
30
+ <Button
31
+ aria-label={labelAdd}
32
+ data-testid={labelAdd}
33
+ disabled={addbuttonDisabled}
34
+ icon={<AddIcon />}
35
+ iconVariant="start"
36
+ size="small"
37
+ variant="ghost"
38
+ onClick={onAddItem}
39
+ >
40
+ {labelAdd}
41
+ </Button>
42
+ )}
39
43
  </div>
40
44
  );
41
45
  };
@@ -10,7 +10,7 @@ const meta: Meta<typeof Modal> = {
10
10
  control: {
11
11
  type: 'select'
12
12
  },
13
- options: ['small', 'medium', 'large']
13
+ options: ['small', 'medium', 'large', 'xlarge', 'fullscreen']
14
14
  }
15
15
  },
16
16
  component: Modal
@@ -109,3 +109,34 @@ export const AsPassive: Story = {
109
109
  </Modal>
110
110
  )
111
111
  };
112
+
113
+ export const fullscreenWithCustomMargins: Story = {
114
+ args: {
115
+ fullscreenMargins: {
116
+ bottom: 20,
117
+ left: 40,
118
+ right: 10,
119
+ top: 90
120
+ },
121
+ hasCloseButton: true,
122
+ open: true,
123
+ size: 'fullscreen'
124
+ },
125
+ render: (args) => (
126
+ <Modal {...args}>
127
+ <Modal.Header>Modal title</Modal.Header>
128
+ <Modal.Body>
129
+ <p>
130
+ Occaecat consectetur amet officia magna. Eu sunt aute duis duis cillum
131
+ irure mollit ex aute excepteur eu id cillum.
132
+ </p>
133
+ </Modal.Body>
134
+ <Modal.Actions
135
+ labels={{
136
+ cancel: 'Cancel',
137
+ confirm: 'Confirm'
138
+ }}
139
+ />
140
+ </Modal>
141
+ )
142
+ };
@@ -1,11 +1,37 @@
1
1
  import { makeStyles } from 'tss-react/mui';
2
2
 
3
- const useStyles = makeStyles()((theme) => ({
3
+ const useStyles = makeStyles<{
4
+ bottom?: number;
5
+ left?: number;
6
+ right?: number;
7
+ top?: number;
8
+ }>()((theme, props) => ({
4
9
  modal: {
5
10
  '& .MuiDialog-paper': {
6
11
  gap: theme.spacing(2),
7
12
  padding: theme.spacing(2)
8
13
  },
14
+ '&[data-size="fullscreen"]': {
15
+ zIndex: 0
16
+ },
17
+ '&[data-size="fullscreen"] .MuiBackdrop-root': {
18
+ display: 'none'
19
+ },
20
+ '&[data-size="fullscreen"] .MuiDialog-container': {
21
+ alignItems: 'flex-end',
22
+ height: '100vh',
23
+ justifyContent: 'flex-end'
24
+ },
25
+ '&[data-size="fullscreen"] .MuiDialog-paper': {
26
+ bottom: props?.bottom ?? 0,
27
+ left: props?.left ?? 0,
28
+ margin: 0,
29
+ maxHeight: 'unset',
30
+ maxWidth: 'unset',
31
+ position: 'absolute',
32
+ right: props?.right ?? 0,
33
+ top: props?.top ?? 0
34
+ },
9
35
  '&[data-size="large"] .MuiDialog-paper': {
10
36
  maxWidth: '640px',
11
37
  width: '640px'
@@ -35,7 +61,8 @@ const useStyles = makeStyles()((theme) => ({
35
61
  color: theme.palette.text.secondary,
36
62
  margin: theme.spacing(1, 0, 1, 0),
37
63
  width: '90%'
38
- }
64
+ },
65
+ height: '100%'
39
66
  },
40
67
  modalCloseButton: {
41
68
  position: 'absolute',
@@ -1,6 +1,8 @@
1
1
  import React, { ReactElement } from 'react';
2
2
 
3
- import { Dialog as MuiDialog } from '@mui/material';
3
+ import { equals } from 'ramda';
4
+
5
+ import { Dialog as MuiDialog, Slide } from '@mui/material';
4
6
  import { Close as CloseIcon } from '@mui/icons-material';
5
7
 
6
8
  import { AriaLabelingAttributes } from '../../@types/aria-attributes';
@@ -10,13 +12,19 @@ import { useStyles } from './Modal.styles';
10
12
 
11
13
  export type ModalProps = {
12
14
  children: React.ReactNode;
15
+ fullscreenMargins?: {
16
+ bottom?: number;
17
+ left?: number;
18
+ right?: number;
19
+ top?: number;
20
+ };
13
21
  hasCloseButton?: boolean;
14
22
  onClose?: (
15
23
  event: object,
16
24
  reason: 'escapeKeyDown' | 'backdropClick' | 'closeButton'
17
25
  ) => void;
18
26
  open: boolean;
19
- size?: 'small' | 'medium' | 'large' | 'xlarge';
27
+ size?: 'small' | 'medium' | 'large' | 'xlarge' | 'fullscreen';
20
28
  } & AriaLabelingAttributes;
21
29
 
22
30
  /** *
@@ -28,12 +36,24 @@ const Modal = ({
28
36
  onClose,
29
37
  open,
30
38
  size = 'small',
39
+ fullscreenMargins = {
40
+ bottom: 0,
41
+ left: 0,
42
+ right: 0,
43
+ top: 0
44
+ },
31
45
  ...attr
32
46
  }: ModalProps): ReactElement => {
33
- const { classes } = useStyles();
47
+ const { classes } = useStyles(fullscreenMargins);
48
+
49
+ const isFullscreen = equals(size, 'fullscreen');
34
50
 
35
51
  return (
36
52
  <MuiDialog
53
+ TransitionComponent={isFullscreen ? Slide : undefined}
54
+ TransitionProps={{
55
+ direction: 'up'
56
+ }}
37
57
  className={classes.modal}
38
58
  data-size={size}
39
59
  open={open}
@@ -8,3 +8,5 @@ export * from './Tooltip';
8
8
  export * from './Menu';
9
9
  export * from './List';
10
10
  export * from './ItemComposition';
11
+ export * from './Avatar';
12
+ export * from './CollapsibleItem';
@@ -17,3 +17,4 @@ export * from './panelSize';
17
17
  export * from './sanitizedHTML';
18
18
  export * from './docsURL';
19
19
  export * from './useInfiniteScrollListing';
20
+ export * from './useLicenseExpirationWarning';
@@ -0,0 +1,2 @@
1
+ export const labelLicenseWarning = (module, daysUntilExpiration): string =>
2
+ `The ${module} license will expire in ${daysUntilExpiration} days`;
@@ -0,0 +1,137 @@
1
+ import dayjs from 'dayjs';
2
+
3
+ import { TestQueryProvider, Method, SnackbarProvider } from '@centreon/ui';
4
+
5
+ import { labelLicenseWarning } from './translatedLabel';
6
+
7
+ import { useLicenseExpirationWarning } from '.';
8
+
9
+ const getMockedResponse = (expirationDate): object => ({
10
+ result: {
11
+ module: {
12
+ entities: [
13
+ {
14
+ description: 'Centreon Auto Discovery',
15
+ id: 'centreon-autodiscovery-server',
16
+ label: 'Centreon',
17
+ license: {
18
+ expiration_date: expirationDate,
19
+ host_limit: -1,
20
+ host_usage: 1,
21
+ is_valid: true,
22
+ required: true
23
+ },
24
+ type: 'module',
25
+ version: {
26
+ available: '23.10.0',
27
+ current: '23.10.0',
28
+ installed: true,
29
+ outdated: false
30
+ }
31
+ },
32
+ {
33
+ description: 'Centreon License Manager',
34
+ id: 'centreon-license-manager',
35
+ label: 'Centreon',
36
+ license: {
37
+ required: false
38
+ },
39
+ type: 'module',
40
+ version: {
41
+ available: '23.10.0',
42
+ current: '23.10.0',
43
+ installed: true,
44
+ outdated: false
45
+ }
46
+ },
47
+ {
48
+ description: 'Centreon Monitoring Connectors Manager',
49
+ id: 'centreon-pp-manager',
50
+ label: 'Centreon',
51
+ license: {
52
+ required: false
53
+ },
54
+ type: 'module',
55
+ version: {
56
+ available: '23.10.0',
57
+ current: '23.10.0',
58
+ installed: true,
59
+ outdated: false
60
+ }
61
+ }
62
+ ],
63
+ pagination: {
64
+ limit: 3,
65
+ offset: 0,
66
+ total: 3
67
+ }
68
+ },
69
+ widget: {
70
+ entities: [],
71
+ pagination: {
72
+ limit: 0,
73
+ offset: 0,
74
+ total: 0
75
+ }
76
+ }
77
+ },
78
+ status: true
79
+ });
80
+
81
+ const mockRequest = ({ expirationDate }: { expirationDate }): void => {
82
+ cy.interceptAPIRequest({
83
+ alias: 'getLicenseInformations',
84
+ method: Method.GET,
85
+ path: '**internal.php?object=centreon_module&action=list',
86
+ response: getMockedResponse(expirationDate)
87
+ });
88
+ };
89
+
90
+ const TestComponent = (): JSX.Element => {
91
+ useLicenseExpirationWarning({
92
+ module: 'centreon-autodiscovery-server'
93
+ });
94
+
95
+ return <div />;
96
+ };
97
+
98
+ const TestWithQueryProvider = (): JSX.Element => {
99
+ return (
100
+ <TestQueryProvider>
101
+ <SnackbarProvider>
102
+ <TestComponent />
103
+ </SnackbarProvider>
104
+ </TestQueryProvider>
105
+ );
106
+ };
107
+
108
+ const initialize = (): void => {
109
+ cy.viewport('macbook-13');
110
+
111
+ cy.mount({
112
+ Component: <TestWithQueryProvider />
113
+ });
114
+ };
115
+
116
+ const currentDate = dayjs();
117
+
118
+ describe('License', () => {
119
+ beforeEach(initialize);
120
+
121
+ it('does not display any warning message when the license expires in more than 15 days from the current date', () => {
122
+ mockRequest({ expirationDate: currentDate.add(20, 'day') });
123
+
124
+ cy.waitForRequest('@getLicenseInformations');
125
+
126
+ cy.findByText(
127
+ labelLicenseWarning('centreon-autodiscovery-server', 20)
128
+ ).should('not.exist');
129
+ });
130
+ it('displays a warning message when the license expires within 15 days', () => {
131
+ mockRequest({ expirationDate: currentDate.add(10.5, 'day') });
132
+
133
+ cy.findByText(labelLicenseWarning('centreon-autodiscovery-server', 10));
134
+
135
+ cy.matchImageSnapshot();
136
+ });
137
+ });
@@ -0,0 +1,52 @@
1
+ import { useEffect } from 'react';
2
+
3
+ import dayjs from 'dayjs';
4
+ import { path, pipe, propEq, find, lt, isNil } from 'ramda';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { useFetchQuery, useSnackbar } from '..';
8
+
9
+ import { labelLicenseWarning } from './translatedLabel';
10
+
11
+ const legacyBaseEndpoint = './api/internal.php';
12
+ const extensionsEndpoint = `${legacyBaseEndpoint}?object=centreon_module&action=list`;
13
+
14
+ interface Props {
15
+ module: string;
16
+ }
17
+
18
+ export const useLicenseExpirationWarning = ({ module }: Props): void => {
19
+ const { t } = useTranslation();
20
+ const { showWarningMessage } = useSnackbar();
21
+
22
+ const { fetchQuery } = useFetchQuery({
23
+ getEndpoint: () => extensionsEndpoint,
24
+ getQueryKey: () => [module]
25
+ });
26
+
27
+ const currentDate = dayjs();
28
+
29
+ const getExpirationDate = pipe(
30
+ path(['result', 'module', 'entities']),
31
+ find(propEq('id', module)),
32
+ path(['license', 'expiration_date'])
33
+ ) as (data) => string;
34
+
35
+ useEffect(() => {
36
+ fetchQuery().then((response) => {
37
+ const expirationDate = getExpirationDate(response);
38
+ if (isNil(expirationDate)) {
39
+ return;
40
+ }
41
+
42
+ const daysUntilExpiration = dayjs(expirationDate).diff(
43
+ currentDate,
44
+ 'day'
45
+ );
46
+
47
+ if (lt(daysUntilExpiration, 15)) {
48
+ showWarningMessage(t(labelLicenseWarning(module, daysUntilExpiration)));
49
+ }
50
+ });
51
+ }, []);
52
+ };