@centreon/ui 23.10.91 → 23.10.93

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.91",
3
+ "version": "23.10.93",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "eslint": "eslint ./src --ext .js,.jsx,.ts,.tsx --max-warnings 0",
@@ -0,0 +1,116 @@
1
+ import { makeStyles } from 'tss-react/mui';
2
+ import { equals } from 'ramda';
3
+
4
+ import { Theme, alpha } from '@mui/material';
5
+
6
+ interface GetBackgroundAndColorProps {
7
+ status: string;
8
+ theme: Theme;
9
+ }
10
+
11
+ const getColor = ({ theme, status }: GetBackgroundAndColorProps): string =>
12
+ equals(theme.palette.mode, 'dark')
13
+ ? theme.palette.common.white
14
+ : theme.palette[status].main;
15
+
16
+ export const useStyles = makeStyles()((theme) => ({
17
+ item: {
18
+ '& .MuiListItemText-secondary': {
19
+ textWrap: 'pretty'
20
+ },
21
+ '&[data-variant="error"]': {
22
+ '& .MuiListItemIcon-root': {
23
+ color: getColor({ status: 'error', theme })
24
+ },
25
+ '&:hover': {
26
+ background: alpha(
27
+ theme.palette.error.main,
28
+ theme.palette.action.focusOpacity
29
+ ),
30
+ color: getColor({ status: 'error', theme })
31
+ },
32
+ color: theme.palette.error.main
33
+ },
34
+ '&[data-variant="info"]': {
35
+ '& .MuiListItemIcon-root': {
36
+ color: getColor({ status: 'info', theme })
37
+ },
38
+ '&:hover': {
39
+ background: alpha(
40
+ theme.palette.info.main,
41
+ theme.palette.action.focusOpacity
42
+ ),
43
+ color: getColor({ status: 'info', theme })
44
+ },
45
+ color: theme.palette.info.main
46
+ },
47
+ '&[data-variant="pending"]': {
48
+ '& .MuiListItemIcon-root': {
49
+ color: getColor({ status: 'pending', theme })
50
+ },
51
+ '&:hover': {
52
+ background: alpha(
53
+ theme.palette.pending.main,
54
+ theme.palette.action.focusOpacity
55
+ ),
56
+ color: getColor({ status: 'pending', theme })
57
+ },
58
+ color: theme.palette.pending.main
59
+ },
60
+ '&[data-variant="primary"]': {
61
+ '& .MuiListItemIcon-root': {
62
+ color: getColor({ status: 'primary', theme })
63
+ },
64
+ '&:hover': {
65
+ background: alpha(
66
+ theme.palette.primary.main,
67
+ theme.palette.action.focusOpacity
68
+ ),
69
+ color: getColor({ status: 'primary', theme })
70
+ },
71
+ color: theme.palette.primary.main
72
+ },
73
+ '&[data-variant="secondary"]': {
74
+ '& .MuiListItemIcon-root': {
75
+ color: getColor({ status: 'secondary', theme })
76
+ },
77
+ '&:hover': {
78
+ background: alpha(
79
+ theme.palette.secondary.main,
80
+ theme.palette.action.focusOpacity
81
+ ),
82
+ color: getColor({ status: 'secondary', theme })
83
+ },
84
+ color: theme.palette.secondary.main
85
+ },
86
+ '&[data-variant="success"]': {
87
+ '& .MuiListItemIcon-root': {
88
+ color: getColor({ status: 'success', theme })
89
+ },
90
+ '&:hover': {
91
+ background: alpha(
92
+ theme.palette.success.main,
93
+ theme.palette.action.focusOpacity
94
+ ),
95
+ color: getColor({ status: 'success', theme })
96
+ },
97
+ color: theme.palette.success.main
98
+ },
99
+ '&[data-variant="warning"]': {
100
+ '& .MuiListItemIcon-root': {
101
+ color: getColor({ status: 'warning', theme })
102
+ },
103
+ '&:hover': {
104
+ background: alpha(
105
+ theme.palette.warning.main,
106
+ theme.palette.action.focusOpacity
107
+ ),
108
+ color: getColor({ status: 'warning', theme })
109
+ },
110
+ color: theme.palette.warning.main
111
+ }
112
+ },
113
+ list: {
114
+ width: '100%'
115
+ }
116
+ }));
@@ -24,6 +24,68 @@ const actions = [
24
24
  }
25
25
  ];
26
26
 
