@bundle-stats/ui 4.13.0-beta.9 → 4.13.0-rc.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 (198) hide show
  1. package/lib/app/app.js +4 -8
  2. package/lib/app/app.js.map +1 -1
  3. package/lib/app/app.module.css +2 -2
  4. package/lib/assets/icons.svg +70 -67
  5. package/lib/assets/icons.svg.js +14 -12
  6. package/lib/assets/icons.svg.js.map +1 -1
  7. package/lib/components/bundle-assets/bundle-assets.js +44 -10
  8. package/lib/components/bundle-assets/bundle-assets.js.map +1 -1
  9. package/lib/components/bundle-assets/bundle-assets.module.css +0 -5
  10. package/lib/components/bundle-assets-totals/bundle-assets-totals.js +14 -12
  11. package/lib/components/bundle-assets-totals/bundle-assets-totals.js.map +1 -1
  12. package/lib/components/bundle-modules/bundle-modules.i18n.js +1 -1
  13. package/lib/components/bundle-modules/bundle-modules.js +38 -15
  14. package/lib/components/bundle-modules/bundle-modules.js.map +1 -1
  15. package/lib/components/bundle-modules/bundle-modules.module.css +0 -5
  16. package/lib/components/bundle-packages/bundle-packages.js +43 -9
  17. package/lib/components/bundle-packages/bundle-packages.js.map +1 -1
  18. package/lib/components/bundle-packages/bundle-packages.module.css +0 -5
  19. package/lib/components/delta/delta.js +1 -1
  20. package/lib/components/delta/delta.js.map +1 -1
  21. package/lib/components/delta/delta.module.css +10 -1
  22. package/lib/components/jobs-header/jobs-header.module.css +0 -4
  23. package/lib/components/metrics-display-selector/metrics-display-selector.js +41 -4
  24. package/lib/components/metrics-display-selector/metrics-display-selector.js.map +1 -1
  25. package/lib/components/metrics-display-selector/metrics-display-selector.module.css +15 -7
  26. package/lib/components/metrics-treemap/index.js +4 -0
  27. package/lib/components/metrics-treemap/index.js.map +1 -1
  28. package/lib/components/metrics-treemap/metrics-treemap.constants.js +3 -0
  29. package/lib/components/metrics-treemap/metrics-treemap.constants.js.map +1 -0
  30. package/lib/components/metrics-treemap/metrics-treemap.i18n.js +6 -0
  31. package/lib/components/metrics-treemap/metrics-treemap.i18n.js.map +1 -0
  32. package/lib/components/metrics-treemap/metrics-treemap.js +100 -62
  33. package/lib/components/metrics-treemap/metrics-treemap.js.map +1 -1
  34. package/lib/components/metrics-treemap/metrics-treemap.module.css +139 -51
  35. package/lib/components/metrics-treemap/metrics-treemap.utils.js +109 -0
  36. package/lib/components/metrics-treemap/metrics-treemap.utils.js.map +1 -0
  37. package/lib/components/module-info/module-info.js +1 -2
  38. package/lib/components/module-info/module-info.js.map +1 -1
  39. package/lib/components/run-info/run-info.module.css +7 -7
  40. package/lib/css/variables.css +14 -13
  41. package/lib/hooks/metrics-display-type.js +45 -24
  42. package/lib/hooks/metrics-display-type.js.map +1 -1
  43. package/lib/hooks/search-params.js +1 -0
  44. package/lib/hooks/search-params.js.map +1 -1
  45. package/lib/layout/box/box.module.css +4 -2
  46. package/lib/layout/flex-stack/flex-stack.js +3 -4
  47. package/lib/layout/flex-stack/flex-stack.js.map +1 -1
  48. package/lib/layout/flex-stack/flex-stack.module.css +19 -23
  49. package/lib/ui/button/button.js +5 -4
  50. package/lib/ui/button/button.js.map +1 -1
  51. package/lib/ui/button/button.module.css +55 -53
  52. package/lib/ui/dropdown/dropdown.js +2 -2
  53. package/lib/ui/dropdown/dropdown.js.map +1 -1
  54. package/lib/ui/filters/filters.js +2 -2
  55. package/lib/ui/filters/filters.js.map +1 -1
  56. package/lib/ui/filters/filters.module.css +8 -1
  57. package/lib/ui/hover-card/hover-card.js +1 -1
  58. package/lib/ui/hover-card/hover-card.js.map +1 -1
  59. package/lib/ui/icon/icon.js +2 -1
  60. package/lib/ui/icon/icon.js.map +1 -1
  61. package/lib/ui/input-search/input-search.js +1 -1
  62. package/lib/ui/input-search/input-search.js.map +1 -1
  63. package/lib/ui/loader/loader.js +1 -1
  64. package/lib/ui/loader/loader.js.map +1 -1
  65. package/lib/ui/table/table.module.css +30 -0
  66. package/lib/ui/tag/tag.module.css +3 -3
  67. package/lib/ui/toolbar/toolbar.module.css +1 -3
  68. package/lib-esm/app/app.js +4 -8
  69. package/lib-esm/app/app.js.map +1 -1
  70. package/lib-esm/app/app.module.css +2 -2
  71. package/lib-esm/assets/icons.svg +70 -67
  72. package/lib-esm/assets/icons.svg.js +14 -12
  73. package/lib-esm/assets/icons.svg.js.map +1 -1
  74. package/lib-esm/components/bundle-assets/bundle-assets.js +45 -11
  75. package/lib-esm/components/bundle-assets/bundle-assets.js.map +1 -1
  76. package/lib-esm/components/bundle-assets/bundle-assets.module.css +0 -5
  77. package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.js +15 -13
  78. package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.js.map +1 -1
  79. package/lib-esm/components/bundle-modules/bundle-modules.i18n.js +1 -1
  80. package/lib-esm/components/bundle-modules/bundle-modules.js +39 -16
  81. package/lib-esm/components/bundle-modules/bundle-modules.js.map +1 -1
  82. package/lib-esm/components/bundle-modules/bundle-modules.module.css +0 -5
  83. package/lib-esm/components/bundle-packages/bundle-packages.js +44 -10
  84. package/lib-esm/components/bundle-packages/bundle-packages.js.map +1 -1
  85. package/lib-esm/components/bundle-packages/bundle-packages.module.css +0 -5
  86. package/lib-esm/components/delta/delta.js +1 -1
  87. package/lib-esm/components/delta/delta.js.map +1 -1
  88. package/lib-esm/components/delta/delta.module.css +10 -1
  89. package/lib-esm/components/jobs-header/jobs-header.module.css +0 -4
  90. package/lib-esm/components/metrics-display-selector/metrics-display-selector.js +41 -4
  91. package/lib-esm/components/metrics-display-selector/metrics-display-selector.js.map +1 -1
  92. package/lib-esm/components/metrics-display-selector/metrics-display-selector.module.css +15 -7
  93. package/lib-esm/components/metrics-treemap/index.js +1 -0
  94. package/lib-esm/components/metrics-treemap/index.js.map +1 -1
  95. package/lib-esm/components/metrics-treemap/metrics-treemap.constants.js +2 -0
  96. package/lib-esm/components/metrics-treemap/metrics-treemap.constants.js.map +1 -0
  97. package/lib-esm/components/metrics-treemap/metrics-treemap.i18n.js +3 -0
  98. package/lib-esm/components/metrics-treemap/metrics-treemap.i18n.js.map +1 -0
  99. package/lib-esm/components/metrics-treemap/metrics-treemap.js +102 -63
  100. package/lib-esm/components/metrics-treemap/metrics-treemap.js.map +1 -1
  101. package/lib-esm/components/metrics-treemap/metrics-treemap.module.css +139 -51
  102. package/lib-esm/components/metrics-treemap/metrics-treemap.utils.js +105 -0
  103. package/lib-esm/components/metrics-treemap/metrics-treemap.utils.js.map +1 -0
  104. package/lib-esm/components/module-info/module-info.js +1 -2
  105. package/lib-esm/components/module-info/module-info.js.map +1 -1
  106. package/lib-esm/components/run-info/run-info.module.css +7 -7
  107. package/lib-esm/css/variables.css +14 -13
  108. package/lib-esm/hooks/metrics-display-type.js +46 -24
  109. package/lib-esm/hooks/metrics-display-type.js.map +1 -1
  110. package/lib-esm/hooks/search-params.js +1 -0
  111. package/lib-esm/hooks/search-params.js.map +1 -1
  112. package/lib-esm/layout/box/box.module.css +4 -2
  113. package/lib-esm/layout/flex-stack/flex-stack.js +3 -4
  114. package/lib-esm/layout/flex-stack/flex-stack.js.map +1 -1
  115. package/lib-esm/layout/flex-stack/flex-stack.module.css +19 -23
  116. package/lib-esm/ui/button/button.js +5 -4
  117. package/lib-esm/ui/button/button.js.map +1 -1
  118. package/lib-esm/ui/button/button.module.css +55 -53
  119. package/lib-esm/ui/dropdown/dropdown.js +2 -2
  120. package/lib-esm/ui/dropdown/dropdown.js.map +1 -1
  121. package/lib-esm/ui/filters/filters.js +2 -2
  122. package/lib-esm/ui/filters/filters.js.map +1 -1
  123. package/lib-esm/ui/filters/filters.module.css +8 -1
  124. package/lib-esm/ui/hover-card/hover-card.js +1 -1
  125. package/lib-esm/ui/hover-card/hover-card.js.map +1 -1
  126. package/lib-esm/ui/icon/icon.js +2 -1
  127. package/lib-esm/ui/icon/icon.js.map +1 -1
  128. package/lib-esm/ui/input-search/input-search.js +1 -1
  129. package/lib-esm/ui/input-search/input-search.js.map +1 -1
  130. package/lib-esm/ui/loader/loader.js +1 -1
  131. package/lib-esm/ui/loader/loader.js.map +1 -1
  132. package/lib-esm/ui/table/table.module.css +30 -0
  133. package/lib-esm/ui/tag/tag.module.css +3 -3
  134. package/lib-esm/ui/toolbar/toolbar.module.css +1 -3
  135. package/package.json +9 -9
  136. package/src/app/app.jsx +4 -12
  137. package/src/app/app.module.css +2 -2
  138. package/src/assets/icons/check.svg +4 -0
  139. package/src/assets/icons.svg +70 -67
  140. package/src/assets/icons.svg.jsx +12 -12
  141. package/src/components/bundle-assets/bundle-assets.jsx +82 -25
  142. package/src/components/bundle-assets/bundle-assets.module.css +0 -5
  143. package/src/components/bundle-assets-totals/bundle-assets-totals.jsx +53 -46
  144. package/src/components/bundle-modules/bundle-modules.i18n.ts +1 -1
  145. package/src/components/bundle-modules/bundle-modules.module.css +0 -5
  146. package/src/components/bundle-modules/bundle-modules.tsx +116 -53
  147. package/src/components/bundle-packages/bundle-packages.jsx +77 -20
  148. package/src/components/bundle-packages/bundle-packages.module.css +0 -5
  149. package/src/components/delta/delta.jsx +1 -1
  150. package/src/components/delta/delta.module.css +10 -1
  151. package/src/components/jobs-header/jobs-header.module.css +0 -4
  152. package/src/components/metrics-display-selector/metrics-display-selector.module.css +15 -7
  153. package/src/components/metrics-display-selector/metrics-display-selector.stories.tsx +33 -0
  154. package/src/components/metrics-display-selector/metrics-display-selector.tsx +83 -25
  155. package/src/components/metrics-treemap/__tests__/metrics-treemap.utils.ts +188 -0
  156. package/src/components/metrics-treemap/index.ts +1 -0
  157. package/src/components/metrics-treemap/metrics-treemap.constants.ts +52 -0
  158. package/src/components/metrics-treemap/metrics-treemap.i18n.ts +2 -0
  159. package/src/components/metrics-treemap/metrics-treemap.module.css +139 -51
  160. package/src/components/metrics-treemap/metrics-treemap.stories.tsx +44 -10
  161. package/src/components/metrics-treemap/metrics-treemap.tsx +329 -121
  162. package/src/components/metrics-treemap/metrics-treemap.utils.ts +162 -0
  163. package/src/components/module-info/module-info.tsx +1 -1
  164. package/src/components/run-info/run-info.module.css +7 -7
  165. package/src/css/variables.css +14 -13
  166. package/src/hooks/metrics-display-type.ts +79 -29
  167. package/src/hooks/search-params.js +1 -0
  168. package/src/layout/box/box.module.css +4 -2
  169. package/src/layout/flex-stack/flex-stack.module.css +19 -23
  170. package/src/layout/flex-stack/flex-stack.tsx +2 -8
  171. package/src/ui/button/button.module.css +55 -53
  172. package/src/ui/button/button.stories.tsx +16 -9
  173. package/src/ui/button/button.tsx +8 -7
  174. package/src/ui/dropdown/dropdown.tsx +2 -3
  175. package/src/ui/filters/filters.module.css +8 -1
  176. package/src/ui/filters/filters.tsx +2 -3
  177. package/src/ui/hover-card/hover-card.tsx +1 -1
  178. package/src/ui/icon/icon.tsx +2 -1
  179. package/src/ui/input-search/input-search.tsx +1 -0
  180. package/src/ui/loader/loader.jsx +1 -1
  181. package/src/ui/table/table.module.css +30 -0
  182. package/src/ui/tag/tag.module.css +3 -3
  183. package/src/ui/toolbar/toolbar.module.css +1 -3
  184. package/types/components/bundle-modules/bundle-modules.i18n.d.ts +1 -1
  185. package/types/components/delta/delta.d.ts +2 -2
  186. package/types/components/metrics-display-selector/metrics-display-selector.d.ts +3 -1
  187. package/types/components/metrics-treemap/index.d.ts +1 -0
  188. package/types/components/metrics-treemap/metrics-treemap.constants.d.ts +20 -0
  189. package/types/components/metrics-treemap/metrics-treemap.d.ts +7 -5
  190. package/types/components/metrics-treemap/metrics-treemap.i18n.d.ts +2 -0
  191. package/types/components/metrics-treemap/metrics-treemap.utils.d.ts +5 -0
  192. package/types/hooks/metrics-display-type.d.ts +8 -2
  193. package/types/ui/dropdown/dropdown.d.ts +0 -1
  194. package/types/ui/icon/icon.d.ts +4 -2
  195. package/types/ui/loader/loader.d.ts +2 -2
  196. package/lib/components/bundle-assets-totals/bundle-assets-totals.module.css +0 -7
  197. package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.module.css +0 -7
  198. package/src/components/bundle-assets-totals/bundle-assets-totals.module.css +0 -7
