@bundle-stats/ui 4.13.0-beta.16 → 4.13.0-beta.17

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 (81) hide show
  1. package/lib/assets/icons.svg +70 -67
  2. package/lib/assets/icons.svg.js +14 -12
  3. package/lib/assets/icons.svg.js.map +1 -1
  4. package/lib/components/bundle-assets/bundle-assets.js +39 -7
  5. package/lib/components/bundle-assets/bundle-assets.js.map +1 -1
  6. package/lib/components/bundle-assets-totals/bundle-assets-totals.js +4 -4
  7. package/lib/components/bundle-assets-totals/bundle-assets-totals.js.map +1 -1
  8. package/lib/components/bundle-modules/bundle-modules.js +28 -7
  9. package/lib/components/bundle-modules/bundle-modules.js.map +1 -1
  10. package/lib/components/bundle-packages/bundle-packages.js +39 -7
  11. package/lib/components/bundle-packages/bundle-packages.js.map +1 -1
  12. package/lib/components/metrics-display-selector/metrics-display-selector.js +41 -4
  13. package/lib/components/metrics-display-selector/metrics-display-selector.js.map +1 -1
  14. package/lib/components/metrics-display-selector/metrics-display-selector.module.css +15 -7
  15. package/lib/components/metrics-treemap/index.js +4 -0
  16. package/lib/components/metrics-treemap/index.js.map +1 -1
  17. package/lib/components/metrics-treemap/metrics-treemap.constants.js +3 -0
  18. package/lib/components/metrics-treemap/metrics-treemap.constants.js.map +1 -0
  19. package/lib/components/metrics-treemap/metrics-treemap.js +57 -38
  20. package/lib/components/metrics-treemap/metrics-treemap.js.map +1 -1
  21. package/lib/components/metrics-treemap/metrics-treemap.module.css +55 -25
  22. package/lib/components/metrics-treemap/metrics-treemap.utils.js +70 -0
  23. package/lib/components/metrics-treemap/metrics-treemap.utils.js.map +1 -0
  24. package/lib/hooks/metrics-display-type.js +56 -24
  25. package/lib/hooks/metrics-display-type.js.map +1 -1
  26. package/lib/ui/icon/icon.js +1 -0
  27. package/lib/ui/icon/icon.js.map +1 -1
  28. package/lib-esm/assets/icons.svg +70 -67
  29. package/lib-esm/assets/icons.svg.js +14 -12
  30. package/lib-esm/assets/icons.svg.js.map +1 -1
  31. package/lib-esm/components/bundle-assets/bundle-assets.js +40 -8
  32. package/lib-esm/components/bundle-assets/bundle-assets.js.map +1 -1
  33. package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.js +5 -5
  34. package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.js.map +1 -1
  35. package/lib-esm/components/bundle-modules/bundle-modules.js +29 -8
  36. package/lib-esm/components/bundle-modules/bundle-modules.js.map +1 -1
  37. package/lib-esm/components/bundle-packages/bundle-packages.js +40 -8
  38. package/lib-esm/components/bundle-packages/bundle-packages.js.map +1 -1
  39. package/lib-esm/components/metrics-display-selector/metrics-display-selector.js +41 -4
  40. package/lib-esm/components/metrics-display-selector/metrics-display-selector.js.map +1 -1
  41. package/lib-esm/components/metrics-display-selector/metrics-display-selector.module.css +15 -7
  42. package/lib-esm/components/metrics-treemap/index.js +1 -0
  43. package/lib-esm/components/metrics-treemap/index.js.map +1 -1
  44. package/lib-esm/components/metrics-treemap/metrics-treemap.constants.js +2 -0
  45. package/lib-esm/components/metrics-treemap/metrics-treemap.constants.js.map +1 -0
  46. package/lib-esm/components/metrics-treemap/metrics-treemap.js +58 -39
  47. package/lib-esm/components/metrics-treemap/metrics-treemap.js.map +1 -1
  48. package/lib-esm/components/metrics-treemap/metrics-treemap.module.css +55 -25
  49. package/lib-esm/components/metrics-treemap/metrics-treemap.utils.js +65 -0
  50. package/lib-esm/components/metrics-treemap/metrics-treemap.utils.js.map +1 -0
  51. package/lib-esm/hooks/metrics-display-type.js +57 -24
  52. package/lib-esm/hooks/metrics-display-type.js.map +1 -1
  53. package/lib-esm/ui/icon/icon.js +1 -0
  54. package/lib-esm/ui/icon/icon.js.map +1 -1
  55. package/package.json +7 -7
  56. package/src/assets/icons/check.svg +4 -0
  57. package/src/assets/icons.svg +70 -67
  58. package/src/assets/icons.svg.jsx +12 -12
  59. package/src/components/bundle-assets/bundle-assets.jsx +76 -20
  60. package/src/components/bundle-assets-totals/bundle-assets-totals.jsx +5 -4
  61. package/src/components/bundle-modules/bundle-modules.tsx +76 -16
  62. package/src/components/bundle-packages/bundle-packages.jsx +71 -16
  63. package/src/components/metrics-display-selector/metrics-display-selector.module.css +15 -7
  64. package/src/components/metrics-display-selector/metrics-display-selector.stories.tsx +33 -0
  65. package/src/components/metrics-display-selector/metrics-display-selector.tsx +83 -25
  66. package/src/components/metrics-treemap/__tests__/metrics-treemap.utils.ts +143 -0
  67. package/src/components/metrics-treemap/index.ts +1 -0
  68. package/src/components/metrics-treemap/metrics-treemap.constants.ts +43 -0
  69. package/src/components/metrics-treemap/metrics-treemap.module.css +55 -25
  70. package/src/components/metrics-treemap/metrics-treemap.stories.tsx +44 -10
  71. package/src/components/metrics-treemap/metrics-treemap.tsx +247 -84
  72. package/src/components/metrics-treemap/metrics-treemap.utils.ts +107 -0
  73. package/src/hooks/metrics-display-type.ts +87 -29
  74. package/src/ui/icon/icon.tsx +1 -0
  75. package/types/components/metrics-display-selector/metrics-display-selector.d.ts +3 -1
  76. package/types/components/metrics-treemap/index.d.ts +1 -0
  77. package/types/components/metrics-treemap/metrics-treemap.constants.d.ts +15 -0
  78. package/types/components/metrics-treemap/metrics-treemap.d.ts +7 -5
  79. package/types/components/metrics-treemap/metrics-treemap.utils.d.ts +4 -0
  80. package/types/hooks/metrics-display-type.d.ts +8 -2
  81. package/types/ui/icon/icon.d.ts +2 -0
