@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
@@ -5,9 +5,10 @@ import { SECTIONS, COMPONENT } from '@bundle-stats/utils';
5
5
  import { SortAction } from '../../types';
6
6
  import config from '../../config.json';
7
7
  import I18N from '../../i18n';
8
- import { Box } from '../../layout/box';
9
8
  import { MetricsDisplayType } from '../../constants';
9
+ import { Box } from '../../layout/box';
10
10
  import { FlexStack } from '../../layout/flex-stack';
11
+ import { Stack } from '../../layout/stack';
11
12
  import { Button } from '../../ui/button';
12
13
  import { ControlGroup } from '../../ui/control-group';
13
14
  import { EmptySet } from '../../ui/empty-set';
@@ -21,7 +22,7 @@ import { Tooltip } from '../../ui/tooltip';
21
22
  import { ComponentLink } from '../component-link';
22
23
  import { MetricsTable } from '../metrics-table';
23
24
  import { MetricsTableHeader } from '../metrics-table-header';
24
- import { MetricsTreemap } from '../metrics-treemap';
25
+ import { MetricsTreemap, getTreemapNodes, getTreemapNodesGroupedByPath } from '../metrics-treemap';
25
26
  import { MetricsTableOptions } from '../metrics-table-options';
26
27
  import { MetricsDisplaySelector } from '../metrics-display-selector';
27
28
  import { MetricsTableTitle } from '../metrics-table-title';
@@ -33,6 +34,10 @@ import * as I18N_MODULES from './bundle-modules.i18n';
33
34
  import css from './bundle-modules.module.css';
34
35
  import { useMetricsDisplayType } from '../../hooks/metrics-display-type';
35
36
 
37
+ const DISPLAY_TYPE_GROUPS = {
38
+ [MetricsDisplayType.TREEMAP]: ['folder'],
39
+ };
40
+
36
41
  interface RowHeaderProps {
37
42
  row: ReportMetricModuleRow;
38
43
  filters?: any;
@@ -60,6 +65,59 @@ const RowHeader = (props: RowHeaderProps) => {
60
65
  );
61
66
  };
