@atlaspack/inspector-frontend 0.1.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 (164) hide show
  1. package/.atlaspackrc +4 -0
  2. package/.eslintrc.json +19 -0
  3. package/CHANGELOG.md +12 -0
  4. package/dist/atlassian-dark-brand-refresh.91b786da.js +2 -0
  5. package/dist/atlassian-dark-brand-refresh.91b786da.js.map +1 -0
  6. package/dist/atlassian-dark-future.59ebadca.js +2 -0
  7. package/dist/atlassian-dark-future.59ebadca.js.map +1 -0
  8. package/dist/atlassian-dark-increased-contrast.ff6775f2.js +2 -0
  9. package/dist/atlassian-dark-increased-contrast.ff6775f2.js.map +1 -0
  10. package/dist/atlassian-dark.ad679134.js +2 -0
  11. package/dist/atlassian-dark.ad679134.js.map +1 -0
  12. package/dist/atlassian-legacy-dark.8aa27f7f.js +2 -0
  13. package/dist/atlassian-legacy-dark.8aa27f7f.js.map +1 -0
  14. package/dist/atlassian-legacy-light.2eb372ce.js +2 -0
  15. package/dist/atlassian-legacy-light.2eb372ce.js.map +1 -0
  16. package/dist/atlassian-light-brand-refresh.fadcab0a.js +2 -0
  17. package/dist/atlassian-light-brand-refresh.fadcab0a.js.map +1 -0
  18. package/dist/atlassian-light-future.612afe8a.js +2 -0
  19. package/dist/atlassian-light-future.612afe8a.js.map +1 -0
  20. package/dist/atlassian-light-increased-contrast.7161cd79.js +2 -0
  21. package/dist/atlassian-light-increased-contrast.7161cd79.js.map +1 -0
  22. package/dist/atlassian-light.bc343d4c.js +2 -0
  23. package/dist/atlassian-light.bc343d4c.js.map +1 -0
  24. package/dist/atlassian-shape.b92d69c0.js +2 -0
  25. package/dist/atlassian-shape.b92d69c0.js.map +1 -0
  26. package/dist/atlassian-spacing.60ddd8e7.js +2 -0
  27. package/dist/atlassian-spacing.60ddd8e7.js.map +1 -0
  28. package/dist/atlassian-typography-adg3.f88947f6.js +2 -0
  29. package/dist/atlassian-typography-adg3.f88947f6.js.map +1 -0
  30. package/dist/atlassian-typography-modernized.42016c51.js +2 -0
  31. package/dist/atlassian-typography-modernized.42016c51.js.map +1 -0
  32. package/dist/atlassian-typography-refreshed.ec0d111b.js +2 -0
  33. package/dist/atlassian-typography-refreshed.ec0d111b.js.map +1 -0
  34. package/dist/atlassian-typography.66d7e8f4.js +2 -0
  35. package/dist/atlassian-typography.66d7e8f4.js.map +1 -0
  36. package/dist/badge-light.7e55986a.png +0 -0
  37. package/dist/custom-theme.4680282a.js +2 -0
  38. package/dist/custom-theme.4680282a.js.map +1 -0
  39. package/dist/drag-handle.136830d3.js +2 -0
  40. package/dist/drag-handle.136830d3.js.map +1 -0
  41. package/dist/drag-handle.63bdb345.css +2 -0
  42. package/dist/drag-handle.63bdb345.css.map +1 -0
  43. package/dist/index.13289f53.js +28 -0
  44. package/dist/index.13289f53.js.map +1 -0
  45. package/dist/index.a41fafce.css +2 -0
  46. package/dist/index.a41fafce.css.map +1 -0
  47. package/dist/index.html +1 -0
  48. package/dist/index.runtime.3c39d71d.js +2 -0
  49. package/dist/index.runtime.3c39d71d.js.map +1 -0
  50. package/dist/refractor.2c1fd9a1.js +2 -0
  51. package/dist/refractor.2c1fd9a1.js.map +1 -0
  52. package/index.html +11 -0
  53. package/jest.config.js +16 -0
  54. package/package.json +64 -0
  55. package/src/APIError.test.ts +72 -0
  56. package/src/APIError.tsx +29 -0
  57. package/src/AppRoutes.tsx +56 -0
  58. package/src/hack-feature-flags.ts +6 -0
  59. package/src/main.tsx +50 -0
  60. package/src/test/stubCssModule.js +1 -0
  61. package/src/ui/App.module.css +122 -0
  62. package/src/ui/App.module.css.d.ts +8 -0
  63. package/src/ui/AppLayout/AppLayout.tsx +26 -0
  64. package/src/ui/AppLayout/SidebarNavigation/LinkItem.tsx +26 -0
  65. package/src/ui/AppLayout/SidebarNavigation/SidebarNavigation.tsx +45 -0
  66. package/src/ui/AppLayout/TopNavigation/Logo.module.css +12 -0
  67. package/src/ui/AppLayout/TopNavigation/Logo.module.css.d.ts +4 -0
  68. package/src/ui/AppLayout/TopNavigation/Logo.tsx +11 -0
  69. package/src/ui/AppLayout/TopNavigation/TopNavigation.module.css +14 -0
  70. package/src/ui/AppLayout/TopNavigation/TopNavigation.module.css.d.ts +3 -0
  71. package/src/ui/AppLayout/TopNavigation/TopNavigation.tsx +45 -0
  72. package/src/ui/AppLayout/TopNavigation/badge-light.png +0 -0
  73. package/src/ui/AppLayout/TopNavigation/logo-light.png +0 -0
  74. package/src/ui/DefaultLoadingIndicator/DefaultLoadingIndicator.module.css +9 -0
  75. package/src/ui/DefaultLoadingIndicator/DefaultLoadingIndicator.module.css.d.ts +3 -0
  76. package/src/ui/DefaultLoadingIndicator/DefaultLoadingIndicator.test.tsx +15 -0
  77. package/src/ui/DefaultLoadingIndicator/DefaultLoadingIndicator.tsx +14 -0
  78. package/src/ui/app/StatsPage.tsx +77 -0
  79. package/src/ui/app/cache/CacheKeysIndexPage.tsx +13 -0
  80. package/src/ui/app/cache/CacheKeysPage.module.css +11 -0
  81. package/src/ui/app/cache/CacheKeysPage.module.css.d.ts +4 -0
  82. package/src/ui/app/cache/CacheKeysPage.tsx +23 -0
  83. package/src/ui/app/cache/[key]/CacheValuePage.tsx +40 -0
  84. package/src/ui/app/cache/ui/CacheKeyList.module.css +40 -0
  85. package/src/ui/app/cache/ui/CacheKeyList.module.css.d.ts +7 -0
  86. package/src/ui/app/cache/ui/CacheKeyList.tsx +187 -0
  87. package/src/ui/app/cache-invalidation/CacheInvalidationPage.tsx +22 -0
  88. package/src/ui/app/cache-invalidation/[fileId]/CacheInvalidationFilePage.tsx +22 -0
  89. package/src/ui/app/cache-invalidation/ui/CacheFileList.module.css +40 -0
  90. package/src/ui/app/cache-invalidation/ui/CacheFileList.module.css.d.ts +7 -0
  91. package/src/ui/app/cache-invalidation/ui/CacheFileList.tsx +185 -0
  92. package/src/ui/app/treemap/BottomPanelResizeState.test.ts +25 -0
  93. package/src/ui/app/treemap/BottomPanelResizeState.tsx +48 -0
  94. package/src/ui/app/treemap/FoamTreemapPage.module.css +24 -0
  95. package/src/ui/app/treemap/FoamTreemapPage.module.css.d.ts +6 -0
  96. package/src/ui/app/treemap/FoamTreemapPage.tsx +47 -0
  97. package/src/ui/app/treemap/controllers/RelatedBundlesController.tsx +41 -0
  98. package/src/ui/app/treemap/controllers/UrlFocusController.tsx +33 -0
  99. package/src/ui/app/treemap/ui/BottomPanel/BottomPanel.module.css +24 -0
  100. package/src/ui/app/treemap/ui/BottomPanel/BottomPanel.module.css.d.ts +5 -0
  101. package/src/ui/app/treemap/ui/BottomPanel/BottomPanel.tsx +24 -0
  102. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AdvancedSettings.module.css +13 -0
  103. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AdvancedSettings.module.css.d.ts +5 -0
  104. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AdvancedSettings.tsx +53 -0
  105. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/AssetTable.tsx +135 -0
  106. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTable.module.css +7 -0
  107. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTable.module.css.d.ts +3 -0
  108. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTable.tsx +123 -0
  109. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTableModel.tsx +18 -0
  110. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTableRow.module.css +20 -0
  111. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTableRow.module.css.d.ts +6 -0
  112. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTableRow.tsx +79 -0
  113. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/getFileURL.test.ts +19 -0
  114. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/getFileURL.ts +24 -0
  115. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/FocusedGroupInfo.module.css +20 -0
  116. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/FocusedGroupInfo.module.css.d.ts +5 -0
  117. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/FocusedGroupInfo.tsx +42 -0
  118. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/FocusedGroupInfoInner.module.css +29 -0
  119. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/FocusedGroupInfoInner.module.css.d.ts +6 -0
  120. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/FocusedGroupInfoInner.tsx +107 -0
  121. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/GraphContainer.module.css +7 -0
  122. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/GraphContainer.module.css.d.ts +3 -0
  123. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/GraphContainer.tsx +20 -0
  124. package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/SourceCodeURL.tsx +5 -0
  125. package/src/ui/app/treemap/ui/BundleGraphRenderer.module.css +13 -0
  126. package/src/ui/app/treemap/ui/BundleGraphRenderer.module.css.d.ts +4 -0
  127. package/src/ui/app/treemap/ui/BundleGraphRenderer.tsx +95 -0
  128. package/src/ui/app/treemap/ui/FocusBreadcrumbs/FocusBreadcrumbs.module.css +6 -0
  129. package/src/ui/app/treemap/ui/FocusBreadcrumbs/FocusBreadcrumbs.module.css.d.ts +3 -0
  130. package/src/ui/app/treemap/ui/FocusBreadcrumbs/FocusBreadcrumbs.tsx +49 -0
  131. package/src/ui/app/treemap/ui/SigmaGraph.module.css +5 -0
  132. package/src/ui/app/treemap/ui/SigmaGraph.module.css.d.ts +3 -0
  133. package/src/ui/app/treemap/ui/SigmaGraph.tsx +80 -0
  134. package/src/ui/app/treemap/ui/Treemap.tsx +14 -0
  135. package/src/ui/app/treemap/ui/TreemapRenderer/ImpactScore.module.css +32 -0
  136. package/src/ui/app/treemap/ui/TreemapRenderer/ImpactScore.module.css.d.ts +5 -0
  137. package/src/ui/app/treemap/ui/TreemapRenderer/ImpactScore.tsx +24 -0
  138. package/src/ui/app/treemap/ui/TreemapRenderer/TreemapRenderer.module.css +14 -0
  139. package/src/ui/app/treemap/ui/TreemapRenderer/TreemapRenderer.module.css.d.ts +4 -0
  140. package/src/ui/app/treemap/ui/TreemapRenderer/TreemapRenderer.tsx +271 -0
  141. package/src/ui/app/treemap/ui/TreemapRenderer/TreemapTooltip.module.css +15 -0
  142. package/src/ui/app/treemap/ui/TreemapRenderer/TreemapTooltip.module.css.d.ts +4 -0
  143. package/src/ui/app/treemap/ui/TreemapRenderer/TreemapTooltip.tsx +111 -0
  144. package/src/ui/app/treemap/ui/TreemapRenderer/controllers/useStableCallback.test.ts +27 -0
  145. package/src/ui/app/treemap/ui/TreemapRenderer/controllers/useStableCallback.ts +21 -0
  146. package/src/ui/app/treemap/ui/TreemapRenderer/useMouseMoveController.ts +20 -0
  147. package/src/ui/globals.css +26 -0
  148. package/src/ui/globals.css.d.ts +1 -0
  149. package/src/ui/globals.d.ts +9 -0
  150. package/src/ui/model/ViewModel.test.ts +31 -0
  151. package/src/ui/model/ViewModel.ts +62 -0
  152. package/src/ui/not-found/NotFoundPage.module.css +7 -0
  153. package/src/ui/not-found/NotFoundPage.module.css.d.ts +3 -0
  154. package/src/ui/not-found/NotFoundPage.tsx +9 -0
  155. package/src/ui/types/Graph.tsx +12 -0
  156. package/src/ui/util/ErrorBoundary.module.css +3 -0
  157. package/src/ui/util/ErrorBoundary.module.css.d.ts +3 -0
  158. package/src/ui/util/ErrorBoundary.test.tsx +65 -0
  159. package/src/ui/util/ErrorBoundary.tsx +75 -0
  160. package/src/ui/util/colorPalette.tsx +122 -0
  161. package/src/ui/util/formatBytes.test.ts +13 -0
  162. package/src/ui/util/formatBytes.tsx +9 -0
  163. package/src/ui/util/getRandomDarkerColor.tsx +31 -0
  164. package/tsconfig.json +12 -0
