@bundle-stats/ui 4.13.0-beta.0 → 4.13.0-beta.2

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 (49) hide show
  1. package/lib/components/bundle-assets/index.js +2 -2
  2. package/lib/components/bundle-assets/index.js.map +1 -1
  3. package/lib/components/bundle-modules/index.js +2 -2
  4. package/lib/components/bundle-modules/index.js.map +1 -1
  5. package/lib/components/bundle-packages/index.js +2 -2
  6. package/lib/components/bundle-packages/index.js.map +1 -1
  7. package/lib/components/metrics-display-selector/metrics-display-selector.js +2 -8
  8. package/lib/components/metrics-display-selector/metrics-display-selector.js.map +1 -1
  9. package/lib/components/metrics-treemap/metrics-treemap.js +68 -48
  10. package/lib/components/metrics-treemap/metrics-treemap.js.map +1 -1
  11. package/lib/components/metrics-treemap/metrics-treemap.module.css +57 -41
  12. package/lib/hooks/entry-info.js +12 -0
  13. package/lib/hooks/entry-info.js.map +1 -0
  14. package/lib/query-state.js +1 -1
  15. package/lib/query-state.js.map +1 -1
  16. package/lib/ui/button/button.module.css +1 -0
  17. package/lib-esm/components/bundle-assets/index.js +3 -3
  18. package/lib-esm/components/bundle-assets/index.js.map +1 -1
  19. package/lib-esm/components/bundle-modules/index.js +2 -2
  20. package/lib-esm/components/bundle-modules/index.js.map +1 -1
  21. package/lib-esm/components/bundle-packages/index.js +3 -3
  22. package/lib-esm/components/bundle-packages/index.js.map +1 -1
  23. package/lib-esm/components/metrics-display-selector/metrics-display-selector.js +2 -8
  24. package/lib-esm/components/metrics-display-selector/metrics-display-selector.js.map +1 -1
  25. package/lib-esm/components/metrics-treemap/metrics-treemap.js +71 -50
  26. package/lib-esm/components/metrics-treemap/metrics-treemap.js.map +1 -1
  27. package/lib-esm/components/metrics-treemap/metrics-treemap.module.css +57 -41
  28. package/lib-esm/hooks/entry-info.js +8 -0
  29. package/lib-esm/hooks/entry-info.js.map +1 -0
  30. package/lib-esm/query-state.js +1 -1
  31. package/lib-esm/query-state.js.map +1 -1
  32. package/lib-esm/ui/button/button.module.css +1 -0
  33. package/package.json +4 -6
  34. package/src/components/bundle-assets/index.jsx +8 -3
  35. package/src/components/bundle-modules/index.tsx +2 -2
  36. package/src/components/bundle-packages/index.jsx +8 -3
  37. package/src/components/metrics-display-selector/metrics-display-selector.tsx +4 -8
  38. package/src/components/metrics-treemap/metrics-treemap.module.css +57 -41
  39. package/src/components/metrics-treemap/metrics-treemap.tsx +142 -105
  40. package/src/hooks/entry-info.ts +19 -0
  41. package/src/query-state.tsx +1 -1
  42. package/src/ui/button/button.module.css +1 -0
  43. package/types/hooks/entry-info.d.ts +8 -0
  44. package/lib/hooks/show-entry.js +0 -13
  45. package/lib/hooks/show-entry.js.map +0 -1
  46. package/lib-esm/hooks/show-entry.js +0 -9
  47. package/lib-esm/hooks/show-entry.js.map +0 -1
  48. package/src/hooks/show-entry.ts +0 -17
  49. package/types/hooks/show-entry.d.ts +0 -10
@@ -6,9 +6,14 @@ import * as webpack from '@bundle-stats/utils/lib-esm/webpack/compare';
6
6
  import { useRowsFilter } from '../../hooks/rows-filter';
7
7
  import { useRowsSort } from '../../hooks/rows-sort';
8
8
  import { useSearchParams } from '../../hooks/search-params';
