@centreon/ui 23.10.65-develop.0 → 23.10.65

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/Dashboard/Dashboard.styles.ts +6 -1
  3. package/src/Dashboard/Item.tsx +4 -1
  4. package/src/Graph/Gauge/Gauge.stories.tsx +10 -0
  5. package/src/Graph/Gauge/Gauge.tsx +10 -1
  6. package/src/Graph/Gauge/PieData.tsx +8 -8
  7. package/src/Graph/Gauge/ResponsiveGauge.tsx +32 -23
  8. package/src/Graph/Gauge/Thresholds.tsx +2 -25
  9. package/src/Graph/Gauge/models.ts +1 -0
  10. package/src/Graph/SingleBar/ResponsiveSingleBar.tsx +15 -9
  11. package/src/Graph/SingleBar/SingleBar.stories.tsx +10 -0
  12. package/src/Graph/SingleBar/ThresholdLine.tsx +3 -5
  13. package/src/Graph/SingleBar/models.ts +2 -0
  14. package/src/Graph/Text/Text.stories.tsx +13 -0
  15. package/src/Graph/Text/Text.tsx +19 -12
  16. package/src/Graph/common/timeSeries/index.test.ts +106 -3
  17. package/src/Graph/common/timeSeries/index.ts +67 -4
  18. package/src/Graph/common/timeSeries/models.ts +4 -1
  19. package/src/Graph/common/utils.ts +4 -2
  20. package/src/InputField/Select/Autocomplete/Multi/index.tsx +4 -2
  21. package/src/InputField/Select/Autocomplete/index.tsx +3 -1
  22. package/src/InputField/Text/index.tsx +10 -7
  23. package/src/RichTextEditor/ContentEditable.tsx +0 -1
  24. package/src/RichTextEditor/RichTextEditor.cypress.spec.tsx +5 -13
  25. package/src/RichTextEditor/RichTextEditor.tsx +5 -2
  26. package/src/RichTextEditor/plugins/AutoLinkPlugin/index.tsx +40 -1
  27. package/src/RichTextEditor/plugins/FloatingLinkEditorPlugin.tsx +33 -8
  28. package/src/RichTextEditor/plugins/ToolbarPlugin/index.tsx +10 -4
  29. package/src/ThemeProvider/index.tsx +6 -4
  30. package/src/api/useFetchQuery/index.ts +10 -8
  31. package/src/components/Inputs/Switch/Switch.stories.tsx +22 -0
  32. package/src/components/Inputs/Switch/Switch.styles.ts +13 -0
  33. package/src/components/Inputs/Switch/Switch.tsx +18 -0
  34. package/src/components/Inputs/index.ts +1 -0
  35. package/src/components/Modal/Modal.styles.ts +3 -1
  36. package/src/components/index.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "23.10.65-develop.0",
3
+ "version": "23.10.65",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "eslint": "eslint ./src --ext .js,.jsx,.ts,.tsx --max-warnings 0",
@@ -79,7 +79,12 @@ export const useDashboardItemStyles = makeStyles<{ hasHeader: boolean }>()(
79
79
  '&:hover': {
80
80
  backgroundColor: theme.palette.action.hover
81
81
  },
82
- cursor: 'move',
82
+ '&[data-canMove="false"]': {
83
+ cursor: 'default'
84
+ },
85
+ '&[data-canMove="true"]': {
86
+ cursor: 'move'
87
+ },
83
88
  padding: theme.spacing(0.5, 2)
84
89
  },