@@ -1,35 +1,69 @@
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 {
17
+ type ReportMetricRow,
18
+ type MetricRunInfo,
19
+ type MetricRunInfoBaseline,
20
+ getMetricRunInfo,
21
+ METRIC_TYPE_CONFIGS,
22
+ } from '@bundle-stats/utils';
7
23
 
8
24
  import { Stack } from '../../layout/stack';
9
25
  import { FileName } from '../../ui/file-name';
10
26
  import { Delta } from '../delta';
11
27
  import { RunInfo } from '../run-info';
28
+ import type { TreeLeaf, TreeNode, Tree } from './metrics-treemap.constants';
29
+ import * as I18N from './metrics-treemap.i18n';
12
30
  import css from './metrics-treemap.module.css';
31
+ import { resolveGroupDeltaType } from './metrics-treemap.utils';
13
32
 
14
- const SQUARIFY_RATIO = 1.33;
15
- const PADDING_OUTER = 0;
33
+ const SQUARIFY_RATIO = 1.66;
34
+ const PADDING_OUTER = 1;
16
35
  const PADDING_INNER = 2;
17
-
18
- type TileSizeDisplay = 'minimal' | 'small' | 'default';
36
+ const NESTED_PADDING = 4;
37
+ const NESTED_PADDING_TOP = 16 + NESTED_PADDING * 2;
38
+ const NESTED_PADDING_LEFT = 8;
19
39
 