@@ -1,24 +1,34 @@
1
- import React, { RefObject, forwardRef, useCallback, useMemo, useRef } from 'react';
1
+ import React, {
2
+ type ComponentProps,
3
+ type MouseEventHandler,
4
+ type ReactNode,
5
+ type Ref,
6
+ type RefObject,
7
+ forwardRef,
8
+ useCallback,
9
+ useMemo,
10
+ useRef,
11
+ } from 'react';
2
12
  import { useDebounce, useHoverDirty, useMeasure, useMouseHovered } from 'react-use';
3
13
  import cx from 'classnames';
4
- import type { ReportMetricRow, MetricRunInfo, MetricRunInfoBaseline } from '@bundle-stats/utils';
5
- import { hierarchy, treemap, treemapSquarify } from 'd3';
14
+ import { HierarchyRectangularNode, hierarchy, treemap, treemapSquarify } from 'd3';
6
15
  import { Tooltip, TooltipArrow, TooltipAnchor, useTooltipState } from 'ariakit/tooltip';
16
+ import type { ReportMetricRow, MetricRunInfo, MetricRunInfoBaseline } from '@bundle-stats/utils';
7
17
 
8
18
  import { Stack } from '../../layout/stack';
9
19
  import { FileName } from '../../ui/file-name';
10
20
  import { Delta } from '../delta';
11
21
  import { RunInfo } from '../run-info';
22
+ import type { TreeLeaf, TreeNode, Tree } from './metrics-treemap.constants';
12
23
  import css from './metrics-treemap.module.css';
13
24
 
14
25
  const SQUARIFY_RATIO = 1.33;
15
- const PADDING_OUTER = 0;
26
+ const PADDING_OUTER = 2;
16
27
  const PADDING_INNER = 2;
17
-
18
- type TileSizeDisplay = 'minimal' | 'small' | 'default';
28
+ const GROUPED_PADDING_TOP = 16;
19
29
 
