@centreon/ui 24.11.12 → 24.11.13

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.11.12",
3
+ "version": "24.11.13",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -0,0 +1,147 @@
1
+ import {
2
+ dateTimeFormat,
3
+ getXAxisTickFormat,
4
+ useLocaleDateTimeFormat
5
+ } from '@centreon/ui';
6
+
7
+ import { Typography, useTheme } from '@mui/material';
8
+
9
+ import dayjs from 'dayjs';
10
+
11
+ import { userAtom } from '@centreon/ui-context';
12
+ import { Axis } from '@visx/visx';
13
+
14
+ import { scaleTime } from '@visx/scale';
15
+ import { BarRounded } from '@visx/shape';
16
+ import { useAtomValue } from 'jotai';
17
+ import { equals } from 'ramda';
18
+ import { useCallback } from 'react';
19
+ import { Tooltip } from '../../components';
20
+ import { margins } from '../common/margins';
21
+ import type { TimelineProps } from './models';
22
+ import { useStyles } from './timeline.styles';
23
+ import { useTimeline } from './useTimeline';
24
+
25
+ interface Props extends TimelineProps {
26
+ width: number;
27
+ height: number;
28
+ }
29
+
30
+ const axisPadding = 4;
31
+
32
+ const Timeline = ({
33
+ data,
34
+ startDate,
35
+ endDate,
36
+ width,
37
+ height,
38
+ TooltipContent,
39
+ tooltipClassName
40
+ }: Props) => {
41
+ const { classes, cx } = useStyles();
42
+ const { format } = useLocaleDateTimeFormat();
43
+ const { timezone } = useAtomValue(userAtom);
44
+
45
+ const theme = useTheme();
46
+
47
+ const xScale = scaleTime({
48
+ domain: [new Date(startDate), new Date(endDate)],
49
+ range: [margins.left, width - margins.right],
50
+ clamp: true
51
+ });
52
+
53
+ const numTicks = Math.min(Math.ceil(width / 82), 12);
54
+
55
+ const { getTimeDifference } = useTimeline();
56
+
57
+ const getFormattedStart = useCallback(
58
+ (start) =>
59
+ format({
60
+ date: dayjs(start).tz(timezone).toDate(),
61
+ formatString: dateTimeFormat
62
+ }),
63
+ [dateTimeFormat, timezone]
64
+ );
65
+
66
+ const getFormattedEnd = useCallback(
67
+ (end) =>
68
+ format({
69
+ date: dayjs(end).tz(timezone).toDate(),
70
+ formatString: dateTimeFormat
71
+ }),
72
+ [dateTimeFormat, timezone]
73
+ );
74
+
75
+ return (
76
+ <svg width={width} height={height + axisPadding}>
77
+ {data.map(({ start, end, color }, idx) => (
78
+ <Tooltip
79
+ hasCaret
80
+ classes={{
81
+ tooltip: cx(classes.tooltip, tooltipClassName)
82
+ }}
83
+ followCursor={false}
84
+ key={`rect-${start}--${end}`}
85
+ label={
86
+ TooltipContent ? (
87
+ <TooltipContent
88
+ start={getFormattedStart(start)}
89
+ end={getFormattedEnd(end)}
90
+ color={color}
91
+ duration={getTimeDifference({
92
+ start: dayjs(start),
93
+ end: dayjs(end)
94
+ })}
95
+ />
96
+ ) : (
97
+ <div style={{ color }}>
98
+ <Typography variant="body2">
99
+ {getTimeDifference({ start: dayjs(start), end: dayjs(end) })}
100
+ </Typography>
101
+ <Typography variant="body2">{`${format({ date: start, formatString: 'L LT' })} - ${format({ date: end, formatString: 'L LT' })}`}</Typography>
102
+ </div>
103
+ )
104
+ }
105
+ position="top"
106
+ >
107
+ <g>
108
+ <BarRounded
109
+ x={xScale(dayjs(start).tz(timezone))}
110
+ y={0}
111
+ width={
112
+ xScale(dayjs(end).tz(timezone)) -
113
+ xScale(dayjs(start).tz(timezone))
114
+ }
115
+ height={height - margins.bottom}
116
+ fill={color}
117
+ left={equals(idx, 0)}
118
+ radius={4}
119
+ right={equals(idx, data.length - 1)}
120
+ />
121
+ </g>
122
+ </Tooltip>
123
+ ))}
124
+
125
+ <Axis.AxisBottom
126
+ top={height - margins.bottom + axisPadding}
127
+ scale={xScale}
128
+ numTicks={numTicks}
129
+ tickFormat={(value) =>
130
+ format({
131
+ date: new Date(value),
132
+ formatString: getXAxisTickFormat({ end: endDate, start: startDate })
133
+ })
134
+ }
135
+ stroke={theme.palette.text.primary}
136
+ tickStroke={theme.palette.text.primary}
137
+ tickLabelProps={() => ({
138
+ fill: theme.palette.text.primary,
139
+ fontSize: theme.typography.caption.fontSize,
140
+ textAnchor: 'middle'
141
+ })}
142
+ />
143
+ </svg>
144
+ );
145
+ };
146
+
147
+ export default Timeline;
@@ -0,0 +1,148 @@
1
+ import { userAtom } from '@centreon/ui-context';
2
+ import { Provider, createStore } from 'jotai';
3
+ import Timeline from './Timeline';
4
+ import { Tooltip } from './models';
5
+
6
+ const data = [
7
+ {
8
+ start: '2024-09-09T10:57:42+02:00',
9
+ end: '2024-09-09T11:15:00+02:00',
10
+ color: 'green'
11
+ },
12
+ {
13
+ start: '2024-09-09T11:15:00+02:00',
14
+ end: '2024-09-09T11:30:00+02:00',
15
+ color: 'red'
16
+ },
17
+ {
18
+ start: '2024-09-09T11:30:00+02:00',
19
+ end: '2024-09-09T11:45:00+02:00',
20
+ color: 'gray'
21
+ },
22
+ {
23
+ start: '2024-09-09T11:45:00+02:00',
24
+ end: '2024-09-09T12:00:00+02:00',
25
+ color: 'green'
26
+ },
27
+ {
28
+ start: '2024-09-09T12:00:00+02:00',
29
+ end: '2024-09-09T12:20:00+02:00',
30
+ color: 'red'
31
+ },
32
+ {
33
+ start: '2024-09-09T12:20:00+02:00',
34
+ end: '2024-09-09T12:40:00+02:00',
35
+ color: 'gray'
36
+ },
37
+ {
38
+ start: '2024-09-09T12:40:00+02:00',
39
+ end: '2024-09-09T12:57:42+02:00',
40
+ color: 'green'
41
+ }
42
+ ];
43
+
44
+ const startDate = '2024-09-09T10:57:42+02:00';
45
+ const endDate = '2024-09-09T12:57:42+02:00';
46
+
47
+ const TooltipContent = ({ start, end, color, duration }: Tooltip) => (
48
+ <div
49
+ data-testid="tooltip-content"
50
+ style={{
51
+ display: 'flex',
52
+ flexDirection: 'column',
53
+ justifyContent: 'center',
54
+ alignItems: 'center',
55
+ gap: '10px',
56
+ padding: '5px'
57
+ }}
58
+ >
59
+ <span>{start}</span>
60
+ <span>{end}</span>
61
+ <span>{color}</span>
62
+ <span>{duration}</span>
63
+ </div>
64
+ );
65
+
66
+ const store = createStore();
67
+ store.set(userAtom, { timezone: 'Europe/Paris', locale: 'en' });
68
+
69
+ const initialize = (displayDefaultTooltip = true): void => {
70
+ cy.mount({
71
+ Component: (
72
+ <Provider store={store}>
73
+ <div
74
+ style={{
75
+ height: '100px',
76
+ width: '70%'
77
+ }}
78
+ >
79
+ <Timeline
80
+ data={data}
81
+ startDate={startDate}
82
+ endDate={endDate}
83
+ TooltipContent={displayDefaultTooltip ? undefined : TooltipContent}
84
+ />
85
+ </div>
86
+ </Provider>
87
+ )
88
+ });
89
+ };
90
+
91
+ describe('Timeline', () => {
92
+ it('checks that the correct number of bars are rendered', () => {
93
+ initialize();
94
+
95
+ cy.get('path').should('have.length', data.length);
96
+
97
+ cy.makeSnapshot();
98
+ });
99
+
100
+ it('checks that each bar has the correct color', () => {
101
+ initialize();
102
+
103
+ data.forEach(({ color }, index) => {
104
+ cy.get('path').eq(index).should('have.attr', 'fill', color);
105
+ });
106
+ });
107
+
108
+ it('displays tooltip with correct information when hovered over a bar', () => {
109
+ initialize(false);
110
+
111
+ cy.get('path').first().trigger('mouseover');
112
+
113
+ cy.get('[data-testid="tooltip-content"]').within(() => {
114
+ cy.contains('09/09/2024 10:57 AM').should('be.visible');
115
+ cy.contains('09/09/2024 11:15 AM').should('be.visible');
116
+ cy.contains('green').should('be.visible');
117
+ cy.contains('17 minutes').should('be.visible');
118
+ });
119
+
120
+ cy.makeSnapshot();
121
+ });
122
+
123
+ it('displays the default tooltip with correct information when hovered over a bar', () => {
124
+ initialize();
125
+
126
+ cy.get('path').first().trigger('mouseover');
127
+
128
+ cy.get('[role="tooltip"]').within(() => {
129
+ cy.contains('09/09/2024 10:57 AM')
130
+ .should('be.visible')
131
+ .and('have.css', 'color', 'rgb(0, 128, 0)');
132
+ cy.contains('09/09/2024 11:15 AM')
133
+ .should('be.visible')
134
+ .and('have.css', 'color', 'rgb(0, 128, 0)');
135
+ cy.contains('17 minutes')
136
+ .should('be.visible')
137
+ .and('have.css', 'color', 'rgb(0, 128, 0)');
138
+ });
139
+
140
+ cy.makeSnapshot();
141
+ });
142
+
143
+ it('displays correct tick labels on the x-axis', () => {
144
+ initialize();
145
+
146
+ cy.get('.visx-axis-bottom .visx-axis-tick').first().contains('11:00');
147
+ });
148
+ });
@@ -0,0 +1,91 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { Typography } from '@mui/material';
4
+ import Timeline from './Timeline';
5
+
6
+ const data = [
7
+ {
8
+ start: '2024-09-25T21:00:42+01:00',
9
+ end: '2024-09-25T21:15:00+01:00',
10
+ color: 'gray'
11
+ },
12
+ {
13
+ start: '2024-09-25T21:15:00+01:00',
14
+ end: '2024-09-25T21:54:00+01:00',
15
+ color: 'green'
16
+ },
17
+ {
18
+ start: '2024-09-25T21:54:00+01:00',
19
+ end: '2024-09-25T22:30:00+01:00',
20
+ color: 'red'
21
+ }
22
+ ];
23
+
24
+ const startDate = '2024-09-25T21:00:42+01:00';
25
+ const endDate = '2024-09-25T22:30:00+01:00';
26
+
27
+ const Template = (args): JSX.Element => {
28
+ return (
29
+ <div style={{ width: '700px', height: '100px' }}>
30
+ <Timeline {...args} />
31
+ </div>
32
+ );
33
+ };
34
+
35
+ const meta: Meta<typeof Timeline> = {
36
+ component: Timeline,
37
+ parameters: {
38
+ chromatic: {
39
+ delay: 1000
40
+ }
41
+ },
42
+ render: Template
43
+ };
44
+
45
+ export default meta;
46
+ type Story = StoryObj<typeof Timeline>;
47
+
48
+ export const Normal: Story = {
49
+ args: {
50
+ data,
51
+ startDate,
52
+ endDate
53
+ }
54
+ };
55
+
56
+ export const WithoutData: Story = {
57
+ args: {
58
+ data: [],
59
+ startDate,
60
+ endDate
61
+ }
62
+ };
63
+
64
+ export const WithSmallerTimeRangeThanData: Story = {
65
+ args: {
66
+ data,
67
+ startDate,
68
+ endDate: '2024-09-25T22:00:00+01:00'
69
+ }
70
+ };
71
+
72
+ export const WithCustomTooltip: Story = {
73
+ args: {
74
+ data,
75
+ startDate,
76
+ endDate,
77
+ TooltipContent: ({ duration, color }) => (
78
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '8px' }}>
79
+ <div
80
+ style={{
81
+ backgroundColor: color,
82
+ width: '20px',
83
+ height: '20px',
84
+ borderRadius: '4px'
85
+ }}
86
+ />
87
+ <Typography>{duration}</Typography>
88
+ </div>
89
+ )
90
+ }
91
+ };
@@ -0,0 +1,14 @@
1
+ import { ParentSize } from '../..';
2
+
3
+ import ResponsiveTimeline from './ResponsiveTimeline';
4
+ import type { TimelineProps } from './models';
5
+
6
+ const Timeline = (props: TimelineProps): JSX.Element => (
7
+ <ParentSize>
8
+ {({ width, height }) => (
9
+ <ResponsiveTimeline {...props} height={height} width={width} />
10
+ )}
11
+ </ParentSize>
12
+ );
13
+
14
+ export default Timeline;
@@ -0,0 +1 @@
1
+ export { default as Timeline } from './Timeline';
@@ -0,0 +1,20 @@
1
+ export interface Data {
2
+ start: string;
3
+ end: string;
4
+ color: string;
5
+ }
6
+
7
+ export interface Tooltip {
8
+ start: string;
9
+ end: string;
10
+ color: string;
11
+ duration: string;
12
+ }
13
+
14
+ export interface TimelineProps {
15
+ data: Array<Data>;
16
+ startDate: string;
17
+ endDate: string;
18
+ TooltipContent?: (props: Tooltip) => JSX.Element;
19
+ tooltipClassName?: string;
20
+ }
@@ -0,0 +1,11 @@
1
+ import { makeStyles } from 'tss-react/mui';
2
+
3
+ export const useStyles = makeStyles()((theme) => ({
4
+ tooltip: {
5
+ backgroundColor: theme.palette.background.paper,
6
+ color: theme.palette.text.primary,
7
+ padding: theme.spacing(1),
8
+ boxShadow: theme.shadows[3],
9
+ maxWidth: 'none'
10
+ }
11
+ }));
@@ -0,0 +1,6 @@
1
+ export const labelYear = 'year';
2
+ export const labelMonth = 'month';
3
+ export const labelDay = 'day';
4
+ export const labelHour = 'hour';
5
+ export const labelMinutes = 'minutes';
6
+ export const labelMinute = 'minute';
@@ -0,0 +1,90 @@
1
+ import dayjs, { Dayjs } from 'dayjs';
2
+
3
+ import { usePluralizedTranslation } from '@centreon/ui';
4
+ import { lt } from 'ramda';
5
+ import { useCallback } from 'react';
6
+ import {
7
+ labelDay,
8
+ labelHour,
9
+ labelMinute,
10
+ labelMonth,
11
+ labelYear
12
+ } from './translatedLabel';
13
+
14
+ interface StartEndProps {
15
+ start: Dayjs;
16
+ end: Dayjs;
17
+ }
18
+
19
+ interface GetWidthProps extends StartEndProps {
20
+ timezone: string;
21
+ xScale;
22
+ }
23
+
24
+ interface UseTimelineState {
25
+ getTimeDifference: (props: StartEndProps) => string;
26
+ getWidth: (props: GetWidthProps) => number;
27
+ }
28
+
29
+ export const useTimeline = (): UseTimelineState => {
30
+ const { pluralizedT } = usePluralizedTranslation();
31
+
32
+ const getTimeDifference = useCallback(
33
+ ({ start, end }: StartEndProps): string => {
34
+ const diffInMilliseconds = end.diff(start);
35
+ const diffDuration = dayjs.duration(diffInMilliseconds);
36
+
37
+ const timeUnits = [
38
+ {
39
+ value: diffDuration.years(),
40
+ unit: pluralizedT({ label: labelYear, count: diffDuration.years() })
41
+ },
42
+ {
43
+ value: diffDuration.months(),
44
+ unit: pluralizedT({ label: labelMonth, count: diffDuration.months() })
45
+ },
46
+ {
47
+ value: diffDuration.days(),
48
+ unit: pluralizedT({ label: labelDay, count: diffDuration.days() })
49
+ },
50
+ {
51
+ value: diffDuration.hours(),
52
+ unit: pluralizedT({ label: labelHour, count: diffDuration.hours() })
53
+ },
54
+ {
55
+ value: diffDuration.minutes(),
56
+ unit: pluralizedT({
57
+ label: labelMinute,
58
+ count: diffDuration.minutes()
59
+ })
60
+ }
61
+ ];
62
+
63
+ const readableUnits = timeUnits
64
+ .filter((unit) => unit.value > 0)
65
+ .map((unit) => `${unit.value} ${unit.unit}`);
66
+
67
+ return readableUnits.slice(0, 2).join(', ');
68
+ },
69
+ []
70
+ );
71
+
72
+ const getWidth = useCallback(
73
+ ({ start, end, timezone, xScale }: GetWidthProps): number => {
74
+ const baseWidth =
75
+ xScale(dayjs(end).tz(timezone)) - xScale(dayjs(start).tz(timezone));
76
+
77
+ if (Number.isNaN(baseWidth) || lt(baseWidth, 0)) {
78
+ return 0;
79
+ }
80
+
81
+ return baseWidth;
82
+ },
83
+ []
84
+ );
85
+
86
+ return {
87
+ getTimeDifference,
88
+ getWidth
89
+ };
90
+ };
@@ -9,6 +9,7 @@ export { Text as GraphText } from './Text';
9
9
  export { HeatMap } from './HeatMap';
