@scality/core-ui 0.165.0 → 0.166.0

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 (50) hide show
  1. package/dist/components/barchartv2/Barchart.component.d.ts +19 -0
  2. package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
  3. package/dist/components/barchartv2/Barchart.component.js +31 -5
  4. package/dist/components/barchartv2/ChartTooltip.d.ts +18 -0
  5. package/dist/components/barchartv2/ChartTooltip.d.ts.map +1 -0
  6. package/dist/components/barchartv2/ChartTooltip.js +31 -0
  7. package/dist/components/barchartv2/utils.d.ts +0 -7
  8. package/dist/components/barchartv2/utils.d.ts.map +1 -1
  9. package/dist/components/barchartv2/utils.js +1 -29
  10. package/dist/components/chartlegend/ChartLegend.d.ts +9 -0
  11. package/dist/components/chartlegend/ChartLegend.d.ts.map +1 -1
  12. package/dist/components/chartlegend/ChartLegend.js +38 -9
  13. package/dist/components/chartlegend/ChartLegendWrapper.d.ts +4 -0
  14. package/dist/components/chartlegend/ChartLegendWrapper.d.ts.map +1 -1
  15. package/dist/components/chartlegend/ChartLegendWrapper.js +23 -2
  16. package/dist/components/constants.d.ts +2 -0
  17. package/dist/components/constants.d.ts.map +1 -1
  18. package/dist/components/constants.js +6 -0
  19. package/dist/components/constrainedtext/Constrainedtext.component.d.ts +3 -1
  20. package/dist/components/constrainedtext/Constrainedtext.component.d.ts.map +1 -1
  21. package/dist/components/constrainedtext/Constrainedtext.component.js +2 -2
  22. package/dist/components/date/FormattedDateTime.d.ts +2 -1
  23. package/dist/components/date/FormattedDateTime.d.ts.map +1 -1
  24. package/dist/components/date/FormattedDateTime.js +10 -0
  25. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +3 -1
  26. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -1
  27. package/dist/components/linetimeseriechart/linetimeseriechart.component.js +5 -7
  28. package/dist/components/text/Text.component.js +1 -1
  29. package/dist/components/toast/Toast.component.d.ts.map +1 -1
  30. package/dist/components/toast/Toast.component.js +24 -11
  31. package/dist/next.d.ts +1 -0
  32. package/dist/next.d.ts.map +1 -1
  33. package/dist/next.js +1 -0
  34. package/package.json +1 -2
  35. package/src/lib/components/barchartv2/Barchart.component.test.tsx +99 -1
  36. package/src/lib/components/barchartv2/Barchart.component.tsx +39 -11
  37. package/src/lib/components/barchartv2/ChartTooltip.tsx +76 -0
  38. package/src/lib/components/barchartv2/utils.ts +2 -33
  39. package/src/lib/components/chartlegend/ChartLegend.test.tsx +218 -0
  40. package/src/lib/components/chartlegend/ChartLegend.tsx +42 -8
  41. package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +75 -29
  42. package/src/lib/components/constants.ts +11 -0
  43. package/src/lib/components/constrainedtext/Constrainedtext.component.tsx +5 -2
  44. package/src/lib/components/date/FormattedDateTime.tsx +15 -1
  45. package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +8 -6
  46. package/src/lib/components/text/Text.component.tsx +1 -1
  47. package/src/lib/components/toast/Toast.component.tsx +27 -19
  48. package/src/lib/next.ts +1 -0
  49. package/stories/constrainedtext.stories.tsx +4 -1
  50. package/stories/linetimeseriechart.stories.tsx +30 -25
