@centreon/ui 24.4.2 → 24.4.4
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 +1 -1
- package/src/Graph/HeatMap/HeatMap.styles.tsx +1 -0
- package/src/Graph/HeatMap/ResponsiveHeatMap.tsx +12 -8
- package/src/Graph/HeatMap/model.ts +1 -0
- package/src/Graph/LineChart/BasicComponents/useFilterLines.ts +2 -4
- package/src/Graph/LineChart/Legend/InteractiveValue.tsx +3 -3
- package/src/Graph/LineChart/Legend/Legend.styles.ts +78 -32
- package/src/Graph/LineChart/Legend/LegendContent.tsx +12 -6
- package/src/Graph/LineChart/Legend/LegendHeader.tsx +69 -21
- package/src/Graph/LineChart/Legend/index.tsx +51 -23
- package/src/Graph/LineChart/Legend/models.ts +5 -0
- package/src/Graph/LineChart/Legend/useLegend.ts +9 -10
- package/src/Graph/LineChart/LineChart.tsx +12 -1
- package/src/InputField/Select/Autocomplete/Connected/Multi/index.test.tsx +1 -0
- package/src/InputField/Select/Autocomplete/Connected/index.test.tsx +1 -0
- package/src/InputField/Select/Autocomplete/Connected/index.tsx +3 -0
- package/src/InputField/Text/index.tsx +6 -1
- package/src/ThemeProvider/palettes.ts +4 -4
- package/src/api/buildListingEndpoint/models.ts +2 -1
- package/src/api/customFetch.ts +12 -3
- package/src/api/useFetchQuery/index.ts +15 -4
- package/src/api/useMutationQuery/index.ts +4 -1
- package/src/utils/useInfiniteScrollListing.ts +22 -7
- package/src/utils/useLocaleDateTimeFormat/index.ts +3 -0
- package/src/Graph/LineChart/Legend/Marker.tsx +0 -43
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
|
|
3
3
|
import { scaleLinear } from '@visx/scale';
|
|
4
|
-
import { equals, gt, lt } from 'ramda';
|
|
4
|
+
import { T, equals, gt, lt } from 'ramda';
|
|
5
5
|
|
|
6
6
|
import { Box } from '@mui/material';
|
|
7
7
|
|
|
@@ -20,7 +20,8 @@ const ResponsiveHeatMap = <TData,>({
|
|
|
20
20
|
tiles,
|
|
21
21
|
arrowClassName,
|
|
22
22
|
tooltipContent,
|
|
23
|
-
tileSizeFixed
|
|
23
|
+
tileSizeFixed,
|
|
24
|
+
displayTooltipCondition = T
|
|
24
25
|
}: HeatMapProps<TData> & { width: number }): JSX.Element | null => {
|
|
25
26
|
const { classes, cx } = useHeatMapStyles();
|
|
26
27
|
|
|
@@ -73,12 +74,15 @@ const ResponsiveHeatMap = <TData,>({
|
|
|
73
74
|
tooltip: classes.heatMapTooltip
|
|
74
75
|
}}
|
|
75
76
|
followCursor={false}
|
|
76
|
-
label={
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
label={
|
|
78
|
+
displayTooltipCondition?.(data) &&
|
|
79
|
+
tooltipContent?.({
|
|
80
|
+
backgroundColor,
|
|
81
|
+
data,
|
|
82
|
+
id,
|
|
83
|
+
isSmallestSize
|
|
84
|
+
})
|
|
85
|
+
}
|
|
82
86
|
position="right-start"
|
|
83
87
|
>
|
|
84
88
|
<div className={classes.heatMapTileContent}>
|
|
@@ -21,6 +21,7 @@ export interface HeatMapProps<TData> {
|
|
|
21
21
|
data,
|
|
22
22
|
isSmallestSize
|
|
23
23
|
}: ChildrenProps<TData>) => ReactElement | boolean | null;
|
|
24
|
+
displayTooltipCondition?: (data: TData) => boolean;
|
|
24
25
|
tileSizeFixed?: boolean;
|
|
25
26
|
tiles: Array<Tile<TData>>;
|
|
26
27
|
tooltipContent?: ({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Dispatch, SetStateAction, useEffect } from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { equals, propEq, reject } from 'ramda';
|
|
4
4
|
|
|
5
5
|
import { Line } from '../../common/timeSeries/models';
|
|
6
6
|
|
|
@@ -51,9 +51,7 @@ const useFilterLines = ({
|
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
setLinesGraph(sortedLines);
|
|
54
|
+
setLinesGraph(filteredLines);
|
|
57
55
|
}, [lines, displayThreshold]);
|
|
58
56
|
|
|
59
57
|
return { displayedLines, newLines: linesGraph ?? lines };
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { Typography } from '@mui/material';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useLegendValueStyles } from './Legend.styles';
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
value?: string | null;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
const InteractiveValue = ({ value }: Props): JSX.Element | null => {
|
|
10
|
-
const { classes } =
|
|
10
|
+
const { classes } = useLegendValueStyles({});
|
|
11
11
|
if (!value) {
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
|
-
<Typography className={classes.
|
|
16
|
+
<Typography className={classes.text} variant="h6">
|
|
17
17
|
{value}
|
|
18
18
|
</Typography>
|
|
19
19
|
);
|
|
@@ -6,66 +6,112 @@ interface MakeStylesProps {
|
|
|
6
6
|
limitLegendRows?: boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export const legendWidth = 21;
|
|
10
|
+
const legendItemHeight = 5.25;
|
|
11
|
+
const legendItemHeightCompact = 1.75;
|
|
12
|
+
|
|
9
13
|
export const useStyles = makeStyles<MakeStylesProps>()(
|
|
10
14
|
(theme, { limitLegendRows }) => ({
|
|
11
15
|
highlight: {
|
|
12
16
|
color: theme.typography.body1.color
|
|
13
17
|
},
|
|
14
18
|
item: {
|
|
15
|
-
|
|
16
|
-
gridTemplateColumns: 'min-content minmax(50px, 1fr)',
|
|
17
|
-
marginBottom: theme.spacing(1)
|
|
19
|
+
minWidth: theme.spacing(legendWidth)
|
|
18
20
|
},
|
|
19
21
|
items: {
|
|
22
|
+
'&[data-mode="compact"]': {
|
|
23
|
+
gridAutoRows: theme.spacing(legendItemHeightCompact),
|
|
24
|
+
height: limitLegendRows
|
|
25
|
+
? theme.spacing(legendItemHeightCompact * 2 + 1.5)
|
|
26
|
+
: 'unset'
|
|
27
|
+
},
|
|
28
|
+
columnGap: theme.spacing(3),
|
|
20
29
|
display: 'grid',
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
maxHeight: limitLegendRows
|
|
30
|
+
gridAutoRows: theme.spacing(legendItemHeight),
|
|
31
|
+
gridTemplateColumns: `repeat(auto-fit, ${theme.spacing(legendWidth)})`,
|
|
32
|
+
maxHeight: limitLegendRows
|
|
33
|
+
? theme.spacing(legendItemHeight * 2 + 1)
|
|
34
|
+
: 'unset',
|
|
24
35
|
overflowY: 'auto',
|
|
36
|
+
rowGap: theme.spacing(1),
|
|
25
37
|
width: '100%'
|
|
26
38
|
},
|
|
27
39
|
legend: {
|
|
28
40
|
marginLeft: margin.left,
|
|
29
41
|
marginRight: margin.right,
|
|
30
|
-
|
|
31
|
-
overflowX: 'hidden',
|
|
32
|
-
overflowY: 'auto'
|
|
42
|
+
overflow: 'hidden'
|
|
33
43
|
},
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
minMaxAvgContainer: {
|
|
45
|
+
columnGap: theme.spacing(0.5),
|
|
46
|
+
display: 'grid',
|
|
47
|
+
gridTemplateColumns: 'repeat(2, min-content)',
|
|
48
|
+
whiteSpace: 'nowrap'
|
|
37
49
|
},
|
|
38
|
-
|
|
50
|
+
normal: {
|
|
51
|
+
color: theme.palette.text.primary
|
|
52
|
+
},
|
|
53
|
+
toggable: {
|
|
54
|
+
cursor: 'pointer'
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
interface StylesProps {
|
|
60
|
+
color?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const useLegendHeaderStyles = makeStyles<StylesProps>()(
|
|
64
|
+
(theme, { color }) => ({
|
|
65
|
+
container: {
|
|
39
66
|
display: 'flex',
|
|
40
67
|
flexDirection: 'row',
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
overflow: 'hidden',
|
|
44
|
-
textOverflow: 'ellipsis'
|
|
68
|
+
gap: theme.spacing(0.5),
|
|
69
|
+
width: '100%'
|
|
45
70
|
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
marginLeft: 'auto',
|
|
49
|
-
marginRight: theme.spacing(0.5),
|
|
50
|
-
overflow: 'hidden',
|
|
51
|
-
textOverflow: 'ellipsis'
|
|
71
|
+
disabled: {
|
|
72
|
+
color: theme.palette.text.disabled
|
|
52
73
|
},
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
icon: {
|
|
75
|
+
backgroundColor: color,
|
|
76
|
+
borderRadius: theme.shape.borderRadius,
|
|
77
|
+
height: theme.spacing(1.5),
|
|
78
|
+
minWidth: theme.spacing(1.5),
|
|
79
|
+
width: theme.spacing(1.5)
|
|
80
|
+
},
|
|
81
|
+
legendName: {
|
|
82
|
+
'&[data-mode="compact"]': {
|
|
83
|
+
maxWidth: theme.spacing(legendWidth * 0.5)
|
|
84
|
+
},
|
|
85
|
+
maxWidth: theme.spacing(legendWidth * 0.75)
|
|
86
|
+
},
|
|
87
|
+
markerAndLegendName: {
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
display: 'flex',
|
|
90
|
+
flexDirection: 'row',
|
|
91
|
+
gap: theme.spacing(0.5)
|
|
55
92
|
},
|
|
56
93
|
minMaxAvgContainer: {
|
|
57
94
|
columnGap: theme.spacing(0.5),
|
|
58
95
|
display: 'grid',
|
|
59
|
-
gridAutoRows: theme.spacing(2),
|
|
60
96
|
gridTemplateColumns: 'repeat(2, min-content)',
|
|
61
97
|
whiteSpace: 'nowrap'
|
|
62
98
|
},
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
},
|
|
67
|
-
toggable: {
|
|
68
|
-
cursor: 'pointer'
|
|
99
|
+
text: {
|
|
100
|
+
fontWeight: theme.typography.fontWeightMedium,
|
|
101
|
+
lineHeight: 1
|
|
69
102
|
}
|
|
70
103
|
})
|
|
71
104
|
);
|
|
105
|
+
|
|
106
|
+
export const useLegendContentStyles = makeStyles()((theme) => ({
|
|
107
|
+
minMaxAvgValue: { fontWeight: theme.typography.fontWeightMedium },
|
|
108
|
+
text: {
|
|
109
|
+
lineHeight: 0.9
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
export const useLegendValueStyles = makeStyles()({
|
|
114
|
+
text: {
|
|
115
|
+
lineHeight: 1.4
|
|
116
|
+
}
|
|
117
|
+
});
|
|
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
2
2
|
|
|
3
3
|
import { Typography } from '@mui/material';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { useLegendContentStyles } from './Legend.styles';
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
8
|
data: string;
|
|
@@ -10,15 +10,21 @@ interface Props {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const LegendContent = ({ data, label }: Props): JSX.Element => {
|
|
13
|
-
const { classes } =
|
|
13
|
+
const { classes, cx } = useLegendContentStyles();
|
|
14
14
|
|
|
15
15
|
const { t } = useTranslation();
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
|
-
<div data-testid={label}>
|
|
19
|
-
<Typography variant="caption">
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
<div className={classes.text} data-testid={label}>
|
|
19
|
+
<Typography className={classes.text} component="span" variant="caption">
|
|
20
|
+
{t(label)}:{' '}
|
|
21
|
+
<Typography
|
|
22
|
+
className={cx(classes.minMaxAvgValue, classes.text)}
|
|
23
|
+
component="span"
|
|
24
|
+
variant="caption"
|
|
25
|
+
>
|
|
26
|
+
{data}
|
|
27
|
+
</Typography>
|
|
22
28
|
</Typography>
|
|
23
29
|
</div>
|
|
24
30
|
);
|
|
@@ -1,43 +1,91 @@
|
|
|
1
1
|
import { includes, isEmpty, split } from 'ramda';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Typography } from '@mui/material';
|
|
4
4
|
|
|
5
|
+
import { EllipsisTypography, formatMetricValue } from '../../..';
|
|
5
6
|
import { Line } from '../../common/timeSeries/models';
|
|
7
|
+
import { Tooltip } from '../../../components';
|
|
6
8
|
|
|
7
|
-
import {
|
|
9
|
+
import { useLegendHeaderStyles } from './Legend.styles';
|
|
10
|
+
import { LegendDisplayMode } from './models';
|
|
11
|
+
import LegendContent from './LegendContent';
|
|
8
12
|
|
|
9
13
|
interface Props {
|
|
14
|
+
color: string;
|
|
15
|
+
disabled?: boolean;
|
|
10
16
|
line: Line;
|
|
17
|
+
minMaxAvg?;
|
|
18
|
+
value?: string | null;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
|
-
const LegendHeader = ({
|
|
14
|
-
|
|
21
|
+
const LegendHeader = ({
|
|
22
|
+
line,
|
|
23
|
+
color,
|
|
24
|
+
disabled,
|
|
25
|
+
value,
|
|
26
|
+
minMaxAvg
|
|
27
|
+
}: Props): JSX.Element => {
|
|
28
|
+
const { classes, cx } = useLegendHeaderStyles({ color });
|
|
29
|
+
|
|
15
30
|
const { unit, name, legend } = line;
|
|
16
31
|
|
|
17
32
|
const legendName = legend || name;
|
|
18
|
-
const
|
|
33
|
+
const hasUnit = !isEmpty(unit);
|
|
34
|
+
const unitName = `(${unit})`;
|
|
19
35
|
const metricName = includes('#', legendName)
|
|
20
36
|
? split('#')(legendName)[1]
|
|
21
37
|
: legendName;
|
|
22
38
|
|
|
39
|
+
const getEndText = (): string => {
|
|
40
|
+
if (value) {
|
|
41
|
+
return `${value}${hasUnit ? ` ${unit}` : ''}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return hasUnit ? ` ${unitName}` : '';
|
|
45
|
+
};
|
|
46
|
+
|
|
23
47
|
return (
|
|
24
|
-
<div className={classes.
|
|
25
|
-
<Tooltip
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
<div className={classes.container}>
|
|
49
|
+
<Tooltip
|
|
50
|
+
followCursor={false}
|
|
51
|
+
label={
|
|
52
|
+
minMaxAvg ? (
|
|
53
|
+
<div>
|
|
54
|
+
<Typography>{`${legendName} ${unitName}`}</Typography>
|
|
55
|
+
<div className={classes.minMaxAvgContainer}>
|
|
56
|
+
{minMaxAvg.map(({ label, value: subValue }) => (
|
|
57
|
+
<LegendContent
|
|
58
|
+
data={formatMetricValue({
|
|
59
|
+
unit: line.unit,
|
|
60
|
+
value: subValue
|
|
61
|
+
})}
|
|
62
|
+
key={label}
|
|
63
|
+
label={label}
|
|
64
|
+
/>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
) : (
|
|
69
|
+
`${legendName} ${unitName}`
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
placement="top"
|
|
38
73
|
>
|
|
39
|
-
|
|
40
|
-
|
|
74
|
+
<div className={classes.markerAndLegendName}>
|
|
75
|
+
<div className={cx(classes.icon, { [classes.disabled]: disabled })} />
|
|
76
|
+
<EllipsisTypography
|
|
77
|
+
className={cx(classes.text, classes.legendName)}
|
|
78
|
+
data-mode={
|
|
79
|
+
value ? LegendDisplayMode.Compact : LegendDisplayMode.Normal
|
|
80
|
+
}
|
|
81
|
+
>
|
|
82
|
+
{metricName}
|
|
83
|
+
</EllipsisTypography>
|
|
84
|
+
</div>
|
|
85
|
+
</Tooltip>
|
|
86
|
+
{hasUnit && (
|
|
87
|
+
<Typography className={classes.text}>{getEndText()}</Typography>
|
|
88
|
+
)}
|
|
41
89
|
</div>
|
|
42
90
|
);
|
|
43
91
|
};
|
|
@@ -16,8 +16,7 @@ import { timeValueAtom } from '../InteractiveComponents/interactionWithGraphAtom
|
|
|
16
16
|
import InteractiveValue from './InteractiveValue';
|
|
17
17
|
import { useStyles } from './Legend.styles';
|
|
18
18
|
import LegendHeader from './LegendHeader';
|
|
19
|
-
import
|
|
20
|
-
import { GetMetricValueProps } from './models';
|
|
19
|
+
import { GetMetricValueProps, LegendDisplayMode } from './models';
|
|
21
20
|
import useInteractiveValues from './useInteractiveValues';
|
|
22
21
|
import useLegend from './useLegend';
|
|
23
22
|
import LegendContent from './LegendContent';
|
|
@@ -29,6 +28,7 @@ interface Props {
|
|
|
29
28
|
lines: Array<Line>;
|
|
30
29
|
renderExtraComponent?: ReactNode;
|
|
31
30
|
setLinesGraph: Dispatch<SetStateAction<Array<Line> | null>>;
|
|
31
|
+
shouldDisplayLegendInCompactMode: boolean;
|
|
32
32
|
timeSeries: Array<TimeValue>;
|
|
33
33
|
toggable?: boolean;
|
|
34
34
|
xScale;
|
|
@@ -43,7 +43,8 @@ const MainLegend = ({
|
|
|
43
43
|
renderExtraComponent,
|
|
44
44
|
displayAnchor = true,
|
|
45
45
|
setLinesGraph,
|
|
46
|
-
xScale
|
|
46
|
+
xScale,
|
|
47
|
+
shouldDisplayLegendInCompactMode
|
|
47
48
|
}: Props): JSX.Element => {
|
|
48
49
|
const { classes, cx } = useStyles({ limitLegendRows });
|
|
49
50
|
const theme = useTheme();
|
|
@@ -85,9 +86,13 @@ const MainLegend = ({
|
|
|
85
86
|
selectMetricLine(metric_id);
|
|
86
87
|
};
|
|
87
88
|
|
|
89
|
+
const mode = shouldDisplayLegendInCompactMode
|
|
90
|
+
? LegendDisplayMode.Compact
|
|
91
|
+
: LegendDisplayMode.Normal;
|
|
92
|
+
|
|
88
93
|
return (
|
|
89
94
|
<div className={classes.legend}>
|
|
90
|
-
<div className={classes.items}>
|
|
95
|
+
<div className={classes.items} data-mode={mode}>
|
|
91
96
|
{displayedLines.map((line) => {
|
|
92
97
|
const { color, display, highlight, metric_id } = line;
|
|
93
98
|
|
|
@@ -126,22 +131,37 @@ const MainLegend = ({
|
|
|
126
131
|
onMouseEnter={(): void => highlightLine(metric_id)}
|
|
127
132
|
onMouseLeave={(): void => clearHighlight()}
|
|
128
133
|
>
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
134
|
+
<LegendHeader
|
|
135
|
+
color={markerColor}
|
|
136
|
+
disabled={!display}
|
|
137
|
+
line={line}
|
|
138
|
+
minMaxAvg={
|
|
139
|
+
shouldDisplayLegendInCompactMode ? minMaxAvg : undefined
|
|
140
|
+
}
|
|
141
|
+
value={
|
|
142
|
+
shouldDisplayLegendInCompactMode
|
|
143
|
+
? interactiveValue
|
|
144
|
+
: undefined
|
|
145
|
+
}
|
|
146
|
+
/>
|
|
147
|
+
{!shouldDisplayLegendInCompactMode && (
|
|
148
|
+
<div>
|
|
149
|
+
{displayAnchor && (
|
|
150
|
+
<InteractiveValue value={interactiveValue} />
|
|
151
|
+
)}
|
|
152
|
+
{!interactiveValue && (
|
|
153
|
+
<div className={classes.minMaxAvgContainer}>
|
|
154
|
+
{minMaxAvg.map(({ label, value }) => (
|
|
155
|
+
<LegendContent
|
|
156
|
+
data={getMetricValue({ unit: line.unit, value })}
|
|
157
|
+
key={label}
|
|
158
|
+
label={label}
|
|
159
|
+
/>
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
145
165
|
</Box>
|
|
146
166
|
);
|
|
147
167
|
})}
|
|
@@ -152,8 +172,15 @@ const MainLegend = ({
|
|
|
152
172
|
};
|
|
153
173
|
|
|
154
174
|
const Legend = (props: Props): JSX.Element => {
|
|
155
|
-
const {
|
|
156
|
-
|
|
175
|
+
const {
|
|
176
|
+
toggable,
|
|
177
|
+
limitLegendRows,
|
|
178
|
+
timeSeries,
|
|
179
|
+
lines,
|
|
180
|
+
base,
|
|
181
|
+
displayAnchor,
|
|
182
|
+
shouldDisplayLegendInCompactMode
|
|
183
|
+
} = props;
|
|
157
184
|
const timeValue = useAtomValue(timeValueAtom);
|
|
158
185
|
|
|
159
186
|
return useMemoComponent({
|
|
@@ -165,7 +192,8 @@ const Legend = (props: Props): JSX.Element => {
|
|
|
165
192
|
base,
|
|
166
193
|
toggable,
|
|
167
194
|
limitLegendRows,
|
|
168
|
-
displayAnchor
|
|
195
|
+
displayAnchor,
|
|
196
|
+
shouldDisplayLegendInCompactMode
|
|
169
197
|
]
|
|
170
198
|
});
|
|
171
199
|
};
|
|
@@ -32,20 +32,19 @@ const useLegend = ({ lines, setLinesGraph }: Props): LegendActions => {
|
|
|
32
32
|
find(propEq(metric_id, 'metric_id'), lines) as Line;
|
|
33
33
|
|
|
34
34
|
const toggleMetricLine = (metric_id): void => {
|
|
35
|
-
const
|
|
35
|
+
const data = lines.map((line) => ({
|
|
36
|
+
...line,
|
|
37
|
+
display: equals(line.metric_id, metric_id) ? !line.display : line.display
|
|
38
|
+
}));
|
|
36
39
|
|
|
37
|
-
setLinesGraph(
|
|
38
|
-
...reject(propEq(metric_id, 'metric_id'), lines),
|
|
39
|
-
{ ...line, display: !line.display }
|
|
40
|
-
]);
|
|
40
|
+
setLinesGraph(data);
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
const highlightLine = (metric_id): void => {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
];
|
|
44
|
+
const data = lines.map((line) => ({
|
|
45
|
+
...line,
|
|
46
|
+
highlight: equals(line.metric_id, metric_id)
|
|
47
|
+
}));
|
|
49
48
|
|
|
50
49
|
setLinesGraph(data);
|
|
51
50
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MutableRefObject, useMemo, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { Group, Tooltip } from '@visx/visx';
|
|
4
|
-
import { flatten, isNil, pluck } from 'ramda';
|
|
4
|
+
import { flatten, gt, isNil, lte, pluck, reduce } from 'ramda';
|
|
5
5
|
|
|
6
6
|
import { ClickAwayListener, Fade, Skeleton, useTheme } from '@mui/material';
|
|
7
7
|
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { useIntersection } from './useLineChartIntersection';
|
|
34
34
|
import { CurveType } from './BasicComponents/Lines/models';
|
|
35
35
|
import Thresholds from './BasicComponents/Thresholds';
|
|
36
|
+
import { legendWidth } from './Legend/Legend.styles';
|
|
36
37
|
|
|
37
38
|
const extraMargin = 10;
|
|
38
39
|
|
|
@@ -161,6 +162,15 @@ const LineChart = ({
|
|
|
161
162
|
const displayLegend = legend?.display ?? true;
|
|
162
163
|
const displayTooltip = !isNil(tooltip?.renderComponent);
|
|
163
164
|
|
|
165
|
+
const legendItemsWidth = reduce(
|
|
166
|
+
(acc) => acc + legendWidth * 8 + 24,
|
|
167
|
+
0,
|
|
168
|
+
displayedLines
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const shouldDisplayLegendInCompactMode =
|
|
172
|
+
lte(graphWidth, 808) && gt(legendItemsWidth, graphWidth);
|
|
173
|
+
|
|
164
174
|
if (!isInViewport) {
|
|
165
175
|
return (
|
|
166
176
|
<Skeleton
|
|
@@ -294,6 +304,7 @@ const LineChart = ({
|
|
|
294
304
|
lines={newLines}
|
|
295
305
|
renderExtraComponent={legend?.renderExtraComponent}
|
|
296
306
|
setLinesGraph={setLinesGraph}
|
|
307
|
+
shouldDisplayLegendInCompactMode={shouldDisplayLegendInCompactMode}
|
|
297
308
|
timeSeries={timeSeries}
|
|
298
309
|
xScale={xScale}
|
|
299
310
|
/>
|
|
@@ -32,6 +32,7 @@ import useFetchQuery from '../../../../api/useFetchQuery';
|
|
|
32
32
|
|
|
33
33
|
export interface ConnectedAutoCompleteFieldProps<TData> {
|
|
34
34
|
allowUniqOption?: boolean;
|
|
35
|
+
baseEndpoint?: string;
|
|
35
36
|
conditionField?: keyof SelectEntry;
|
|
36
37
|
field: string;
|
|
37
38
|
getEndpoint: ({ search, page }) => string;
|
|
@@ -60,6 +61,7 @@ const ConnectedAutocompleteField = (
|
|
|
60
61
|
displayOptionThumbnail,
|
|
61
62
|
queryKey,
|
|
62
63
|
allowUniqOption,
|
|
64
|
+
baseEndpoint,
|
|
63
65
|
...props
|
|
64
66
|
}: ConnectedAutoCompleteFieldProps<TData> &
|
|
65
67
|
Omit<AutocompleteFieldProps, 'options'>): JSX.Element => {
|
|
@@ -87,6 +89,7 @@ const ConnectedAutocompleteField = (
|
|
|
87
89
|
const { fetchQuery, isFetching, prefetchNextPage } = useFetchQuery<
|
|
88
90
|
ListingModel<TData>
|
|
89
91
|
>({
|
|
92
|
+
baseEndpoint,
|
|
90
93
|
fetchHeaders: getRequestHeaders,
|
|
91
94
|
getEndpoint: (params) => {
|
|
92
95
|
return getEndpoint({
|
|
@@ -80,6 +80,7 @@ export type Props = {
|
|
|
80
80
|
autoSizeCustomPadding?: number;
|
|
81
81
|
autoSizeDefaultWidth?: number;
|
|
82
82
|
className?: string;
|
|
83
|
+
containerClassName?: string;
|
|
83
84
|
dataTestId: string;
|
|
84
85
|
debounced?: boolean;
|
|
85
86
|
displayErrorInTooltip?: boolean;
|
|
@@ -112,6 +113,7 @@ const TextField = forwardRef(
|
|
|
112
113
|
autoSizeCustomPadding,
|
|
113
114
|
defaultValue,
|
|
114
115
|
required = false,
|
|
116
|
+
containerClassName,
|
|
115
117
|
...rest
|
|
116
118
|
}: Props,
|
|
117
119
|
ref: React.ForwardedRef<HTMLDivElement>
|
|
@@ -142,7 +144,10 @@ const TextField = forwardRef(
|
|
|
142
144
|
}, [innerValue, debounced, defaultValue]);
|
|
143
145
|
|
|
144
146
|
return (
|
|
145
|
-
<Box
|
|
147
|
+
<Box
|
|
148
|
+
className={containerClassName}
|
|
149
|
+
sx={{ width: autoSize ? 'auto' : '100%' }}
|
|
150
|
+
>
|
|
146
151
|
<Tooltip placement="top" title={tooltipTitle}>
|
|
147
152
|
<MuiTextField
|
|
148
153
|
data-testid={dataTestId}
|
|
@@ -143,7 +143,7 @@ declare module '@mui/material/Badge' {
|
|
|
143
143
|
export const lightPalette: PaletteOptions = {
|
|
144
144
|
action: {
|
|
145
145
|
acknowledged: '#67532C',
|
|
146
|
-
acknowledgedBackground: '#
|
|
146
|
+
acknowledgedBackground: '#DFD2B9',
|
|
147
147
|
activatedOpacity: 0.12,
|
|
148
148
|
active: '#666666',
|
|
149
149
|
disabled: '#999999',
|
|
@@ -153,7 +153,7 @@ export const lightPalette: PaletteOptions = {
|
|
|
153
153
|
hover: 'rgba(0, 0, 0, 0.06)',
|
|
154
154
|
hoverOpacity: 0.06,
|
|
155
155
|
inDowntime: '#4B2352',
|
|
156
|
-
inDowntimeBackground: '#
|
|
156
|
+
inDowntimeBackground: '#E5D8F3',
|
|
157
157
|
selected: 'rgba(102, 102, 102, 0.3)',
|
|
158
158
|
selectedOpacity: 0.3
|
|
159
159
|
},
|
|
@@ -283,7 +283,7 @@ export const lightPalette: PaletteOptions = {
|
|
|
283
283
|
export const darkPalette: PaletteOptions = {
|
|
284
284
|
action: {
|
|
285
285
|
acknowledged: '#67532C',
|
|
286
|
-
acknowledgedBackground: '#
|
|
286
|
+
acknowledgedBackground: '#745F35',
|
|
287
287
|
activatedOpacity: 0.3,
|
|
288
288
|
active: '#B5B5B5',
|
|
289
289
|
disabled: '#999999',
|
|
@@ -293,7 +293,7 @@ export const darkPalette: PaletteOptions = {
|
|
|
293
293
|
hover: 'rgba(255, 255, 255, 0.16)',
|
|
294
294
|
hoverOpacity: 0.16,
|
|
295
295
|
inDowntime: '#4B2352',
|
|
296
|
-
inDowntimeBackground: '#
|
|
296
|
+
inDowntimeBackground: '#512980',
|
|
297
297
|
selected: 'rgba(255, 255, 255, 0.5)',
|
|
298
298
|
selectedOpacity: 0.5
|
|
299
299
|
},
|
package/src/api/customFetch.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { equals } from 'ramda';
|
|
1
|
+
import { equals, isNil, startsWith } from 'ramda';
|
|
2
2
|
import { JsonDecoder } from 'ts.data.json';
|
|
3
3
|
|
|
4
4
|
import { Method } from './useMutationQuery';
|
|
@@ -22,6 +22,7 @@ export interface CatchErrorProps {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
interface CustomFetchProps<T> {
|
|
25
|
+
baseEndpoint?: string;
|
|
25
26
|
catchError?: (props: CatchErrorProps) => void;
|
|
26
27
|
decoder?: JsonDecoder.Decoder<T>;
|
|
27
28
|
defaultFailureMessage?: string;
|
|
@@ -42,10 +43,18 @@ export const customFetch = <T>({
|
|
|
42
43
|
defaultFailureMessage = 'Something went wrong',
|
|
43
44
|
isMutation = false,
|
|
44
45
|
payload,
|
|
45
|
-
method = 'GET'
|
|
46
|
+
method = 'GET',
|
|
47
|
+
baseEndpoint = './api/latest'
|
|
46
48
|
}: CustomFetchProps<T>): Promise<T | ResponseError> => {
|
|
47
49
|
const defaultOptions = { headers, method, signal };
|
|
48
50
|
|
|
51
|
+
const formattedEndpoint =
|
|
52
|
+
!isNil(baseEndpoint) &&
|
|
53
|
+
!startsWith(baseEndpoint, endpoint) &&
|
|
54
|
+
!startsWith('./api/internal.php', endpoint)
|
|
55
|
+
? `${baseEndpoint}${endpoint}`
|
|
56
|
+
: endpoint;
|
|
57
|
+
|
|
49
58
|
const options = isMutation
|
|
50
59
|
? {
|
|
51
60
|
...defaultOptions,
|
|
@@ -53,7 +62,7 @@ export const customFetch = <T>({
|
|
|
53
62
|
}
|
|
54
63
|
: defaultOptions;
|
|
55
64
|
|
|
56
|
-
return fetch(
|
|
65
|
+
return fetch(formattedEndpoint, options)
|
|
57
66
|
.then((response) => {
|
|
58
67
|
if (equals(response.status, 204)) {
|
|
59
68
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
|
|
3
3
|
import 'ulog';
|
|
4
4
|
import {
|
|
@@ -10,13 +10,14 @@ import {
|
|
|
10
10
|
} from '@tanstack/react-query';
|
|
11
11
|
import { JsonDecoder } from 'ts.data.json';
|
|
12
12
|
import anylogger from 'anylogger';
|
|
13
|
-
import { has, includes, not, omit } from 'ramda';
|
|
13
|
+
import { has, includes, isNil, not, omit } from 'ramda';
|
|
14
14
|
|
|
15
15
|
import { CatchErrorProps, customFetch, ResponseError } from '../customFetch';
|
|
16
16
|
import useSnackbar from '../../Snackbar/useSnackbar';
|
|
17
17
|
import { useDeepCompare } from '../../utils';
|
|
18
18
|
|
|
19
19
|
export interface UseFetchQueryProps<T> {
|
|
20
|
+
baseEndpoint?: string;
|
|
20
21
|
catchError?: (props: CatchErrorProps) => void;
|
|
21
22
|
decoder?: JsonDecoder.Decoder<T>;
|
|
22
23
|
defaultFailureMessage?: string;
|
|
@@ -57,13 +58,17 @@ const useFetchQuery = <T extends object>({
|
|
|
57
58
|
fetchHeaders,
|
|
58
59
|
isPaginated,
|
|
59
60
|
queryOptions,
|
|
60
|
-
httpCodesBypassErrorSnackbar = []
|
|
61
|
+
httpCodesBypassErrorSnackbar = [],
|
|
62
|
+
baseEndpoint
|
|
61
63
|
}: UseFetchQueryProps<T>): UseFetchQueryState<T> => {
|
|
64
|
+
const dataRef = useRef<T | undefined>(undefined);
|
|
65
|
+
|
|
62
66
|
const { showErrorMessage } = useSnackbar();
|
|
63
67
|
|
|
64
68
|
const queryData = useQuery<T | ResponseError, Error>({
|
|
65
69
|
queryFn: ({ signal }): Promise<T | ResponseError> =>
|
|
66
70
|
customFetch<T>({
|
|
71
|
+
baseEndpoint,
|
|
67
72
|
catchError,
|
|
68
73
|
decoder,
|
|
69
74
|
defaultFailureMessage,
|
|
@@ -96,6 +101,7 @@ const useFetchQuery = <T extends object>({
|
|
|
96
101
|
queryClient.prefetchQuery({
|
|
97
102
|
queryFn: ({ signal }): Promise<T | ResponseError> =>
|
|
98
103
|
customFetch<T>({
|
|
104
|
+
baseEndpoint,
|
|
99
105
|
catchError,
|
|
100
106
|
decoder,
|
|
101
107
|
defaultFailureMessage,
|
|
@@ -137,6 +143,7 @@ const useFetchQuery = <T extends object>({
|
|
|
137
143
|
return queryClient.fetchQuery({
|
|
138
144
|
queryFn: ({ signal }): Promise<T | ResponseError> =>
|
|
139
145
|
customFetch<T>({
|
|
146
|
+
baseEndpoint,
|
|
140
147
|
catchError,
|
|
141
148
|
decoder,
|
|
142
149
|
defaultFailureMessage,
|
|
@@ -154,6 +161,10 @@ const useFetchQuery = <T extends object>({
|
|
|
154
161
|
[queryData.data]
|
|
155
162
|
);
|
|
156
163
|
|
|
164
|
+
if (!isNil(data)) {
|
|
165
|
+
dataRef.current = data;
|
|
166
|
+
}
|
|
167
|
+
|
|
157
168
|
const errorData = queryData.data as ResponseError | undefined;
|
|
158
169
|
|
|
159
170
|
useEffect(() => {
|
|
@@ -171,7 +182,7 @@ const useFetchQuery = <T extends object>({
|
|
|
171
182
|
|
|
172
183
|
return {
|
|
173
184
|
...omit(['data', 'error'], queryData),
|
|
174
|
-
data,
|
|
185
|
+
data: dataRef.current,
|
|
175
186
|
error: errorData?.isError ? omit(['isError'], errorData) : null,
|
|
176
187
|
fetchQuery,
|
|
177
188
|
prefetchNextPage,
|
|
@@ -23,6 +23,7 @@ export enum Method {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export type UseMutationQueryProps<T, TMeta> = {
|
|
26
|
+
baseEndpoint?: string;
|
|
26
27
|
catchError?: (props: CatchErrorProps) => void;
|
|
27
28
|
decoder?: JsonDecoder.Decoder<T>;
|
|
28
29
|
defaultFailureMessage?: string;
|
|
@@ -68,7 +69,8 @@ const useMutationQuery = <T extends object, TMeta>({
|
|
|
68
69
|
method,
|
|
69
70
|
onMutate,
|
|
70
71
|
onError,
|
|
71
|
-
onSuccess
|
|
72
|
+
onSuccess,
|
|
73
|
+
baseEndpoint
|
|
72
74
|
}: UseMutationQueryProps<T, TMeta>): UseMutationQueryState<T> => {
|
|
73
75
|
const { showErrorMessage } = useSnackbar();
|
|
74
76
|
|
|
@@ -83,6 +85,7 @@ const useMutationQuery = <T extends object, TMeta>({
|
|
|
83
85
|
const { _meta, ...payload } = _payload || {};
|
|
84
86
|
|
|
85
87
|
return customFetch<T>({
|
|
88
|
+
baseEndpoint,
|
|
86
89
|
catchError,
|
|
87
90
|
decoder,
|
|
88
91
|
defaultFailureMessage,
|
|
@@ -5,33 +5,42 @@ import { PrimitiveAtom, useAtom } from 'jotai';
|
|
|
5
5
|
import { JsonDecoder } from 'ts.data.json';
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
+
QueryParameter,
|
|
8
9
|
buildListingEndpoint,
|
|
9
10
|
useFetchQuery,
|
|
10
11
|
useIntersectionObserver
|
|
11
12
|
} from '@centreon/ui';
|
|
12
13
|
|
|
13
14
|
import type { Listing } from '../api/models';
|
|
14
|
-
|
|
15
|
-
const limit = 100;
|
|
15
|
+
import { Parameters } from '../api/buildListingEndpoint/models';
|
|
16
16
|
|
|
17
17
|
interface UseInfiniteScrollListing<T> {
|
|
18
18
|
elementRef: (node) => void;
|
|
19
19
|
elements: Array<T>;
|
|
20
20
|
isLoading: boolean;
|
|
21
|
+
total?: number;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
interface UseInfiniteScrollListingProps<T> {
|
|
24
|
-
|
|
25
|
+
customQueryParameters?: Array<QueryParameter>;
|
|
26
|
+
decoder?: JsonDecoder.Decoder<Listing<T>>;
|
|
25
27
|
endpoint: string;
|
|
28
|
+
limit?: number;
|
|
26
29
|
pageAtom: PrimitiveAtom<number>;
|
|
30
|
+
parameters?: Parameters;
|
|
27
31
|
queryKeyName: string;
|
|
32
|
+
suspense?: boolean;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
export const useInfiniteScrollListing = <T>({
|
|
31
36
|
queryKeyName,
|
|
32
37
|
endpoint,
|
|
33
38
|
decoder,
|
|
34
|
-
pageAtom
|
|
39
|
+
pageAtom,
|
|
40
|
+
suspense = true,
|
|
41
|
+
parameters,
|
|
42
|
+
customQueryParameters,
|
|
43
|
+
limit = 100
|
|
35
44
|
}: UseInfiniteScrollListingProps<T>): UseInfiniteScrollListing<T> => {
|
|
36
45
|
const [maxPage, setMaxPage] = useState(1);
|
|
37
46
|
|
|
@@ -46,14 +55,15 @@ export const useInfiniteScrollListing = <T>({
|
|
|
46
55
|
getEndpoint: (params) =>
|
|
47
56
|
buildListingEndpoint({
|
|
48
57
|
baseEndpoint: endpoint,
|
|
49
|
-
|
|
58
|
+
customQueryParameters,
|
|
59
|
+
parameters: { limit, page: params?.page || page, ...parameters }
|
|
50
60
|
}),
|
|
51
61
|
getQueryKey: () => [queryKeyName, page],
|
|
52
62
|
isPaginated: true,
|
|
53
63
|
queryOptions: {
|
|
54
64
|
refetchOnMount: false,
|
|
55
65
|
refetchOnWindowFocus: false,
|
|
56
|
-
suspense: equals(page, 1)
|
|
66
|
+
suspense: suspense && equals(page, 1)
|
|
57
67
|
}
|
|
58
68
|
});
|
|
59
69
|
|
|
@@ -98,9 +108,14 @@ export const useInfiniteScrollListing = <T>({
|
|
|
98
108
|
});
|
|
99
109
|
}, [data]);
|
|
100
110
|
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
return () => setPage(1);
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
101
115
|
return {
|
|
102
116
|
elementRef,
|
|
103
117
|
elements: elements.current || [],
|
|
104
|
-
isLoading
|
|
118
|
+
isLoading,
|
|
119
|
+
total: data?.meta.total
|
|
105
120
|
};
|
|
106
121
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import dayjs from 'dayjs';
|
|
2
2
|
import humanizeDuration from 'humanize-duration';
|
|
3
3
|
import { useAtomValue } from 'jotai';
|
|
4
|
+
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
|
4
5
|
|
|
5
6
|
import { userAtom } from '@centreon/ui-context';
|
|
6
7
|
|
|
@@ -20,6 +21,8 @@ export interface LocaleDateTimeFormat {
|
|
|
20
21
|
toTime: (date: Date | string) => string;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
dayjs.extend(localizedFormat);
|
|
25
|
+
|
|
23
26
|
const dateFormat = 'L';
|
|
24
27
|
const timeFormat = 'LT';
|
|
25
28
|
const dateTimeFormat = `${dateFormat} ${timeFormat}`;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { equals } from 'ramda';
|
|
2
|
-
import { makeStyles } from 'tss-react/mui';
|
|
3
|
-
|
|
4
|
-
export enum LegendMarkerVariant {
|
|
5
|
-
'dot',
|
|
6
|
-
'bar'
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface StylesProps {
|
|
10
|
-
color?: string;
|
|
11
|
-
variant: LegendMarkerVariant;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const useStyles = makeStyles<StylesProps>()((theme, { color, variant }) => ({
|
|
15
|
-
disabled: {
|
|
16
|
-
color: theme.palette.text.disabled
|
|
17
|
-
},
|
|
18
|
-
icon: {
|
|
19
|
-
backgroundColor: color,
|
|
20
|
-
borderRadius: equals(LegendMarkerVariant.dot, variant) ? '50%' : 0,
|
|
21
|
-
height: equals(LegendMarkerVariant.dot, variant) ? 9 : '100%',
|
|
22
|
-
marginRight: theme.spacing(0.5),
|
|
23
|
-
width: 9
|
|
24
|
-
}
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
interface Props {
|
|
28
|
-
color: string;
|
|
29
|
-
disabled?: boolean;
|
|
30
|
-
variant?: LegendMarkerVariant;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const LegendMarker = ({
|
|
34
|
-
disabled,
|
|
35
|
-
color,
|
|
36
|
-
variant = LegendMarkerVariant.bar
|
|
37
|
-
}: Props): JSX.Element => {
|
|
38
|
-
const { classes, cx } = useStyles({ color, variant });
|
|
39
|
-
|
|
40
|
-
return <div className={cx(classes.icon, { [classes.disabled]: disabled })} />;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export default LegendMarker;
|