@bundle-stats/ui 4.13.0-beta.9 → 4.13.0-rc.1

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