20
30
  /**
21
- * Resolve the tile size using predefined values to avoid
31
+ * Resolve the tile's size using predefined values to avoid
22
32
  * computing the size of the content for every tile
23
33
  */
24
34
  const PADDING_TOP = 4;
@@ -29,6 +39,20 @@ const VERTICAL_SPACING = 4;
29
39
  const LINE_HEIGHT = 16;
30
40
  const LINE_HEIGHT_SMALL = 13.3;
31
41
 
42
+ function resolveTileGroupSizeDisplay(width: number, height: number): TileSizeDisplay {
43
+ if (height < GROUPED_PADDING_TOP || width < 24) {
44
+ return 'minimal';
45
+ }
46
+
47
+ if (height < GROUPED_PADDING_TOP + 8) {
48
+ return 'small';
49
+ }
50
+
51
+ return 'default';
52
+ }
53
+
54
+ type TileSizeDisplay = 'minimal' | 'small' | 'default';
55
+
32
56
  function resolveTileSizeDisplay(width: number, height: number): TileSizeDisplay {
33
57
  if (
34
58
  height > PADDING_TOP + PADDING_BOTTOM + VERTICAL_SPACING + LINE_HEIGHT * 2 &&
@@ -73,12 +97,21 @@ const TileTooltipContent = (props: TileTooltipContentProps) => {
73
97
  };
74
98
 
75
99
  interface TileContentProps {
76
- sizeDisplay: 'minimal' | 'small' | 'default';
100
+ /**
101
+ * Tile id - metric label or basename for grouped tiles
102
+ */
77
103
  item: ReportMetricRow;
104
+ /**
105
+ * The estimated size of the tile
106
+ */
107
+ sizeDisplay: 'minimal' | 'small' | 'default';
108
+ /**
109
+ * Metric run info
110
+ */
78
111
  runInfo: MetricRunInfo;
79
112
  }
80
113
 
81
- const TileContent = forwardRef((props: TileContentProps, ref: React.Ref<HTMLDivElement>) => {
114
+ const TileContent = forwardRef((props: TileContentProps, ref: Ref<HTMLDivElement>) => {
82
115
  const { sizeDisplay, item, runInfo } = props;
83
116
 
84
117
  if (sizeDisplay === 'minimal') {
@@ -87,12 +120,12 @@ const TileContent = forwardRef((props: TileContentProps, ref: React.Ref<HTMLDivE
87
120
 
88
121
  return (
89
122
  <div className={css.tileContent} ref={ref}>
90
- <FileName as="p" className={css.leadContentLabel} name={item.label} />
123
+ <FileName as="p" className={css.tileContentLabel} name={item.label} />
91
124
  {sizeDisplay !== 'small' && (
92
- <p className={css.leadContentValue}>
93
- <span className={css.leadContentMetric}>{runInfo.displayValue}</span>
125
+ <p className={css.tileContentValue}>
126
+ <span className={css.tileContentMetric}>{runInfo.displayValue}</span>
94
127
  <Delta
95
- className={css.leadContentDelta}
128
+ className={css.tileContentDelta}
96
129
  displayValue={runInfo.displayDeltaPercentage}
97
130
  deltaType={runInfo.deltaType}
98
131
  />
@@ -142,25 +175,12 @@ const TileContentWithTooltip = (props: TileContentProps & { parentRef: RefObject
142
175
  );
143
176
  };
144
177
 
145
- interface TreeNode {
146
- id: string;
147
- value: number;
148
- item: ReportMetricRow;
149
- children?: Array<TreeNode>;
150
- }
151
-
152
- interface RootTree {
153
- id: string;
154
- value: number;
155
- children: Array<TreeNode>;
156
- }
157
-
158
178
  interface TileProps {
159
179
  left: number;
160
180
  top: number;
161
181
  width: number;
162
182
  height: number;
163
- data: TreeNode;
183
+ data: TreeLeaf;
164
184
  onClick?: (id: string) => void;
165
185
  }
166
186
 
@@ -171,7 +191,13 @@ const Tile = (props: TileProps) => {
171
191
  const runInfo = item.runs?.[0] as MetricRunInfo;
172
192
 
173
193
  const sizeDisplay = useMemo(() => resolveTileSizeDisplay(width, height), [width, height]);
174
- const handleOnClick = useCallback(() => onClick?.(item.key), [item.key, onClick]);
194
+ const handleOnClick: MouseEventHandler<HTMLButtonElement> = useCallback(
195
+ (event) => {
196
+ event.stopPropagation();
197
+ onClick?.(item.key);
198
+ },
199
+ [item.key, onClick],
200
+ );
175
201
 
176
202
  const contentRef = useRef<HTMLButtonElement>(null);
177
203
  const hover = useHoverDirty(contentRef);
@@ -179,7 +205,8 @@ const Tile = (props: TileProps) => {
179
205
  const className = cx(
180
206
  css.tile,
181
207
  css[`tile-${runInfo.deltaType}`],
182
- sizeDisplay === 'small' && css.tileSmall,
208
+ sizeDisplay === 'small' && css.tileSizeSmall,
209
+ sizeDisplay === 'default' && css.tileSizeDefault,
183
210
  left === 0 && css.tileFirstCol,
184
211
  );
185
212
 
@@ -203,96 +230,232 @@ const Tile = (props: TileProps) => {
203
230
  )}
204
231
  </button>
205
232
  );
233
+ /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus */
234
+ };
235
+
236
+ interface TileGroupProps extends ComponentProps<'div'> {
237
+ /**
238
+ * Node title
239
+ */
240
+ title?: string;
241
+ /**
242
+ * Node id
243
+ */
244
+ id: string;
245
+ /**
246
+ * Node children
247
+ */
248
+ childNodes: HierarchyRectangularNode<TreeNode>['children'];
249
+ /**
250
+ * On click tile handler
251
+ */
252
+ onItemClick: TileProps['onClick'];
253
+ /**
254
+ * On click group title
255
+ */
256
+ onGroupClick?: (groupId: string) => void;
257
+
258
+ /**
259
+ * Node rectangle dimensions
260
+ */
261
+ width?: number;
262
+ height?: number;
263
+
264
+ /**
265
+ * Node rectangle relative coordinates (relative to parent)
266
+ */
267
+ left: number;
268
+ top: number;
269
+
270
+ /**
271
+ * Node rectangle absolute coordinates (relative to canvas)
272
+ */
273
+ absoluteLeft: number;
274
+ absoluteTop: number;
275
+ }
276
+
277
+ const TileGroup = (props: TileGroupProps) => {
278
+ const {
279
+ title = '',
280
+ id,
281
+ childNodes,
282
+ onItemClick,
283
+ onGroupClick,
284
+ left,
285
+ top,
286
+ width,
287
+ height,
288
+ absoluteLeft,
289
+ absoluteTop,
290
+ } = props;
291
+
292
+ const onClick: MouseEventHandler<HTMLDivElement> = useCallback(
293
+ (event) => {
294
+ onGroupClick?.(id);
295
+ event.stopPropagation();
296
+ },
297
+ [onGroupClick, id],
298
+ );
299
+
300
+ const displaySize = useMemo(
301
+ () => resolveTileGroupSizeDisplay(width || 0, height || 0),
302
+ [width, height],
303
+ );
304
+
305
+ if (title && displaySize === 'minimal') {
306
+ return (
307
+ <div
308
+ onClick={onClick}
309
+ role="button"
310
+ aria-label="View children entries"
311
+ className={css.tileGroup}
312
+ style={{ left, top, width, height }}
313
+ />
314
+ );
315
+ }
316
+
317
+ if (title && displaySize === 'small') {
318
+ return (
319
+ <div
320
+ onClick={onClick}
321
+ role="button"
322
+ aria-label="View children entries"
323
+ style={{ left, top, width, height }}
324
+ className={css.tileGroup}
325
+ >
326
+ <div className={css.tileGroupTitle}>{title}</div>
327
+ </div>
328
+ );
329
+ }
330
+
331
+ return (
332
+ <div
333
+ onClick={onClick}
334
+ role="button"
335
+ aria-label="View children entries"
336
+ className={cx(css.tileGroup, css.tileGroupSizeDefault)}
337
+ style={{ left, top, width, height }}
338
+ >
339
+ {title && <div className={css.tileGroupTitle}>{title}</div>}
340
+ {childNodes?.map((childNode) => {
341
+ if ('children' in childNode) {
342
+ return (
343
+ <TileGroup
344
+ title={childNode.data.label}
345
+ id={childNode.data.id}
346
+ childNodes={childNode.children}
347
+ left={childNode.x0 - absoluteLeft}
348
+ top={childNode.y0 - absoluteTop}
349
+ absoluteLeft={childNode.x0}
350
+ absoluteTop={childNode.y0}
351
+ width={childNode.x1 - childNode.x0}
352
+ height={childNode.y1 - childNode.y0}
353
+ onItemClick={onItemClick}
354
+ onGroupClick={onGroupClick}
355
+ key={childNode.data.id}
356
+ />
357
+ );
358
+ }
359
+
360
+ return (
361
+ <Tile
362
+ data={childNode.data as TreeLeaf}
363
+ left={childNode.x0 - absoluteLeft}
364
+ top={childNode.y0 - absoluteTop}
365
+ width={childNode.x1 - childNode.x0}
366
+ height={childNode.y1 - childNode.y0}
367
+ onClick={onItemClick}
368
+ key={childNode.data.id}
369
+ />
370
+ );
371
+ })}
372
+ </div>
373
+ );
206
374
  };
207
375
 
208
376
  interface UseMetricsTreemapHierarchyParams {
209
- items: Array<ReportMetricRow>;
377
+ treeNodes: Tree;
210
378
  width: number;
211
379
  height: number;
380
+ nested: boolean;
212
381
  }
213
382
 
214
383
  function useMetricsTreemapHierarchy(params: UseMetricsTreemapHierarchyParams) {
215
- const { items, width, height } = params;
384
+ const { treeNodes, width, height, nested } = params;
216
385
 
217
386
  const treemapHierarchy = useMemo(() => {
218
- const children = items.map((item) => {
219
- /**
220
- * Set tile value to the largest run value to allow to
221
- * display the impact of deleted/new entries
222
- */
223
- const values = item.runs.map((run) => run?.value || 0);
224
- const value = Math.max(...values);
225
-
226
- return {
227
- id: item.key,
228
- value,
229
- item,
230
- };
231
- });
232
-
233
- const treemapData = {
234
- id: 'root',
235
- name: 'root',
236
- value: 0,
237
- children,
238
- };
387
+ const customHierarchy = hierarchy<Tree>(treeNodes);
239
388
 
240
- const customHierarchy = hierarchy<RootTree>(treemapData);
241
389
  customHierarchy.sum((node) => node.value);
390
+ // sort by value descending for all cases
242
391
  // @ts-expect-error
243
392
  customHierarchy.sort((a, b) => b.value - a.value);
244
393
 
245
394
  return customHierarchy;
246
- }, [items]);
395
+ }, [treeNodes]);
247
396
 
248
- const tiles = useMemo(() => {
249
- const createTremapLayout = treemap()
397
+ const treemapNodes = useMemo(() => {
398
+ let createTremapLayout = treemap()
250
399
  .size([width, height])
251
400
  .tile(treemapSquarify.ratio(SQUARIFY_RATIO))
252
401
  .paddingOuter(PADDING_OUTER)
253
402
  .paddingInner(PADDING_INNER);
254
- // @ts-expect-error
255
- const tremapLayout = createTremapLayout(treemapHierarchy);
256
403
 
257
- return tremapLayout.leaves().filter((tile) => Boolean(tile.parent));
404
+ // Add padding top for group title
405
+ if (nested) {
406
+ createTremapLayout = createTremapLayout.paddingTop(GROUPED_PADDING_TOP);
407
+ }
408
+
409
+ // @ts-expect-error
410
+ return createTremapLayout(treemapHierarchy) as HierarchyRectangularNode<TreeNode>;
258
411
  }, [height, width, treemapHierarchy]);
259
412
 
260
- return tiles;
413
+ return treemapNodes;
261
414
  }
262
415
 
263
416
  interface MetricsTreemapProps {
264
- items: Array<ReportMetricRow>;
265
- emptyMessage?: React.ReactNode;
417
+ treeNodes: Tree;
418
+ emptyMessage?: ReactNode;
419
+ nested?: boolean;
266
420
  onItemClick?: (entryId: string) => void;
421
+ onGroupClick?: (entryId: string) => void;
267
422
  }
268
423
 
269
- export const MetricsTreemap = (props: MetricsTreemapProps & React.ComponentProps<'div'>) => {
270
- const { className = '', items, onItemClick, emptyMessage = 'No data', ...restProps } = props;
424
+ export const MetricsTreemap = (props: MetricsTreemapProps & ComponentProps<'div'>) => {
425
+ const {
426
+ className = '',
427
+ treeNodes,
428
+ onItemClick,
429
+ onGroupClick,
430
+ emptyMessage = 'No data',
431
+ nested = false,
432
+ ...restProps
433
+ } = props;
271
434
 
272
435
  const [containerRef, { width, height }] = useMeasure<HTMLDivElement>();
273
- const tiles = useMetricsTreemapHierarchy({ items, width, height });
436
+ const rootNode = useMetricsTreemapHierarchy({ treeNodes, width, height, nested });
274
437
 
275
438
  return (
276
- <div className={cx(css.root, className)} {...restProps} ref={containerRef}>
277
- {tiles.length > 0 ? (
439
+ <div
440
+ className={cx(css.root, nested && css.nested, className)}
441
+ {...restProps}
442
+ ref={containerRef}
443
+ >
444
+ {rootNode.children && rootNode.children.length > 0 ? (
278
445
  <div className={css.canvas}>
279
- {tiles.map((tile) => {
280
- if (!tile.parent) {
281
- return null;
282
- }
283
-
284
- return (
285
- <Tile
286
- data={tile.data as TreeNode}
287
- left={tile.x0}
288
- top={tile.y0}
289
- width={tile.x1 - tile.x0}
290
- height={tile.y1 - tile.y0}
291
- onClick={onItemClick}
292
- key={tile.id}
293
- />
294
- );
295
- })}
446
+ <TileGroup
447
+ title={nested ? rootNode.data.label : undefined}
448
+ id={rootNode.data.id}
449
+ childNodes={rootNode.children}
450
+ onItemClick={onItemClick}
451
+ onGroupClick={onGroupClick}
452
+ left={rootNode.x0}
453
+ top={rootNode.y0}
454
+ absoluteLeft={rootNode.x0}
455
+ absoluteTop={rootNode.y0}
456
+ width={rootNode.x1 - rootNode.x0}
457
+ height={rootNode.y1 - rootNode.y0}
458
+ />
296
459
  </div>
297
460
  ) : (
298
461
  <div className={css.emptyMessage}>
@@ -0,0 +1,107 @@
1
+ import type { ReportMetricRow } from '@bundle-stats/utils';
2
+
3
+ import type { TreeLeaf, TreeNodeChildren, Tree } from './metrics-treemap.constants';
4
+
5
+ const ROOT_LABEL = '(root)';
6
+
7
+ /**
8
+ * Set node value to the largest run value to allow to
9
+ * display the impact of deleted/new entries
10
+ */
11
+ function getReportMetricRowMaxValue(item: ReportMetricRow): number {
12
+ const values = item.runs.map((run) => run?.value || 0);
13
+ return Math.max(...values);
14
+ }
15
+
16
+ /**
17
+ * Generate treemap node list(flat)
18
+ */
19
+ export function getTreemapNodes(items: Array<ReportMetricRow>): Tree {
20
+ const rootChildren: Array<TreeLeaf> = items.map((item) => ({
21
+ id: item.key,
22
+ label: item.key,
23
+ value: getReportMetricRowMaxValue(item),
24
+ item,
25
+ }));
26
+
27
+ return {
28
+ id: '',
29
+ label: ROOT_LABEL,
30
+ value: 0,
31
+ children: rootChildren,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Recursively set treemap nodes
37
+ */
38
+ function setTreeNode(
39
+ nodes: TreeNodeChildren,
40
+ slugs: Array<string>,
41
+ currentSlugId: number,
42
+ newNode: TreeLeaf,
43
+ ): void {
44
+ const baseSlugs = slugs.slice(0, currentSlugId);
45
+ const [currentSlug, ...restSlug] = slugs.slice(currentSlugId);
46
+
47
+ // Add to current nodes if there are no child nodes
48
+ if (!currentSlug) {
49
+ nodes.push(newNode);
50
+ return;
51
+ }
52
+
53
+ const currentNodePath = [...baseSlugs, currentSlug].join('/');
54
+
55
+ // Check for existing parent in current nodes
56
+ let parentNode = nodes.find((treeNode) => treeNode.id === currentNodePath) as Tree;
57
+
58
+ // Create the new parentNode if missing
59
+ if (!parentNode) {
60
+ parentNode = {
61
+ id: currentNodePath,
62
+ label: currentSlug,
63
+ value: 0,
64
+ children: [],
65
+ };
66
+
67
+ nodes.push(parentNode);
68
+ }
69
+
70
+ // If there are no other slugs, the new node is as leaf and we add it to the parent
71
+ if (restSlug.length === 0) {
72
+ parentNode.children.push(newNode);
73
+ return;
74
+ }
75
+
76
+ setTreeNode(parentNode.children, slugs, currentSlugId + 1, newNode);
77
+ }
78
+
79
+ /**
80
+ * Generate treemap nodes tree by directory
81
+ */
82
+ export function getTreemapNodesGroupedByPath(items: Array<ReportMetricRow>): Tree {
83
+ const treeNodes: TreeNodeChildren = [];
84
+
85
+ items.forEach((item) => {
86
+ // const normalizedPath = item.key.replace(/^\.?\//, ''); // replace './' or '/' prefix
87
+ const slugs = item.key.split('/');
88
+ const baseSlugs = slugs.slice(0, -1);
89
+ const baseName = slugs.slice(-1)[0];
90
+
91
+ const treeNode = {
92
+ id: item.key,
93
+ label: baseName,
94
+ value: getReportMetricRowMaxValue(item),
95
+ item,
96
+ };
97
+
98
+ setTreeNode(treeNodes, baseSlugs, 0, treeNode);
99
+ });
100
+
101
+ return {
102
+ id: '',
103
+ label: ROOT_LABEL,
104
+ value: 0,
105
+ children: treeNodes,
106
+ };
107
+ }
@@ -4,53 +4,111 @@ import { StringParam, useQueryParams } from 'use-query-params';
4
4
 
5
5
  import { MetricsDisplayType } from '../constants';
6
6
 
7
- const DEFAULT_DISPLAY_TYPE = MetricsDisplayType.TABLE;
8
- const PARAM_NAME = 'dt';
7
+ const DEFAULT_VALUE = MetricsDisplayType.TABLE;
8
+ const DEFAULT_GROUP_BY = '';
9
9
 
10
- type UseMetricsDisplayType = [MetricsDisplayType, (value: MetricsDisplayType) => void];
10
+ // Display type query param name
11
+ const QUERY_PARAM_VALUE = 'dt';
12
+ // Display type group by param name
13
+ const QUERY_PARAM_GROUP_BY = 'dtg';
11
14
 
12
- export const useMetricsDisplayType = (): UseMetricsDisplayType => {
13
- const [{ [PARAM_NAME]: queryDisplayType }, setQueryParams] = useQueryParams({
14
- [PARAM_NAME]: StringParam,
15
+ type UseMetricsDisplayType = [
16
+ { value: MetricsDisplayType; groupBy?: string },
17
+ (value: MetricsDisplayType, newGroupBy?: string) => void,
18
+ ];
19
+
20
+ export const useMetricsDisplayType = (
21
+ groups?: Partial<Record<MetricsDisplayType, Array<string>>>,
22
+ ): UseMetricsDisplayType => {
23
+ const [queryParams, setQueryParams] = useQueryParams({
24
+ [QUERY_PARAM_VALUE]: StringParam,
25
+ [QUERY_PARAM_GROUP_BY]: StringParam,
15
26
  });
16
- const [storageDisplayType, setStoreageDisplayType] = useLocalStorage(
17
- PARAM_NAME,
18
- MetricsDisplayType.TABLE,
27
+
28
+ const { [QUERY_PARAM_VALUE]: queryParamValue, [QUERY_PARAM_GROUP_BY]: queryParamGroupBy } =
29
+ queryParams;
30
+
31
+ const [storageValue, setStorageValue] = useLocalStorage(QUERY_PARAM_VALUE, DEFAULT_VALUE);
32
+ const [storageGroupBy, setStorageGroupBy] = useLocalStorage(
33
+ QUERY_PARAM_GROUP_BY,
34
+ DEFAULT_GROUP_BY,
19
35
  );
20
36
 
21
- const displayType = useMemo(() => {
22
- const value = queryDisplayType || storageDisplayType;
37
+ /**
38
+ * Normalize display type value
39
+ */
40
+ const value = useMemo(() => {
41
+ const resolvedValue = queryParamValue || storageValue;
42
+
43
+ if (!resolvedValue) {
44
+ return DEFAULT_VALUE;
45
+ }
46
+
47
+ if (!Object.values(MetricsDisplayType).includes(resolvedValue as MetricsDisplayType)) {
48
+ return DEFAULT_VALUE;
49
+ }
50
+
51
+ return resolvedValue as MetricsDisplayType;
52
+ }, [queryParamValue, storageValue]);
53
+
54
+ /**
55
+ * Normalize display type group by
56
+ */
57
+ const groupBy = useMemo(() => {
58
+ const resolvedValue = queryParamGroupBy || storageGroupBy;
23
59
 
24
- if (!value) {
25
- return DEFAULT_DISPLAY_TYPE;
60
+ if (!resolvedValue) {
61
+ return DEFAULT_GROUP_BY;
26
62
  }
27
63
 
28
- if (!Object.values(MetricsDisplayType).includes(value as MetricsDisplayType)) {
29
- return DEFAULT_DISPLAY_TYPE;
64
+ if (!groups?.[value]?.includes(resolvedValue as MetricsDisplayType)) {
65
+ return DEFAULT_GROUP_BY;
30
66
  }
31
67
 
32
- return value as MetricsDisplayType;
33
- }, [queryDisplayType, storageDisplayType]);
68
+ return resolvedValue;
69
+ }, [queryParamGroupBy, storageGroupBy]);
34
70
 
71
+ /**
72
+ * Synchronize query params when values are different
73
+ */
35
74
  useEffect(() => {
36
- // Update query string display type when missing
37
- if (queryDisplayType !== displayType) {
38
- setQueryParams({ [PARAM_NAME]: displayType });
75
+ if (value !== queryParamValue || groupBy !== queryParamGroupBy) {
76
+ setQueryParams({ [QUERY_PARAM_VALUE]: value, [QUERY_PARAM_GROUP_BY]: groupBy });
39
77
  }
78
+ }, [value, queryParamValue, groupBy, queryParamGroupBy, setQueryParams]);
40
79
 
41
- // Update storage state when query string is set and different
42
- if (storageDisplayType !== displayType) {
43
- setStoreageDisplayType(queryDisplayType as MetricsDisplayType);
80
+ /**
81
+ * Synchronize storeage when values are different
82
+ */
83
+ useEffect(() => {
84
+ if (value !== storageValue) {
85
+ setStorageValue(value);
86
+ }
87
+ if (groupBy !== storageValue) {
88
+ setStorageGroupBy(groupBy);
44
89
  }
45
- }, [displayType, queryDisplayType, storageDisplayType, setQueryParams, setStoreageDisplayType]);
90
+ }, [
91
+ value,
92
+ storageValue,
93
+ groupBy,
94
+ storageGroupBy,
95
+ setQueryParams,
96
+ setStorageValue,
97
+ setStorageGroupBy,
98
+ ]);
46
99
 
47
100
  const setDisplayType = useCallback(
48
- (value: MetricsDisplayType) => {
49
- setStoreageDisplayType(value);
50
- setQueryParams({ [PARAM_NAME]: value });
101
+ (newValue: MetricsDisplayType, newGroupBy: string = DEFAULT_GROUP_BY) => {
102
+ setStorageValue(newValue);
103
+ setStorageGroupBy(newGroupBy);
104
+
105
+ setQueryParams({
106
+ [QUERY_PARAM_VALUE]: newValue,
107
+ [QUERY_PARAM_GROUP_BY]: newGroupBy,
108
+ });
51
109
  },
52
- [setStoreageDisplayType, setQueryParams],
110
+ [setStorageValue, setQueryParams],
53
111
  );
54
112
 
55
- return [displayType, setDisplayType];
113
+ return [{ value, groupBy }, setDisplayType];
56
114
  };
@@ -9,6 +9,7 @@ const ICONS = {
9
9
  CANCEL: 'close',
10
10
  CHEVRON_DOWN: 'chevron-down',
11
11
  CHEVRON_UP: 'chevron-up',
12
+ CHECK: 'check',
12
13
  CLOSE: 'close',
13
14
  CLOCK: 'clock',
14
15
  COMMIT: 'commit',