62
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
+
63
121
  interface BundleModulesProps extends React.ComponentProps<'div'> {
64
122
  jobs?: Array<Job>;
65
123
  items?: Array<ReportMetricModuleRow>;
@@ -116,7 +174,7 @@ export const BundleModules = (props: BundleModulesProps) => {
116
174
  const rootClassName = cx(css.root, className);
117
175
  const jobLabels = jobs?.map((job) => job?.label);
118
176
 
119
- const [displayType, setDisplayType] = useMetricsDisplayType();
177
+ const [displayType, setDisplayType] = useMetricsDisplayType(DISPLAY_TYPE_GROUPS);
120
178
 
121
179
  const filterFieldsData = useMemo(
122
180
  () => generateFilterFieldsData({ filters, chunks, compareMode: jobs.length > 1 }),
@@ -170,12 +228,17 @@ export const BundleModules = (props: BundleModulesProps) => {
170
228
 
171
229
  return (
172
230
  <>
173
- <div className={rootClassName}>
231
+ <Stack space="xsmall" as="section" className={rootClassName}>
174
232
  <Toolbar
175
233
  className={css.toolbar}
176
234
  renderActions={({ actionClassName }) => (
177
235
  <FlexStack space="xxsmall" className={cx(css.dropdown, actionClassName)}>
178
- <MetricsDisplaySelector onSelect={setDisplayType} value={displayType} />
236
+ <MetricsDisplaySelector
237
+ onSelect={setDisplayType}
238
+ value={displayType.value}
239
+ groupBy={displayType.groupBy}
240
+ groups={DISPLAY_TYPE_GROUPS}
241
+ />
179
242
  <MetricsTableOptions
180
243
  handleViewAll={resetAllFilters}
181
244
  handleResetFilters={resetFilters}
@@ -197,55 +260,55 @@ export const BundleModules = (props: BundleModulesProps) => {
197
260
  />
198
261
  </FlexStack>
199
262
  </Toolbar>
200
- <Box padding={['xsmall', 'small']} className={css.metricSelector}>
201
- <ControlGroup as="nav">
202
- <Button
203
- outline
204
- active={moduleMetric === ModuleMetric.SIZE}
205
- size="small"
206
- type="button"
207
- onClick={() => setModuleMetric(ModuleMetric.SIZE)}
208
- >
209
- <Tooltip title="Size (excluding duplicate modules)">Module size</Tooltip>
210
- </Button>
211
- <Button
212
- outline
213
- active={moduleMetric === ModuleMetric.TOTAL_SIZE}
214
- size="small"
215
- type="button"
216
- onClick={() => setModuleMetric(ModuleMetric.TOTAL_SIZE)}
217
- >
218
- <Tooltip title="Size (including duplicate modules)">Module total size</Tooltip>
219
- </Button>
220
- </ControlGroup>
263
+ <Box outline as="main">
264
+ <Box padding={['xsmall', 'small']} className={css.metricSelector}>
265
+ <ControlGroup as="nav">
266
+ <Button
267
+ outline
268
+ active={moduleMetric === ModuleMetric.SIZE}
269
+ size="small"
270
+ type="button"
271
+ onClick={() => setModuleMetric(ModuleMetric.SIZE)}
272
+ >
273
+ <Tooltip title="Size (excluding duplicate modules)">Module size</Tooltip>
274
+ </Button>
275
+ <Button
276
+ outline
277
+ active={moduleMetric === ModuleMetric.TOTAL_SIZE}
278
+ size="small"
279
+ type="button"
280
+ onClick={() => setModuleMetric(ModuleMetric.TOTAL_SIZE)}
281
+ >
282
+ <Tooltip title="Size (including duplicate modules)">Module total size</Tooltip>
283
+ </Button>
284
+ </ControlGroup>
285
+ </Box>
286
+ {displayType.value === MetricsDisplayType.TABLE && (
287
+ <MetricsTable
288
+ className={css.table}
289
+ items={items}
290
+ runs={jobs}
291
+ renderRowHeader={renderRowHeader}
292
+ emptyMessage={emptyMessage}
293
+ showHeaderSum
294
+ title={metricsTableTitle}
295
+ sort={sort}
296
+ updateSort={updateSort}
297
+ />
298
+ )}
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
+ />
309
+ )}
221
310
  </Box>
222
- {displayType === MetricsDisplayType.TABLE && (
223
- <MetricsTable
224
- className={css.table}
225
- items={items}
226
- runs={jobs}
227
- renderRowHeader={renderRowHeader}
228
- emptyMessage={emptyMessage}
229
- showHeaderSum
230
- title={metricsTableTitle}
231
- sort={sort}
232
- updateSort={updateSort}
233
- />
234
- )}
235
- {displayType === MetricsDisplayType.TREEMAP && (
236
- <>
237
- <Table compact>
238
- <MetricsTableHeader
239
- metricTitle={metricsTableTitle}
240
- showSum
241
- jobs={jobs}
242
- rows={items}
243
- />
244
- </Table>
245
- <MetricsTreemap emptyMessage={emptyMessage} items={items} onItemClick={showEntryInfo} />
246
- </>
247
- )}
248
- </div>
311
+ </Stack>
249
312
  {entryItem && (
250
313
  <ModuleInfo
251
314
  className={css.moduleInfo}
@@ -5,7 +5,9 @@ import { COMPONENT, PACKAGE_FILTERS, PACKAGES_SEPARATOR, SECTIONS } from '@bundl
5
5
 
6
6
  import config from '../../config.json';
7
7
  import I18N from '../../i18n';
8
+ import { Box } from '../../layout/box';
8
9
  import { FlexStack } from '../../layout/flex-stack';
10
+ import { Stack } from '../../layout/stack';
9
11
  import { InputSearch } from '../../ui/input-search';
10
12
  import { EmptySet } from '../../ui/empty-set';
11
13
  import { Filters } from '../../ui/filters';
@@ -22,9 +24,13 @@ import { SEARCH_PLACEHOLDER } from './bundle-packages.i18n';
22
24
  import css from './bundle-packages.module.css';
23
25
  import { MetricsDisplaySelector } from '../metrics-display-selector';
24
26
  import { MetricsDisplayType } from '../../constants';
25
- import { MetricsTreemap } from '../metrics-treemap';
27
+ import { MetricsTreemap, getTreemapNodes, getTreemapNodesGroupedByPath } from '../metrics-treemap';
26
28
  import { useMetricsDisplayType } from '../../hooks/metrics-display-type';
27
29
 
30
+ const DISPLAY_TYPE_GROUPS = {
31
+ [MetricsDisplayType.TREEMAP]: ['folder'],
32
+ };
33
+
28
34
  const getDropdownFilters = ({ compareMode, filters }) => ({
29
35
  [PACKAGE_FILTERS.CHANGED]: {
30
36
  label: 'Changed',
@@ -113,6 +119,60 @@ RowHeader.propTypes = {
113
119
  search: PropTypes.string,
114
120
  };
115
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
+
116
176
  export const BundlePackages = (props) => {
117
177
  const {
118
178
  className,
@@ -137,7 +197,7 @@ export const BundlePackages = (props) => {
137
197
 
138
198
  const jobLabels = jobs?.map((job) => job?.label);
139
199
 
140
- const [displayType, setDisplayType] = useMetricsDisplayType();
200
+ const [displayType, setDisplayType] = useMetricsDisplayType(DISPLAY_TYPE_GROUPS);
141
201
 
142
202
  const dropdownFilters = useMemo(
143
203
  () => getDropdownFilters({ compareMode: jobs?.length > 1, filters }),
@@ -191,12 +251,12 @@ export const BundlePackages = (props) => {
191
251
 
192
252
  return (
193
253
  <>
194
- <section className={cx(css.root, className)}>
254
+ <Stack space="xsmall" as="section" className={cx(css.root, className)}>
195
255
  <Toolbar
196
256
  className={css.toolbar}
197
257
  renderActions={({ actionClassName }) => (
198
258
  <FlexStack space="xxsmall" className={cx(css.dropdown, actionClassName)}>
199
- <MetricsDisplaySelector onSelect={setDisplayType} value={displayType} />
259
+ <MetricsDisplaySelector onSelect={setDisplayType} value={displayType.value} groupBy={displayType.groupBy} groups={DISPLAY_TYPE_GROUPS} />
200
260
  <MetricsTableOptions
201
261
  handleViewAll={resetAllFilters}
202
262
  handleResetFilters={resetFilters}
@@ -219,8 +279,8 @@ export const BundlePackages = (props) => {
219
279
  />
220
280
  </FlexStack>
221
281
  </Toolbar>
222
- <main>
223
- {displayType === MetricsDisplayType.TABLE && (
282
+ <Box outline as="main">
283
+ {displayType.value === MetricsDisplayType.TABLE && (
224
284
  <MetricsTable
225
285
  runs={jobs}
226
286
  items={items}
@@ -232,21 +292,18 @@ export const BundlePackages = (props) => {
232
292
  updateSort={updateSort}
233
293
  />
234
294
  )}
235
- {displayType === MetricsDisplayType.TREEMAP && (
236
- <>
237
- <Table compact>
238
- <MetricsTableHeader
239
- metricTitle={metricsTableTitle}
240
- showSum
241
- jobs={jobs}
242
- rows={items}
243
- />
244
- </Table>
245
- <MetricsTreemap emptyMessage={emptyMessage} items={items} onItemClick={showEntryInfo} />
246
- </>
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
+ />
247
304
  )}
248
- </main>
249
- </section>
305
+ </Box>
306
+ </Stack>
250
307
  {entryItem && (
251
308
  <PackageInfo
252
309
  className={css.packageInfo}
@@ -2,14 +2,9 @@
2
2
  color: var(--color-text-light);
3
3
  }
4
4
 
5
- .toolbar {
6
- border-bottom: 1px solid var(--color-outline);
7
- }
8
-
9
5
  .toolbarSearch {
10
6
  width: 100%;
11
7
  max-width: 240px;
12
- margin-right: -1px; /** collapse border */
13
8
  }
14
9
 
15
10
  .hoverCard {
@@ -4,7 +4,7 @@ import cx from 'classnames';
4
4
 
5
5
  import css from './delta.module.css';
6
6
 
7
- export const Delta = ({ className, displayValue, deltaType, inverted }) => {
7
+ export const Delta = ({ className = '', displayValue, deltaType, inverted = false }) => {
8
8
  const rootClassName = cx(css.root, className, inverted && css.inverted, css[deltaType]);
9
9
  return <span className={rootClassName}>{displayValue}</span>;
10
10
  };
@@ -4,6 +4,10 @@
4
4
  white-space: nowrap;
5
5
  }
6
6
 
7
+ .CHANGE {
8
+ color: var(--color-info);
9
+ }
10
+
7
11
  .HIGH_NEGATIVE {
8
12
  color: var(--color-red-dark);
9
13
  }
@@ -29,11 +33,16 @@
29
33
  }
30
34
 
31
35
  .inverted {
32
- border-radius: var(--radius-small);
36
+ padding: 0 2px;
37
+ border-radius: var(--radius-xsmall);
33
38
  background-color: var(--color-gray-ultra-light);
34
39
  color: var(--color-light);
35
40
  }
36
41
 
42
+ .inverted.CHANGE {
43
+ background: var(--color-info-light);
44
+ }
45
+
37
46
  .inverted.HIGH_NEGATIVE {
38
47
  background: var(--color-red-dark);
39
48
  }
@@ -24,10 +24,6 @@
24
24
  background: linear-gradient(90deg, transparent, var(--color-background));
25
25
  }
26
26
 
27
- .item + .item {
28
- margin-left: var(--space-small);
29
- }
30
-
31
27
  .item:first-child {
32
28
  border-left: 0;
33
29
  padding-left: 0;
@@ -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
+ };
@@ -4,44 +4,102 @@ import cx from 'classnames';
4
4
  import { MetricsDisplayType } from '../../constants';
5
5
  import { Button } from '../../ui/button';
6
6
  import { ControlGroup } from '../../ui/control-group';
7
+ import { Dropdown, DropdownItem } from '../../ui/dropdown';
7
8
  import { Icon } from '../../ui/icon';
8
9
  import css from './metrics-display-selector.module.css';
10
+ import { FlexStack } from '../../layout';
11
+
12
+ const DEFAULT_GROUP_BY = '';
13
+
14
+ const METRICS_DISPLAY_MAP = {
15
+ [MetricsDisplayType.TABLE]: {
16
+ glyph: Icon.ICONS.TABLE,
17
+ label: 'Table',
18
+ },
19
+ [MetricsDisplayType.TREEMAP]: {
20
+ glyph: Icon.ICONS.TREEMAP,
21
+ label: 'Treemap',
22
+ },
23
+ };
24
+
25
+ const displayTypes = Object.values(MetricsDisplayType);
9
26
 
10
27
  interface MetricsDisplaySelectorProps {
11
28
  value: MetricsDisplayType;
12
- onSelect: (newValue: MetricsDisplayType) => void;
29
+ groupBy?: string;
30
+ groups?: Partial<Record<MetricsDisplayType, Array<string>>>;
31
+ onSelect: (newValue: MetricsDisplayType, groupBy?: string) => void;
13
32
  }
14
33
 
15
34
  export const MetricsDisplaySelector = (
16
35
  props: MetricsDisplaySelectorProps &
17
36
  Omit<React.ComponentProps<typeof ControlGroup>, 'onSelect' | 'value'>,
18
37
  ) => {
19
- const { className = '', value, onSelect, ...restProps } = props;
38
+ const { value, groupBy = DEFAULT_GROUP_BY, groups = {}, onSelect, ...restProps } = props;
20
39
 
21
40
  return (
22
- <ControlGroup className={cx(css.root, className)} {...restProps}>
23
- <Button
24
- outline
25
- active={value === MetricsDisplayType.TABLE}
26
- size="small"
27
- type="button"
28
- glyph={Icon.ICONS.TABLE}
29
- onClick={() => onSelect(MetricsDisplayType.TABLE)}
30
- className={css.button}
31
- >
32
- Table
33
- </Button>
34
- <Button
35
- outline
36
- active={value === MetricsDisplayType.TREEMAP}
37
- size="small"
38
- type="button"
39
- glyph={Icon.ICONS.TREEMAP}
40
- onClick={() => onSelect(MetricsDisplayType.TREEMAP)}
41
- className={cx(css.button)}
42
- >
43
- Treemap
44
- </Button>
41
+ <ControlGroup {...restProps}>
42
+ {displayTypes.map((displayType, index) => {
43
+ const displayGroups = groups?.[displayType];
44
+ const displayProps = METRICS_DISPLAY_MAP[displayType];
45
+ const isLast = displayTypes.length - 1 === index;
46
+ const isActive = displayType === value;
47
+
48
+ if (!displayGroups || displayGroups.length === 0) {
49
+ return (
50
+ <Button
51
+ outline
52
+ active={isActive}
53
+ size="small"
54
+ type="button"
55
+ glyph={displayProps.glyph}
56
+ onClick={() => onSelect(displayType)}
57
+ className={isLast && css.itemLast}
58
+ key={displayType}
59
+ >
60
+ {displayProps.label}
61
+ </Button>
62
+ );
63
+ }
64
+
65
+ const displayGroupsData = [
66
+ {
67
+ value: DEFAULT_GROUP_BY,
68
+ label: 'No groups',
69
+ },
70
+ ...displayGroups.map((displayGroup) => ({
71
+ value: displayGroup,
72
+ label: `Group by ${displayGroup}`,
73
+ })),
74
+ ];
75
+
76
+ return (
77
+ <Dropdown
78
+ glyph={displayProps.glyph}
79
+ label={displayProps.label}
80
+ className={cx(isLast && css.itemLast, isActive && css.buttonActive)}
81
+ key={displayType}
82
+ >
83
+ {displayGroupsData.map((displayGroupData) => {
84
+ const isGroupActive = isActive && displayGroupData.value === groupBy;
85
+
86
+ return (
87
+ <DropdownItem
88
+ onClick={() => onSelect(displayType, displayGroupData.value)}
89
+ isActive={isGroupActive}
90
+ className={cx(css.dropdownItem, isGroupActive && css.dropdownItemActive)}
91
+ key={displayGroupData.value}
92
+ >
93
+ <FlexStack space="xxxsmall" alignItems="center">
94
+ <Icon glyph={Icon.ICONS.CHECK} size="small" className={css.dropdownItemIcon} />
95
+ <span>{displayGroupData.label}</span>
96
+ </FlexStack>
97
+ </DropdownItem>
98
+ );
99
+ })}
100
+ </Dropdown>
101
+ );
102
+ })}
45
103
  </ControlGroup>
46
104
  );
47
105
  };