20
40
  /**
21
- * Resolve the tile size using predefined values to avoid
41
+ * Resolve the tile's size using predefined values to avoid
22
42
  * computing the size of the content for every tile
23
43
  */
24
44
  const PADDING_TOP = 4;
25
45
  const PADDING_BOTTOM = 2; // substracted to allow longer texts to appear
26
- const PADDING_LEFT = 16;
46
+ const PADDING_LEFT = 2;
27
47
  const PADDING_RIGHT = 4; // substracted to allow longer texts to not break
28
48
  const VERTICAL_SPACING = 4;
29
49
  const LINE_HEIGHT = 16;
30
50
  const LINE_HEIGHT_SMALL = 13.3;
31
51
 
32
- function resolveLeafSizeDisplay(width: number, height: number): TileSizeDisplay {
52
+ function resolveTileGroupSizeDisplay(width: number, height: number): TileSizeDisplay {
53
+ if (height < NESTED_PADDING_TOP || width < 24) {
54
+ return 'minimal';
55
+ }
56
+
57
+ if (height < NESTED_PADDING_TOP + 8) {
58
+ return 'small';
59
+ }
60
+
61
+ return 'default';
62
+ }
63
+
64
+ type TileSizeDisplay = 'minimal' | 'small' | 'default';
65
+
66
+ function resolveTileSizeDisplay(width: number, height: number): TileSizeDisplay {
33
67
  if (
34
68
  height > PADDING_TOP + PADDING_BOTTOM + VERTICAL_SPACING + LINE_HEIGHT * 2 &&
35
69
  width > PADDING_LEFT + PADDING_RIGHT + 128
@@ -47,19 +81,19 @@ function resolveLeafSizeDisplay(width: number, height: number): TileSizeDisplay
47
81
  return 'minimal';
48
82
  }
49
83
 
50
- interface LeafTooltipContentProps {
84
+ interface TileTooltipContentProps {
51
85
  item: ReportMetricRow;
52
86
  }
53
87
 
54
- const LeafTooltipContent = (props: LeafTooltipContentProps) => {
88
+ const TileTooltipContent = (props: TileTooltipContentProps) => {
55
89
  const { item } = props;
56
90
 
57
91
  const currentRun = item.runs[0] as MetricRunInfo;
58
92
  const baselineRun = item.runs[item.runs.length - 1] as MetricRunInfoBaseline;
59
93
 
60
94
  return (
61
- <Stack space="small" className={css.leafTooltipContent}>
62
- <h3 className={css.leafTooltipContentTitle}>
95
+ <Stack space="small" className={css.tileTooltipContent}>
96
+ <h3 className={css.tileTooltipContentTitle}>
63
97
  <FileName as="code" name={item.label} />
64
98
  </h3>
65
99
  <RunInfo
@@ -72,27 +106,40 @@ const LeafTooltipContent = (props: LeafTooltipContentProps) => {
72
106
  );
73
107
  };
74
108
 
75
- interface LeafContentProps {
76
- sizeDisplay: 'minimal' | 'small' | 'default';
109
+ interface TileContentProps {
110
+ /**
111
+ * Node label
112
+ */
113
+ label: string;
114
+ /**
115
+ * Tile id - metric label or basename for grouped tiles
116
+ */
77
117
  item: ReportMetricRow;
118
+ /**
119
+ * The estimated size of the tile
120
+ */
121
+ sizeDisplay: 'minimal' | 'small' | 'default';
122
+ /**
123
+ * Metric run info
124
+ */
78
125
  runInfo: MetricRunInfo;
79
126
  }
80
127
 
81
- const LeafContent = forwardRef((props: LeafContentProps, ref: React.Ref<HTMLDivElement>) => {
82
- const { sizeDisplay, item, runInfo } = props;
128
+ const TileContent = forwardRef((props: TileContentProps, ref: Ref<HTMLDivElement>) => {
129
+ const { label, sizeDisplay, item, runInfo } = props;
83
130
 
84
131
  if (sizeDisplay === 'minimal') {
85
- return <div className={css.leafContent} ref={ref} />;
132
+ return <div className={css.tileContent} ref={ref} />;
86
133
  }
87
134
 
88
135
  return (
89
- <div className={css.leafContent} ref={ref}>
90
- <FileName as="p" className={css.leadContentLabel} name={item.label} />
136
+ <div className={css.tileContent} ref={ref}>
137
+ <FileName as="p" className={css.tileContentLabel} name={label || item.label} />
91
138
  {sizeDisplay !== 'small' && (
92
- <p className={css.leadContentValue}>
93
- <span className={css.leadContentMetric}>{runInfo.displayValue}</span>
139
+ <p className={css.tileContentValue}>
140
+ <span className={css.tileContentMetric}>{runInfo.displayValue}</span>
94
141
  <Delta
95
- className={css.leadContentDelta}
142
+ className={css.tileContentDelta}
96
143
  displayValue={runInfo.displayDeltaPercentage}
97
144
  deltaType={runInfo.deltaType}
98
145
  />
@@ -102,8 +149,8 @@ const LeafContent = forwardRef((props: LeafContentProps, ref: React.Ref<HTMLDivE
102
149
  );
103
150
  });
104
151
 
105
- const LeafContentWithTooltip = (props: LeafContentProps & { parentRef: RefObject<Element> }) => {
106
- const { sizeDisplay, item, runInfo, parentRef } = props;
152
+ const TileContentWithTooltip = (props: TileContentProps & { parentRef: RefObject<Element> }) => {
153
+ const { label, sizeDisplay, item, runInfo, parentRef } = props;
107
154
 
108
155
  const pointer = useMouseHovered(parentRef, { whenHovered: true });
109
156
 
@@ -131,168 +178,329 @@ const LeafContentWithTooltip = (props: LeafContentProps & { parentRef: RefObject
131
178
 
132
179
  return (
133
180
  <>
134
- <TooltipAnchor state={tooltipState} className={css.leafContentTooltipAnchor}>
135
- <LeafContent sizeDisplay={sizeDisplay} item={item} runInfo={runInfo} />
181
+ <TooltipAnchor state={tooltipState} className={css.tileContentTooltipAnchor}>
182
+ <TileContent label={label} sizeDisplay={sizeDisplay} item={item} runInfo={runInfo} />
136
183
  </TooltipAnchor>
137
184
  <Tooltip state={tooltipState} className={css.tooltip}>
138
- <TooltipArrow state={tooltipState} size={16} className={css.leafTooltipArrow} />
139
- <LeafTooltipContent item={item} />
185
+ <TooltipArrow state={tooltipState} size={16} className={css.tileTooltipArrow} />
186
+ <TileTooltipContent item={item} />
140
187
  </Tooltip>
141
188
  </>
142
189
  );
143
190
  };
144
191
 
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
- interface LeafProps {
192
+ interface TileProps {
159
193
  left: number;
160
194
  top: number;
161
195
  width: number;
162
196
  height: number;
163
- data: TreeNode;
197
+ data: TreeLeaf;
164
198
  onClick?: (id: string) => void;
165
199
  }
166
200
 
167
- const Leaf = (props: LeafProps) => {
201
+ const Tile = (props: TileProps) => {
168
202
  const { left, top, width, height, data, onClick } = props;
169
203
 
170
- const { item } = data;
204
+ const { item, label } = data;
171
205
  const runInfo = item.runs?.[0] as MetricRunInfo;
172
206
 
173
- const sizeDisplay = useMemo(() => resolveLeafSizeDisplay(width, height), [width, height]);
174
- const handleOnClick = useCallback(() => onClick?.(item.key), [item.key, onClick]);
207
+ const sizeDisplay = useMemo(() => resolveTileSizeDisplay(width, height), [width, height]);
208
+ const handleOnClick: MouseEventHandler<HTMLDivElement> = useCallback(
209
+ (event) => {
210
+ event.stopPropagation();
211
+ onClick?.(item.key);
212
+ },
213
+ [item.key, onClick],
214
+ );
175
215
 
176
- const leafContentRef = useRef<HTMLButtonElement>(null);
177
- const hover = useHoverDirty(leafContentRef);
216
+ const contentRef = useRef<HTMLDivElement>(null);
217
+ const hover = useHoverDirty(contentRef);
178
218
 
179
- const leafClassName = cx(
180
- css.leaf,
181
- css[`leaf-${runInfo.deltaType}`],
182
- sizeDisplay === 'small' && css.leafSmall,
183
- left === 0 && css.leafFirstCol,
219
+ const className = cx(
220
+ css.tile,
221
+ css[`tile-${runInfo.deltaType}`],
222
+ sizeDisplay === 'small' && css.tileSizeSmall,
223
+ sizeDisplay === 'default' && css.tileSizeDefault,
224
+ left === 0 && css.tileFirstCol,
184
225
  );
185
226
 
227
+ /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus */
186
228
  return (
187
- <button
188
- type="button"
229
+ <div
189
230
  onClick={handleOnClick}
190
- ref={leafContentRef}
231
+ role="button"
232
+ aria-label={I18N.TILE_LABEL}
233
+ ref={contentRef}
191
234
  style={{ left, top, width, height }}
192
- className={leafClassName}
235
+ className={className}
193
236
  >
194
237
  {hover ? (
195
- <LeafContentWithTooltip
238
+ <TileContentWithTooltip
239
+ label={label}
196
240
  sizeDisplay={sizeDisplay}
197
241
  item={item}
198
242
  runInfo={runInfo}
199
- parentRef={leafContentRef}
243
+ parentRef={contentRef}
200
244
  />
201
245
  ) : (
202
- <LeafContent sizeDisplay={sizeDisplay} item={item} runInfo={runInfo} />
246
+ <TileContent label={label} sizeDisplay={sizeDisplay} item={item} runInfo={runInfo} />
203
247
  )}
204
- </button>
248
+ </div>
249
+ );
250
+ };
251
+
252
+ interface TileGroupProps extends ComponentProps<'div'> {
253
+ /**
254
+ * Node title
255
+ */
256
+ title?: string;
257
+ /**
258
+ * Group sum
259
+ */
260
+ total?: Tree['total'];
261
+ /**
262
+ * Node id
263
+ */
264
+ id: string;
265
+ /**
266
+ * Node children
267
+ */
268
+ childNodes: HierarchyRectangularNode<TreeNode>['children'];
269
+ /**
270
+ * On click tile handler
271
+ */
272
+ onItemClick: TileProps['onClick'];
273
+ /**
274
+ * On click group title
275
+ */
276
+ onGroupClick?: (groupId: string) => void;
277
+
278
+ /**
279
+ * Node rectangle dimensions
280
+ */
281
+ width?: number;
282
+ height?: number;
283
+
284
+ /**
285
+ * Node rectangle relative coordinates (relative to parent)
286
+ */
287
+ left: number;
288
+ top: number;
289
+
290
+ /**
291
+ * Node rectangle absolute coordinates (relative to canvas)
292
+ */
293
+ absoluteLeft: number;
294
+ absoluteTop: number;
295
+ }
296
+
297
+ const TileGroup = (props: TileGroupProps) => {
298
+ const {
299
+ title = '',
300
+ total,
301
+ id,
302
+ childNodes,
303
+ onItemClick,
304
+ onGroupClick,
305
+ left,
306
+ top,
307
+ width,
308
+ height,
309
+ absoluteLeft,
310
+ absoluteTop,
311
+ } = props;
312
+
313
+ const onClick: MouseEventHandler<HTMLDivElement> = useCallback(
314
+ (event) => {
315
+ onGroupClick?.(id);
316
+ event.stopPropagation();
317
+ },
318
+ [onGroupClick, id],
319
+ );
320
+
321
+ const displaySize = useMemo(
322
+ () => resolveTileGroupSizeDisplay(width || 0, height || 0),
323
+ [width, height],
324
+ );
325
+
326
+ const metricRunInfo =
327
+ total &&
328
+ getMetricRunInfo(METRIC_TYPE_CONFIGS.METRIC_TYPE_FILE_SIZE, total.current, total.baseline);
329
+ const groupDeltaType = resolveGroupDeltaType(metricRunInfo);
330
+ const rootClassName = cx(css.tileGroup, css[`tileGroup--${groupDeltaType}`]);
331
+
332
+ if (title && displaySize === 'minimal') {
333
+ return (
334
+ <div
335
+ onClick={onClick}
336
+ role="button"
337
+ aria-label={I18N.TILE_GROUP_LABEL}
338
+ className={rootClassName}
339
+ style={{ left, top, width, height }}
340
+ />
341
+ );
342
+ }
343
+
344
+ if (title && displaySize === 'small') {
345
+ return (
346
+ <div
347
+ onClick={onClick}
348
+ role="button"
349
+ aria-label={I18N.TILE_GROUP_LABEL}
350
+ style={{ left, top, width, height }}
351
+ className={rootClassName}
352
+ >
353
+ <div className={css.tileGroupTitle}>{title}</div>
354
+ </div>
355
+ );
356
+ }
357
+
358
+ return (
359
+ <div
360
+ onClick={onClick}
361
+ role="button"
362
+ aria-label={I18N.TILE_GROUP_LABEL}
363
+ className={cx(rootClassName, css.tileGroupSizeDefault)}
364
+ style={{ left, top, width, height }}
365
+ >
366
+ {title && (
367
+ <div className={css.tileGroupTitle}>
368
+ {title}
369
+ {metricRunInfo && (
370
+ <span className={css.tileGroupTitleTotal}>
371
+ {`${metricRunInfo.displayValue}`}
372
+ {'displayDelta' in metricRunInfo && ` (${metricRunInfo.displayDeltaPercentage})`}
373
+ </span>
374
+ )}
375
+ </div>
376
+ )}
377
+ {childNodes?.map((childNode) => {
378
+ if ('children' in childNode) {
379
+ const groupData = childNode.data as Tree;
380
+
381
+ return (
382
+ <TileGroup
383
+ title={groupData.label}
384
+ total={groupData.total}
385
+ id={groupData.id}
386
+ childNodes={childNode.children}
387
+ left={childNode.x0 - absoluteLeft}
388
+ top={childNode.y0 - absoluteTop}
389
+ absoluteLeft={childNode.x0}
390
+ absoluteTop={childNode.y0}
391
+ width={childNode.x1 - childNode.x0}
392
+ height={childNode.y1 - childNode.y0}
393
+ onItemClick={onItemClick}
394
+ onGroupClick={onGroupClick}
395
+ key={groupData.id}
396
+ />
397
+ );
398
+ }
399
+
400
+ return (
401
+ <Tile
402
+ data={childNode.data as TreeLeaf}
403
+ left={childNode.x0 - absoluteLeft}
404
+ top={childNode.y0 - absoluteTop}
405
+ width={childNode.x1 - childNode.x0}
406
+ height={childNode.y1 - childNode.y0}
407
+ onClick={onItemClick}
408
+ key={childNode.data.id}
409
+ />
410
+ );
411
+ })}
412
+ </div>
205
413
  );
206
414
  };
207
415
 
208
416
  interface UseMetricsTreemapHierarchyParams {
209
- items: Array<ReportMetricRow>;
417
+ treeNodes: Tree;
210
418
  width: number;
211
419
  height: number;
420
+ nested: boolean;
212
421
  }
213
422
 
214
423
  function useMetricsTreemapHierarchy(params: UseMetricsTreemapHierarchyParams) {
215
- const { items, width, height } = params;
424
+ const { treeNodes, width, height, nested } = params;
216
425
 
217
426
  const treemapHierarchy = useMemo(() => {
218
- const children = items.map((item) => {
219
- /**
220
- * Set leaf value to the largest run value to allow to
221
- * display the impact of deleted / items
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
- };
427
+ const customHierarchy = hierarchy<Tree>(treeNodes);
239
428
 
240
- const customHierarchy = hierarchy<RootTree>(treemapData);
241
429
  customHierarchy.sum((node) => node.value);
430
+ // sort by value descending for all cases
242
431
  // @ts-expect-error
243
432
  customHierarchy.sort((a, b) => b.value - a.value);
244
433
 
245
434
  return customHierarchy;
246
- }, [items]);
435
+ }, [treeNodes]);
247
436
 
248
- const cells = useMemo(() => {
249
- const createTremapLayout = treemap()
437
+ const treemapNodes = useMemo(() => {
438
+ let createTremapLayout = treemap()
250
439
  .size([width, height])
251
440
  .tile(treemapSquarify.ratio(SQUARIFY_RATIO))
252
- .paddingOuter(PADDING_OUTER)
253
- .paddingInner(PADDING_INNER);
254
- // @ts-expect-error
255
- const tremapLayout = createTremapLayout(treemapHierarchy);
441
+ .paddingOuter(PADDING_OUTER);
442
+
443
+ // Add padding top for group title
444
+ if (nested) {
445
+ createTremapLayout = createTremapLayout
446
+ .padding(NESTED_PADDING)
447
+ .paddingLeft(NESTED_PADDING_LEFT)
448
+ .paddingTop(NESTED_PADDING_TOP);
449
+ } else {
450
+ createTremapLayout = createTremapLayout.paddingInner(PADDING_INNER);
451
+ }
256
452
 
257
- return tremapLayout.leaves().filter((leaf) => Boolean(leaf.parent));
453
+ // @ts-expect-error
454
+ return createTremapLayout(treemapHierarchy) as HierarchyRectangularNode<TreeNode>;
258
455
  }, [height, width, treemapHierarchy]);
259
456
 
260
- return cells;
457
+ return treemapNodes;
261
458
  }
262
459
 
263
460
  interface MetricsTreemapProps {
264
- items: Array<ReportMetricRow>;
265
- emptyMessage?: React.ReactNode;
461
+ treeNodes: Tree;
462
+ emptyMessage?: ReactNode;
463
+ nested?: boolean;
266
464
  onItemClick?: (entryId: string) => void;
465
+ onGroupClick?: (entryId: string) => void;
267
466
  }
268
467
 
269
- export const MetricsTreemap = (props: MetricsTreemapProps & React.ComponentProps<'div'>) => {
270
- const { className = '', items, onItemClick, emptyMessage = 'No data', ...restProps } = props;
468
+ export const MetricsTreemap = (props: MetricsTreemapProps & ComponentProps<'div'>) => {
469
+ const {
470
+ className = '',
471
+ treeNodes,
472
+ onItemClick,
473
+ onGroupClick,
474
+ emptyMessage = 'No data',
475
+ nested = false,
476
+ ...restProps
477
+ } = props;
271
478
 
272
479
  const [containerRef, { width, height }] = useMeasure<HTMLDivElement>();
273
- const cells = useMetricsTreemapHierarchy({ items, width, height });
480
+ const rootNode = useMetricsTreemapHierarchy({ treeNodes, width, height, nested });
274
481
 
275
482
  return (
276
- <div className={cx(css.root, className)} {...restProps} ref={containerRef}>
277
- {cells.length > 0 ? (
483
+ <div
484
+ className={cx(css.root, nested && css.nested, className)}
485
+ {...restProps}
486
+ ref={containerRef}
487
+ >
488
+ {rootNode.children && rootNode.children?.length > 0 ? (
278
489
  <div className={css.canvas}>
279
- {cells.map((leaf) => {
280
- if (!leaf.parent) {
281
- return null;
282
- }
283
-
284
- return (
285
- <Leaf
286
- data={leaf.data as TreeNode}
287
- left={leaf.x0}
288
- top={leaf.y0}
289
- width={leaf.x1 - leaf.x0}
290
- height={leaf.y1 - leaf.y0}
291
- onClick={onItemClick}
292
- key={leaf.id}
293
- />
294
- );
295
- })}
490
+ <TileGroup
491
+ title={nested ? rootNode.data.label : undefined}
492
+ total={(rootNode.data as Tree).total}
493
+ id={rootNode.data.id}
494
+ childNodes={rootNode.children}
495
+ onItemClick={onItemClick}
496
+ onGroupClick={onGroupClick}
497
+ left={rootNode.x0}
498
+ top={rootNode.y0}
499
+ absoluteLeft={rootNode.x0}
500
+ absoluteTop={rootNode.y0}
501
+ width={rootNode.x1 - rootNode.x0}
502
+ height={rootNode.y1 - rootNode.y0}
503
+ />
296
504
  </div>
297
505
  ) : (
298
506
  <div className={css.emptyMessage}>