9
- import { useShowEntry } from '../../hooks/show-entry';
9
+ import { useEntryInfo } from '../../hooks/entry-info';
10
10
  import { BundlePackages as BundlePackagesComponent } from './bundle-packages';
11
- import { getDuplicatePackages, getAddRowDuplicateFlag, getRowFilter, getCustomSort } from './bundle-packages.utils';
11
+ import {
12
+ getDuplicatePackages,
13
+ getAddRowDuplicateFlag,
14
+ getRowFilter,
15
+ getCustomSort,
16
+ } from './bundle-packages.utils';
12
17
 
13
18
  export const BundlePackages = (props) => {
14
19
  const { jobs, search, filters, setState, sortBy, direction, ...restProps } = props;
@@ -55,7 +60,7 @@ export const BundlePackages = (props) => {
55
60
  getCustomSort,
56
61
  });
57
62
 
58
- const { hideEntryInfo, showEntryInfo } = useShowEntry({ setState });
63
+ const [hideEntryInfo, showEntryInfo] = useEntryInfo({ setState });
59
64
 
60
65
  return (
61
66
  <BundlePackagesComponent
@@ -25,26 +25,22 @@ export const MetricsDisplaySelector = (
25
25
  active={value === MetricsDisplayType.TABLE}
26
26
  size="small"
27
27
  type="button"
28
+ glyph={Icon.ICONS.TABLE}
28
29
  onClick={() => onSelect(MetricsDisplayType.TABLE)}
29
30
  className={css.button}
30
31
  >
31
- <FlexStack space="xxxsmall" alignItems="center">
32
- <Icon glyph={Icon.ICONS.TABLE} className={css.icon} />
33
- <span>Table</span>
34
- </FlexStack>
32
+ Table
35
33
  </Button>
36
34
  <Button
37
35
  outline
38
36
  active={value === MetricsDisplayType.TREEMAP}
39
37
  size="small"
40
38
  type="button"
39
+ glyph={Icon.ICONS.TREEMAP}
41
40
  onClick={() => onSelect(MetricsDisplayType.TREEMAP)}
42
41
  className={cx(css.button)}
43
42
  >
44
- <FlexStack space="xxxsmall" alignItems="center">
45
- <Icon glyph={Icon.ICONS.TREEMAP} className={css.icon} />
46
- <span>Treemap</span>
47
- </FlexStack>
43
+ Treemap
48
44
  </Button>
49
45
  </FlexStack>
50
46
  );
@@ -12,6 +12,7 @@
12
12
  height: 100%;
13
13
  fill: var(--color-text);
14
14
  font-family: var(--font-family);
15
+ background: var(--color-outline);
15
16
  }
16
17
 
17
18
  /** Tile overlay fill background transition */
@@ -20,29 +21,31 @@
20
21
  transition-property: opacity;
21
22
  }
22
23
 