@@ -1 +1 @@
1
- {"version":3,"file":"Toast.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/toast/Toast.component.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAU,MAAM,OAAO,CAAC;AAM1C,OAAO,EAAE,aAAa,EAAkB,MAAM,wBAAwB,CAAC;AAKvE,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEnE,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,SAAS,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IACrC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B,CAAC;AAEF,eAAO,MAAM,qBAAqB,WAAY,MAAM,WAYnD,CAAC;AA6DF,iBAAS,KAAK,CAAC,EACb,IAAI,EACJ,OAAO,EACP,OAAO,EACP,QAAsB,EACtB,MAAe,EACf,WAAkB,EAClB,QAAe,EACf,IAAsC,EACtC,KAAqB,EACrB,eAAuB,EACvB,KAAK,GACN,EAAE,UAAU,kDAmEZ;AAED,OAAO,EAAE,KAAK,EAAE,CAAC"}
1
+ {"version":3,"file":"Toast.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/toast/Toast.component.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAU,MAAM,OAAO,CAAC;AAM1C,OAAO,EAAE,aAAa,EAAkB,MAAM,wBAAwB,CAAC;AAKvE,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEnE,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,SAAS,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IACrC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B,CAAC;AAEF,eAAO,MAAM,qBAAqB,WAAY,MAAM,WAYnD,CAAC;AAsFF,iBAAS,KAAK,CAAC,EACb,IAAI,EACJ,OAAO,EACP,OAAO,EACP,QAAsB,EACtB,MAAe,EACf,WAAkB,EAClB,QAAe,EACf,IAAsC,EACtC,KAAqB,EACrB,eAAuB,EACvB,KAAK,GACN,EAAE,UAAU,kDAmDZ;AAED,OAAO,EAAE,KAAK,EAAE,CAAC"}
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { motion } from 'framer-motion';
3
2
  import { useRef } from 'react';
4
3
  import { useTheme } from 'styled-components';
5
4
  import { Box } from '../box/Box';
@@ -75,6 +74,29 @@ const ContentContainer = styled.div `
75
74
  padding: 0px 16px;
76
75
  position: relative;
77
76
  `;
77
+ const FadingToast = styled.div `
78
+ align-items: flex-end;
79
+ background-color: ${props => props.theme.backgroundLevel1};
80
+ border: 1px solid ${props => props.theme.border};
81
+ box-shadow: 0px 4px 10px 4px #000;
82
+ display: flex;
83
+ border-radius: 4px;
84
+ position: relative;
85
+
86
+ @keyframes toastEnter {
87
+ from {
88
+ opacity: 0;
89
+ transform: translateY(-20px);
90
+ }
91
+ to {
92
+ opacity: 1;
93
+ transform: translateY(0);
94
+ }
95
+ }
96
+ `;
97
+ const ToastEnter = styled(FadingToast) `
98
+ animation: toastEnter 0.3s ease forwards;
99
+ `;
78
100
  function Toast({ open, message, onClose, position = 'top-right', status = 'info', autoDismiss = true, duration = 5000, icon = _jsx(DefaultIcon, { status: status }), width = DEFAULT_WIDTH, withProgressBar = false, style, }) {
79
101
  const ref = useRef(null);
80
102
  const { params } = useToastParameters({
@@ -85,7 +107,6 @@ function Toast({ open, message, onClose, position = 'top-right', status = 'info'
85
107
  const positionStyle = positionOutput[position];
86
108
  const bgColor = useGetBackgroundColor(status);
87
109
  const rgbBgColor = useGetRgbBackgroundColor(status);
88
- const theme = useTheme();
89
110
  if (!open) {
90
111
  return null;
91
112
  }
@@ -93,14 +114,6 @@ function Toast({ open, message, onClose, position = 'top-right', status = 'info'
93
114
  position: 'fixed',
94
115
  ...(style || positionStyle),
95
116
  width,
96
- }, children: [_jsxs(motion.div, { initial: { opacity: 0, y: -20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: 20 }, transition: { duration: 0.3 }, style: {
97
- alignItems: 'flex-end',
98
- backgroundColor: theme.backgroundLevel1,
99
- border: `1px solid ${theme.border}`,
100
- boxShadow: '0px 4px 10px 4px #000',
101
- display: 'flex',
102
- borderRadius: '4px',
103
- position: 'relative',
104
- }, children: [_jsx(IconContainer, { bgColor: rgbBgColor, children: icon }), _jsx(ContentContainer, { children: _jsx(BasicText, { children: message }) }), _jsx(Box, { display: "flex", alignItems: "center", alignSelf: "stretch", children: _jsx(Button, { icon: _jsx(Icon, { name: "Close", size: "lg", color: "textSecondary" }), onClick: params === null || params === void 0 ? void 0 : params.onClose, "aria-label": "Close", tooltip: { overlay: 'Close', placement: 'top' } }) })] }, "toast"), withProgressBar && (_jsx(DurationBasedProgressBar, { duration: autoDismiss ? duration : null, color: bgColor }))] }));
117
+ }, children: [_jsxs(ToastEnter, { children: [_jsx(IconContainer, { bgColor: rgbBgColor, children: icon }), _jsx(ContentContainer, { children: _jsx(BasicText, { children: message }) }), _jsx(Box, { display: "flex", alignItems: "center", alignSelf: "stretch", children: _jsx(Button, { icon: _jsx(Icon, { name: "Close", size: "lg", color: "textSecondary" }), onClick: params === null || params === void 0 ? void 0 : params.onClose, "aria-label": "Close", tooltip: { overlay: 'Close', placement: 'top' } }) })] }), withProgressBar && (_jsx(DurationBasedProgressBar, { duration: autoDismiss ? duration : null, color: bgColor }))] }));
105
118
  }