85
90
  widgetPadding: {
@@ -15,6 +15,7 @@ import { useMemoComponent } from '../utils';
15
15
  import { useDashboardItemStyles } from './Dashboard.styles';
16
16
 
17
17
  interface DashboardItemProps {
18
+ canMove?: boolean;
18
19
  children: ReactElement;
19
20
  className?: string;
20
21
  disablePadding?: boolean;
@@ -37,7 +38,8 @@ const Item = forwardRef(
37
38
  onMouseUp,
38
39
  onTouchEnd,
39
40
  id,
40
- disablePadding = false
41
+ disablePadding = false,
42
+ canMove = false
41
43
  }: DashboardItemProps,
42
44
  ref: ForwardedRef<HTMLDivElement>
43
45
  ): ReactElement => {
@@ -66,6 +68,7 @@ const Item = forwardRef(
66
68
  <div
67
69
  {...listeners}
68
70
  className={classes.widgetHeader}
71
+ data-canMove={canMove}
69
72
  data-testid={`${id}_move_panel`}
70
73
  >
71
74
  {header}
@@ -134,3 +134,13 @@ export const withRangeThresholds: Story = {
134
134
  },
135
135
  render: Template
136
136
  };
137
+
138
+ export const RawValue: Story = {
139
+ args: {
140
+ data: dataLastWeek,
141
+ displayAsRaw: true,
142
+ thresholdTooltipLabels: ['Warning', 'Critical'],
143
+ thresholds: [0.5, 0.6]
144
+ },
145
+ render: Template
146
+ };
@@ -7,11 +7,18 @@ import { Metric } from '../common/timeSeries/models';
7
7
  import ResponsiveGauge from './ResponsiveGauge';
8
8
 
9
9
  interface Props {
10
+ baseColor?: string;
10
11
  data?: LineChartData;
12
+ displayAsRaw?: boolean;
11
13
  thresholds: Thresholds;
12
14
  }
13
15
 
14
- export const Gauge = ({ thresholds, data }: Props): JSX.Element | null => {
16
+ export const Gauge = ({
17
+ thresholds,
18
+ data,
19
+ displayAsRaw,
20
+ baseColor
21
+ }: Props): JSX.Element | null => {
15
22
  if (!data) {
16
23
  return null;
17
24
  }
@@ -22,6 +29,8 @@ export const Gauge = ({ thresholds, data }: Props): JSX.Element | null => {
22
29
  <Responsive.ParentSize>
23
30
  {({ width, height }) => (
24
31
  <ResponsiveGauge
32
+ baseColor={baseColor}
33
+ displayAsRaw={displayAsRaw}
25
34
  height={height}
26
35
  metric={metric}
27
36
  thresholds={thresholds}
@@ -16,7 +16,8 @@ const PieData = ({
16
16
  metric,
17
17
  adaptedMaxValue,
18
18
  thresholds,
19
- radius
19
+ radius,
20
+ baseColor
20
21
  }: Omit<
21
22
  GaugeProps,
22
23
  'width' | 'height' | 'showTooltip' | 'hideTooltip' | 'thresholdTooltipLabels'
@@ -24,13 +25,12 @@ const PieData = ({
24
25
  const theme = useTheme();
25
26
 
26
27
  const pieData = [metric.data[0], adaptedMaxValue - metric.data[0]];
27
- const pieColor = !thresholds.enabled
28
- ? theme.palette.success.main
29
- : getColorFromDataAndTresholds({
30
- data: metric.data[0],
31
- theme,
32
- thresholds
33
- });
28
+ const pieColor = getColorFromDataAndTresholds({
29
+ baseColor,
30
+ data: metric.data[0],
31
+ theme,
32
+ thresholds
33
+ });
34
34
 
35
35
  const getDataColor = scaleOrdinal({
36
36
  domain: pieData,
@@ -7,7 +7,7 @@ import { Tooltip } from '@visx/visx';
7
7
  import { Box, Fade, useTheme } from '@mui/material';
8
8
 
9
9
  import { Metric } from '../common/timeSeries/models';
10
- import { formatMetricValue } from '../common/timeSeries';
10
+ import { formatMetricValueWithUnit } from '../common/timeSeries';
11
11
  import { getColorFromDataAndTresholds } from '../common/utils';
12
12
  import { margins } from '../common/margins';
13
13
 
@@ -15,7 +15,8 @@ import Thresholds from './Thresholds';
15
15
  import PieData from './PieData';
16
16
  import { GaugeProps } from './models';
17
17
 
18
- interface Props extends Pick<GaugeProps, 'thresholds'> {
18
+ interface Props extends Pick<GaugeProps, 'thresholds' | 'baseColor'> {
19
+ displayAsRaw?: boolean;
19
20
  height: number;
20
21
  metric: Metric;
21
22
  width: number;
@@ -30,7 +31,9 @@ const ResponsiveGauge = ({
30
31
  width,
31
32
  height,
32
33
  thresholds,
33
- metric
34
+ metric,
35
+ displayAsRaw,
36
+ baseColor
34
37
  }: Props): JSX.Element => {
35
38
  const svgRef = useRef<SVGSVGElement>(null);
36
39
 
@@ -50,28 +53,36 @@ const ResponsiveGauge = ({
50
53
  const centerY = innerHeight / 2;
51
54
  const centerX = innerWidth / 2;
52
55
  const radius = Math.min(innerWidth, innerHeight) / 2;
53
- const thresholdValues = flatten([
54
- pluck('value', thresholds.warning),
55
- pluck('value', thresholds.critical)
56
- ]);
56
+ const thresholdValues = thresholds.enabled
57
+ ? flatten([
58
+ pluck('value', thresholds.warning),
59
+ pluck('value', thresholds.critical)
60
+ ])
61
+ : [0];
57
62
  const adaptedMaxValue = Math.max(
58
63
  metric.maximum_value || 0,
59
64
  Math.max(...thresholdValues) * 1.1,
60
65
  head(metric.data) as number
61
66
  );
62
67
 
63
- const pieColor = !thresholds.enabled
64
- ? theme.palette.success.main
65
- : getColorFromDataAndTresholds({
66
- data: metric.data[0],
67
- theme,
68
- thresholds
69
- });
68
+ const pieColor = getColorFromDataAndTresholds({
69
+ baseColor,
70
+ data: metric.data[0],
71
+ theme,
72
+ thresholds
73
+ });
70
74
 
71
75
  const svgTop = svgRef.current?.getBoundingClientRect().top || 0;
72
76
  const svgLeft = svgRef.current?.getBoundingClientRect().left || 0;
73
77
 
74
- const isSmallHeight = height < 250;
78
+ const isSmallWidget = height < 240;
79
+
80
+ const gaugeValue = formatMetricValueWithUnit({
81
+ base: 1000,
82
+ isRaw: displayAsRaw,
83
+ unit: metric.unit,
84
+ value: metric.data[0]
85
+ });
75
86
 
76
87
  return (
77
88
  <Box sx={{ position: 'relative' }}>
@@ -87,6 +98,7 @@ const ResponsiveGauge = ({
87
98
  />
88
99
  <PieData
89
100
  adaptedMaxValue={adaptedMaxValue}
101
+ baseColor={baseColor}
90
102
  metric={metric}
91
103
  radius={radius}
92
104
  thresholds={thresholds}
@@ -97,18 +109,15 @@ const ResponsiveGauge = ({
97
109
  style={{
98
110
  fill: pieColor,
99
111
  ...theme.typography.h3,
100
- fontSize: Math.min(width, height) / 7
112
+ fontSize:
113
+ Math.min(width, height) / 7 -
114
+ (isSmallWidget ? 0 : (gaugeValue?.length || 0) * 2)
101
115
  }}
102
116
  textAnchor="middle"
103
117
  x="50%"
104
- y={isSmallHeight ? 140 : 100 + Math.min(width, height) / 3}
118
+ y={isSmallWidget ? 130 : height - height / 2.3}
105
119
  >
106
- {formatMetricValue({
107
- base: 1000,
108
- unit: metric.unit,
109
- value: metric.data[0]
110
- })}{' '}
111
- {metric.unit}
120
+ {gaugeValue}
112
121
  </text>
113
122
  </svg>
114
123
  <Fade in={tooltipOpen && thresholds.enabled}>
@@ -16,7 +16,7 @@ const Thresholds = ({
16
16
  showTooltip,
17
17
  hideTooltip,
18
18
  metric
19
- }: Omit<GaugeProps, 'width' | 'height'>): JSX.Element => {
19
+ }: Omit<GaugeProps, 'width' | 'height'>): JSX.Element | null => {
20
20
  const theme = useTheme();
21
21
 
22
22
  const criticalThresholdValues = pluck('value', thresholds.critical);
@@ -65,30 +65,7 @@ const Thresholds = ({
65
65
  });
66
66
 
67
67
  if (!thresholds.enabled) {
68
- return (
69
- <Pie
70
- data={successThresholdArc}
71
- endAngle={-Math.PI / 2}
72
- innerRadius={radius - thresholdThickness}
73
- outerRadius={radius}
74
- pieSortValues={() => -1}
75
- pieValue={(d) => d.value}
76
- startAngle={Math.PI / 2}
77
- >
78
- {(pie) => (
79
- <AnimatedPie<{ name: string; value: number }>
80
- {...pie}
81
- animate
82
- getColor={(arc) => successThresholdScaleOrdinal(arc.data.name)}
83
- getKey={(arc) => `${arc.data.name}_${arc.data.value}`}
84
- hideTooltip={hideTooltip}
85
- metric={metric}
86
- showTooltip={showTooltip}
87
- thresholds={thresholds}
88
- />
89
- )}
90
- </Pie>
91
- );
68
+ return null;
92
69
  }
93
70
 
94
71
  const arcs = [
@@ -3,6 +3,7 @@ import { Metric } from '../common/timeSeries/models';
3
3
 
4
4
  export interface GaugeProps {
5
5
  adaptedMaxValue: number;
6
+ baseColor?: string;
6
7
  height: number;
7
8
  hideTooltip: () => void;
8
9
  metric: Metric;
@@ -9,7 +9,7 @@ import { useSpring, animated } from '@react-spring/web';
9
9
  import { alpha, Box, Fade, useTheme } from '@mui/material';
10
10
 
11
11
  import {
12
- formatMetricValue,
12
+ formatMetricValueWithUnit,
13
13
  getMetricWithLatestData
14
14
  } from '../common/timeSeries';
15
15
  import { Metric } from '../common/timeSeries/models';
@@ -34,16 +34,21 @@ const ResponsiveSingleBar = ({
34
34
  data,
35
35
  thresholds,
36
36
  width,
37
- height
37
+ height,
38
+ displayAsRaw,
39
+ baseColor
38
40
  }: Props): JSX.Element => {
39
41
  const theme = useTheme();
40
42
 
41
43
  const metric = getMetricWithLatestData(data) as Metric;
42
44
  const latestMetricData = head(metric.data) as number;
43
- const thresholdValues = flatten([
44
- pluck('value', thresholds.warning),
45
- pluck('value', thresholds.critical)
46
- ]);
45
+ const thresholdValues = thresholds.enabled
46
+ ? flatten([
47
+ pluck('value', thresholds.warning),
48
+ pluck('value', thresholds.critical)
49
+ ])
50
+ : [0];
51
+
47
52
  const adaptedMaxValue = Math.max(
48
53
  metric.maximum_value || 0,
49
54
  Math.max(...thresholdValues) * 1.1,
@@ -66,6 +71,7 @@ const ResponsiveSingleBar = ({
66
71
  const barColor = useMemo(
67
72
  () =>
68
73
  getColorFromDataAndTresholds({
74
+ baseColor,
69
75
  data: latestMetricData,
70
76
  theme,
71
77
  thresholds
@@ -81,12 +87,12 @@ const ResponsiveSingleBar = ({
81
87
  x="50%"
82
88
  y={25}
83
89
  >
84
- {formatMetricValue({
90
+ {formatMetricValueWithUnit({
85
91
  base: 1000,
92
+ isRaw: displayAsRaw,
86
93
  unit: metric.unit,
87
94
  value: metric.data[0]
88
- })}{' '}
89
- {metric.unit}
95
+ })}
90
96
  </text>
91
97
  );
92
98
 
@@ -156,3 +156,13 @@ export const rangeThresholds: Story = {
156
156
  },
157
157
  render: Template
158
158
  };
159
+
160
+ export const RawValue: Story = {
161
+ args: {
162
+ data: dataLastWeek,
163
+ displayAsRaw: true,
164
+ thresholdTooltipLabels: ['Warning', 'Critical'],
165
+ thresholds: [0.5, 1.5]
166
+ },
167
+ render: Template
168
+ };
@@ -4,13 +4,11 @@ import { useTheme } from '@mui/material';
4
4
 
5
5
  import { groupMargin } from './Thresholds';
6
6
 
7
- const lineMargin = 10;
8
-
9
- const thresholdLineHeight = 60 + 2 * lineMargin;
10
-
7
+ export const barHeight = 72;
11
8
  export const margin = 40;
12
9
 
13
- export const barHeight = 50;
10
+ const lineMargin = 10;
11
+ const thresholdLineHeight = barHeight + 2 * lineMargin;
14
12
 
15
13
  interface Props {
16
14
  hideTooltip: () => void;
@@ -1,6 +1,8 @@
1
1
  import { LineChartData, Thresholds } from '../common/models';
2
2
 
3
3
  export interface SingleBarProps {
4
+ baseColor?: string;
4
5
  data?: LineChartData;
6
+ displayAsRaw?: boolean;
5
7
  thresholds: Thresholds;
6
8
  }
@@ -52,3 +52,16 @@ export const critical: Story = {
52
52
  },
53
53
  render: Template
54
54
  };
55
+
56
+ export const rawValue: Story = {
57
+ args: {
58
+ data: dataLastWeek,
59
+ displayAsRaw: true,
60
+ labels: {
61
+ critical: 'Critical',
62
+ warning: 'Warning'
63
+ },
64
+ thresholds: [0.5, 1.5]
65
+ },
66
+ render: Template
67
+ };
@@ -4,7 +4,7 @@ import { Typography, useTheme } from '@mui/material';
4
4
 
5
5
  import { LineChartData, Thresholds } from '../common/models';
6
6
  import {
7
- formatMetricValue,
7
+ formatMetricValueWithUnit,
8
8
  getMetricWithLatestData
9
9
  } from '../common/timeSeries';
10
10
  import { getColorFromDataAndTresholds } from '../common/utils';
@@ -12,7 +12,9 @@ import { getColorFromDataAndTresholds } from '../common/utils';
12
12
  import { useTextStyles } from './Text.styles';
13
13
 
14
14
  interface Props {
15
+ baseColor?: string;
15
16
  data?: LineChartData;
17
+ displayAsRaw?: boolean;
16
18
  labels: {
17
19
  critical: string;
18
20
  warning: string;
@@ -23,7 +25,9 @@ interface Props {
23
25
  export const Text = ({
24
26
  thresholds,
25
27
  data,
26
- labels
28
+ displayAsRaw,
29
+ labels,
30
+ baseColor
27
31
  }: Props): JSX.Element | null => {
28
32
  const theme = useTheme();
29
33
  const { classes } = useTextStyles();
@@ -37,20 +41,21 @@ export const Text = ({
37
41
  const metricUnit = metric?.unit ?? '';
38
42
 
39
43
  const color = getColorFromDataAndTresholds({
44
+ baseColor,
40
45
  data: metricValue,
41
46
  theme,
42
47
  thresholds
43
48
  });
44
49
 
45
50
  const warningThresholdLabels = thresholds.warning.map(({ value }) =>
46
- formatMetricValue({
51
+ formatMetricValueWithUnit({
47
52
  unit: metricUnit,
48
53
  value
49
54
  })
50
55
  );
51
56
 
52
57
  const criticalThresholdLabels = thresholds.critical.map(({ value }) =>
53
- formatMetricValue({
58
+ formatMetricValueWithUnit({
54
59
  unit: metricUnit,
55
60
  value
56
61
  })
@@ -58,24 +63,26 @@ export const Text = ({
58
63
 
59
64
  return (
60
65
  <div className={classes.graphText}>
61
- <Typography sx={{ color }} variant="h3">
62
- {formatMetricValue({
63
- unit: metricUnit,
64
- value: metricValue
65
- })}{' '}
66
- {metricUnit}
66
+ <Typography sx={{ color }} variant="h2">
67
+ <strong>
68
+ {formatMetricValueWithUnit({
69
+ isRaw: displayAsRaw,
70
+ unit: metricUnit,
71
+ value: metricValue
72
+ })}
73
+ </strong>
67
74
  </Typography>
68
75
  {thresholds.enabled && (
69
76
  <div className={classes.thresholds}>
70
77
  <Typography sx={{ color: 'warning.main' }} variant="h5">
71
78
  {labels.warning}
72
79
  {': '}
73
- {warningThresholdLabels.join(' - ')} {metricUnit}
80
+ {warningThresholdLabels.join(' - ')}
74
81
  </Typography>
75
82
  <Typography sx={{ color: 'error.main' }} variant="h5">
76
83
  {labels.critical}
77
84
  {': '}
78
- {criticalThresholdLabels.join(' - ')} {metricUnit}
85
+ {criticalThresholdLabels.join(' - ')}
79
86
  </Typography>
80
87
  </div>
81
88
  )}
@@ -335,10 +335,10 @@ describe('timeSeries', () => {
335
335
  describe(timeSeries.formatMetricValue, () => {
336
336
  const cases: Array<TestCase> = [
337
337
  [218857269, '', 1000, '218.86m'],
338
- [218857269, '', 1024, '208.72M'],
338
+ [218857269, '', 1024, '208.72 M'],
339
339
  [0.12232323445, '', 1000, '0.12'],
340
- [1024, 'B', 1000, '1K'],
341
- [1024, 'B', 1024, '1K'],
340
+ [1024, 'B', 1000, '1 KB'],
341
+ [1024, 'B', 1024, '1 KB'],
342
342
  [null, 'B', 1024, null]
343
343
  ];
344
344
 
@@ -504,3 +504,106 @@ describe('timeSeries', () => {
504
504
  });
505
505
  });
506
506
  });
507
+
508
+ describe('Format value with unit', () => {
509
+ const units = [
510
+ 'B',
511
+ 'bytes',
512
+ 'bytespersecond',
513
+ 'B/s',
514
+ 'B/sec',
515
+ 'o',
516
+ 'octets',
517
+ 'b/s',
518
+ 'b',
519
+ 'ms',
520
+ '%',
521
+ ''
522
+ ];
523
+
524
+ const getExpectedResult = (unit): string => {
525
+ if (unit === '') {
526
+ return '324.23m';
527
+ }
528
+
529
+ return `309.21 M${unit}`;
530
+ };
531
+
532
+ const humanReadableTestCases = units.map((unit) => {
533
+ if (unit === '%') {
534
+ return {
535
+ expectedResult: '45.56%',
536
+ unit,
537
+ value: 45.55678
538
+ };
539
+ }
540
+
541
+ if (unit === 'ms') {
542
+ return {
543
+ expectedResult: '34.23 seconds',
544
+ unit,
545
+ value: 34232
546
+ };
547
+ }
548
+
549
+ return {
550
+ expectedResult: getExpectedResult(unit),
551
+ unit,
552
+ value: 324234232.34233
553
+ };
554
+ });
555
+
556
+ const rawTestCases = units.map((unit) => {
557
+ if (unit === '%') {
558
+ return {
559
+ expectedResult: '45.55678%',
560
+ unit,
561
+ value: 45.55678
562
+ };
563
+ }
564
+
565
+ if (unit === 'ms') {
566
+ return {
567
+ expectedResult: '34232 ms',
568
+ unit,
569
+ value: 34232
570
+ };
571
+ }
572
+
573
+ return {
574
+ expectedResult:
575
+ unit === '' ? '324234232.34233 ' : `324234232.34233 ${unit}`,
576
+ unit,
577
+ value: 324234232.34233
578
+ };
579
+ });
580
+
581
+ describe('Format the value as human readable', () => {
582
+ it.each(humanReadableTestCases)(
583
+ 'formats the value with $unit',
584
+ ({ value, unit, expectedResult }) => {
585
+ expect(
586
+ timeSeries.formatMetricValueWithUnit({
587
+ unit,
588
+ value
589
+ })
590
+ ).toEqual(expectedResult);
591
+ }
592
+ );
593
+ });
594
+
595
+ describe('Format the value as raw', () => {
596
+ it.each(rawTestCases)(
597
+ 'formats the value with $unit',
598
+ ({ value, unit, expectedResult }) => {
599
+ expect(
600
+ timeSeries.formatMetricValueWithUnit({
601
+ isRaw: true,
602
+ unit,
603
+ value
604
+ })
605
+ ).toEqual(expectedResult);
606
+ }
607
+ );
608
+ });
609
+ });
@@ -28,7 +28,10 @@ import {
28
28
  lt,
29
29
  identity,
30
30
  head,
31
- last
31
+ last,
32
+ cond,
33
+ always,
34
+ T
32
35
  } from 'ramda';
33
36
 
34
37
  import { margin } from '../../LineChart/common';
@@ -440,6 +443,37 @@ const getRightScale = ({
440
443
  });
441
444
  };
442
445
 
446
+ const formatTime = (value: number): string => {
447
+ if (value < 1000) {
448
+ return `${numeral(value).format('0.[00]a')} ms`;
449
+ }
450
+
451
+ const t = numeral(value / 1000).format('0.[00]a');
452
+
453
+ return `${t} seconds`;
454
+ };
455
+
456
+ const registerMsUnitToNumeral = (): null => {
457
+ try {
458
+ numeral.register('format', 'milliseconds', {
459
+ format: (value) => {
460
+ return formatTime(value);
461
+ },
462
+ regexps: {
463
+ format: /(ms)/,
464
+ unformat: /(ms)/
465
+ },
466
+ unformat: () => ''
467
+ });
468
+
469
+ return null;
470
+ } catch (_) {
471
+ return null;
472
+ }
473
+ };
474
+
475
+ registerMsUnitToNumeral();
476
+
443
477
  const formatMetricValue = ({
444
478
  value,
445
479
  unit,
@@ -463,11 +497,14 @@ const formatMetricValue = ({
463
497
 
464
498
  const base1024 = base2Units.includes(unit) || Number(base) === 1024;
465
499
 
466
- const formatSuffix = base1024 ? ' ib' : 'a';
500
+ const formatSuffix = cond([
501
+ [equals('ms'), always(' ms')],
502
+ [T, always(base1024 ? ' ib' : 'a')]
503
+ ])(unit);
467
504
 
468
505
  const formattedMetricValue = numeral(Math.abs(value))
469
506
  .format(`0.[00]${formatSuffix}`)
470
- .replace(/\s|i|B/g, '');
507
+ .replace(/iB/g, unit);
471
508
 
472
509
  if (lt(value, 0)) {
473
510
  return `-${formattedMetricValue}`;
@@ -476,6 +513,31 @@ const formatMetricValue = ({
476
513
  return formattedMetricValue;
477
514
  };
478
515
 
516
+ const formatMetricValueWithUnit = ({
517
+ value,
518
+ unit,
519
+ base = 1000,
520
+ isRaw = false
521
+ }: FormatMetricValueProps & { isRaw?: boolean }): string | null => {
522
+ if (isNil(value)) {
523
+ return null;
524
+ }
525
+
526
+ if (isRaw) {
527
+ const unitText = equals('%', unit) ? unit : ` ${unit}`;
528
+
529
+ return `${value}${unitText}`;
530
+ }
531
+
532
+ if (equals('%', unit)) {
533
+ return `${numeral(Math.abs(value)).format('0.[00]')}%`;
534
+ }
535
+
536
+ const formattedMetricValue = formatMetricValue({ base, unit, value });
537
+
538
+ return formattedMetricValue;
539
+ };
540
+
479
541
  const getStackedYScale = ({
480
542
  leftScale,
481
543
  rightScale
@@ -557,5 +619,6 @@ export {
557
619
  getStackedYScale,
558
620
  getTimeValue,
559
621
  bisectDate,
560
- getMetricWithLatestData
622
+ getMetricWithLatestData,
623
+ formatMetricValueWithUnit
561
624
  };
@@ -14,6 +14,8 @@ interface DsData {
14
14
  export interface Metric {
15
15
  average_value: number | null;
16
16
  crit: number | null;
17
+ critical_high_threshold: number | null;
18
+ critical_low_threshold: number | null;
17
19
  data: Array<number>;
18
20
  ds_data: DsData;
19
21
  legend: string;
@@ -21,7 +23,8 @@ export interface Metric {
21
23
  metric: string;
22
24
  minimum_value: number | null;
23
25
  unit: string;
24
- warn: number | null;
26
+ warning_high_threshold: number | null;
27
+ warning_low_threshold: number | null;
25
28
  }
26
29
 
27
30
  type TimeSeries = { timeTick: string };
@@ -17,6 +17,7 @@ import { Theme } from '@mui/material';
17
17
  import { Thresholds } from './models';
18
18
 
19
19
  interface GetColorFromDataAndThresholdsProps {
20
+ baseColor?: string;
20
21
  data: number;
21
22
  theme: Theme;
22
23
  thresholds: Thresholds;
@@ -25,10 +26,11 @@ interface GetColorFromDataAndThresholdsProps {
25
26
  export const getColorFromDataAndTresholds = ({
26
27
  data,
27
28
  thresholds,
28
- theme
29
+ theme,
30
+ baseColor
29
31
  }: GetColorFromDataAndThresholdsProps): string => {
30
32
  if (!thresholds.enabled) {
31
- return theme.palette.success.main;
33
+ return baseColor || theme.palette.primary.main;
32
34
  }
33
35
 
34
36
  const criticalValues = pluck('value', thresholds.critical).sort();
@@ -1,7 +1,7 @@
1
1
  import { includes, map, prop, reject } from 'ramda';
2
2
  import { makeStyles } from 'tss-react/mui';
3
3
 
4
- import { Chip } from '@mui/material';
4
+ import { Chip, ChipProps } from '@mui/material';
5
5
  import { UseAutocompleteProps } from '@mui/material/useAutocomplete';
6
6
 
7
7
  import Autocomplete, { Props as AutocompleteProps } from '..';
@@ -18,7 +18,6 @@ const useStyles = makeStyles()((theme) => ({
18
18
  width: theme.spacing(1.5)
19
19
  },
20
20
  tag: {
21
- backgroundColor: theme.palette.divider,
22
21
  fontSize: theme.typography.caption.fontSize
23
22
  }
24
23
  }));
@@ -33,6 +32,7 @@ export interface Props
33
32
  UseAutocompleteProps<SelectEntry, Multiple, DisableClearable, FreeSolo>,
34
33
  'multiple'
35
34
  > {
35
+ chipProps?: ChipProps;
36
36
  disableSortedOptions?: boolean;
37
37
  getTagLabel?: (option) => string;
38
38
  optionProperty?: string;
@@ -45,6 +45,7 @@ const MultiAutocompleteField = ({
45
45
  optionProperty = 'name',
46
46
  getOptionLabel = (option): string => option.name,
47
47
  getTagLabel = (option): string => option[optionProperty],
48
+ chipProps,
48
49
  ...props
49
50
  }: Props): JSX.Element => {
50
51
  const { classes } = useStyles();
@@ -60,6 +61,7 @@ const MultiAutocompleteField = ({
60
61
  label={getTagLabel(option)}
61
62
  size="medium"
62
63
  {...getTagProps({ index })}
64
+ {...chipProps}
63
65
  />
64
66
  ));
65
67
 
@@ -32,6 +32,7 @@ export type Props = {
32
32
  displayPopupIcon?: boolean;
33
33
  endAdornment?: React.ReactElement;
34
34
  error?: string;
35
+ getOptionItemLabel?: (option) => string;
35
36
  hideInput?: boolean;
36
37
  label: string;
37
38
  loading?: boolean;
@@ -158,6 +159,7 @@ const AutocompleteField = ({
158
159
  autoSize = false,
159
160
  autoSizeDefaultWidth = 0,
160
161
  autoSizeCustomPadding,
162
+ getOptionItemLabel = (option) => option.name,
161
163
  ...autocompleteProps
162
164
  }: Props): JSX.Element => {
163
165
  const { classes, cx } = useStyles({ hideInput });
@@ -249,7 +251,7 @@ const AutocompleteField = ({
249
251
  <Option
250
252
  thumbnailUrl={displayOptionThumbnail ? option.url : undefined}
251
253
  >
252
- {option.name}
254
+ {getOptionItemLabel(option)}
253
255
  </Option>
254
256
  </li>
255
257
  );
@@ -1,6 +1,6 @@
1
1
  import { forwardRef, useCallback } from 'react';
2
2
 
3
- import { isNil } from 'ramda';
3
+ import { equals, isNil } from 'ramda';
4
4
  import { makeStyles } from 'tss-react/mui';
5
5
 
6
6
  import {
@@ -9,7 +9,8 @@ import {
9
9
  TextFieldProps,
10
10
  Theme,
11
11
  Tooltip,
12
- Typography
12
+ Typography,
13
+ Box
13
14
  } from '@mui/material';
14
15
 
15
16
  import { getNormalizedId } from '../../utils';
@@ -18,8 +19,9 @@ import useAutoSize from './useAutoSize';
18
19
 
19
20
  const useStyles = makeStyles<{ displayAsBlock: boolean }>()(
20
21
  (theme: Theme, { displayAsBlock }) => ({
21
- compact: {
22
- fontSize: 'x-small'
22
+ autoSizeCompact: {
23
+ paddingRight: theme.spacing(1),
24
+ paddingTop: theme.spacing(0.6)
23
25
  },
24
26
  hiddenText: {
25
27
  display: 'table',
@@ -140,7 +142,7 @@ const TextField = forwardRef(
140
142
  }, [innerValue, debounced, defaultValue]);
141
143
 
142
144
  return (
143
- <>
145
+ <Box sx={{ width: autoSize ? 'auto' : '100%' }}>
144
146
  <Tooltip placement="top" title={tooltipTitle}>
145
147
  <MuiTextField
146
148
  data-testid={dataTestId}
@@ -162,7 +164,8 @@ const TextField = forwardRef(
162
164
  className: cx(
163
165
  classes.inputBase,
164
166
  {
165
- [classes.transparent]: transparent
167
+ [classes.transparent]: transparent,
168
+ [classes.autoSizeCompact]: autoSize && equals(size, 'compact')
166
169
  },
167
170
  className
168
171
  ),
@@ -195,7 +198,7 @@ const TextField = forwardRef(
195
198
  {rest.value || externalValueForAutoSize || innerValue}
196
199
  </Typography>
197
200
  )}
198
- </>
201
+ </Box>
199
202
  );
200
203
  }
201
204
  );
@@ -160,7 +160,6 @@ const ContentEditable = ({
160
160
  className,
161
161
  isFocused && classes.inputFocused
162
162
  )}
163
- id={namespace}
164
163
  >
165
164
  {editable && isTextEmpty && (
166
165
  <Typography className={classes.placeholder}>
@@ -56,7 +56,7 @@ describe('Rich Text Editor', () => {
56
56
  .should('be.visible')
57
57
  .and('have.value', '');
58
58
 
59
- cy.get('#RichTextEditor').contains('Type here...');
59
+ cy.findByLabelText('RichTextEditor').parent().contains('Type here...');
60
60
  });
61
61
 
62
62
  it('displays changes with undo redo buttons', () => {
@@ -187,11 +187,6 @@ describe('Rich Text Editor', () => {
187
187
  });
188
188
 
189
189
  it('displays editor when editable props is false and an initialState exist', () => {
190
- cy.get('[data-testid="RichTextEditor"]').should(
191
- 'have.attr',
192
- 'contenteditable',
193
- 'false'
194
- );
195
190
  cy.findByLabelText('Undo').should('not.exist');
196
191
  cy.findByLabelText('Redo').should('not.exist');
197
192
  cy.findByLabelText('bold').should('not.exist');
@@ -216,11 +211,6 @@ describe('Rich Text Editor', () => {
216
211
  });
217
212
 
218
213
  it('displays editor when editable props is false and an initialState exist', () => {
219
- cy.get('[data-testid="RichTextEditor"]').should(
220
- 'have.attr',
221
- 'contenteditable',
222
- 'false'
223
- );
224
214
  cy.findByLabelText('Undo').should('be.disabled');
225
215
  cy.findByLabelText('Redo').should('be.disabled');
226
216
  cy.findByLabelText('bold').should('be.disabled');
@@ -245,12 +235,14 @@ describe('Rich Text Editor', () => {
245
235
  });
246
236
  });
247
237
  it('displays editor with custom namespace', () => {
248
- cy.get('#CypressComponentTest > p').should('be.exist');
238
+ cy.get('[data-testid="CypressComponentTest"] > p').should('be.exist');
249
239
  cy.get('[data-testid="CypressComponentTest"]').should('be.exist');
250
240
  });
251
241
 
252
242
  it('displays editor with custom placeholder', () => {
253
- cy.get('#CypressComponentTest > p').contains('Type Cypress test...');
243
+ cy.get('[data-testid="CypressComponentTest"]')
244
+ .parent()
245
+ .contains('Type Cypress test...');
254
246
  });
255
247
 
256
248
  it('displays editor with custom minimum input height', () => {
@@ -23,6 +23,7 @@ import FloatingLinkEditorPlugin from './plugins/FloatingLinkEditorPlugin';
23
23
  export interface RichTextEditorProps {
24
24
  contentClassName?: string;
25
25
  disabled?: boolean;
26
+ displayBlockButtons?: boolean;
26
27
  displayMacrosButton?: boolean;
27
28
  editable: boolean;
28
29
  editorState?: string;
@@ -132,9 +133,10 @@ const RichTextEditor = ({
132
133
  onBlur,
133
134
  contentClassName,
134
135
  displayMacrosButton = false,
135
- disabled,
136
+ disabled = false,
136
137
  openLinkInNewTab = true,
137
- initialize
138
+ initialize,
139
+ displayBlockButtons = true
138
140
  }: RichTextEditorProps): JSX.Element => {
139
141
  const { classes } = useStyles({ toolbarPositions });
140
142
 
@@ -174,6 +176,7 @@ const RichTextEditor = ({
174
176
  <div className={classes.toolbar}>
175
177
  <ToolbarPlugin
176
178
  disabled={disabled}
179
+ displayBlockButtons={displayBlockButtons}
177
180
  displayMacrosButton={displayMacrosButton}
178
181
  editable={editable}
179
182
  />
@@ -1,7 +1,11 @@
1
+ import { useEffect, useState } from 'react';
2
+
1
3
  import {
2
4
  AutoLinkPlugin,
3
5
  LinkMatcher
4
6
  } from '@lexical/react/LexicalAutoLinkPlugin';
7
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
8
+ import { BLUR_COMMAND, COMMAND_PRIORITY_LOW, FOCUS_COMMAND } from 'lexical';
5
9
 
6
10
  interface LinkAttributes {
7
11
  rel?: null | string;
@@ -56,7 +60,42 @@ const AutoCompleteLinkPlugin = ({
56
60
  openLinkInNewTab
57
61
  }: {
58
62
  openLinkInNewTab: boolean;
59
- }): JSX.Element => {
63
+ }): JSX.Element | null => {
64
+ const [editor] = useLexicalComposerContext();
65
+ const [hasFocus, setFocus] = useState(false);
66
+
67
+ useEffect(
68
+ () =>
69
+ editor.registerCommand(
70
+ BLUR_COMMAND,
71
+ () => {
72
+ setFocus(false);
73
+
74
+ return false;
75
+ },
76
+ COMMAND_PRIORITY_LOW
77
+ ),
78
+ []
79
+ );
80
+
81
+ useEffect(
82
+ () =>
83
+ editor.registerCommand(
84
+ FOCUS_COMMAND,
85
+ () => {
86
+ setFocus(true);
87
+
88
+ return false;
89
+ },
90
+ COMMAND_PRIORITY_LOW
91
+ ),
92
+ []
93
+ );
94
+
95
+ if (!hasFocus) {
96
+ return null;
97
+ }
98
+
60
99
  return <AutoLinkPlugin matchers={getMatchers(openLinkInNewTab)} />;
61
100
  };
62
101
 
@@ -11,7 +11,7 @@ import { mergeRegister } from '@lexical/utils';
11
11
  import { useAtom, useAtomValue, useSetAtom } from 'jotai';
12
12
  import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
13
13
  import { useTranslation } from 'react-i18next';
14
- import { equals, isNil } from 'ramda';
14
+ import { dec, equals, gt, isNil, replace } from 'ramda';
15
15
 
16
16
  import { Popper, IconButton, Paper, Link, Box } from '@mui/material';
17
17
  import EditIcon from '@mui/icons-material/Edit';
@@ -56,6 +56,7 @@ const FloatingLinkEditor = ({
56
56
  x: 0,
57
57
  y: 0
58
58
  });
59
+ const [editedUrl, setEditedUrl] = useState('');
59
60
 
60
61
  const [editMode, setEditMode] = useAtom(editLinkModeAtom);
61
62
  const [linkUrl, setLinkUrl] = useAtom(linkValueAtom);
@@ -65,9 +66,10 @@ const FloatingLinkEditor = ({
65
66
  const acceptOrCancelNewLinkValue = useCallback(
66
67
  (event): void => {
67
68
  const { value } = event.target;
68
- if (event.key === 'Enter') {
69
- event.preventDefault();
70
69
 
70
+ event.preventDefault();
71
+
72
+ if (event.key === 'Enter') {
71
73
  if (value !== '') {
72
74
  editor.dispatchCommand(TOGGLE_LINK_COMMAND, {
73
75
  target: openLinkInNewTab ? '_blank' : undefined,
@@ -75,14 +77,36 @@ const FloatingLinkEditor = ({
75
77
  });
76
78
  }
77
79
  setEditMode(false);
78
- } else if (event.key === 'Escape') {
79
- event.preventDefault();
80
+ }
81
+
82
+ if (event.key === 'Escape') {
80
83
  setEditMode(false);
81
84
  }
82
85
  },
83
86
  [setEditMode, setLinkUrl]
84
87
  );
85
88
 
89
+ const enterInEditMode = useCallback(() => {
90
+ setEditedUrl(linkUrl);
91
+ setEditMode(true);
92
+ }, [linkUrl]);
93
+
94
+ const changeValue = useCallback((event): void => {
95
+ const { value } = event.target;
96
+
97
+ const matched = value.match(/https?:\/\//g);
98
+
99
+ if (gt(matched.length, 1)) {
100
+ setEditedUrl(
101
+ replace(matched.join(''), matched[dec(matched.length)], value)
102
+ );
103
+
104
+ return;
105
+ }
106
+
107
+ setEditedUrl(value);
108
+ }, []);
109
+
86
110
  useEffect(() => {
87
111
  const isPositioned =
88
112
  rangeRect && (!equals(rangeRect.x, 0) || !equals(rangeRect.y, 0));
@@ -126,9 +150,9 @@ const FloatingLinkEditor = ({
126
150
  <InputField
127
151
  autoFocus
128
152
  dataTestId="InputLinkField"
129
- defaultValue={linkUrl}
130
153
  label={t(labelInputLink)}
131
154
  size="small"
155
+ value={editedUrl}
132
156
  onBlur={(event): void => {
133
157
  const { value } = event.target;
134
158
 
@@ -139,7 +163,8 @@ const FloatingLinkEditor = ({
139
163
  }
140
164
  setEditMode(false);
141
165
  }}
142
- onKeyDown={acceptOrCancelNewLinkValue}
166
+ onChange={changeValue}
167
+ onKeyUp={acceptOrCancelNewLinkValue}
143
168
  />
144
169
  ) : (
145
170
  <Box component="span" sx={{ margin: '10px' }}>
@@ -156,7 +181,7 @@ const FloatingLinkEditor = ({
156
181
  aria-label={labelEditLink}
157
182
  size="small"
158
183
  sx={{ marginLeft: '5px' }}
159
- onClick={(): void => setEditMode(true)}
184
+ onClick={enterInEditMode}
160
185
  >
161
186
  <EditIcon fontSize="small" />
162
187
  </IconButton>
@@ -8,7 +8,8 @@ import MacrosButton from './MacrosButton';
8
8
  import BlockButtons from './BlockButtons';
9
9
 
10
10
  interface Props {
11
- disabled?: boolean;
11
+ disabled: boolean;
12
+ displayBlockButtons: boolean;
12
13
  displayMacrosButton?: boolean;
13
14
  editable: boolean;
14
15
  }
@@ -33,15 +34,20 @@ export const useStyles = makeStyles()((theme) => ({
33
34
  const ToolbarPlugin = ({
34
35
  editable,
35
36
  displayMacrosButton,
36
- disabled
37
+ disabled,
38
+ displayBlockButtons
37
39
  }: Props): JSX.Element | null => {
38
40
  const { classes } = useStyles();
39
41
 
40
42
  return editable ? (
41
43
  <div className={classes.container}>
42
44
  <UndoRedoButtons disabled={disabled} />
43
- <Divider flexItem orientation="vertical" />
44
- <BlockButtons disabled={disabled} />
45
+ {displayBlockButtons && (
46
+ <>
47
+ <Divider flexItem orientation="vertical" />
48
+ <BlockButtons disabled={disabled} />
49
+ </>
50
+ )}
45
51
  <Divider flexItem orientation="vertical" />
46
52
  <FormatButtons disabled={disabled} />
47
53
  {displayMacrosButton && (
@@ -48,8 +48,7 @@ const getInputBaseRootStyle = ({
48
48
 
49
49
  if (equals(size, 'compact')) {
50
50
  return {
51
- padding: '8px 8px',
52
- paddingRight: '0px'
51
+ padding: '0px 0px 0px 8px'
53
52
  };
54
53
  }
55
54
  if (equals(size, 'small')) {
@@ -75,7 +74,6 @@ const getInputBaseRootStyle = ({
75
74
  const getInputBaseInputStyle = ({ size }: InputBaseProps): CSSInterpolation => {
76
75
  if (equals(size, 'compact')) {
77
76
  return {
78
- fontSize: 'x-small',
79
77
  minHeight: '32px'
80
78
  };
81
79
  }
@@ -123,6 +121,7 @@ export const getTheme = (mode: ThemeMode): ThemeOptions => ({
123
121
  MuiChip: {
124
122
  styleOverrides: {
125
123
  root: ({ ownerState, theme }) => ({
124
+ backgroundColor: !ownerState.color && theme.palette.divider,
126
125
  ...(equals(ownerState.size, 'medium') && {
127
126
  borderRadius: theme.spacing(1.25),
128
127
  fontSize: theme.typography.body2.fontSize,
@@ -189,7 +188,10 @@ export const getTheme = (mode: ThemeMode): ThemeOptions => ({
189
188
  },
190
189
  MuiInputBase: {
191
190
  styleOverrides: {
192
- root: ({ ownerState }) => getInputBaseInputStyle(ownerState)
191
+ root: ({ ownerState, theme }) => ({
192
+ ...getInputBaseInputStyle(ownerState),
193
+ backgroundColor: theme.palette.background.paper
194
+ })
193
195
  }
194
196
  },
195
197
  MuiList: {
@@ -90,14 +90,6 @@ const useFetchQuery = <T extends object>({
90
90
  }
91
91
  };
92
92
 
93
- useEffect(() => {
94
- return (): void => {
95
- queryClient.cancelQueries(getQueryKey());
96
- };
97
- }, []);
98
-
99
- manageError();
100
-
101
93
  const prefetchQuery = ({ endpointParams, queryKey }): void => {
102
94
  queryClient.prefetchQuery(
103
95
  queryKey,
@@ -162,6 +154,16 @@ const useFetchQuery = <T extends object>({
162
154
 
163
155
  const errorData = queryData.data as ResponseError | undefined;
164
156
 
157
+ useEffect(() => {
158
+ return (): void => {
159
+ queryClient.cancelQueries(getQueryKey());
160
+ };
161
+ }, []);
162
+
163
+ useEffect(() => {
164
+ manageError();
165
+ }, [queryData.data]);
166
+
165
167
  return {
166
168
  ...omit(['data'], queryData),
167
169
  data,
@@ -0,0 +1,22 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import Switch from './Switch';
4
+
5
+ const meta: Meta<typeof Switch> = {
6
+ component: Switch
7
+ };
8
+
9
+ export default meta;
10
+ type Story = StoryObj<typeof Switch>;
11
+
12
+ export const notChecked: Story = {
13
+ args: {
14
+ checked: false
15
+ }
16
+ };
17
+
18
+ export const checked: Story = {
19
+ args: {
20
+ checked: true
21
+ }
22
+ };
@@ -0,0 +1,13 @@
1
+ import { makeStyles } from 'tss-react/mui';
2
+
3
+ export const useSwitchStyles = makeStyles()((theme) => ({
4
+ switch: {
5
+ '& .MuiSwitch-thumb': {
6
+ backgroundColor: theme.palette.common.white
7
+ },
8
+ '& span[data-checked="true"] + .MuiSwitch-track': {
9
+ backgroundColor: theme.palette.success.main,
10
+ opacity: 1
11
+ }
12
+ }
13
+ }));
@@ -0,0 +1,18 @@
1
+ import { SwitchProps, Switch as MuiSwitch } from '@mui/material';
2
+
3
+ import { useSwitchStyles } from './Switch.styles';
4
+
5
+ const Switch = ({ checked, ...props }: SwitchProps): JSX.Element => {
6
+ const { classes } = useSwitchStyles();
7
+
8
+ return (
9
+ <MuiSwitch
10
+ checked={checked}
11
+ className={classes.switch}
12
+ data-checked={checked}
13
+ {...props}
14
+ />
15
+ );
16
+ };
17
+
18
+ export default Switch;
@@ -0,0 +1 @@
1
+ export { default as Switch } from './Switch/Switch';
@@ -48,10 +48,12 @@ const useStyles = makeStyles<{
48
48
  }
49
49
  },
50
50
  modalActions: {
51
+ bottom: 0,
51
52
  display: 'flex',
52
53
  flexDirection: 'row',
53
54
  gap: theme.spacing(2),
54
- justifyContent: 'flex-end'
55
+ justifyContent: 'flex-end',
56
+ position: 'sticky'
55
57
  },
56
58
  modalBody: {
57
59
  '& > p': {
@@ -10,3 +10,4 @@ export * from './List';
10
10
  export * from './ItemComposition';
11
11
  export * from './Avatar';
12
12
  export * from './CollapsibleItem';
13
+ export * from './Inputs';