10
10
  export { BarStack } from './BarStack';
11
11
  export { PieChart } from './PieChart';
12
+ export { Timeline } from './Timeline';
12
13
  export * from './Tree';
13
14
  export type { LineChartData } from './common/models';
14
15
  export * from './common/timeSeries';
@@ -1,7 +1,7 @@
1
1
  import { EmptyRow } from '../Row/EmptyRow';
2
2
 
3
3
  interface EmptyResultProps {
4
- label: string;
4
+ label: string | JSX.Element;
5
5
  }
6
6
 
7
7
  const EmptyResult = ({ label }: EmptyResultProps): JSX.Element => (
@@ -61,7 +61,7 @@ import {
61
61
  SortOrder
62
62
  } from './models';
63
63
  import { subItemsPivotsAtom } from './tableAtoms';
64
- import { labelNoResultFound } from './translatedLabels';
64
+ import { labelNoResultFound as defaultLabelNoResultFound } from './translatedLabels';
65
65
  import useStyleTable from './useStyleTable';
66
66
 
67
67
  const subItemPrefixKey = 'listing';
@@ -140,6 +140,7 @@ export interface Props<TRow> {
140
140
  viewerModeConfiguration?: ViewerModeConfiguration;
141
141
  widthToMoveTablePagination?: number;
142
142
  isActionBarVisible: boolean;
143
+ labelNoResultFound?: string | JSX.Element;
143
144
  }
144
145
 
145
146
  const defaultColumnConfiguration = {
@@ -199,7 +200,8 @@ const Listing = <
199
200
  labelCollapse: 'Collapse',
200
201
  labelExpand: 'Expand'
201
202
  },
202
- isActionBarVisible = true
203
+ isActionBarVisible = true,
204
+ labelNoResultFound = defaultLabelNoResultFound
203
205
  }: Props<TRow>): JSX.Element => {
204
206
  const currentVisibleColumns = getVisibleColumns({
205
207
  columnConfiguration,
@@ -703,7 +705,11 @@ const Listing = <
703
705
  (loading ? (
704
706
  <SkeletonLoader rows={limit} />
705
707
  ) : (
706
- <EmptyResult label={t(labelNoResultFound)} />
708
+ <EmptyResult
709
+ label={
710
+ labelNoResultFound || t(defaultLabelNoResultFound)
711
+ }
712
+ />
707
713
  ))}
708
714
  </TableBody>
709
715
  </Table>
@@ -739,6 +745,7 @@ export const MemoizedListing = <TRow extends { id: string | number }>({
739
745
  moveTablePagination,
740
746
  widthToMoveTablePagination,
741
747
  listingVariant,
748
+ labelNoResultFound,
742
749
  ...props
743
750
  }: MemoizedListingProps<TRow>): JSX.Element =>
744
751
  useMemoComponent({
@@ -761,6 +768,7 @@ export const MemoizedListing = <TRow extends { id: string | number }>({
761
768
  sortOrder={sortOrder}
762
769
  totalRows={totalRows}
763
770
  widthToMoveTablePagination={widthToMoveTablePagination}
771
+ labelNoResultFound={labelNoResultFound}
764
772
  {...props}
765
773
  />
766
774
  ),
@@ -783,7 +791,8 @@ export const MemoizedListing = <TRow extends { id: string | number }>({
783
791
  sortOrder,
784
792
  sortField,
785
793
  innerScrollDisabled,
786
- listingVariant
794
+ listingVariant,
795
+ labelNoResultFound
787
796
  ]
788
797
  });
789
798
 
@@ -4,8 +4,8 @@ export const useStyles = makeStyles()((theme) => ({
4
4
  pageLayout: {
5
5
  display: 'grid',
6
6
  gridTemplateRows: 'auto 1fr',
7
- height: '100%',
8
- overflow: 'hidden'
7
+ overflow: 'hidden',
8
+ height: '100%'
9
9
  },
10
10
  pageLayoutActions: {
11
11
  '& > span': {