106
119
  export { Toast };
package/dist/next.d.ts CHANGED
@@ -14,6 +14,7 @@ export { Box } from './components/box/Box';
14
14
  export { Input } from './components/inputv2/inputv2';
15
15
  export { Accordion } from './components/accordion/Accordion.component';
16
16
  export { Barchart, BarchartSortFn, BarchartTooltipFn, } from './components/barchartv2/Barchart.component';
17
+ export { ChartTooltip } from './components/barchartv2/ChartTooltip';
17
18
  export { ChartLegendWrapper } from './components/chartlegend/ChartLegendWrapper';
18
19
  export { ChartLegend } from './components/chartlegend/ChartLegend';
19
20
  export { LineTimeSerieChart } from './components/linetimeseriechart/linetimeseriechart.component';
@@ -1 +1 @@
1
- {"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../src/lib/next.ts"],"names":[],"mappings":"AAAA,OAAO,2CAA2C,CAAC;AACnD,OAAO,aAAa,CAAC;AACrB,OAAO,EAAE,MAAM,EAAE,MAAM,0CAA0C,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,4CAA4C,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,sCAAsC,CAAC;AACjE,OAAO,EAAE,KAAK,EAAE,MAAM,wCAAwC,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EACL,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,uDAAuD,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6CAA6C,CAAC;AACjF,OAAO,EAAE,MAAM,EAAE,MAAM,0CAA0C,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,wDAAwD,CAAC;AACxF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAC3F,OAAO,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAC3C,OAAO,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AACvE,OAAO,EACL,QAAQ,EACR,cAAc,EACd,iBAAiB,GAClB,MAAM,4CAA4C,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6CAA6C,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,8DAA8D,CAAC"}
1
+ {"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../src/lib/next.ts"],"names":[],"mappings":"AAAA,OAAO,2CAA2C,CAAC;AACnD,OAAO,aAAa,CAAC;AACrB,OAAO,EAAE,MAAM,EAAE,MAAM,0CAA0C,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,4CAA4C,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,sCAAsC,CAAC;AACjE,OAAO,EAAE,KAAK,EAAE,MAAM,wCAAwC,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EACL,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,uDAAuD,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6CAA6C,CAAC;AACjF,OAAO,EAAE,MAAM,EAAE,MAAM,0CAA0C,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,wDAAwD,CAAC;AACxF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAC3F,OAAO,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAC3C,OAAO,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AACvE,OAAO,EACL,QAAQ,EACR,cAAc,EACd,iBAAiB,GAClB,MAAM,4CAA4C,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6CAA6C,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,8DAA8D,CAAC"}
package/dist/next.js CHANGED
@@ -14,6 +14,7 @@ export { Box } from './components/box/Box';
14
14
  export { Input } from './components/inputv2/inputv2';
15
15
  export { Accordion } from './components/accordion/Accordion.component';
16
16
  export { Barchart, } from './components/barchartv2/Barchart.component';
17
+ export { ChartTooltip } from './components/barchartv2/ChartTooltip';
17
18
  export { ChartLegendWrapper } from './components/chartlegend/ChartLegendWrapper';
18
19
  export { ChartLegend } from './components/chartlegend/ChartLegend';
19
20
  export { LineTimeSerieChart } from './components/linetimeseriechart/linetimeseriechart.component';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scality/core-ui",
3
- "version": "0.165.0",
3
+ "version": "0.166.0",
4
4
  "description": "Scality common React component library",