27
+ const actionsWithVariants = [
28
+ {
29
+ label: 'No variant',
30
+ onClick: (): void => undefined
31
+ },
32
+ {
33
+ label: 'Primary',
34
+ onClick: (): void => undefined,
35
+ variant: 'primary'
36
+ },
37
+ {
38
+ label: 'Secondary',
39
+ onClick: (): void => undefined,
40
+ variant: 'secondary'
41
+ },
42
+ {
43
+ label: 'Success',
44
+ onClick: (): void => undefined,
45
+ variant: 'success'
46
+ },
47
+ {
48
+ label: 'Warning',
49
+ onClick: (): void => undefined,
50
+ variant: 'warning'
51
+ },
52
+ {
53
+ label: 'Error',
54
+ onClick: (): void => undefined,
55
+ variant: 'error'
56
+ },
57
+ {
58
+ label: 'Info',
59
+ onClick: (): void => undefined,
60
+ variant: 'info'
61
+ },
62
+ {
63
+ label: 'Pending',
64
+ onClick: (): void => undefined,
65
+ variant: 'pending'
66
+ }
67
+ ];
68
+
69
+ const actionsWithSecondaryLabel = [
70
+ {
71
+ Icon: EditIcon,
72
+ label: 'Edit',
73
+ onClick: (): void => undefined
74
+ },
75
+ {
76
+ Icon: CopyIcon,
77
+ label: 'Duplicate',
78
+ onClick: (): void => undefined,
79
+ secondaryLabel:
80
+ 'This is a secondary label that the purpose is to give a small description about an action'
81
+ },
82
+ {
83
+ Icon: DeleteIcon,
84
+ label: 'Delete',
85
+ onClick: (): void => undefined
86
+ }
87
+ ];
88
+
27
89
  export default {
28
90
  argTypes: {},
29
91
  component: ActionsList,
@@ -39,3 +101,15 @@ export const Playground = Template.bind({});
39
101
  Playground.args = {
40
102
  actions
41
103
  };
104
+
105
+ export const Variants = Template.bind({});
106
+
107
+ Variants.args = {
108
+ actions: actionsWithVariants
109
+ };
110
+
111
+ export const SecondaryLabel = Template.bind({});
112
+
113
+ SecondaryLabel.args = {
114
+ actions: actionsWithSecondaryLabel
115
+ };
@@ -1,5 +1,4 @@
1
1
  /* eslint-disable react/no-array-index-key */
2
- import { makeStyles } from 'tss-react/mui';
3
2
  import { useTranslation } from 'react-i18next';
4
3
  import { equals } from 'ramda';
5
4
 
@@ -12,10 +11,15 @@ import {
12
11
  Divider
13
12
  } from '@mui/material';
14
13
 
14
+ import { useStyles } from './ActionsList.styles';
15
+ import { ActionVariants } from './models';
16
+
15
17
  interface ActionsType {
16
18
  Icon?: (props: SvgIconProps) => JSX.Element;
17
19
  label: string;
18
20
  onClick?: (e?) => void;
21
+ secondaryLabel?: string;
22
+ variant?: ActionVariants;
19
23
  }
20
24
 
21
25
  interface Props {
@@ -24,12 +28,6 @@ interface Props {
24
28
  listItemClassName?: string;
25
29
  }
26
30
 
27
- const useStyles = makeStyles()({
28
- list: {
29
- maxWidth: '100%'
30
- }
31
- });
32
-
33
31
  const ActionsList = ({
34
32
  className,
35
33
  listItemClassName,
@@ -45,18 +43,28 @@ const ActionsList = ({
45
43
  return <Divider key={`divider_${idx}`} />;
46
44
  }
47
45
 
48
- const { label, Icon, onClick } = action as ActionsType;
46
+ const { label, Icon, onClick, variant, secondaryLabel } =
47
+ action as ActionsType;
49
48
 
50
49
  return (
51
- <MenuItem aria-label={label} id={label} key={label} onClick={onClick}>
50
+ <MenuItem
51
+ aria-label={label}
52
+ className={classes.item}
53
+ data-variant={variant}
54
+ id={label}
55
+ key={label}
56
+ onClick={onClick}
57
+ >
52
58
  {Icon && (
53
59
  <ListItemIcon>
54
60
  <Icon fontSize="small" />
55
61
  </ListItemIcon>
56
62
  )}
57
- <ListItemText className={listItemClassName}>
58
- {t(label)}
59
- </ListItemText>
63
+ <ListItemText
64
+ className={listItemClassName}
65
+ primary={t(label)}
66
+ secondary={secondaryLabel && t(secondaryLabel)}
67
+ />
60
68
  </MenuItem>
61
69
  );
62
70
  })}
@@ -0,0 +1,8 @@
1
+ export type ActionVariants =
2
+ | 'primary'
3
+ | 'secondary'
4
+ | 'success'
5
+ | 'warning'
6
+ | 'error'
7
+ | 'info'
8
+ | 'pending';
@@ -12,7 +12,6 @@ import {
12
12
  export type LabelPlacement = 'bottom' | 'top' | 'end' | 'start' | undefined;
13
13
 
14
14
  interface StyleProps {
15
- disabled: boolean;
16
15
  hasIcon: boolean;
17
16
  labelPlacement: LabelPlacement;
18
17
  }
@@ -26,7 +25,13 @@ const getLabelSpacing = (labelPlacement, theme): string => {
26
25
  };
27
26
 
28
27
  const useStyles = makeStyles<StyleProps>()(
29
- (theme, { disabled, hasIcon, labelPlacement }) => ({
28
+ (theme, { hasIcon, labelPlacement }) => ({
29
+ checkbox: {
30
+ '&.Mui-checked': {
31
+ color: theme.palette.primary.main
32
+ },
33
+ color: theme.palette.primary.main
34
+ },
30
35
  container: hasIcon
31
36
  ? {
32
37
  alignItems: 'center',
@@ -40,13 +45,8 @@ const useStyles = makeStyles<StyleProps>()(
40
45
  fontSize: theme.spacing(10)
41
46
  },
42
47
  label: {
43
- color: disabled
44
- ? theme.palette.action.disabled
45
- : theme.palette.text.secondary,
46
- fontSize: theme.typography.body1.fontSize,
47
- fontWeight: equals(labelPlacement, 'top')
48
- ? theme.typography.fontWeightBold
49
- : theme.typography.fontWeightMedium,
48
+ fontSize: theme.typography.body2.fontSize,
49
+ fontWeight: theme.typography.fontWeightMedium,
50
50
  padding: getLabelSpacing(labelPlacement, theme)
51
51
  }
52
52
  })
@@ -74,7 +74,6 @@ const Checkbox = ({
74
74
  dataTestId
75
75
  }: Props): JSX.Element => {
76
76
  const { classes, cx } = useStyles({
77
- disabled,
78
77
  hasIcon: !!Icon,
79
78
  labelPlacement
80
79
  });
@@ -86,6 +85,7 @@ const Checkbox = ({
86
85
  control={
87
86
  <MuiCheckbox
88
87
  checked={checked}
88
+ className={classes.checkbox}
89
89
  color="primary"
90
90
  disabled={disabled}
91
91
  id={label}
@@ -67,21 +67,23 @@ const mountListing = (): void => {
67
67
 
68
68
  cy.mount({
69
69
  Component: (
70
- <Listing
71
- checkable
72
- columns={columnsWithSubItems}
73
- currentPage={1}
74
- limit={10}
75
- rows={listingWithSubItems}
76
- subItems={{
77
- canCheckSubItems: false,
78
- enable: true,
79
- labelCollapse: 'Collapse',
80
- labelExpand: 'Expand',
81
- rowProperty: 'subItems'
82
- }}
83
- totalRows={10}
84
- />
70
+ <div style={{ height: '100vh' }}>
71
+ <Listing
72
+ checkable
73
+ columns={columnsWithSubItems}
74
+ currentPage={1}
75
+ limit={10}
76
+ rows={listingWithSubItems}
77
+ subItems={{
78
+ canCheckSubItems: false,
79
+ enable: true,
80
+ labelCollapse: 'Collapse',
81
+ labelExpand: 'Expand',
82
+ rowProperty: 'subItems'
83
+ }}
84
+ totalRows={10}
85
+ />
86
+ </div>
85
87
  )
86
88
  });
87
89
  };
@@ -30,6 +30,11 @@ const useListingStyles = makeStyles<StylesProps>()(
30
30
  height: '100%',
31
31
  width: '100%'
32
32
  },
33
+ listingContainer: {
34
+ height: '100%',
35
+ overflow: 'hidden',
36
+ width: '100%'
37
+ },
33
38
  loadingIndicator: {
34
39
  height: loadingIndicatorHeight,
35
40
  width: '100%'
@@ -34,7 +34,8 @@ type Story = StoryObj<typeof Listing>;
34
34
 
35
35
  const useStyles = makeStyles()((theme) => ({
36
36
  listing: {
37
- backgroundColor: theme.palette.background.default
37
+ backgroundColor: theme.palette.background.default,
38
+ height: '100vh'
38
39
  }
39
40
  }));
40
41
 
@@ -35,12 +35,12 @@ import { Box, LinearProgress, Table, TableBody } from '@mui/material';
35
35
  import { ListingVariant } from '@centreon/ui-context';
36
36
 
37
37
  import { useKeyObserver, useMemoComponent } from '../utils';
38
+ import { ParentSize } from '..';
38
39
 
39
40
  import ListingActionBar from './ActionBar';
40
41
  import Cell from './Cell';
41
42
  import DataCell from './Cell/DataCell';
42
43
  import Checkbox from './Checkbox';
43
- import getCumulativeOffset from './getCumulativeOffset';
44
44
  import {
45
45
  Column,
46
46
  ColumnConfiguration,
@@ -51,9 +51,8 @@ import {
51
51
  } from './models';
52
52
  import ListingRow from './Row/Row';
53
53
  import { labelNoResultFound } from './translatedLabels';
54
- import useResizeObserver from './useResizeObserver';
55
54
  import useStyleTable from './useStyleTable';
56
- import { loadingIndicatorHeight, useListingStyles } from './Listing.styles';
55
+ import { useListingStyles } from './Listing.styles';
57
56
  import { EmptyResult } from './EmptyResult/EmptyResult';
58
57
  import { SkeletonLoader } from './Row/SkeletonLoaderRows';
59
58
  import { ListingHeader } from './Header';
@@ -194,7 +193,6 @@ const Listing = <TRow extends { id: RowId }>({
194
193
 
195
194
  const { t } = useTranslation();
196
195
 
197
- const [tableTopOffset, setTableTopOffset] = React.useState(0);
198
196
  const [hoveredRowId, setHoveredRowId] = React.useState<RowId | null>(null);
199
197
  const [shiftKeyDownRowPivot, setShiftKeyDownRowPivot] = React.useState<
200
198
  number | null
@@ -228,7 +226,7 @@ const Listing = <TRow extends { id: RowId }>({
228
226
  [rows, subItemsPivots, subItems]
229
227
  );
230
228
 
231
- const { classes, theme } = useListingStyles({
229
+ const { classes } = useListingStyles({
232
230
  dataStyle,
233
231
  getGridTemplateColumn,
234
232
  limit,
@@ -236,13 +234,6 @@ const Listing = <TRow extends { id: RowId }>({
236
234
  rows: rowsToDisplay
237
235
  });
238
236
 
239
- useResizeObserver({
240
- onResize: () => {
241
- setTableTopOffset(getCumulativeOffset(containerRef.current));
242
- },
243
- ref: containerRef
244
- });
245
-
246
237
  const { isShiftKeyDown } = useKeyObserver();
247
238
 
248
239
  const haveSameId = (row: TRow, rowToCompare: TRow): boolean =>
@@ -434,18 +425,6 @@ const Listing = <TRow extends { id: RowId }>({
434
425
 
435
426
  const emptyRows = limit - Math.min(limit, totalRows - currentPage * limit);
436
427
 
437
- const tableMaxHeight = (): string => {
438
- if (innerScrollDisabled) {
439
- return '100%';
440
- }
441
-
442
- return `calc(100vh - ${tableTopOffset}px - ${
443
- actionBarRef.current?.offsetHeight
444
- }px - ${
445
- dataStyle.header.height
446
- }px - ${loadingIndicatorHeight}px - ${theme.spacing(1)})`;
447
- };
448
-
449
428
  const changeLimit = (updatedLimit: string): void => {
450
429
  onLimitChange?.(Number(updatedLimit));
451
430
  };
@@ -480,7 +459,7 @@ const Listing = <TRow extends { id: RowId }>({
480
459
  );
481
460
 
482
461
  return (
483
- <>
462
+ <div className={classes.listingContainer}>
484
463
  {loading && rows.length > 0 && (
485
464
  <LinearProgress className={classes.loadingIndicator} />
486
465
  )}
@@ -516,142 +495,152 @@ const Listing = <TRow extends { id: RowId }>({
516
495
  onSelectColumns={onSelectColumns}
517
496
  />
518
497
  </div>
519
- <Box
520
- className={classes.tableWrapper}
521
- component="div"
522
- style={{
523
- maxHeight: tableMaxHeight()
498
+ <ParentSize
499
+ parentSizeStyles={{
500
+ height: '100%',
501
+ overflowY: 'auto',
502
+ width: '100%'
524
503
  }}
525
504
  >
526
- <Table
527
- stickyHeader
528
- className={classes.table}
529
- component="div"
530
- role={undefined}
531
- size="small"
532
- >
533
- <ListingHeader
534
- areColumnsEditable={areColumnsEditable}
535
- checkable={checkable}
536
- columnConfiguration={columnConfiguration}
537
- columns={columns}
538
- listingVariant={listingVariant}
539
- memoProps={headerMemoProps}
540
- predefinedRowsSelection={predefinedRowsSelection}
541
- rowCount={limit - emptyRows}
542
- selectedRowCount={selectedRows.length}
543
- sortField={sortField}
544
- sortOrder={sortOrder}
545
- onSelectAllClick={selectAllRows}
546
- onSelectColumns={onSelectColumns}
547
- onSelectRowsWithCondition={onSelectRowsWithCondition}
548
- onSort={onSort}
549
- />
550
-
551
- <TableBody
552
- className={classes.tableBody}
505
+ {({ height }) => (
506
+ <Box
507
+ className={classes.tableWrapper}
553
508
  component="div"
554
- onMouseLeave={clearHoveredRow}
509
+ style={{
510
+ height: innerScrollDisabled ? '100%' : `calc(${height}px - 4px)`
511
+ }}
555
512
  >
556
- {rowsToDisplay.map((row, index) => {
557
- const isRowSelected = isSelected(row);
558
- const isRowHovered = equals(hoveredRowId, getId(row));
559
- const isSubItem = allSubItemIds.includes(row.id);
560
-
561
- return (
562
- <ListingRow
563
- checkable={
564
- checkable && (!isSubItem || subItems.canCheckSubItems)
565
- }
566
- columnConfiguration={columnConfiguration}
567
- columnIds={columns.map(prop('id'))}
568
- disableRowCondition={disableRowCondition}
569
- isHovered={isRowHovered}
570
- isSelected={isRowSelected}
571
- isShiftKeyDown={isShiftKeyDown}
572
- key={
573
- gte(limit, performanceRowsLimit)
574
- ? `row_${index}`
575
- : getId(row)
576
- }
577
- lastSelectionIndex={lastSelectionIndex}
578
- limit={limit}
579
- listingVariant={listingVariant}
580
- row={row}
581
- rowColorConditions={rowColorConditions}
582
- shiftKeyDownRowPivot={shiftKeyDownRowPivot}
583
- subItemsPivots={subItemsPivots}
584
- tabIndex={-1}
585
- visibleColumns={visibleColumns}
586
- onClick={(): void => {
587
- onRowClick(row);
588
- }}
589
- onFocus={(): void => hoverRow(row)}
590
- onMouseOver={(): void => hoverRow(row)}
591
- >
592
- {checkable &&
593
- (!isSubItem || subItems.canCheckSubItems ? (
594
- <Cell
595
- align="left"
596
- className={classes.checkbox}
597
- disableRowCondition={disableRowCondition}
598
- isRowHovered={isRowHovered}
599
- row={row}
600
- rowColorConditions={rowColorConditions}
601
- onClick={(event): void => selectRow(event, row)}
602
- >
603
- <Checkbox
604
- checked={isRowSelected}
605
- disabled={
606
- disableRowCheckCondition(row) ||
607
- disableRowCondition(row)
608
- }
609
- inputProps={{
610
- 'aria-label': `Select row ${getId(row)}`
611
- }}
612
- />
613
- </Cell>
614
- ) : (
615
- <Cell
616
- align="left"
617
- disableRowCondition={disableRowCondition}
618
- isRowHovered={isRowHovered}
619
- row={row}
620
- rowColorConditions={rowColorConditions}
621
- />
622
- ))}
623
-
624
- {visibleColumns.map((column) => (
625
- <DataCell
626
- column={column}
513
+ <Table
514
+ stickyHeader
515
+ className={classes.table}
516
+ component="div"
517
+ role={undefined}
518
+ size="small"
519
+ >
520
+ <ListingHeader
521
+ areColumnsEditable={areColumnsEditable}
522
+ checkable={checkable}
523
+ columnConfiguration={columnConfiguration}
524
+ columns={columns}
525
+ listingVariant={listingVariant}
526
+ memoProps={headerMemoProps}
527
+ predefinedRowsSelection={predefinedRowsSelection}
528
+ rowCount={limit - emptyRows}
529
+ selectedRowCount={selectedRows.length}
530
+ sortField={sortField}
531
+ sortOrder={sortOrder}
532
+ onSelectAllClick={selectAllRows}
533
+ onSelectColumns={onSelectColumns}
534
+ onSelectRowsWithCondition={onSelectRowsWithCondition}
535
+ onSort={onSort}
536
+ />
537
+
538
+ <TableBody
539
+ className={classes.tableBody}
540
+ component="div"
541
+ onMouseLeave={clearHoveredRow}
542
+ >
543
+ {rowsToDisplay.map((row, index) => {
544
+ const isRowSelected = isSelected(row);
545
+ const isRowHovered = equals(hoveredRowId, getId(row));
546
+ const isSubItem = allSubItemIds.includes(row.id);
547
+
548
+ return (
549
+ <ListingRow
550
+ checkable={
551
+ checkable && (!isSubItem || subItems.canCheckSubItems)
552
+ }
553
+ columnConfiguration={columnConfiguration}
554
+ columnIds={columns.map(prop('id'))}
627
555
  disableRowCondition={disableRowCondition}
628
- getHighlightRowCondition={getHighlightRowCondition}
629
- isRowHovered={isRowHovered}
630
- isRowSelected={isRowSelected}
631
- key={`${getId(row)}-${column.id}`}
632
- labelCollapse={subItems.labelCollapse}
633
- labelExpand={subItems.labelExpand}
556
+ isHovered={isRowHovered}
557
+ isSelected={isRowSelected}
558
+ isShiftKeyDown={isShiftKeyDown}
559
+ key={
560
+ gte(limit, performanceRowsLimit)
561
+ ? `row_${index}`
562
+ : getId(row)
563
+ }
564
+ lastSelectionIndex={lastSelectionIndex}
565
+ limit={limit}
634
566
  listingVariant={listingVariant}
635
567
  row={row}
636
568
  rowColorConditions={rowColorConditions}
637
- subItemsRowProperty={subItems?.rowProperty}
638
- />
569
+ shiftKeyDownRowPivot={shiftKeyDownRowPivot}
570
+ subItemsPivots={subItemsPivots}
571
+ tabIndex={-1}
572
+ visibleColumns={visibleColumns}
573
+ onClick={(): void => {
574
+ onRowClick(row);
575
+ }}
576
+ onFocus={(): void => hoverRow(row)}
577
+ onMouseOver={(): void => hoverRow(row)}
578
+ >
579
+ {checkable &&
580
+ (!isSubItem || subItems.canCheckSubItems ? (
581
+ <Cell
582
+ align="left"
583
+ className={classes.checkbox}
584
+ disableRowCondition={disableRowCondition}
585
+ isRowHovered={isRowHovered}
586
+ row={row}
587
+ rowColorConditions={rowColorConditions}
588
+ onClick={(event): void => selectRow(event, row)}
589
+ >
590
+ <Checkbox
591
+ checked={isRowSelected}
592
+ disabled={
593
+ disableRowCheckCondition(row) ||
594
+ disableRowCondition(row)
595
+ }
596
+ inputProps={{
597
+ 'aria-label': `Select row ${getId(row)}`
598
+ }}
599
+ />
600
+ </Cell>
601
+ ) : (
602
+ <Cell
603
+ align="left"
604
+ disableRowCondition={disableRowCondition}
605
+ isRowHovered={isRowHovered}
606
+ row={row}
607
+ rowColorConditions={rowColorConditions}
608
+ />
609
+ ))}
610
+
611
+ {visibleColumns.map((column) => (
612
+ <DataCell
613
+ column={column}
614
+ disableRowCondition={disableRowCondition}
615
+ getHighlightRowCondition={getHighlightRowCondition}
616
+ isRowHovered={isRowHovered}
617
+ isRowSelected={isRowSelected}
618
+ key={`${getId(row)}-${column.id}`}
619
+ labelCollapse={subItems.labelCollapse}
620
+ labelExpand={subItems.labelExpand}
621
+ listingVariant={listingVariant}
622
+ row={row}
623
+ rowColorConditions={rowColorConditions}
624
+ subItemsRowProperty={subItems?.rowProperty}
625
+ />
626
+ ))}
627
+ </ListingRow>
628
+ );
629
+ })}
630
+
631
+ {rows.length < 1 &&
632
+ (loading ? (
633
+ <SkeletonLoader rows={limit} />
634
+ ) : (
635
+ <EmptyResult label={t(labelNoResultFound)} />
639
636
  ))}
640
- </ListingRow>
641
- );
642
- })}
643
-
644
- {rows.length < 1 &&
645
- (loading ? (
646
- <SkeletonLoader rows={limit} />
647
- ) : (
648
- <EmptyResult label={t(labelNoResultFound)} />
649
- ))}
650
- </TableBody>
651
- </Table>
652
- </Box>
637
+ </TableBody>
638
+ </Table>
639
+ </Box>
640
+ )}
641
+ </ParentSize>
653
642
  </div>
654
- </>
643
+ </div>
655
644
  );
656
645
  };
657
646
 
@@ -14,7 +14,7 @@ import Panel from '../Panel';
14
14
 
15
15
  import Filter from './Filter';
16
16
 
17
- import ListingPage from '.';
17
+ import ListingPage, { ListingPageProps } from '.';
18
18
 
19
19
  export default { title: 'Listing Page' };
20
20
 
@@ -59,6 +59,9 @@ const useStyles = makeStyles()((theme) => ({
59
59
  gridGap: theme.spacing(2),
60
60
  gridTemplateColumns: 'auto auto 1fr'
61
61
  },
62
+ page: {
63
+ height: '100vh'
64
+ },
62
65
  title: {
63
66
  alignItems: 'center',
64
67
  gridArea: 'title',
@@ -251,8 +254,18 @@ const DetailsPanelWithTabs = (): JSX.Element => (
251
254
  />
252
255
  );
253
256
 
257
+ const Template = (args: ListingPageProps): JSX.Element => {
258
+ const { classes } = useStyles();
259
+
260
+ return (
261
+ <div className={classes.page}>
262
+ <ListingPage {...args} />
263
+ </div>
264
+ );
265
+ };
266
+
254
267
  export const normal = (): JSX.Element => (
255
- <ListingPage
268
+ <Template
256
269
  filter={<FilterWithContent />}
257
270
  listing={listing}
258
271
  panelOpen={false}
@@ -260,7 +273,7 @@ export const normal = (): JSX.Element => (
260
273
  );
261
274
 
262
275
  export const withOpenPanel = (): JSX.Element => (
263
- <ListingPage
276
+ <Template
264
277
  panelOpen
265
278
  filter={<FilterWithContent />}
266
279
  listing={listing}
@@ -269,7 +282,7 @@ export const withOpenPanel = (): JSX.Element => (
269
282
  );
270
283
 
271
284
  export const withOpenPanelAndTabs = (): JSX.Element => (
272
- <ListingPage
285
+ <Template
273
286
  panelOpen
274
287
  filter={<FilterWithContent />}
275
288
  listing={listing}
@@ -278,7 +291,7 @@ export const withOpenPanelAndTabs = (): JSX.Element => (
278
291
  );
279
292
 
280
293
  export const withFilterDetailsAndOpenPanel = (): JSX.Element => (
281
- <ListingPage
294
+ <Template
282
295
  panelOpen
283
296
  filter={<FilterWithContent />}
284
297
  listing={listing}
@@ -290,7 +303,7 @@ export const withFixedPanel = (): JSX.Element => {
290
303
  const [open, setOpen] = useState(true);
291
304
 
292
305
  return (
293
- <ListingPage
306
+ <Template
294
307
  panelFixed
295
308
  filter={<FilterWithOpenButton onOpen={(): void => setOpen(true)} />}
296
309
  listing={listing}
@@ -304,7 +317,7 @@ export const withResponsivePaginationTable = (): JSX.Element => {
304
317
  const [open, setOpen] = useState(true);
305
318
 
306
319
  return (
307
- <ListingPage
320
+ <Template
308
321
  filter={<FilterWithOpenButton onOpen={(): void => setOpen(true)} />}
309
322
  listing={
310
323
  <Listing
@@ -326,7 +339,7 @@ export const withResponsivePaginationTable = (): JSX.Element => {
326
339
  };
327
340
 
328
341
  export const withALongContent = (): JSX.Element => (
329
- <ListingPage
342
+ <Template
330
343
  filter={<FilterWithContent />}
331
344
  listing={
332
345
  <div>
@@ -1,4 +1,4 @@
1
- import { Suspense, useEffect, useRef, useState } from 'react';
1
+ import { Suspense, useRef } from 'react';
2
2
 
3
3
  import { makeStyles } from 'tss-react/mui';
4
4
 
@@ -6,6 +6,7 @@ import { Box } from '@mui/material';
6
6
 
7
7
  import { useMemoComponent } from '../utils';
8
8
  import WithPanel from '../Panel/WithPanel';
9
+ import { ParentSize } from '..';
9
10
 
10
11
  import FilterSkeleton from './FilterSkeleton';
11
12
  import ListingSkeleton from './ListingSkeleton';
@@ -30,7 +31,7 @@ const useStyles = makeStyles()((theme) => {
30
31
  };
31
32
  });
32
33
 
33
- interface Props {
34
+ export interface ListingPageProps {
34
35
  filter: JSX.Element;
35
36
  fullHeight?: boolean;
36
37
  listing: JSX.Element;
@@ -52,32 +53,10 @@ const ListingPage = ({
52
53
  listingScrollOffset = 16,
53
54
  fullHeight = false,
54
55
  memoListingProps = []
55
- }: Props): JSX.Element => {
56
+ }: ListingPageProps): JSX.Element => {
56
57
  const { classes, cx } = useStyles();
57
- const [listingHeight, setListingHeight] = useState(0);
58
- const listingRef = useRef<HTMLDivElement | null>(null);
59
58
  const filtersRef = useRef<HTMLDivElement | null>(null);
60
59
 
61
- const resize = (): void => {
62
- setListingHeight(window.innerHeight);
63
- };
64
-
65
- useEffect(() => {
66
- window.addEventListener('resize', resize);
67
-
68
- setListingHeight(window.innerHeight);
69
-
70
- return () => {
71
- window.removeEventListener('resize', resize);
72
- };
73
- }, []);
74
-
75
- const listingContainerHeight =
76
- listingHeight -
77
- (filtersRef.current?.getBoundingClientRect().height || 0) -
78
- (filtersRef.current?.getBoundingClientRect().top || 0) -
79
- listingScrollOffset;
80
-
81
60
  const memoListingComponent = useMemoComponent({
82
61
  Component: listing,
83
62
  memoProps: [...memoListingProps]
@@ -90,18 +69,21 @@ const ListingPage = ({
90
69
  </div>
91
70
 
92
71
  <WithPanel fixed={panelFixed} open={panelOpen} panel={panel}>
93
- <Box
94
- className={classes.listing}
95
- ref={listingRef}
96
- sx={{
97
- ...(fullHeight && { height: '100%' }),
98
- maxHeight: listingContainerHeight
99
- }}
100
- >
101
- <Suspense fallback={<ListingSkeleton />}>
102
- {memoListingComponent}
103
- </Suspense>
104
- </Box>
72
+ <ParentSize>
73
+ {({ height }) => (
74
+ <Box
75
+ className={classes.listing}
76
+ sx={{
77
+ height: `calc(${height}px - ${listingScrollOffset}px)`,
78
+ ...(fullHeight && { height: '100%' })
79
+ }}
80
+ >
81
+ <Suspense fallback={<ListingSkeleton />}>
82
+ {memoListingComponent}
83
+ </Suspense>
84
+ </Box>
85
+ )}
86
+ </ParentSize>
105
87
  </WithPanel>
106
88
  </div>
107
89
  );
@@ -0,0 +1,62 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { Delete as DeleteIcon, Save as SaveIcon } from '@mui/icons-material';
4
+
5
+ import { IconButton } from '../..';
6
+
7
+ import { ConfirmationTooltip } from './ConfirmationTooltip';
8
+
9
+ const meta: Meta<typeof ConfirmationTooltip> = {
10
+ component: ConfirmationTooltip
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof ConfirmationTooltip>;
15
+
16
+ export const Default: Story = {
17
+ args: {
18
+ children: (toggleTooltip) => (
19
+ <IconButton icon={<SaveIcon />} onClick={toggleTooltip} />
20
+ ),
21
+ labels: {
22
+ cancel: 'Cancel',
23
+ confirm: {
24
+ label: 'Save'
25
+ }
26
+ },
27
+ onConfirm: () => undefined
28
+ }
29
+ };
30
+
31
+ export const WithConfirmVariant: Story = {
32
+ args: {
33
+ children: (toggleTooltip) => (
34
+ <IconButton icon={<DeleteIcon color="error" />} onClick={toggleTooltip} />
35
+ ),
36
+ confirmVariant: 'error',
37
+ labels: {
38
+ cancel: 'Cancel',
39
+ confirm: {
40
+ label: 'Delete'
41
+ }
42
+ },
43
+ onConfirm: () => undefined
44
+ }
45
+ };
46
+
47
+ export const WithSecondaryLabel: Story = {
48
+ args: {
49
+ children: (toggleTooltip) => (
50
+ <IconButton icon={<DeleteIcon color="error" />} onClick={toggleTooltip} />
51
+ ),
52
+ confirmVariant: 'error',
53
+ labels: {
54
+ cancel: 'Cancel',
55
+ confirm: {
56
+ label: 'Delete',
57
+ secondaryLabel: 'This action will delete the current item'
58
+ }
59
+ },
60
+ onConfirm: () => undefined
61
+ }
62
+ };
@@ -0,0 +1,19 @@
1
+ import { equals } from 'ramda';
2
+ import { makeStyles } from 'tss-react/mui';
3
+
4
+ import { ThemeMode } from '@centreon/ui-context';
5
+
6
+ export const useStyles = makeStyles()((theme) => ({
7
+ list: {
8
+ padding: 0
9
+ },
10
+ paper: {
11
+ backgroundColor: equals(theme.palette.mode, ThemeMode.dark)
12
+ ? theme.palette.background.widget
13
+ : theme.palette.background.default,
14
+ borderRadius: theme.shape.borderRadius,
15
+ boxShadow: theme.shadows[3],
16
+ marginLeft: theme.spacing(2),
17
+ maxWidth: '350px'
18
+ }
19
+ }));
@@ -0,0 +1,67 @@
1
+ import { MouseEvent, useState } from 'react';
2
+
3
+ import { ClickAwayListener, Box, Popper } from '@mui/material';
4
+
5
+ import TooltipContent from './TooltipContent';
6
+ import { Props } from './models';
7
+ import { useStyles } from './ConfirmationTooltip.styles';
8
+
9
+ export const ConfirmationTooltip = ({
10
+ children,
11
+ labels,
12
+ onConfirm,
13
+ confirmVariant
14
+ }: Props): JSX.Element => {
15
+ const { classes } = useStyles();
16
+
17
+ const [anchorElement, setAnchorElement] = useState<
18
+ HTMLButtonElement | undefined
19
+ >(undefined);
20
+
21
+ const close = (): void => {
22
+ setAnchorElement(undefined);
23
+ };
24
+
25
+ const open = (event: MouseEvent<HTMLButtonElement>): void => {
26
+ setAnchorElement(event.currentTarget);
27
+ };
28
+
29
+ const confirm = (): void => {
30
+ close();
31
+ onConfirm();
32
+ };
33
+
34
+ const actions = [
35
+ {
36
+ action: close,
37
+ label: labels.cancel
38
+ },
39
+ {
40
+ action: confirm,
41
+ label: labels.confirm.label,
42
+ secondaryLabel: labels.confirm.secondaryLabel,
43
+ variant: confirmVariant
44
+ }
45
+ ];
46
+
47
+ const isOpen = Boolean(anchorElement);
48
+
49
+ return (
50
+ <ClickAwayListener onClickAway={close}>
51
+ <div>
52
+ {children(isOpen ? close : open)}
53
+ <Popper
54
+ anchorEl={anchorElement}
55
+ open={isOpen}
56
+ popperOptions={{
57
+ placement: 'bottom-start'
58
+ }}
59
+ >
60
+ <Box className={classes.paper}>
61
+ <TooltipContent actions={actions} />
62
+ </Box>
63
+ </Popper>
64
+ </div>
65
+ </ClickAwayListener>
66
+ );
67
+ };
@@ -0,0 +1,31 @@
1
+ import { ActionVariants } from '../../../ActionsList/models';
2
+ import { ActionsList } from '../../..';
3
+
4
+ import { useStyles } from './ConfirmationTooltip.styles';
5
+
6
+ interface Props {
7
+ actions: Array<{
8
+ action: () => void;
9
+ label: string;
10
+ secondaryLabel?: string;
11
+ variant?: ActionVariants;
12
+ }>;
13
+ }
14
+
15
+ const TooltipContent = ({ actions }: Props): JSX.Element => {
16
+ const { classes } = useStyles();
17
+
18
+ return (
19
+ <ActionsList
20
+ actions={actions.map(({ label, action, variant, secondaryLabel }) => ({
21
+ label,
22
+ onClick: action,
23
+ secondaryLabel,
24
+ variant
25
+ }))}
26
+ className={classes.list}
27
+ />
28
+ );
29
+ };
30
+
31
+ export default TooltipContent;
@@ -0,0 +1,18 @@
1
+ import { ReactElement } from 'react';
2
+
3
+ import { ActionVariants } from '../../../ActionsList/models';
4
+
5
+ interface Labels {
6
+ cancel: string;
7
+ confirm: {
8
+ label: string;
9
+ secondaryLabel?: string;
10
+ };
11
+ }
12
+
13
+ export interface Props {
14
+ children: (toggleTooltip) => ReactElement;
15
+ confirmVariant?: ActionVariants;
16
+ labels: Labels;
17
+ onConfirm: () => void;
18
+ }
@@ -1,21 +1,35 @@
1
1
  import { ReactElement, ReactNode } from 'react';
2
2
 
3
- import { Tooltip as MuiTooltip } from '@mui/material';
3
+ import {
4
+ Tooltip as MuiTooltip,
5
+ TooltipProps as MuiTooltipProps
6
+ } from '@mui/material';
4
7
 
5
8
  import { AriaLabelingAttributes } from '../../@types/aria-attributes';
6
9
  import { DataTestAttributes } from '../../@types/data-attributes';
7
10
 
8
- import { useStyles } from './Tooltip.styles';
9
-
10
11
  export type TooltipProps = {
11
12
  children: ReactElement;
12
13
  followCursor?: boolean;
13
14
  hasCaret?: boolean;
14
15
  isOpen?: boolean;
15
16
  label: ReactNode;
16
- position?: 'top' | 'bottom' | 'left' | 'right';
17
+ position?:
18
+ | 'top'
19
+ | 'bottom'
20
+ | 'left'
21
+ | 'right'
22
+ | 'bottom-end'
23
+ | 'bottom-start'
24
+ | 'left-end'
25
+ | 'left-start'
26
+ | 'right-end'
27
+ | 'right-start'
28
+ | 'top-end'
29
+ | 'top-start';
17
30
  } & AriaLabelingAttributes &
18
- DataTestAttributes;
31
+ DataTestAttributes &
32
+ Omit<MuiTooltipProps, 'title'>;
19
33
 
20
34
  const Tooltip = ({
21
35
  children,
@@ -26,12 +40,9 @@ const Tooltip = ({
26
40
  hasCaret = false,
27
41
  ...attr
28
42
  }: TooltipProps): ReactElement => {
29
- const { classes } = useStyles();
30
-
31
43
  return (
32
44
  <MuiTooltip
33
45
  arrow={hasCaret}
34
- className={classes.tooltip}
35
46
  followCursor={followCursor}
36
47
  open={isOpen}
37
48
  placement={position}
@@ -1,5 +0,0 @@
1
- import { makeStyles } from 'tss-react/mui';
2
-
3
- export const useStyles = makeStyles()(() => ({
4
- tooltip: {}
5
- }));