@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
@@ -9,7 +9,6 @@ export const SvgIcons = (props) => (
9
9
  strokeWidth="2"
10
10
  strokeLinecap="round"
11
11
  strokeLinejoin="round"
12
- className="feather feather-arrow-down"
13
12
  id="arrow"
14
13
  xmlns="http://www.w3.org/2000/svg"
15
14
  >
@@ -21,7 +20,6 @@ export const SvgIcons = (props) => (
21
20
  strokeWidth="2"
22
21
  strokeLinecap="round"
23
22
  strokeLinejoin="round"
24
- className="feather feather-arrow-right-circle"
25
23
  viewBox="0 0 24 24"
26
24
  id="arrow-right-circle"
27
25
  xmlns="http://www.w3.org/2000/svg"
@@ -29,13 +27,24 @@ export const SvgIcons = (props) => (
29
27
  <circle cx="12" cy="12" r="10" />
30
28
  <path d="m12 16 4-4-4-4M8 12h8" />
31
29
  </symbol>
30
+ <symbol
31
+ viewBox="0 0 24 24"
32
+ fill="none"
33
+ stroke="currentColor"
34
+ strokeWidth="2"
35
+ strokeLinecap="round"
36
+ strokeLinejoin="round"
37
+ id="check"
38
+ xmlns="http://www.w3.org/2000/svg"
39
+ >
40
+ <path d="M20 6 9 17l-5-5" />
41
+ </symbol>
32
42
  <symbol
33
43
  fill="none"
34
44
  stroke="currentColor"
35
45
  strokeWidth="2"
36
46
  strokeLinecap="round"
37
47
  strokeLinejoin="round"
38
- className="lucide lucide-chevron-down"
39
48
  viewBox="0 0 24 24"
40
49
  id="chevron-down"
41
50
  xmlns="http://www.w3.org/2000/svg"
@@ -48,7 +57,6 @@ export const SvgIcons = (props) => (
48
57
  strokeWidth="2"
49
58
  strokeLinecap="round"
50
59
  strokeLinejoin="round"
51
- className="lucide lucide-chevron-up"
52
60
  viewBox="0 0 24 24"
53
61
  id="chevron-up"
54
62
  xmlns="http://www.w3.org/2000/svg"
@@ -62,7 +70,6 @@ export const SvgIcons = (props) => (
62
70
  strokeWidth="2"
63
71
  strokeLinecap="round"
64
72
  strokeLinejoin="round"
65
- className="feather feather-clock"
66
73
  id="clock"
67
74
  xmlns="http://www.w3.org/2000/svg"
68
75
  >
@@ -75,7 +82,6 @@ export const SvgIcons = (props) => (
75
82
  strokeWidth="2"
76
83
  strokeLinecap="round"
77
84
  strokeLinejoin="round"
78
- className="feather feather-x-circle"
79
85
  viewBox="0 0 24 24"
80
86
  id="close"
81
87
  xmlns="http://www.w3.org/2000/svg"
@@ -90,7 +96,6 @@ export const SvgIcons = (props) => (
90
96
  strokeWidth="2"
91
97
  strokeLinecap="round"
92
98
  strokeLinejoin="round"
93
- className="feather feather-git-commit"
94
99
  id="commit"
95
100
  xmlns="http://www.w3.org/2000/svg"
96
101
  >
@@ -116,7 +121,6 @@ export const SvgIcons = (props) => (
116
121
  strokeWidth="2"
117
122
  strokeLinecap="round"
118
123
  strokeLinejoin="round"
119
- className="feather feather-external-link"
120
124
  viewBox="0 0 24 24"
121
125
  id="external-link"
122
126
  xmlns="http://www.w3.org/2000/svg"
@@ -130,7 +134,6 @@ export const SvgIcons = (props) => (
130
134
  strokeWidth="2"
131
135
  strokeLinecap="round"
132
136
  strokeLinejoin="round"
133
- className="feather feather-bar-chart"
134
137
  id="filter"
135
138
  xmlns="http://www.w3.org/2000/svg"
136
139
  >
@@ -151,7 +154,6 @@ export const SvgIcons = (props) => (
151
154
  strokeWidth="2"
152
155
  strokeLinecap="round"
153
156
  strokeLinejoin="round"
154
- className="feather feather-help-circle"
155
157
  id="help"
156
158
  xmlns="http://www.w3.org/2000/svg"
157
159
  >
@@ -178,7 +180,6 @@ export const SvgIcons = (props) => (
178
180
  strokeWidth="2"
179
181
  strokeLinecap="round"
180
182
  strokeLinejoin="round"
181
- className="feather feather-menu"
182
183
  id="menu"
183
184
  xmlns="http://www.w3.org/2000/svg"
184
185
  >
@@ -233,7 +234,6 @@ export const SvgIcons = (props) => (
233
234
  strokeWidth="2"
234
235
  strokeLinecap="round"
235
236
  strokeLinejoin="round"
236
- className="feather feather-alert-triangle"
237
237
  id="warning"
238
238
  xmlns="http://www.w3.org/2000/svg"
239
239
  >
@@ -35,7 +35,7 @@ import { MetricsTableOptions } from '../metrics-table-options';
35
35
  import { MetricsTableTitle } from '../metrics-table-title';
36
36
  import { MetricsDisplaySelector } from '../metrics-display-selector';
37
37
  import { MetricsTableHeader } from '../metrics-table-header';
38
- import { MetricsTreemap } from '../metrics-treemap';
38
+ import { MetricsTreemap, getTreemapNodes, getTreemapNodesGroupedByPath } from '../metrics-treemap';
39
39
  import { SEARCH_PLACEHOLDER } from './bundle-assets.i18n';
40
40
  import css from './bundle-assets.module.css';
41
41
 
@@ -43,6 +43,10 @@ const RUN_TITLE_CURRENT = 'Current';
43
43
  const RUN_TITLE_BASELINE = 'Baseline';
44
44
  const RUNS_LABELS = [RUN_TITLE_CURRENT, RUN_TITLE_BASELINE];
45
45
 
46
+ const DISPLAY_TYPE_GROUPS = {
47
+ [MetricsDisplayType.TREEMAP]: ['folder'],
48
+ };
49
+
46
50
  const getFileTypeFilters = (filters) =>
47
51
  Object.entries(FILE_TYPE_LABELS)
48
52
  .map(([key, label]) => ({
@@ -163,6 +167,60 @@ RowHeader.defaultProps = {
163
167
  chunks: [],
164
168
  };
165
169
 
170
+ const ViewMetricsTreemap = (props) => {
171
+ const { metricsTableTitle, jobs, items, displayType, emptyMessage, showEntryInfo, updateSearch } = props;
172
+
173
+ const treeNodes = useMemo(() => {
174
+ if (displayType.groupBy === 'folder') {
175
+ return getTreemapNodesGroupedByPath(items);
176
+ }
177
+
178
+ return getTreemapNodes(items);
179
+ }, [items, displayType.groupBy]);
180
+
181
+ const onGroupClick = useCallback(
182
+ (groupPath) => {
183
+ // Search by group path
184
+ // 1. use `^` to match only the string beggining
185
+ // 2. add `/` suffix to match only exact directories
186
+ // 3. if the group path is empty(root), clear search
187
+ if (groupPath) {
188
+ updateSearch(`^${groupPath}/`);
189
+ } else {
190
+ updateSearch('');
191
+ }
192
+ },
193
+ [updateSearch],
194
+ );
195
+
196
+ return (
197
+ <>
198
+ <Table compact>
199
+ <MetricsTableHeader metricTitle={metricsTableTitle} showSum jobs={jobs} rows={items} />
200
+ </Table>
201
+ <MetricsTreemap
202
+ treeNodes={treeNodes}
203
+ nested={Boolean(displayType.groupBy)}
204
+ emptyMessage={emptyMessage}
205
+ onItemClick={showEntryInfo}
206
+ onGroupClick={onGroupClick}
207
+ />
208
+ </>
209
+ );
210
+ };
211
+
212
+ ViewMetricsTreemap.propTypes = {
213
+ jobs: PropTypes.arrayOf(PropTypes.object).isRequired, // eslint-disable-line react/forbid-prop-types
214
+ items: PropTypes.arrayOf(PropTypes.object).isRequired, // eslint-disable-line react/forbid-prop-types
215
+ metricsTableTitle: PropTypes.node.isRequired,
216
+ displayType: PropTypes.shape({
217
+ groupBy: PropTypes.string,
218
+ }).isRequired,
219
+ emptyMessage: PropTypes.node.isRequired,
220
+ showEntryInfo: PropTypes.func.isRequired,
221
+ updateSearch: PropTypes.func.isRequired,
222
+ };
223
+
166
224
  export const BundleAssets = (props) => {
167
225
  const {
168
226
  className,
@@ -187,7 +245,7 @@ export const BundleAssets = (props) => {
187
245
 
188
246
  const jobLabels = jobs?.map((job) => job?.label);
189
247
 
190
- const [displayType, setDisplayType] = useMetricsDisplayType();
248
+ const [displayType, setDisplayType] = useMetricsDisplayType(DISPLAY_TYPE_GROUPS);
191
249
 
192
250
  const dropdownFilters = useMemo(
193
251
  () => getFilters({ compareMode: jobs?.length > 1, filters }),
@@ -238,7 +296,12 @@ export const BundleAssets = (props) => {
238
296
  <Toolbar
239
297
  renderActions={({ actionClassName }) => (
240
298
  <FlexStack space="xxsmall" className={cx(css.dropdown, actionClassName)}>
241
- <MetricsDisplaySelector onSelect={setDisplayType} value={displayType} />
299
+ <MetricsDisplaySelector
300
+ value={displayType.value}
301
+ groupBy={displayType.groupBy}
302
+ groups={DISPLAY_TYPE_GROUPS}
303
+ onSelect={setDisplayType}
304
+ />
242
305
  <MetricsTableOptions
243
306
  handleViewAll={resetAllFilters}
244
307
  handleResetFilters={resetFilters}
@@ -262,7 +325,7 @@ export const BundleAssets = (props) => {
262
325
  </FlexStack>
263
326
  </Toolbar>
264
327
  <Box outline as="main">
265
- {displayType === MetricsDisplayType.TABLE && (
328
+ {displayType.value === MetricsDisplayType.TABLE && (
266
329
  <MetricsTable
267
330
  runs={jobs}
268
331
  items={items}
@@ -274,22 +337,15 @@ export const BundleAssets = (props) => {
274
337
  updateSort={updateSort}
275
338
  />
276
339
  )}
277
- {displayType === MetricsDisplayType.TREEMAP && (
278
- <>
279
- <Table compact>
280
- <MetricsTableHeader
281
- metricTitle={metricsTableTitle}
282
- showSum
283
- jobs={jobs}
284
- rows={items}
285
- />
286
- </Table>
287
- <MetricsTreemap
288
- emptyMessage={emptyMessage}
289
- items={items}
290
- onItemClick={showEntryInfo}
291
- />
292
- </>
340
+ {displayType.value === MetricsDisplayType.TREEMAP && (
341
+ <ViewMetricsTreemap
342
+ jobs={jobs}
343
+ items={items}
344
+ displayType={displayType}
345
+ emptyMessage={emptyMessage}
346
+ showEntryInfo={showEntryInfo}
347
+ updateSearch={updateSearch}
348
+ />
293
349
  )}
294
350
  </Box>
295
351
  </Stack>
@@ -19,7 +19,7 @@ import { MetricsDisplaySelector } from '../metrics-display-selector';
19
19
  import { MetricsTable } from '../metrics-table';
20
20
  import { MetricsTableTitle } from '../metrics-table-title';
21
21
  import { MetricsTableHeader } from '../metrics-table-header';
22
- import { MetricsTreemap } from '../metrics-treemap';
22
+ import { MetricsTreemap, getTreemapNodes } from '../metrics-treemap';
23
23
  import { ComponentLink } from '../component-link';
24
24
 
25
25
  const metricsTableTitle = (
@@ -72,11 +72,11 @@ export const BundleAssetsTotals = ({
72
72
  <Stack space="xsmall" as="section" className={className}>
73
73
  <Toolbar
74
74
  renderActions={() => (
75
- <MetricsDisplaySelector onSelect={setDisplayType} value={displayType} />
75
+ <MetricsDisplaySelector onSelect={setDisplayType} value={displayType.value} />
76
76
  )}
77
77
  />
78
78
  <Box outline as="main">
79
- {displayType === MetricsDisplayType.TABLE && (
79
+ {displayType.value === MetricsDisplayType.TABLE && (
80
80
  <MetricsTable
81
81
  title={metricsTableTitle}
82
82
  runs={jobs}
@@ -86,7 +86,7 @@ export const BundleAssetsTotals = ({
86
86
  {...restProps}
87
87
  />
88
88
  )}
89
- {displayType === MetricsDisplayType.TREEMAP && (
89
+ {displayType.value === MetricsDisplayType.TREEMAP && (
90
90
  <>
91
91
  <Table compact>
92
92
  <MetricsTableHeader
@@ -98,6 +98,7 @@ export const BundleAssetsTotals = ({
98
98
  </Table>
99
99
  <MetricsTreemap
100
100
  items={items}
101
+ treeNodes={getTreemapNodes(items)}
101
102
  onItemClick={onTreemapItemClick || handleMetricsTreemapItemClick}
102
103
  />
103
104
  </>
@@ -22,7 +22,7 @@ import { Tooltip } from '../../ui/tooltip';
22
22
  import { ComponentLink } from '../component-link';
23
23
  import { MetricsTable } from '../metrics-table';
24
24
  import { MetricsTableHeader } from '../metrics-table-header';
25
- import { MetricsTreemap } from '../metrics-treemap';
25
+ import { MetricsTreemap, getTreemapNodes, getTreemapNodesGroupedByPath } from '../metrics-treemap';
26
26
  import { MetricsTableOptions } from '../metrics-table-options';
27
27
  import { MetricsDisplaySelector } from '../metrics-display-selector';
28
28
  import { MetricsTableTitle } from '../metrics-table-title';
@@ -34,6 +34,10 @@ import * as I18N_MODULES from './bundle-modules.i18n';
34
34
  import css from './bundle-modules.module.css';
35
35
  import { useMetricsDisplayType } from '../../hooks/metrics-display-type';
36
36
 
37
+ const DISPLAY_TYPE_GROUPS = {
38
+ [MetricsDisplayType.TREEMAP]: ['folder'],
39
+ };
40
+
37
41
  interface RowHeaderProps {
38
42
  row: ReportMetricModuleRow;
39
43
  filters?: any;
@@ -61,6 +65,59 @@ const RowHeader = (props: RowHeaderProps) => {
61
65
  );
62
66
  };
63
67
 
68
+ interface ViewMetricsTreemapProps {
69
+ metricsTableTitle: React.ReactNode;
70
+ jobs: Array<any>;
71
+ items: Array<any>;
72
+ displayType: { value: MetricsDisplayType; groupBy?: string };
73
+ emptyMessage: React.ReactNode;
74
+ showEntryInfo: React.ComponentProps<typeof MetricsTreemap>['onItemClick'];
75
+ updateSearch: (newSerarch: string) => void;
76
+ }
77
+
78
+ const ViewMetricsTreemap = (props: ViewMetricsTreemapProps) => {
79
+ const { metricsTableTitle, jobs, items, displayType, emptyMessage, showEntryInfo, updateSearch } =
80
+ props;
81
+
82
+ const treeNodes = useMemo(() => {
83
+ if (displayType.groupBy === 'folder') {
84
+ return getTreemapNodesGroupedByPath(items);
85
+ }
86
+
87
+ return getTreemapNodes(items);
88
+ }, [items, displayType]);
89
+
90
+ const onGroupClick = useCallback(
91
+ (groupPath: string) => {
92
+ // Search by group path
93
+ // 1. use `^` to match only the string beggining
94
+ // 2. add `/` suffix to match only exact directories
95
+ // 3. if the group path is empty(root), clear search
96
+ if (groupPath) {
97
+ updateSearch(`^${groupPath}/`);
98
+ } else {
99
+ updateSearch('');
100
+ }
101
+ },
102
+ [updateSearch],
103
+ );
104
+
105
+ return (
106
+ <>
107
+ <Table compact>
108
+ <MetricsTableHeader metricTitle={metricsTableTitle} showSum jobs={jobs} rows={items} />
109
+ </Table>
110
+ <MetricsTreemap
111
+ treeNodes={treeNodes}
112
+ nested={Boolean(displayType.groupBy)}
113
+ emptyMessage={emptyMessage}
114
+ onItemClick={showEntryInfo}
115
+ onGroupClick={onGroupClick}
116
+ />
117
+ </>
118
+ );
119
+ };
120
+
64
121
  interface BundleModulesProps extends React.ComponentProps<'div'> {
65
122
  jobs?: Array<Job>;
66
123
  items?: Array<ReportMetricModuleRow>;
@@ -117,7 +174,7 @@ export const BundleModules = (props: BundleModulesProps) => {
117
174
  const rootClassName = cx(css.root, className);
118
175
  const jobLabels = jobs?.map((job) => job?.label);
119
176
 
120
- const [displayType, setDisplayType] = useMetricsDisplayType();
177
+ const [displayType, setDisplayType] = useMetricsDisplayType(DISPLAY_TYPE_GROUPS);
121
178
 
122
179
  const filterFieldsData = useMemo(
123
180
  () => generateFilterFieldsData({ filters, chunks, compareMode: jobs.length > 1 }),
@@ -176,7 +233,12 @@ export const BundleModules = (props: BundleModulesProps) => {
176
233
  className={css.toolbar}
177
234
  renderActions={({ actionClassName }) => (
178
235
  <FlexStack space="xxsmall" className={cx(css.dropdown, actionClassName)}>
179
- <MetricsDisplaySelector onSelect={setDisplayType} value={displayType} />
236
+ <MetricsDisplaySelector
237
+ onSelect={setDisplayType}
238
+ value={displayType.value}
239
+ groupBy={displayType.groupBy}
240
+ groups={DISPLAY_TYPE_GROUPS}
241
+ />
180
242
  <MetricsTableOptions
181
243
  handleViewAll={resetAllFilters}
182
244
  handleResetFilters={resetFilters}
@@ -221,7 +283,7 @@ export const BundleModules = (props: BundleModulesProps) => {
221
283
  </Button>
222
284
  </ControlGroup>
223
285
  </Box>
224
- {displayType === MetricsDisplayType.TABLE && (
286
+ {displayType.value === MetricsDisplayType.TABLE && (
225
287
  <MetricsTable
226
288
  className={css.table}
227
289
  items={items}
@@ -234,18 +296,16 @@ export const BundleModules = (props: BundleModulesProps) => {
234
296
  updateSort={updateSort}
235
297
  />
236
298
  )}
237
- {displayType === MetricsDisplayType.TREEMAP && (
238
- <>
239
- <Table compact>
240
- <MetricsTableHeader
241
- metricTitle={metricsTableTitle}
242
- showSum
243
- jobs={jobs}
244
- rows={items}
245
- />
246
- </Table>
247
- <MetricsTreemap emptyMessage={emptyMessage} items={items} onItemClick={showEntryInfo} />
248
- </>
299
+ {displayType.value === MetricsDisplayType.TREEMAP && (
300
+ <ViewMetricsTreemap
301
+ metricsTableTitle={metricsTableTitle}
302
+ jobs={jobs}
303
+ items={items}
304
+ displayType={displayType}
305
+ emptyMessage={emptyMessage}
306
+ showEntryInfo={showEntryInfo}
307
+ updateSearch={updateSearch}
308
+ />
249
309
  )}
250
310
  </Box>
251
311
  </Stack>
@@ -24,9 +24,13 @@ import { SEARCH_PLACEHOLDER } from './bundle-packages.i18n';
24
24
  import css from './bundle-packages.module.css';
25
25
  import { MetricsDisplaySelector } from '../metrics-display-selector';
26
26
  import { MetricsDisplayType } from '../../constants';
27
- import { MetricsTreemap } from '../metrics-treemap';
27
+ import { MetricsTreemap, getTreemapNodes, getTreemapNodesGroupedByPath } from '../metrics-treemap';
28
28
  import { useMetricsDisplayType } from '../../hooks/metrics-display-type';
29
29
 
30
+ const DISPLAY_TYPE_GROUPS = {
31
+ [MetricsDisplayType.TREEMAP]: ['folder'],
32
+ };
33
+
30
34
  const getDropdownFilters = ({ compareMode, filters }) => ({
31
35
  [PACKAGE_FILTERS.CHANGED]: {
32
36
  label: 'Changed',
@@ -115,6 +119,60 @@ RowHeader.propTypes = {
115
119
  search: PropTypes.string,
116
120
  };
117
121
 
122
+ const ViewMetricsTreemap = (props) => {
123
+ const { metricsTableTitle, jobs, items, displayType, emptyMessage, showEntryInfo, updateSearch } = props;
124
+
125
+ const treeNodes = useMemo(() => {
126
+ if (displayType.groupBy === 'folder') {
127
+ return getTreemapNodesGroupedByPath(items);
128
+ }
129
+
130
+ return getTreemapNodes(items);
131
+ }, [items, displayType.groupBy]);
132
+
133
+ const onGroupClick = useCallback(
134
+ (groupPath) => {
135
+ // Search by group path
136
+ // 1. use `^` to match only the string beggining
137
+ // 2. add `/` suffix to match only exact directories
138
+ // 3. if the group path is empty(root), clear search
139
+ if (groupPath) {
140
+ updateSearch(`^${groupPath}/`);
141
+ } else {
142
+ updateSearch('');
143
+ }
144
+ },
145
+ [updateSearch],
146
+ );
147
+
148
+ return (
149
+ <>
150
+ <Table compact>
151
+ <MetricsTableHeader metricTitle={metricsTableTitle} showSum jobs={jobs} rows={items} />
152
+ </Table>
153
+ <MetricsTreemap
154
+ treeNodes={treeNodes}
155
+ nested={Boolean(displayType.groupBy)}
156
+ emptyMessage={emptyMessage}
157
+ onItemClick={showEntryInfo}
158
+ onGroupClick={onGroupClick}
159
+ />
160
+ </>
161
+ );
162
+ };
163
+
164
+ ViewMetricsTreemap.propTypes = {
165
+ jobs: PropTypes.arrayOf(PropTypes.object).isRequired, // eslint-disable-line react/forbid-prop-types
166
+ items: PropTypes.arrayOf(PropTypes.object).isRequired, // eslint-disable-line react/forbid-prop-types
167
+ metricsTableTitle: PropTypes.node.isRequired,
168
+ displayType: PropTypes.shape({
169
+ groupBy: PropTypes.string,
170
+ }).isRequired,
171
+ emptyMessage: PropTypes.node.isRequired,
172
+ showEntryInfo: PropTypes.func.isRequired,
173
+ updateSearch: PropTypes.func.isRequired,
174
+ };
175
+
118
176
  export const BundlePackages = (props) => {
119
177
  const {
120
178
  className,
@@ -139,7 +197,7 @@ export const BundlePackages = (props) => {
139
197
 
140
198
  const jobLabels = jobs?.map((job) => job?.label);
141
199
 
142
- const [displayType, setDisplayType] = useMetricsDisplayType();
200
+ const [displayType, setDisplayType] = useMetricsDisplayType(DISPLAY_TYPE_GROUPS);
143
201
 
144
202
  const dropdownFilters = useMemo(
145
203
  () => getDropdownFilters({ compareMode: jobs?.length > 1, filters }),
@@ -198,7 +256,7 @@ export const BundlePackages = (props) => {
198
256
  className={css.toolbar}
199
257
  renderActions={({ actionClassName }) => (
200
258
  <FlexStack space="xxsmall" className={cx(css.dropdown, actionClassName)}>
201
- <MetricsDisplaySelector onSelect={setDisplayType} value={displayType} />
259
+ <MetricsDisplaySelector onSelect={setDisplayType} value={displayType.value} groupBy={displayType.groupBy} groups={DISPLAY_TYPE_GROUPS} />
202
260
  <MetricsTableOptions
203
261
  handleViewAll={resetAllFilters}
204
262
  handleResetFilters={resetFilters}
@@ -222,7 +280,7 @@ export const BundlePackages = (props) => {
222
280
  </FlexStack>
223
281
  </Toolbar>
224
282
  <Box outline as="main">
225
- {displayType === MetricsDisplayType.TABLE && (
283
+ {displayType.value === MetricsDisplayType.TABLE && (
226
284
  <MetricsTable
227
285
  runs={jobs}
228
286
  items={items}
@@ -234,18 +292,15 @@ export const BundlePackages = (props) => {
234
292
  updateSort={updateSort}
235
293
  />
236
294
  )}
237
- {displayType === MetricsDisplayType.TREEMAP && (
238
- <>
239
- <Table compact>
240
- <MetricsTableHeader
241
- metricTitle={metricsTableTitle}
242
- showSum
243
- jobs={jobs}
244
- rows={items}
245
- />
246
- </Table>
247
- <MetricsTreemap emptyMessage={emptyMessage} items={items} onItemClick={showEntryInfo} />
248
- </>
295
+ {displayType.value === MetricsDisplayType.TREEMAP && (
296
+ <ViewMetricsTreemap
297
+ jobs={jobs}
298
+ items={items}
299
+ displayType={displayType}
300
+ emptyMessage={emptyMessage}
301
+ showEntryInfo={showEntryInfo}
302
+ updateSearch={updateSearch}
303
+ />
249
304
  )}
250
305
  </Box>
251
306
  </Stack>
@@ -1,10 +1,18 @@
1
- .button {
2
- text-decoration: none;
1
+ .buttonActive {
2
+ background: var(--color-highlight);
3
3
  }
4
4
 
5
- .buttonActive,
6
- .buttonActive:active,
7
- .buttonActive:focus {
8
- color: var(--color-primary);
9
- border-color: var(--color-primary);
5
+ /* explicit ControlGroup last item styles to override ariakit injected sibling nodes */
6
+ .itemLast {
7
+ border-top-right-radius: var(--radius-small) !important;
8
+ border-bottom-right-radius: var(--radius-small) !important;
9
+ }
10
+
11
+ .dropdownItemIcon {
12
+ opacity: 0;
13
+ transition: var(--transition-out);
14
+ }
15
+
16
+ .dropdownItemActive .dropdownItemIcon {
17
+ opacity: 1;
10
18
  }
@@ -0,0 +1,33 @@
1
+ import { action } from '@storybook/addon-actions'; // eslint-disable-line
2
+ import { Meta, StoryObj } from '@storybook/react';
3
+
4
+ import { MetricsDisplayType } from '../../constants';
5
+ import { MetricsDisplaySelector } from '.';
6
+
7
+ const meta: Meta<typeof MetricsDisplaySelector> = {
8
+ title: 'Components/MetricsDisplaySelector',
9
+ component: MetricsDisplaySelector,
10
+ args: {
11
+ onSelect: action('SELECT'),
12
+ },
13
+ } as Meta;
14
+
15
+ export default meta;
16
+
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ export const Default: Story = {
20
+ args: {
21
+ value: MetricsDisplayType.TABLE,
22
+ },
23
+ };
24
+
25
+ export const TreemapGroups: Story = {
26
+ args: {
27
+ value: MetricsDisplayType.TREEMAP,
28
+ groupBy: '',
29
+ groups: {
30
+ [MetricsDisplayType.TREEMAP]: ['folder'],
31
+ },
32
+ },
33
+ };