@@ -0,0 +1,49 @@
1
+ import {observer} from 'mobx-react-lite';
2
+ import {Link, useSearchParams} from 'react-router';
3
+ import {viewModel} from '../../../../model/ViewModel';
4
+ import * as styles from './FocusBreadcrumbs.module.css';
5
+ import qs from 'qs';
6
+
7
+ export const FocusBreadcrumbs = observer(() => {
8
+ const [searchParams] = useSearchParams();
9
+ const bundleEl = viewModel.focusedBundle ? (
10
+ <Link to={`/app/treemap?bundle=${viewModel.focusedBundle.id}`}>
11
+ {viewModel.focusedBundle.label}
12
+ </Link>
13
+ ) : null;
14
+
15
+ const focusedGroup = viewModel.focusedGroup
16
+ ? viewModel.focusedGroup.id.split('/').map((part, i, arr) => {
17
+ const candidatePath = arr.slice(0, i + 1).join('/');
18
+ return (
19
+ <div key={i}>
20
+ <Link
21
+ to={`/app/treemap?${qs.stringify({
22
+ bundle:
23
+ viewModel.focusedBundle?.id ?? searchParams.get('bundle'),
24
+ focusedBundleId: viewModel.focusedBundle?.id,
25
+ focusedGroupId: candidatePath,
26
+ })}`}
27
+ >
28
+ {part}
29
+ </Link>
30
+ </div>
31
+ );
32
+ })
33
+ : [];
34
+
35
+ const breadcrumEls = [
36
+ <Link to="/app/treemap">Root</Link>,
37
+ bundleEl,
38
+ ...focusedGroup,
39
+ ];
40
+
41
+ return (
42
+ <div className={styles.focusBreadcrumbs}>
43
+ {breadcrumEls.flatMap((el, i) => [
44
+ <div key={i}>{el}</div>,
45
+ i < breadcrumEls.length - 1 && <div key={i + '-separator'}>&gt;</div>,
46
+ ])}
47
+ </div>
48
+ );
49
+ });
@@ -0,0 +1,5 @@
1
+ .expander {
2
+ height: 100%;
3
+ width: 100%;
4
+ flex: 1;
5
+ }
@@ -0,0 +1,3 @@
1
+ export const __esModule: true;
2
+ export const expander: string;
3
+
@@ -0,0 +1,80 @@
1
+ import Graphology from 'graphology';
2
+ import {useRef} from 'react';
3
+ import forceAtlas2 from 'graphology-layout-forceatlas2';
4
+ import FA2Layout from 'graphology-layout-forceatlas2/worker';
5
+ import {useEffect} from 'react';
6
+ import Sigma from 'sigma';
7
+
8
+ import {Graph} from '../../../types/Graph';
9
+ import * as styles from './SigmaGraph.module.css';
10
+
11
+ /**
12
+ * Renders `Graph` visualisation using Sigma.js.
13
+ */
14
+ export function SigmaGraph<T>({graph}: {graph: Graph<T>}) {
15
+ const visualizationRef = useRef<HTMLDivElement>(null);
16
+
17
+ useEffect(() => {
18
+ if (visualizationRef.current) {
19
+ const graphology = new Graphology();
20
+ const nodes = new Set<string>();
21
+ const edges = new Set<string>();
22
+ for (let node of graph.nodes) {
23
+ if (nodes.has(node.id)) {
24
+ continue;
25
+ }
26
+ nodes.add(node.id);
27
+
28
+ graphology.addNode(node.id, {
29
+ label: node.displayName,
30
+ x: Math.random() * 10000,
31
+ y: Math.random() * 10000,
32
+ size: 6,
33
+ });
34
+ }
35
+
36
+ for (let node of graph.nodes) {
37
+ for (let edge of node.edges) {
38
+ if (nodes.has(node.id) && nodes.has(edge)) {
39
+ if (edges.has(`${node.id} -> ${edge}`)) {
40
+ continue;
41
+ }
42
+
43
+ edges.add(`${node.id} -> ${edge}`);
44
+
45
+ graphology.addEdge(node.id, edge, {});
46
+ }
47
+ }
48
+ }
49
+
50
+ const sensibleSettings = forceAtlas2.inferSettings(graphology);
51
+ const fa2Layout = new FA2Layout(graphology, {
52
+ settings: {
53
+ ...sensibleSettings,
54
+ },
55
+ });
56
+ fa2Layout.start();
57
+
58
+ const renderer = new Sigma(graphology, visualizationRef.current, {
59
+ allowInvalidContainer: true,
60
+ defaultDrawNodeHover: () => {},
61
+ labelRenderedSizeThreshold: 0,
62
+ });
63
+
64
+ // TODO: Listen to enter/leave and highlight the rows
65
+ // renderer.on('enterNode', (e) => {
66
+ // console.log(e);
67
+ // });
68
+ // renderer.on('leaveNode', (e) => {
69
+ // console.log(e);
70
+ // });
71
+
72
+ return () => {
73
+ fa2Layout.stop();
74
+ renderer.kill();
75
+ };
76
+ }
77
+ }, [graph]);
78
+
79
+ return <div className={styles.expander} ref={visualizationRef} />;
80
+ }
@@ -0,0 +1,14 @@
1
+ export type AssetTreeNode = {
2
+ id: string;
3
+ children: Record<string, AssetTreeNode>;
4
+ size: number;
5
+ path: string;
6
+ };
7
+
8
+ export interface Bundle {
9
+ id: string;
10
+ size: number;
11
+ displayName: string;
12
+ filePath: string;
13
+ assetTree: AssetTreeNode;
14
+ }
@@ -0,0 +1,32 @@
1
+ .impactScore {
2
+ position: relative;
3
+ height: 30px;
4
+ border-radius: 4px;
5
+ background-color: var(--ds-surface-sunken);
6
+ border: 1px solid var(--ds-border);
7
+ overflow: hidden;
8
+ }
9
+
10
+ .impactScoreBar {
11
+ position: absolute;
12
+ border-radius: 4px;
13
+ left: 0;
14
+ top: 0;
15
+ bottom: 0;
16
+ opacity: 0.5;
17
+ background-color: green;
18
+ }
19
+
20
+ .impactScoreMessage {
21
+ position: absolute;
22
+ left: 0;
23
+ right: 0;
24
+ width: 100%;
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ height: 100%;
29
+ white-space: nowrap;
30
+ overflow: hidden;
31
+ text-overflow: ellipsis;
32
+ }
@@ -0,0 +1,5 @@
1
+ export const __esModule: true;
2
+ export const impactScore: string;
3
+ export const impactScoreBar: string;
4
+ export const impactScoreMessage: string;
5
+
@@ -0,0 +1,24 @@
1
+ import * as styles from './ImpactScore.module.css';
2
+
3
+ export function ImpactScore({
4
+ parentSize,
5
+ groupSize,
6
+ message,
7
+ }: {
8
+ parentSize: number;
9
+ groupSize: number;
10
+ message: string;
11
+ }) {
12
+ return (
13
+ <div className={styles.impactScore}>
14
+ <div
15
+ className={styles.impactScoreBar}
16
+ style={{width: `${Math.min(1, groupSize / parentSize) * 100}%`}}
17
+ />
18
+
19
+ <div className={styles.impactScoreMessage}>
20
+ <div>{message}</div>
21
+ </div>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,14 @@
1
+ .expander {
2
+ height: 100%;
3
+ width: 100%;
4
+ min-height: 100px;
5
+ min-width: 100px;
6
+ flex: 1;
7
+ }
8
+
9
+ .treemapRenderer {
10
+ height: 100%;
11
+ width: 100%;
12
+ flex: 1;
13
+ position: relative;
14
+ }
@@ -0,0 +1,4 @@
1
+ export const __esModule: true;
2
+ export const expander: string;
3
+ export const treemapRenderer: string;
4
+
@@ -0,0 +1,271 @@
1
+ import {useCallback, useEffect, useRef} from 'react';
2
+ // @ts-expect-error
3
+ import CarrotSearchFoamTree from '@carrotsearch/foamtree';
4
+ import {useSuspenseQuery} from '@tanstack/react-query';
5
+ import {AssetTreeNode, Bundle} from '../Treemap';
6
+ import {SetURLSearchParams, useSearchParams} from 'react-router';
7
+ import qs from 'qs';
8
+ import {autorun, runInAction} from 'mobx';
9
+ import {observer} from 'mobx-react-lite';
10
+ import {BundleData, Group, viewModel} from '../../../../model/ViewModel';
11
+ import {TreemapTooltip} from './TreemapTooltip';
12
+ import * as styles from './TreemapRenderer.module.css';
13
+ import {useStableCallback} from './controllers/useStableCallback';
14
+ import {useMouseMoveController} from './useMouseMoveController';
15
+
16
+ function setup(
17
+ visualization: HTMLDivElement,
18
+ setSearchParams: SetURLSearchParams,
19
+ isDetailView: boolean,
20
+ maxLevels: number,
21
+ stacking: string,
22
+ ) {
23
+ // Foam Tree docs:
24
+ // https://get.carrotsearch.com/foamtree/demo/api/index.html
25
+ // Some options from Atlaspack 1 Visualizer:
26
+ // https://github.com/gregtillbrook/parcel-plugin-bundle-visualiser/blob/ca5440fc61c85e40e7abc220ad99e274c7c104c6/src/buildReportAssets/init.js#L4
27
+ // and Webpack Bundle Analyzer:
28
+ // https://github.com/webpack-contrib/webpack-bundle-analyzer/blob/4a232f0cf7bbfed907a5c554879edd5d6f4b48ce/client/components/Treemap.jsx
29
+ let foamtree = new CarrotSearchFoamTree({
30
+ element: visualization,
31
+ // dataObject: bundleData,
32
+ layout: 'squarified',
33
+ stacking,
34
+ pixelRatio: window.devicePixelRatio || 1,
35
+ maxGroups: Infinity,
36
+ groupLabelMinFontSize: 3,
37
+ maxGroupLevelsDrawn: maxLevels,
38
+ maxGroupLabelLevelsDrawn: maxLevels,
39
+ maxGroupLevelsAttached: maxLevels,
40
+ rolloutDuration: 0,
41
+ pullbackDuration: 0,
42
+ maxLabelSizeForTitleBar: 0, // disable the title bar
43
+ onGroupHover(e: {group: Group; xAbsolute: number; yAbsolute: number}) {
44
+ runInAction(() => {
45
+ if (
46
+ e.group == null ||
47
+ e.group.label == null ||
48
+ e.group.weight == null
49
+ ) {
50
+ viewModel.tooltipState = null;
51
+ return;
52
+ }
53
+
54
+ viewModel.tooltipState = {
55
+ group: e.group,
56
+ };
57
+ });
58
+ },
59
+ onGroupClick(e: {group: Group}) {
60
+ if (!isDetailView) {
61
+ if (e.group.type === 'bundle') {
62
+ setSearchParams((prev) => {
63
+ prev.set('focusedBundleId', e.group.id);
64
+ prev.delete('focusedGroupId');
65
+ return prev;
66
+ });
67
+ }
68
+ } else if (e.group) {
69
+ const focusGroup = e.group;
70
+ this.open(focusGroup);
71
+ this.zoom(focusGroup);
72
+
73
+ setSearchParams((prev) => {
74
+ prev.set('focusedGroupId', focusGroup.id);
75
+ return prev;
76
+ });
77
+ }
78
+ },
79
+ onGroupDoubleClick(e: {group: Group}) {
80
+ this.zoom(e.group);
81
+
82
+ if (e.group.type === 'bundle') {
83
+ setSearchParams((prev) => {
84
+ prev.set('bundle', e.group.id);
85
+ return prev;
86
+ });
87
+ }
88
+ },
89
+ });
90
+
91
+ const onResize = debounce(() => {
92
+ foamtree.resize();
93
+ }, 100);
94
+
95
+ const resizeObserver = new ResizeObserver(onResize);
96
+ resizeObserver.observe(visualization);
97
+
98
+ function debounce(fn: (...args: any[]) => void, delay: number): () => void {
99
+ let timeout: NodeJS.Timeout | null = null;
100
+
101
+ return function (...args: any[]) {
102
+ if (timeout) {
103
+ clearTimeout(timeout);
104
+ }
105
+
106
+ timeout = setTimeout(() => {
107
+ fn(...args);
108
+ }, delay);
109
+ };
110
+ }
111
+
112
+ return {
113
+ foamtree,
114
+ cleanup: () => {
115
+ resizeObserver.disconnect();
116
+ foamtree.dispose();
117
+ },
118
+ };
119
+ }
120
+
121
+ function toBundleData(bundles: Array<Bundle>): BundleData {
122
+ function assetTreeToGroup(assetTree: AssetTreeNode): Group {
123
+ return {
124
+ id: assetTree.id,
125
+ type: 'asset',
126
+ label: assetTree.path,
127
+ weight: assetTree.size,
128
+ groups: Object.entries(assetTree.children).map(([_key, child]) =>
129
+ assetTreeToGroup(child),
130
+ ),
131
+ };
132
+ }
133
+
134
+ return {
135
+ groups: bundles.map((bundle) => {
136
+ return {
137
+ id: bundle.id,
138
+ type: 'bundle',
139
+ label: bundle.displayName,
140
+ weight: bundle.size,
141
+ assetTreeSize: bundle.assetTree.size,
142
+ groups: Object.entries(bundle.assetTree.children).map(([_key, child]) =>
143
+ assetTreeToGroup(child),
144
+ ),
145
+ };
146
+ }),
147
+ };
148
+ }
149
+
150
+ export const TreemapRenderer = observer(() => {
151
+ const [searchParams, setSearchParams] = useSearchParams();
152
+
153
+ const {data} = useSuspenseQuery<{
154
+ bundles: Array<Bundle>;
155
+ totalSize: number;
156
+ }>({
157
+ queryKey: [
158
+ '/api/treemap?' +
159
+ qs.stringify({
160
+ offset: searchParams.get('offset') ?? 0,
161
+ limit: searchParams.get('limit') ?? 10000,
162
+ bundle: searchParams.get('bundle') ?? undefined,
163
+ }),
164
+ ],
165
+ });
166
+
167
+ const visualizationRef = useRef<HTMLDivElement>(null);
168
+ const setSearchParamsMemo = useStableCallback(setSearchParams);
169
+ const bundle = searchParams.get('bundle');
170
+ const foamtreeRef = useRef<CarrotSearchFoamTree | null>(null);
171
+ const maxLevels = Number(searchParams.get('maxLevels') ?? Infinity);
172
+ const stacking = searchParams.get('stacking') ?? 'hierarchical';
173
+ useEffect(() => {
174
+ if (visualizationRef.current) {
175
+ const {cleanup, foamtree} = setup(
176
+ visualizationRef.current,
177
+ setSearchParamsMemo,
178
+ bundle != null,
179
+ maxLevels,
180
+ stacking,
181
+ );
182
+ foamtreeRef.current = foamtree;
183
+
184
+ return () => {
185
+ cleanup();
186
+ };
187
+ }
188
+ }, [bundle, setSearchParamsMemo, maxLevels, stacking]);
189
+
190
+ useEffect(() => {
191
+ return autorun(() => {
192
+ if (!foamtreeRef.current) {
193
+ return;
194
+ }
195
+
196
+ if (viewModel.relatedBundles) {
197
+ const relatedBundlesSet = new Set(
198
+ viewModel.relatedBundles.childBundles.map((b: any) => b.id),
199
+ );
200
+ const bundleData = toBundleData(
201
+ data.bundles.filter(
202
+ (group) =>
203
+ group.id === viewModel.focusedBundle?.id ||
204
+ relatedBundlesSet.has(group.id),
205
+ ),
206
+ );
207
+ runInAction(() => {
208
+ viewModel.data = bundleData;
209
+ });
210
+ foamtreeRef.current.set({
211
+ dataObject: bundleData,
212
+ });
213
+ return;
214
+ }
215
+
216
+ const bundleData = toBundleData(data.bundles);
217
+ runInAction(() => {
218
+ viewModel.data = bundleData;
219
+ });
220
+ if (data.bundles.length === 1) {
221
+ setSearchParams((prev) => {
222
+ prev.set('focusedBundleId', bundleData.groups[0].id);
223
+ return prev;
224
+ });
225
+ }
226
+ foamtreeRef.current.set({
227
+ dataObject: bundleData,
228
+ });
229
+ });
230
+ }, [data, setSearchParams]);
231
+
232
+ useEffect(() => {
233
+ return autorun(() => {
234
+ if (viewModel.hasDetails) {
235
+ return;
236
+ }
237
+
238
+ if (!viewModel.focusedBundle) {
239
+ return;
240
+ }
241
+ if (!viewModel.relatedBundles) {
242
+ return;
243
+ }
244
+
245
+ foamtreeRef.current?.expose([
246
+ viewModel.focusedBundle.id,
247
+ ...viewModel.relatedBundles.childBundles.map((b) => b.id),
248
+ ]);
249
+ });
250
+ }, []);
251
+
252
+ useMouseMoveController();
253
+
254
+ const onMouseLeave = useCallback(() => {
255
+ runInAction(() => {
256
+ viewModel.tooltipState = null;
257
+ });
258
+ }, []);
259
+
260
+ return (
261
+ <div
262
+ className={styles.treemapRenderer}
263
+ onMouseLeave={onMouseLeave}
264
+ onMouseOut={onMouseLeave}
265
+ >
266
+ <div ref={visualizationRef} className={styles.expander} />
267
+
268
+ <TreemapTooltip />
269
+ </div>
270
+ );
271
+ });
@@ -0,0 +1,15 @@
1
+ .treemapTooltip {
2
+ position: absolute;
3
+ background-color: var(--ds-background-input);
4
+ padding: 10px;
5
+ border-radius: 4px;
6
+ border: 1px solid var(--ds-border);
7
+ box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
8
+ z-index: 1000;
9
+ min-width: 400px;
10
+ }
11
+
12
+ .parent {
13
+ padding-left: 10px;
14
+ border-left: 3px solid var(--ds-border);
15
+ }
@@ -0,0 +1,4 @@
1
+ export const __esModule: true;
2
+ export const parent: string;
3
+ export const treemapTooltip: string;
4
+
@@ -0,0 +1,111 @@
1
+ import {observer} from 'mobx-react-lite';
2
+ import {viewModel} from '../../../../model/ViewModel';
3
+ import {formatBytes} from '../../../../util/formatBytes';
4
+ import * as styles from './TreemapTooltip.module.css';
5
+ import {Inline, Stack} from '@atlaskit/primitives';
6
+ import {Code} from '@atlaskit/code';
7
+ import {ImpactScore} from './ImpactScore';
8
+
9
+ export const TreemapTooltip = observer(() => {
10
+ const position = {
11
+ left: viewModel.mouseState.x + 30,
12
+ top: viewModel.mouseState.y,
13
+ };
14
+
15
+ if (!viewModel.tooltipState) {
16
+ return null;
17
+ }
18
+
19
+ const focusedGroup =
20
+ viewModel.focusedGroup?.id !== viewModel.tooltipState.group.id
21
+ ? viewModel.focusedGroup
22
+ : null;
23
+
24
+ const focusedBundle =
25
+ viewModel.focusedBundle?.id !== focusedGroup?.id
26
+ ? viewModel.focusedBundle
27
+ : null;
28
+
29
+ return (
30
+ <div
31
+ className={styles.treemapTooltip}
32
+ style={{
33
+ left: position.left,
34
+ top: position.top,
35
+ }}
36
+ >
37
+ <Stack space="space.100">
38
+ <Stack space="space.100">
39
+ <Inline space="space.100">
40
+ <strong>{viewModel.tooltipState.group.label}</strong>
41
+ <div>(group)</div>
42
+ </Inline>
43
+ <Inline space="space.100">
44
+ <div>Unminified size</div>
45
+ <Code>{formatBytes(viewModel.tooltipState.group.weight)}</Code>
46
+ </Inline>
47
+ </Stack>
48
+
49
+ {focusedGroup && (
50
+ <div className={styles.parent}>
51
+ <Stack space="space.100">
52
+ <Inline space="space.100">
53
+ <strong>{focusedGroup.label}</strong>
54
+ <div>(focused group)</div>
55
+ </Inline>
56
+
57
+ <Inline space="space.100">
58
+ <div>Unminified size</div>
59
+ <Code>
60
+ {formatBytes(
61
+ focusedGroup.assetTreeSize ?? focusedGroup.weight,
62
+ )}
63
+ </Code>
64
+ </Inline>
65
+
66
+ {focusedGroup.assetTreeSize != null && (
67
+ <Inline space="space.100">
68
+ <div>Output size on disk</div>
69
+ <Code>{formatBytes(focusedGroup.weight)}</Code>
70
+ </Inline>
71
+ )}
72
+
73
+ <ImpactScore
74
+ parentSize={focusedGroup.weight}
75
+ groupSize={viewModel.tooltipState.group.weight}
76
+ message={`${viewModel.tooltipState.group.label} is ${Math.round(Math.min(1, viewModel.tooltipState.group.weight / focusedGroup.weight) * 100)}% of ${focusedGroup.label}`}
77
+ />
78
+ </Stack>
79
+ </div>
80
+ )}
81
+
82
+ {focusedBundle && (
83
+ <div className={styles.parent}>
84
+ <Stack space="space.100">
85
+ <Inline space="space.100">
86
+ <strong>{focusedBundle.label}</strong>
87
+ <div>(focused bundle)</div>
88
+ </Inline>
89
+
90
+ <Inline space="space.100">
91
+ <div>Unminified size</div>
92
+ <Code>{formatBytes(focusedBundle.assetTreeSize ?? 0)}</Code>
93
+ </Inline>
94
+
95
+ <Inline space="space.100">
96
+ <div>Output size on disk</div>
97
+ <Code>{formatBytes(focusedBundle.weight)}</Code>
98
+ </Inline>
99
+
100
+ <ImpactScore
101
+ parentSize={focusedBundle.assetTreeSize ?? 0}
102
+ groupSize={viewModel.tooltipState.group.weight}
103
+ message={`${viewModel.tooltipState.group.label} is ${Math.round(Math.min(1, viewModel.tooltipState.group.weight / (focusedBundle.assetTreeSize ?? 0)) * 100)}% of ${focusedBundle.label}`}
104
+ />
105
+ </Stack>
106
+ </div>
107
+ )}
108
+ </Stack>
109
+ </div>
110
+ );
111
+ });
@@ -0,0 +1,27 @@
1
+ import {renderHook} from '@testing-library/react';
2
+ import {useStableCallback} from './useStableCallback';
3
+
4
+ describe('useStableCallback', () => {
5
+ it('should return a stable callback', () => {
6
+ const callback1 = jest.fn();
7
+ const {result, rerender} = renderHook(useStableCallback, {
8
+ initialProps: callback1,
9
+ });
10
+
11
+ expect(result.current).toBeInstanceOf(Function);
12
+
13
+ result.current();
14
+ result.current();
15
+ const previousRef = result.current;
16
+ expect(callback1).toHaveBeenCalledTimes(2);
17
+
18
+ const callback2 = jest.fn();
19
+ rerender(callback2);
20
+ expect(result.current).toBe(previousRef);
21
+
22
+ result.current();
23
+ result.current();
24
+ expect(callback1).toHaveBeenCalledTimes(2);
25
+ expect(callback2).toHaveBeenCalledTimes(2);
26
+ });
27
+ });
@@ -0,0 +1,21 @@
1
+ import {useCallback, useEffect, useRef} from 'react';
2
+
3
+ /**
4
+ * Like `useCallback` but the callback never changes even if the fn captures change.
5
+ *
6
+ * @example
7
+ *
8
+ * const onClick = useStableCallback(() => {
9
+ * console.log(props.value);
10
+ * });
11
+ *
12
+ */
13
+ export function useStableCallback(fn: (...args: any[]) => void) {
14
+ const ref = useRef(fn);
15
+
16
+ useEffect(() => {
17
+ ref.current = fn;
18
+ }, [fn]);
19
+
20
+ return useCallback((...args: any[]) => ref.current(...args), []);
21
+ }