5
5
  "author": "Scality Engineering",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -112,7 +112,6 @@
112
112
  "@js-temporal/polyfill": "^0.4.4",
113
113
  "@storybook/preview-api": "^8.3.6",
114
114
  "downshift": "^7.0.5",
115
- "framer-motion": "^4.1.17",
116
115
  "polished": "3.4.1",
117
116
  "pretty-bytes": "^5.6.0",
118
117
  "react-debounce-input": "3.2.2",
@@ -1,7 +1,8 @@
1
1
  import { render, screen, waitFor } from '@testing-library/react';
2
2
  import { getWrapper } from '../../testUtils';
3
- import { Barchart } from './Barchart.component';
3
+ import { Barchart, CustomTick, formatDate } from './Barchart.component';
4
4
  import { ChartLegendWrapper } from '../chartlegend/ChartLegendWrapper';
5
+ import React from 'react';
5
6
 
6
7
  const ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000;
7
8
  const ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
@@ -384,4 +385,101 @@ describe('Barchart', () => {
384
385
  expect(screen.getByText('Test Right Title')).toBeInTheDocument();
385
386
  expect(screen.getByLabelText('Info')).toBeInTheDocument();
386
387
  });
388
+ describe('formatDate', () => {
389
+ it('should render the CustomTick component with over a day interval', () => {
390
+ const { Wrapper } = getWrapper();
391
+ render(
392
+ <Wrapper>
393
+ <CustomTick
394
+ type={{
395
+ type: 'time',
396
+ timeRange: {
397
+ startDate: new Date('2024-07-05'),
398
+ endDate: new Date('2024-07-07'),
399
+ interval: 2 * ONE_DAY_IN_MILLISECONDS,
400
+ },
401
+ }}
402
+ x={100}
403
+ y={100}
404
+ payload={{ value: new Date('2024-07-05T10:00:00').getTime() }}
405
+ visibleTicksCount={10}
406
+ width={100}
407
+ />
408
+ </Wrapper>,
409
+ );
410
+ expect(screen.getByText('Fri05Jul 10:00')).toBeInTheDocument();
411
+ });
412
+
413
+ it('should render the CustomTick component with day format', () => {
414
+ const { Wrapper } = getWrapper();
415
+ render(
416
+ <Wrapper>
417
+ <CustomTick
418
+ type={{
419
+ type: 'time',
420
+ timeRange: {
421
+ startDate: new Date('2024-07-05'),
422
+ endDate: new Date('2024-07-07'),
423
+ interval: ONE_DAY_IN_MILLISECONDS,
424
+ },
425
+ }}
426
+ x={100}
427
+ y={100}
428
+ payload={{ value: new Date('2024-07-05T10:00:00').getTime() }}
429
+ visibleTicksCount={10}
430
+ width={100}
431
+ />
432
+ </Wrapper>,
433
+ );
434
+ expect(screen.getByText('Fri05Jul')).toBeInTheDocument();
435
+ });
436
+ it('should render the CustomTick component with hour format', () => {
437
+ const { Wrapper } = getWrapper();
438
+ render(
439
+ <Wrapper>
440
+ <CustomTick
441
+ type={{
442
+ type: 'time',
443
+ timeRange: {
444
+ startDate: new Date('2024-07-05'),
445
+ endDate: new Date('2024-07-07'),
446
+ interval: ONE_HOUR_IN_MILLISECONDS,
447
+ },
448
+ }}
449
+ x={100}
450
+ y={100}
451
+ payload={{ value: new Date('2024-07-05T10:00:00').getTime() }}
452
+ visibleTicksCount={10}
453
+ width={100}
454
+ />
455
+ </Wrapper>,
456
+ );
457
+ expect(screen.getByText('10:00')).toBeInTheDocument();
458
+ });
459
+ it('should render the CustomTick component with minute format', () => {
460
+ const { Wrapper } = getWrapper();
461
+ render(
462
+ <Wrapper>
463
+ <CustomTick
464
+ type={{
465
+ type: 'time',
466
+ timeRange: {
467
+ startDate: new Date('2024-07-05'),
468
+ endDate: new Date('2024-07-07'),
469
+ interval: 1000 * 30,
470
+ },
471
+ }}
472
+ x={100}
473
+ y={100}
474
+ payload={{ value: new Date('2024-07-05T10:00:00').getTime() }}
475
+ visibleTicksCount={10}
476
+ width={100}
477
+ />
478
+ </Wrapper>,
479
+ );
480
+ expect(
481
+ screen.getByText(new Date('2024-07-05T10:00:00').getTime()),
482
+ ).toBeInTheDocument();
483
+ });
484
+ });
387
485
  });