23
- .tileOverlay {
24
- fill: var(--color-outline);
24
+ .leafBackground {
25
+ fill: var(--color-background);
26
+ }
27
+
28
+ .leafBackdrop {
29
+ fill: var(--color-background);
25
30
  transition: var(--ui-transition-out);
26
31
  transition-property: opacity;
27
- opacity: 0.4;
32
+ opacity: 0.2;
28
33
  }
29
34
 
30
- .root .tile:hover .tileOverlay {
35
+ .leaf:hover .leafBackdrop {
31
36
  transition: var(--ui-transition-in);
32
- opacity: 0.8;
37
+ opacity: 0.5;
33
38
  }
34
39
 
35
- /** Tile content */
36
- .tileContent {
40
+ /** Leaf wrapper button */
41
+ .leafAction {
37
42
  appearance: none;
38
43
  border: 0;
39
- border-radius: none;
44
+ border-radius: 0;
40
45
  padding: 0;
41
46
  background: none;
42
-
43
47
  width: 100%;
44
48
  height: 100%;
45
- padding: var(--space-xxsmall) var(--space-small);
46
49
  display: flex;
47
50
  align-items: center;
48
51
  font-family: var(--font-family-fixed);
@@ -51,78 +54,90 @@
51
54
  text-align: left;
52
55
  }
53
56
 
54
- .tileContentWrapper {
57
+ .leafContent {
55
58
  width: 100%;
56
- max-height: 100%;
59
+ height: 100%;
57
60
  display: flex;
58
61
  flex-flow: column;
59
- gap: 4px;
62
+ justify-content: center;
63
+ padding: var(--space-xxsmall) var(--space-xxsmall);
64
+ gap: var(--space-xxsmall);
65
+ line-height: var(--line-height-heading);
66
+ }
67
+
68
+ /** Align label to other elements on the first column leafs */
69
+ .leafWrapper[x="0"] .leafContent {
70
+ padding-left: var(--space-small);
60
71
  }
61
72
 
62
- .tileContentLabel {
63
- margin-right: calc(0px - var(--space-xxsmall)); /* take more space if necessary */
64
- margin-bottom: calc(0px - 2px);
65
- flex: 1 1 auto;
66
- max-width: calc(100% + var(--space-xxsmall)); /* take more space if necessary */
73
+ .leadContentLabel {
74
+ flex: 0 1 auto;
75
+ width: 100%;
76
+ min-width: 0;
77
+ max-height: calc(2em * var(--line-height-heading));
67
78
  overflow: hidden;
68
79
  word-break: break-word;
69
80
  }
70
81
 
71
- .tileContentValue {
82
+ .leadContentValue {
72
83
  flex: 0 0 auto;
73
84
  max-width: calc(100% + var(--space-xxsmall));
74
- margin-right: calc(0px - var(--space-xxsmall)); /* take more space if necessary */
75
85
  white-space: nowrap;
76
86
  overflow: hidden;
77
87
  }
78
88
 
79
- .tileContentMetric {
89
+ .leadContentMetric {
80
90
  font-weight: bold;
81
91
  }
82
92
 
83
- .tileContentDelta {
93
+ .leadContentDelta {
84
94
  margin-left: var(--space-xxxsmall);
85
95
  }
86
96
 
87
97
  /** Tile size variation */
88
- .tileSmall .tileContent {
98
+ .leafSmall .leafContent {
99
+ padding: var(--space-xxxsmall) var(--space-xxsmall);
100
+ gap: 0;
89
101
  font-size: var(--size-xsmall);
90
102
  }
91
103
 
92
- .tileSmall .tileContentLabel {
104
+ .leafSmall .leadContentLabel {
93
105
  margin-top: 0;
94
106
  }
95
107
 
96
108
  /** Tile diff variation */
97
- .tile-NO_CHANGE .tileContent {
109
+ .leaf-NO_CHANGE .leafBackdrop {
110
+ fill: transparent;
111
+ }
112
+
113
+ .leaf-NO_CHANGE .leafAction {
98
114
  color: var(--color-text-light);
99
115
  }
100
116
 
101
- .tile-HIGH_NEGATIVE .tileOverlay {
117
+ .leaf-HIGH_NEGATIVE .leafBackdrop {
102
118
  fill: var(--color-red-100);
103
119
  }
104
120
 
105
- .tile-NEGATIVE .tileOverlay {
121
+ .leaf-NEGATIVE .leafBackdrop {
106
122
  fill: var(--color-red-50);
107
123
  }
108
124
 
109
- .tile-LOW_NEGATIVE .tileOverlay {
125
+ .leaf-LOW_NEGATIVE .leafBackdrop {
110
126
  fill: var(--color-red-25);
111
127
  }
112
128
 
113
- .tile-LOW_POSITIVE .tileOverlay {
129
+ .leaf-LOW_POSITIVE .leafBackdrop {
114
130
  fill: var(--color-green-25);
115
131
  }
116
132
 
117
- .tile-POSITIVE .tileOverlay {
133
+ .leaf-POSITIVE .leafBackdrop {
118
134
  fill: var(--color-green-50);
119
135
  }
120
136
 
121
- .tile-HIGH_POSITIVE .tileOverlay {
137
+ .leaf-HIGH_POSITIVE .leafBackdrop {
122
138
  fill: var(--color-green-100);
123
139
  }
124
140
 
125
-
126
141
  /** empty message */
127
142
  .emptyMessage {
128
143
  position: absolute;
@@ -140,7 +155,12 @@
140
155
  text-align: center;
141
156
  }
142
157
 
143
- /** tooltip content */
158
+ /** tooltip */
159
+ .leafContentTooltipAnchor {
160
+ width: 100%;
161
+ height: 100%;
162
+ }
163
+
144
164
  .tooltip {
145
165
  z-index: var(--layer-xxhigh);
146
166
  padding: var(--space-xsmall) var(--space-small);
@@ -154,15 +174,11 @@
154
174
  font-size: var(--size-small);
155
175
  }
156
176
 
157
- .tooltipArrow {
158
- border: var(--space-xxsmall) solid var(--color-background);
159
- border-width: var(--space-xxsmall) var(--space-xxsmall) 0;
160
- border-left-color: transparent;
161
- border-right-color: transparent;
162
- position: absolute;
163
- top: 100%;
177
+ .leafTooltipArrow svg {
178
+ display: block;
179
+ fill: var(--color-background) !important;
164
180
  }
165
181
 
166
- .tooltipContentTitle {
182
+ .leafTooltipContentTitle {
167
183
  font-size: var(--size-medium);
168
184
  }
@@ -1,16 +1,9 @@
1
- import React, { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
1
+ import React, { RefObject, forwardRef, useCallback, useMemo, useRef } from 'react';
2
+ import { useDebounce, useHoverDirty, useMeasure, useMouseHovered } from 'react-use';
2
3
  import cx from 'classnames';
3
- import { useMeasure } from 'react-use';
4
4
  import type { ReportMetricRow, MetricRunInfo, MetricRunInfoBaseline } from '@bundle-stats/utils';
5
- import { hierarchy, treemap, treemapSquarify } from 'd3';
6
- import {
7
- FloatingPortal,
8
- useClientPoint,
9
- useFloating,
10
- useHover,
11
- useInteractions,
12
- } from '@floating-ui/react';
13
- import { arrow, offset } from '@floating-ui/react-dom';
5
+ import { hierarchy, treemap, treemapSquarify, window } from 'd3';
6
+ import { Tooltip, TooltipArrow, TooltipAnchor, useTooltipState, useTooltip } from 'ariakit/tooltip';
14
7
 
15
8
  import { Stack } from '../../layout/stack';
16
9
  import { FileName } from '../../ui/file-name';
@@ -36,10 +29,10 @@ const VERTICAL_SPACING = 4;
36
29
  const LINE_HEIGHT = 16;
37
30
  const LINE_HEIGHT_SMALL = 13.3;
38
31
 
39
- function resolveTileSizeDisplay(width: number, height: number): TileSizeDisplay {
32
+ function resolveLeafSizeDisplay(width: number, height: number): TileSizeDisplay {
40
33
  if (
41
34
  height > PADDING_TOP + PADDING_BOTTOM + VERTICAL_SPACING + LINE_HEIGHT * 2 &&
42
- width > PADDING_LEFT + PADDING_RIGHT + 96
35
+ width > PADDING_LEFT + PADDING_RIGHT + 128
43
36
  ) {
44
37
  return 'default';
45
38
  }
@@ -54,19 +47,20 @@ function resolveTileSizeDisplay(width: number, height: number): TileSizeDisplay
54
47
  return 'minimal';
55
48
  }
56
49
 
57
- interface TooltipContentProps {
58
- data: ReportMetricRow;
50
+ interface LeafTooltipContentProps {
51
+ item: ReportMetricRow;
59
52
  }
60
53
 
61
- const TooltipContent = (props: TooltipContentProps) => {
62
- const { data } = props;
63
- const currentRun = data.runs[0] as MetricRunInfo;
64
- const baselineRun = data.runs[data.runs.length - 1] as MetricRunInfoBaseline;
54
+ const LeafTooltipContent = (props: LeafTooltipContentProps) => {
55
+ const { item } = props;
56
+
57
+ const currentRun = item.runs[0] as MetricRunInfo;
58
+ const baselineRun = item.runs[item.runs.length - 1] as MetricRunInfoBaseline;
65
59
 
66
60
  return (
67
- <Stack space="small" className={css.tooltipContent}>
68
- <h3 className={css.tooltipContentTitle}>
69
- <FileName as="code" name={data.label} />
61
+ <Stack space="small" className={css.leafTooltipContent}>
62
+ <h3 className={css.leafTooltipContentTitle}>
63
+ <FileName as="code" name={item.label} />
70
64
  </h3>
71
65
  <RunInfo
72
66
  current={currentRun.displayValue}
@@ -78,7 +72,77 @@ const TooltipContent = (props: TooltipContentProps) => {
78
72
  );
79
73
  };
80
74
 
81
- const TileButton = forwardRef((props: React.ComponentProps<'button'>, ref) => (
75
+ interface LeafContentProps {
76
+ sizeDisplay: 'minimal' | 'small' | 'default';
77
+ item: ReportMetricRow;
78
+ runInfo: MetricRunInfo;
79
+ }
80
+
81
+ const LeafContent = forwardRef((props: LeafContentProps, ref: React.Ref<HTMLDivElement>) => {
82
+ const { sizeDisplay, item, runInfo } = props;
83
+
84
+ if (sizeDisplay === 'minimal') {
85
+ return <div className={css.leafContent} ref={ref} />;
86
+ }
87
+
88
+ return (
89
+ <div className={css.leafContent} ref={ref}>
90
+ <p className={css.leadContentLabel}>{item.label}</p>
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
+ )}
101
+ </div>
102
+ );
103
+ });
104
+
105
+ const LeafContentWithTooltip = (props: LeafContentProps & { parentRef: RefObject<Element> }) => {
106
+ const { sizeDisplay, item, runInfo, parentRef } = props;
107
+
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?.pageXOffset ?? 0),
119
+ y: pointer.docY - (document?.defaultView?.pageYOffset ?? 0),
120
+ w: 1,
121
+ h: 1,
122
+ };
123
+
124
+ return newRect;
125
+ }, [pointer.docX, pointer.docY]);
126
+
127
+ const tooltipState = useTooltipState({ gutter: 8, getAnchorRect, timeout: 100 });
128
+
129
+ // Update tooltip position when poiner values are changing
130
+ useDebounce(tooltipState.render, 10, [pointer.docX, pointer.docY]);
131
+
132
+ return (
133
+ <>
134
+ <TooltipAnchor state={tooltipState} className={css.leafContentTooltipAnchor}>
135
+ <LeafContent sizeDisplay={sizeDisplay} item={item} runInfo={runInfo} />
136
+ </TooltipAnchor>
137
+ <Tooltip state={tooltipState} className={css.tooltip}>
138
+ <TooltipArrow state={tooltipState} size={16} className={css.leafTooltipArrow} />
139
+ <LeafTooltipContent item={item} />
140
+ </Tooltip>
141
+ </>
142
+ );
143
+ };
144
+
145
+ const LeafAction = forwardRef((props: React.ComponentProps<'button'>, ref) => (
82
146
  // @ts-expect-error
83
147
  <button type="button" xmlns="http://www.w3.org/1999/xhtml" ref={ref} {...props} />
84
148
  ));
@@ -105,98 +169,61 @@ interface LeafProps {
105
169
  onClick?: (id: string) => void;
106
170
  }
107
171
 
108
- const Tile = (props: LeafProps) => {
172
+ const Leaf = (props: LeafProps) => {
109
173
  const { x, y, width, height, data, onClick } = props;
174
+
110
175
  const { item } = data;
111
176
  const runInfo = item.runs?.[0] as MetricRunInfo;
112
177
 
113
- const [isOpen, setIsOpen] = useState(false);
114
- const arrowRef = useRef(null);
115
- const { refs, floatingStyles, context, middlewareData } = useFloating({
116
- placement: 'top',
117
- open: isOpen,
118
- onOpenChange: setIsOpen,
119
- middleware: [arrow({ element: arrowRef }), offset(16)],
120
- });
121
- const clientPoint = useClientPoint(context);
122
- const hover = useHover(context);
123
- const { getReferenceProps, getFloatingProps } = useInteractions([hover, clientPoint]);
124
-
125
- const tileSizeDisplay = useMemo(() => resolveTileSizeDisplay(width, height), [width, height]);
126
-
127
- const handleOnClick = useCallback(() => {
128
- if (onClick) {
129
- onClick(item.key);
130
- }
131
- }, [item.key]);
178
+ const sizeDisplay = useMemo(() => resolveLeafSizeDisplay(width, height), [width, height]);
179
+ const handleOnClick = useCallback(() => onClick?.(item.key), [item.key]);
180
+
181
+ const leafContentRef = useRef<HTMLElement>(null);
182
+ const hover = useHoverDirty(leafContentRef);
183
+
184
+ const leafClassName = cx(
185
+ css.leaf,
186
+ css[`leaf-${runInfo.deltaType}`],
187
+ sizeDisplay === 'small' && css.leafSmall,
188
+ );
132
189
 
133
190
  return (
134
- <g
135
- className={cx(
136
- css.tile,
137
- !item.changed && css['tile-NO_CHANGE'],
138
- css[`tile-${runInfo.deltaType}`],
139
- tileSizeDisplay === 'small' && css.tileSmall,
140
- )}
141
- >
142
- <rect x={x} y={y} width={width} height={height} className={css.tileOverlay} />
143
- <foreignObject height={height} width={width} x={x} y={y}>
144
- <TileButton
145
- onClick={handleOnClick}
146
- className={css.tileContent}
147
- ref={refs.setReference}
148
- {...getReferenceProps()}
149
- >
150
- {tileSizeDisplay !== 'minimal' && (
151
- <div className={css.tileContentWrapper}>
152
- <p className={css.tileContentLabel}>{item.label}</p>
153
- <p className={css.tileContentValue}>
154
- <span className={css.tileContentMetric}>{runInfo.displayValue}</span>
155
- <Delta
156
- className={css.tileContentDelta}
157
- displayValue={runInfo.displayDeltaPercentage}
158
- deltaType={runInfo.deltaType}
159
- />
160
- </p>
161
- </div>
191
+ <g className={leafClassName}>
192
+ <rect x={x} y={y} width={width} height={height} className={css.leafBackground} />
193
+ <rect x={x} y={y} width={width} height={height} className={css.leafBackdrop} />
194
+ <foreignObject height={height} width={width} x={x} y={y} className={css.leafWrapper}>
195
+ <LeafAction onClick={handleOnClick} ref={leafContentRef} className={css.leafAction}>
196
+ {hover ? (
197
+ <LeafContentWithTooltip
198
+ sizeDisplay={sizeDisplay}
199
+ item={item}
200
+ runInfo={runInfo}
201
+ parentRef={leafContentRef}
202
+ />
203
+ ) : (
204
+ <LeafContent sizeDisplay={sizeDisplay} item={item} runInfo={runInfo} />
162
205
  )}
163
- </TileButton>
164
- {isOpen && (
165
- <FloatingPortal>
166
- <div
167
- ref={refs.setFloating}
168
- style={floatingStyles}
169
- {...getFloatingProps()}
170
- className={css.tooltip}
171
- >
172
- <div
173
- ref={arrowRef}
174
- style={{ left: middlewareData.arrow?.x }}
175
- className={css.tooltipArrow}
176
- />
177
- <TooltipContent data={item} />
178
- </div>
179
- </FloatingPortal>
180
- )}
206
+ </LeafAction>
181
207
  </foreignObject>
182
208
  </g>
183
209
  );
184
210
  };
185
211
 
186
- interface MetricsTreemapProps {
212
+ interface UseMetricsTreemapHierarchyParams {
187
213
  items: Array<ReportMetricRow>;
188
- emptyMessage?: React.ReactNode;
189
- onItemClick?: (entryId: string) => void;
214
+ width: number;
215
+ height: number;
190
216
  }
191
217
 
192
- export const MetricsTreemap = (props: MetricsTreemapProps & React.ComponentProps<'div'>) => {
193
- const { className = '', items, onItemClick, emptyMessage = 'No data', ...restProps } = props;
194
-
195
- const [containerRef, { width, height }] = useMeasure<HTMLDivElement>();
218
+ function useMetricsTreemapHierarchy(params: UseMetricsTreemapHierarchyParams) {
219
+ const { items, width, height } = params;
196
220
 
197
221
  const treemapHierarchy = useMemo(() => {
198
222
  const children = items.map((item) => {
199
- // set tile value to the largest run value to display the impact of deleted items
223
+ /**
224
+ * Set leaf value to the largest run value to allow to
225
+ * display the impact of deleted / items
226
+ */
200
227
  const values = item.runs.map((run) => run?.value || 0);
201
228
  const value = Math.max(...values);
202
229
 
@@ -234,29 +261,39 @@ export const MetricsTreemap = (props: MetricsTreemapProps & React.ComponentProps
234
261
  return tremapLayout.leaves().filter((leaf) => Boolean(leaf.parent));
235
262
  }, [height, width, treemapHierarchy]);
236
263
 
264
+ return cells;
265
+ }
266
+
267
+ interface MetricsTreemapProps {
268
+ items: Array<ReportMetricRow>;
269
+ emptyMessage?: React.ReactNode;
270
+ onItemClick?: (entryId: string) => void;
271
+ }
272
+
273
+ export const MetricsTreemap = (props: MetricsTreemapProps & React.ComponentProps<'div'>) => {
274
+ const { className = '', items, onItemClick, emptyMessage = 'No data', ...restProps } = props;
275
+
276
+ const [containerRef, { width, height }] = useMeasure<HTMLDivElement>();
277
+ const cells = useMetricsTreemapHierarchy({ items, width, height });
278
+
237
279
  return (
238
280
  <div className={cx(css.root, className)} {...restProps} ref={containerRef}>
239
281
  {cells.length > 0 ? (
240
- <svg
241
- width={width}
242
- height={height}
243
- viewBox={`0 0 ${width} ${height}`}
244
- className={css.canvas}
245
- >
282
+ <svg viewBox={`0 0 ${width} ${height}`} className={css.canvas}>
246
283
  {cells.map((leaf) => {
247
284
  if (!leaf.parent) {
248
285
  return null;
249
286
  }
250
287
 
251
288
  return (
252
- <Tile
289
+ <Leaf
253
290
  x={leaf.x0}
254
291
  y={leaf.y0}
255
292
  width={leaf.x1 - leaf.x0}
256
293
  height={leaf.y1 - leaf.y0}
257
294
  data={leaf.data as TreeNode}
258
- key={leaf.id}
259
295
  onClick={onItemClick}
296
+ key={leaf.id}
260
297
  />
261
298
  );
262
299
  })}
@@ -0,0 +1,19 @@
1
+ import { useCallback } from 'react';
2
+
3
+ interface UseEntryInfoParams {
4
+ setState: (data: { entryId: string }) => void;
5
+ }
6
+
7
+ type UseEntryInfo = [() => void, (entryId: string) => void];
8
+
9
+ /**
10
+ * Show/hide side panel entry
11
+ */
12
+ export function useEntryInfo(params: UseEntryInfoParams): UseEntryInfo {
13
+ const { setState } = params;
14
+
15
+ const hideEntryInfo = useCallback(() => setState({ entryId: '' }), [setState]);
16
+ const showEntryInfo = useCallback((entryId: string) => setState({ entryId }), [setState]);
17
+
18
+ return [hideEntryInfo, showEntryInfo];
19
+ }
@@ -30,7 +30,7 @@ export const useComponentQueryState = (componentName: string) => {
30
30
 
31
31
  setQueryState({ [componentName]: newState });
32
32
  },
33
- [componentState],
33
+ [componentState, setQueryState],
34
34
  );
35
35
 
36
36
  return [componentState, setState];
@@ -256,6 +256,7 @@ a.root {
256
256
  .small .glyph {
257
257
  width: calc(var(--space-xsmall) + 2px);
258
258
  height: calc(var(--space-xsmall) + 2px);
259
+ margin: 1px; /* compensate to keep the same height when the text is not provided */
259
260
  }
260
261
 
261
262
  .medium {
@@ -0,0 +1,8 @@
1
+ interface UseEntryInfoParams {
2
+ setState: (data: {
3
+ entryId: string;
4
+ }) => void;
5
+ }
6
+ type UseEntryInfo = [() => void, (entryId: string) => void];
7
+ export declare function useEntryInfo(params: UseEntryInfoParams): UseEntryInfo;
8
+ export {};
@@ -1,13 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useShowEntry = void 0;
4
- const react_1 = require("react");
5
- const useShowEntry = (params) => {
6
- const { setState } = params;
7
- return (0, react_1.useMemo)(() => ({
8
- hideEntryInfo: () => setState({ entryId: '' }),
9
- showEntryInfo: (entryId) => setState({ entryId }),
10
- }), [setState]);
11
- };
12
- exports.useShowEntry = useShowEntry;
13
- //# sourceMappingURL=show-entry.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"show-entry.js","sourceRoot":"","sources":["../../src/hooks/show-entry.ts"],"names":[],"mappings":";;;AAAA,iCAAgC;AAMzB,MAAM,YAAY,GAAG,CAAC,MAAwB,EAAE,EAAE;IACvD,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC;IAE5B,OAAO,IAAA,eAAO,EACZ,GAAG,EAAE,CAAC,CAAC;QACL,aAAa,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QAC9C,aAAa,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;KAC1D,CAAC,EACF,CAAC,QAAQ,CAAC,CACX,CAAC;AACJ,CAAC,CAAC;AAVW,QAAA,YAAY,gBAUvB"}
@@ -1,9 +0,0 @@
1
- import { useMemo } from 'react';
2
- export const useShowEntry = (params) => {
3
- const { setState } = params;
4
- return useMemo(() => ({
5
- hideEntryInfo: () => setState({ entryId: '' }),
6
- showEntryInfo: (entryId) => setState({ entryId }),
7
- }), [setState]);
8
- };
9
- //# sourceMappingURL=show-entry.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"show-entry.js","sourceRoot":"","sources":["../../src/hooks/show-entry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAMhC,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAwB,EAAE,EAAE;IACvD,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC;IAE5B,OAAO,OAAO,CACZ,GAAG,EAAE,CAAC,CAAC;QACL,aAAa,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QAC9C,aAAa,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;KAC1D,CAAC,EACF,CAAC,QAAQ,CAAC,CACX,CAAC;AACJ,CAAC,CAAC"}
@@ -1,17 +0,0 @@
1
- import { useMemo } from 'react';
2
-
3
- interface UseEntryIdParams {
4
- setState: (data: { entryId: string }) => void;
5
- }
6
-
7
- export const useShowEntry = (params: UseEntryIdParams) => {
8
- const { setState } = params;
9
-
10
- return useMemo(
11
- () => ({
12
- hideEntryInfo: () => setState({ entryId: '' }),
13
- showEntryInfo: (entryId: string) => setState({ entryId }),
14
- }),
15
- [setState],
16
- );
17
- };