@@ -18,13 +18,9 @@ import { ConstrainedText } from '../constrainedtext/Constrainedtext.component';
18
18
  import { IconHelp } from '../iconhelper/IconHelper';
19
19
  import { Loader } from '../loader/Loader.component';
20
20
  import { Text } from '../text/Text.component';
21
- import {
22
- formatDate,
23
- renderTooltipContent,
24
- UnitRange,
25
- useChartData,
26
- } from './utils';
21
+ import { renderTooltipContent, UnitRange, useChartData } from './utils';
27
22
  import { useChartLegend } from '../chartlegend/ChartLegendWrapper';
23
+ import { FormattedDateTime } from '../date/FormattedDateTime';
28
24
 
29
25
  const CHART_CONSTANTS = {
30
26
  TICK_WIDTH_OFFSET: 5,
@@ -37,7 +33,7 @@ const CHART_CONSTANTS = {
37
33
  top: 0,
38
34
  bottom: 0,
39
35
  },
40
- } as const;
36
+ };
41
37
 
42
38
  /* ---------------------------------- TYPE ---------------------------------- */
43
39
 
@@ -100,7 +96,7 @@ interface CustomTickProps {
100
96
  x: number;
101
97
  y: number;
102
98
  payload: {
103
- value: string | number;
99
+ value: number;
104
100
  };
105
101
  visibleTicksCount: number;
106
102
  width: number;
@@ -109,7 +105,38 @@ interface CustomTickProps {
109
105
 
110
106
  /* ---------------------------------- COMPONENTS ---------------------------------- */
111
107
 
112
- const CustomTick = ({
108
+ /**
109
+ * Formats a date based on the interval
110
+ * @param timestamp - Timestamp
111
+ * @param interval - Interval in milliseconds
112
+ * @returns Formatted string
113
+ */
114
+ export const formatDate = (
115
+ timestamp: number,
116
+ interval: number,
117
+ ): React.ReactNode => {
118
+ const date = new Date(timestamp);
119
+ // More than 24 hours interval - use day and time format
120
+ if (interval > 24 * 60 * 60 * 1000) {
121
+ return (
122
+ <>
123
+ <FormattedDateTime format="chart-date" value={date} />{' '}
124
+ <FormattedDateTime format="time" value={date} />
125
+ </>
126
+ );
127
+ } else if (interval === 24 * 60 * 60 * 1000) {
128
+ // Daily interval - use day format
129
+ return <FormattedDateTime format="chart-date" value={date} />;
130
+ } else if (interval >= 60 * 1000) {
131
+ //Hourly and minute intervals - use minute format
132
+ return <FormattedDateTime format="time" value={date} />;
133
+ } else {
134
+ // minute interval or less - use full timestamp
135
+ return timestamp;
136
+ }
137
+ };
138
+
139
+ export const CustomTick = ({
113
140
  x,
114
141
  y,
115
142
  payload,
@@ -131,10 +158,11 @@ const CustomTick = ({
131
158
  overflow="visible"
132
159
  >
133
160
  <ConstrainedText
161
+ color="textSecondary"
134
162
  text={
135
- <Text variant="Smaller" color="textSecondary">
163
+ <Text variant="Smaller">
136
164
  {type.type === 'time'
137
- ? formatDate(new Date(payload.value), type.timeRange.interval)
165
+ ? formatDate(payload.value, type.timeRange.interval)
138
166
  : String(payload.value)}
139
167
  </Text>
140
168
  }
@@ -0,0 +1,76 @@
1
+ import styled from 'styled-components';
2
+ import { spacing, Stack, Wrap } from '../../spacing';
3
+ import { Text } from '../text/Text.component';
4
+ import { BarchartBars } from './Barchart.component';
5
+ import { fontSize, fontWeight } from '../../style/theme';
6
+ import { LegendShape } from '../chartlegend/ChartLegend';
7
+ import { FormattedDateTime } from '../date/FormattedDateTime';
8
+
9
+ export const ChartTooltipContainer = styled.div`
10
+ background-color: ${({ theme }) => theme.backgroundLevel1};
11
+ padding: ${spacing.r4} ${spacing.r8};
12
+ border-radius: 4px;
13
+ width: max-content;
14
+ max-width: 40rem;
15
+ border: 1px solid ${({ theme }) => theme.border};
16
+ display: flex;
17
+ font-size: ${fontSize.small};
18
+ flex-direction: column;
19
+ gap: 16px;
20
+ align-items: center;
21
+ `;
22
+
23
+ export const ChartTooltipItem = styled.div<{ isHovered: boolean }>`
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 8px;
27
+ font-weight: ${({ isHovered }) =>
28
+ isHovered ? fontWeight.bold : fontWeight.base};
29
+ `;
30
+
31
+ export const ChartTooltip = <T extends BarchartBars>({
32
+ type,
33
+ currentPoint,
34
+ colorSet,
35
+ }: {
36
+ type: 'time' | 'category';
37
+ currentPoint: {
38
+ category: string | number;
39
+ values: { label: T[number]['label']; value: number; isHovered: boolean }[];
40
+ };
41
+ colorSet: Record<string, string>;
42
+ }) => {
43
+ return (
44
+ <ChartTooltipContainer>
45
+ <Text isEmphazed>
46
+ {type === 'time' ? (
47
+ <FormattedDateTime
48
+ format="long-date"
49
+ value={new Date(currentPoint.category)}
50
+ />
51
+ ) : (
52
+ currentPoint.category
53
+ )}
54
+ </Text>
55
+ <Stack direction="vertical" gap="r8" style={{ width: '100%' }}>
56
+ {currentPoint.values.map((value) => {
57
+ return (
58
+ <Wrap key={value.label}>
59
+ <ChartTooltipItem isHovered={value.isHovered}>
60
+ <LegendShape
61
+ color={colorSet[value.label as keyof typeof colorSet]}
62
+ shape="rectangle"
63
+ chartColors={colorSet}
64
+ />
65
+ {value.label}
66
+ </ChartTooltipItem>
67
+ <ChartTooltipItem isHovered={value.isHovered}>
68
+ {value.value}
69
+ </ChartTooltipItem>
70
+ </Wrap>
71
+ );
72
+ })}
73
+ </Stack>
74
+ </ChartTooltipContainer>
75
+ );
76
+ };
@@ -3,7 +3,6 @@ import {
3
3
  BarchartBars,
4
4
  BarchartTooltipFn,
5
5
  } from './Barchart.component';
6
- import { DAY_MONTH_FORMATER, TIME_FORMATER } from '../date/FormattedDateTime';
7
6
  import { TooltipContentProps } from 'recharts';
8
7
  import { chartColors, ChartColors } from '../../style/theme';
9
8
  import { useChartLegend } from '../chartlegend/ChartLegendWrapper';
@@ -84,31 +83,6 @@ const generateTimeRanges = (
84
83
  return ranges;
85
84
  };
86
85
 
87
- /**
88
- * Formats a date based on the interval
89
- * @param date - Date object
90
- * @param interval - Interval in milliseconds
91
- * @returns Formatted string
92
- */
93
- export const formatDate = (date: Date, interval: number): string => {
94
- if (interval > 24 * 60 * 60 * 1000) {
95
- return (
96
- DAY_MONTH_FORMATER.format(date).replace(/[ ,]/g, '') +
97
- ' ' +
98
- TIME_FORMATER.format(date)
99
- );
100
- } else if (interval === 24 * 60 * 60 * 1000) {
101
- // Daily or longer intervals - use day format
102
- return DAY_MONTH_FORMATER.format(date).replace(/[ ,]/g, '');
103
- } else if (interval >= 60 * 1000) {
104
- //Handle hourly and minute intervals - use minute format
105
- return TIME_FORMATER.format(date);
106
- } else {
107
- // Second intervals or less - use full timestamp
108
- return date.toISOString();
109
- }
110
- };
111
-
112
86
  /**
113
87
  * Finds the time range that contains the given date
114
88
  * @param date - Data point date
@@ -149,17 +123,12 @@ export const transformTimeData = <T extends BarchartBars>(
149
123
  type.timeRange.interval,
150
124
  );
151
125
 
152
- const categoryMap = new Map<
153
- string | number,
154
- { [key: string]: string | number }
155
- >();
126
+ const categoryMap = new Map<number, { [key: string]: string | number }>();
156
127
 
157
128
  // Initialize all ranges with zeros
158
129
  timeRanges.forEach((range) => {
159
- // const categoryDisplay = formatDate(range.start, type.timeRange.interval);
160
- const categoryDisplay = range.start.getTime();
161
130
  const initialData: { [key: string]: string | number } = {
162
- category: categoryDisplay,
131
+ category: range.start.getTime(),
163
132
  };
164
133
  barDataKeys.forEach((dataKey) => {
165
134
  initialData[dataKey] = 0;
@@ -0,0 +1,218 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ChartLegend } from './ChartLegend';
3
+ import React from 'react';
4
+ import { ChartLegendWrapper } from './ChartLegendWrapper';
5
+ import userEvent from '@testing-library/user-event';
6
+
7
+ const colorSet = {
8
+ CPU: 'red',
9
+ Memory: 'blue',
10
+ Disk: 'green',
11
+ };
12
+
13
+ describe('ChartLegend', () => {
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+ it('should render all legend items', () => {
18
+ render(
19
+ <ChartLegendWrapper colorSet={colorSet}>
20
+ <ChartLegend shape="line" />
21
+ </ChartLegendWrapper>,
22
+ );
23
+ expect(screen.getByText('CPU')).toBeInTheDocument();
24
+ expect(screen.getByText('Memory')).toBeInTheDocument();
25
+ expect(screen.getByText('Disk')).toBeInTheDocument();
26
+ });
27
+
28
+ it('should have all items selected by default', () => {
29
+ render(
30
+ <ChartLegendWrapper colorSet={colorSet}>
31
+ <ChartLegend shape="line" />
32
+ </ChartLegendWrapper>,
33
+ );
34
+
35
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
36
+ expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
37
+ expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
38
+ });
39
+ it('should not respond to clicks when disabled', () => {
40
+ render(
41
+ <ChartLegendWrapper colorSet={colorSet}>
42
+ <ChartLegend shape="line" disabled />
43
+ </ChartLegendWrapper>,
44
+ );
45
+
46
+ userEvent.click(screen.getByText('CPU'));
47
+ // If disabled, should not select any items
48
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
49
+ expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
50
+ expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
51
+
52
+ userEvent.click(screen.getByText('Memory'), { metaKey: true });
53
+ // If disabled, should not select any items
54
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
55
+ expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
56
+ expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
57
+ });
58
+ describe('Normal Click Behavior', () => {
59
+ it('should select only clicked item when all are selected', () => {
60
+ render(
61
+ <ChartLegendWrapper colorSet={colorSet}>
62
+ <ChartLegend shape="line" />
63
+ </ChartLegendWrapper>,
64
+ );
65
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
66
+ expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
67
+ expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
68
+
69
+ // Click on CPU should select only CPU
70
+ userEvent.click(screen.getByLabelText('CPU selected'));
71
+
72
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
73
+ expect(screen.getByLabelText('Memory not selected')).toBeInTheDocument();
74
+ expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
75
+ });
76
+
77
+ it('should select all items when clicking on the only selected item', () => {
78
+ render(
79
+ <ChartLegendWrapper colorSet={colorSet}>
80
+ <ChartLegend shape="line" />
81
+ </ChartLegendWrapper>,
82
+ );
83
+
84
+ // First click to select only CPU
85
+ userEvent.click(screen.getByLabelText('CPU selected'));
86
+
87
+ // Second click should select all
88
+ userEvent.click(screen.getByLabelText('CPU selected'));
89
+
90
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
91
+ expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
92
+ expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
93
+ });
94
+
95
+ it('should select only clicked item when clicking unselected item', () => {
96
+ render(
97
+ <ChartLegendWrapper colorSet={colorSet}>
98
+ <ChartLegend shape="line" />
99
+ </ChartLegendWrapper>,
100
+ );
101
+
102
+ // Select only CPU first
103
+ userEvent.click(screen.getByLabelText('CPU selected'));
104
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
105
+ expect(screen.getByLabelText('Memory not selected')).toBeInTheDocument();
106
+
107
+ // Click on unselected Memory should select only Memory
108
+ userEvent.click(screen.getByText('Memory'));
109
+ expect(screen.getByLabelText('CPU not selected')).toBeInTheDocument();
110
+ expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
111
+ expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
112
+ });
113
+ });
114
+
115
+ describe('Cmd+Click Behavior', () => {
116
+ it('should add unselected item to selection when cmd+clicking', () => {
117
+ render(
118
+ <ChartLegendWrapper colorSet={colorSet}>
119
+ <ChartLegend shape="line" />
120
+ </ChartLegendWrapper>,
121
+ );
122
+
123
+ // Select only CPU first
124
+ userEvent.click(screen.getByText('CPU'));
125
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
126
+ expect(screen.getByLabelText('Memory not selected')).toBeInTheDocument();
127
+
128
+ // Cmd+click Memory should add it to selection
129
+ userEvent.click(screen.getByText('Memory'), { metaKey: true });
130
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
131
+ expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
132
+ expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
133
+ });
134
+
135
+ it('should remove selected item from selection when cmd+clicking with multiple selected', () => {
136
+ render(
137
+ <ChartLegendWrapper colorSet={colorSet}>
138
+ <ChartLegend shape="line" />
139
+ </ChartLegendWrapper>,
140
+ );
141
+
142
+ // Start with all selected, select only CPU, then add Memory
143
+ userEvent.click(screen.getByText('CPU'));
144
+ userEvent.click(screen.getByText('Memory'), { metaKey: true });
145
+
146
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
147
+ expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
148
+ expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
149
+
150
+ // Cmd+click CPU should remove it
151
+ userEvent.click(screen.getByText('CPU'), { metaKey: true });
152
+ expect(screen.getByLabelText('CPU not selected')).toBeInTheDocument();
153
+ expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
154
+ expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
155
+ });
156
+
157
+ it('should select all when cmd+clicking the only selected item', () => {
158
+ render(
159
+ <ChartLegendWrapper colorSet={colorSet}>
160
+ <ChartLegend shape="line" />
161
+ </ChartLegendWrapper>,
162
+ );
163
+
164
+ // Select only CPU
165
+ userEvent.click(screen.getByText('CPU'));
166
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
167
+ expect(screen.getByLabelText('Memory not selected')).toBeInTheDocument();
168
+ expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
169
+
170
+ // Cmd+click the only selected item should select all
171
+ userEvent.click(screen.getByText('CPU'), { metaKey: true });
172
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
173
+ expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
174
+ expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
175
+ });
176
+
177
+ it('should work with ctrl+click on Windows/Linux', () => {
178
+ render(
179
+ <ChartLegendWrapper colorSet={colorSet}>
180
+ <ChartLegend shape="line" />
181
+ </ChartLegendWrapper>,
182
+ );
183
+
184
+ // Select only CPU first
185
+ userEvent.click(screen.getByText('CPU'));
186
+
187
+ // Ctrl+click Memory should add it to selection
188
+ userEvent.click(screen.getByText('Memory'), { ctrlKey: true });
189
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
190
+ expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
191
+ expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
192
+ });
193
+
194
+ it('should handle mixed normal and cmd+clicks correctly', () => {
195
+ render(
196
+ <ChartLegendWrapper colorSet={colorSet}>
197
+ <ChartLegend shape="line" />
198
+ </ChartLegendWrapper>,
199
+ );
200
+
201
+ // Normal click to select CPU only
202
+ userEvent.click(screen.getByText('CPU'));
203
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
204
+ expect(screen.getByLabelText('Memory not selected')).toBeInTheDocument();
205
+
206
+ // Cmd+click to add Memory
207
+ userEvent.click(screen.getByText('Memory'), { metaKey: true });
208
+ expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
209
+ expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
210
+
211
+ // Normal click on Disk should select only Disk
212
+ userEvent.click(screen.getByText('Disk'));
213
+ expect(screen.getByLabelText('CPU not selected')).toBeInTheDocument();
214
+ expect(screen.getByLabelText('Memory not selected')).toBeInTheDocument();
215
+ expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
216
+ });
217
+ });
218
+ });