@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.
- package/.atlaspackrc +4 -0
- package/.eslintrc.json +19 -0
- package/CHANGELOG.md +12 -0
- package/dist/atlassian-dark-brand-refresh.91b786da.js +2 -0
- package/dist/atlassian-dark-brand-refresh.91b786da.js.map +1 -0
- package/dist/atlassian-dark-future.59ebadca.js +2 -0
- package/dist/atlassian-dark-future.59ebadca.js.map +1 -0
- package/dist/atlassian-dark-increased-contrast.ff6775f2.js +2 -0
- package/dist/atlassian-dark-increased-contrast.ff6775f2.js.map +1 -0
- package/dist/atlassian-dark.ad679134.js +2 -0
- package/dist/atlassian-dark.ad679134.js.map +1 -0
- package/dist/atlassian-legacy-dark.8aa27f7f.js +2 -0
- package/dist/atlassian-legacy-dark.8aa27f7f.js.map +1 -0
- package/dist/atlassian-legacy-light.2eb372ce.js +2 -0
- package/dist/atlassian-legacy-light.2eb372ce.js.map +1 -0
- package/dist/atlassian-light-brand-refresh.fadcab0a.js +2 -0
- package/dist/atlassian-light-brand-refresh.fadcab0a.js.map +1 -0
- package/dist/atlassian-light-future.612afe8a.js +2 -0
- package/dist/atlassian-light-future.612afe8a.js.map +1 -0
- package/dist/atlassian-light-increased-contrast.7161cd79.js +2 -0
- package/dist/atlassian-light-increased-contrast.7161cd79.js.map +1 -0
- package/dist/atlassian-light.bc343d4c.js +2 -0
- package/dist/atlassian-light.bc343d4c.js.map +1 -0
- package/dist/atlassian-shape.b92d69c0.js +2 -0
- package/dist/atlassian-shape.b92d69c0.js.map +1 -0
- package/dist/atlassian-spacing.60ddd8e7.js +2 -0
- package/dist/atlassian-spacing.60ddd8e7.js.map +1 -0
- package/dist/atlassian-typography-adg3.f88947f6.js +2 -0
- package/dist/atlassian-typography-adg3.f88947f6.js.map +1 -0
- package/dist/atlassian-typography-modernized.42016c51.js +2 -0
- package/dist/atlassian-typography-modernized.42016c51.js.map +1 -0
- package/dist/atlassian-typography-refreshed.ec0d111b.js +2 -0
- package/dist/atlassian-typography-refreshed.ec0d111b.js.map +1 -0
- package/dist/atlassian-typography.66d7e8f4.js +2 -0
- package/dist/atlassian-typography.66d7e8f4.js.map +1 -0
- package/dist/badge-light.7e55986a.png +0 -0
- package/dist/custom-theme.4680282a.js +2 -0
- package/dist/custom-theme.4680282a.js.map +1 -0
- package/dist/drag-handle.136830d3.js +2 -0
- package/dist/drag-handle.136830d3.js.map +1 -0
- package/dist/drag-handle.63bdb345.css +2 -0
- package/dist/drag-handle.63bdb345.css.map +1 -0
- package/dist/index.13289f53.js +28 -0
- package/dist/index.13289f53.js.map +1 -0
- package/dist/index.a41fafce.css +2 -0
- package/dist/index.a41fafce.css.map +1 -0
- package/dist/index.html +1 -0
- package/dist/index.runtime.3c39d71d.js +2 -0
- package/dist/index.runtime.3c39d71d.js.map +1 -0
- package/dist/refractor.2c1fd9a1.js +2 -0
- package/dist/refractor.2c1fd9a1.js.map +1 -0
- package/index.html +11 -0
- package/jest.config.js +16 -0
- package/package.json +64 -0
- package/src/APIError.test.ts +72 -0
- package/src/APIError.tsx +29 -0
- package/src/AppRoutes.tsx +56 -0
- package/src/hack-feature-flags.ts +6 -0
- package/src/main.tsx +50 -0
- package/src/test/stubCssModule.js +1 -0
- package/src/ui/App.module.css +122 -0
- package/src/ui/App.module.css.d.ts +8 -0
- package/src/ui/AppLayout/AppLayout.tsx +26 -0
- package/src/ui/AppLayout/SidebarNavigation/LinkItem.tsx +26 -0
- package/src/ui/AppLayout/SidebarNavigation/SidebarNavigation.tsx +45 -0
- package/src/ui/AppLayout/TopNavigation/Logo.module.css +12 -0
- package/src/ui/AppLayout/TopNavigation/Logo.module.css.d.ts +4 -0
- package/src/ui/AppLayout/TopNavigation/Logo.tsx +11 -0
- package/src/ui/AppLayout/TopNavigation/TopNavigation.module.css +14 -0
- package/src/ui/AppLayout/TopNavigation/TopNavigation.module.css.d.ts +3 -0
- package/src/ui/AppLayout/TopNavigation/TopNavigation.tsx +45 -0
- package/src/ui/AppLayout/TopNavigation/badge-light.png +0 -0
- package/src/ui/AppLayout/TopNavigation/logo-light.png +0 -0
- package/src/ui/DefaultLoadingIndicator/DefaultLoadingIndicator.module.css +9 -0
- package/src/ui/DefaultLoadingIndicator/DefaultLoadingIndicator.module.css.d.ts +3 -0
- package/src/ui/DefaultLoadingIndicator/DefaultLoadingIndicator.test.tsx +15 -0
- package/src/ui/DefaultLoadingIndicator/DefaultLoadingIndicator.tsx +14 -0
- package/src/ui/app/StatsPage.tsx +77 -0
- package/src/ui/app/cache/CacheKeysIndexPage.tsx +13 -0
- package/src/ui/app/cache/CacheKeysPage.module.css +11 -0
- package/src/ui/app/cache/CacheKeysPage.module.css.d.ts +4 -0
- package/src/ui/app/cache/CacheKeysPage.tsx +23 -0
- package/src/ui/app/cache/[key]/CacheValuePage.tsx +40 -0
- package/src/ui/app/cache/ui/CacheKeyList.module.css +40 -0
- package/src/ui/app/cache/ui/CacheKeyList.module.css.d.ts +7 -0
- package/src/ui/app/cache/ui/CacheKeyList.tsx +187 -0
- package/src/ui/app/cache-invalidation/CacheInvalidationPage.tsx +22 -0
- package/src/ui/app/cache-invalidation/[fileId]/CacheInvalidationFilePage.tsx +22 -0
- package/src/ui/app/cache-invalidation/ui/CacheFileList.module.css +40 -0
- package/src/ui/app/cache-invalidation/ui/CacheFileList.module.css.d.ts +7 -0
- package/src/ui/app/cache-invalidation/ui/CacheFileList.tsx +185 -0
- package/src/ui/app/treemap/BottomPanelResizeState.test.ts +25 -0
- package/src/ui/app/treemap/BottomPanelResizeState.tsx +48 -0
- package/src/ui/app/treemap/FoamTreemapPage.module.css +24 -0
- package/src/ui/app/treemap/FoamTreemapPage.module.css.d.ts +6 -0
- package/src/ui/app/treemap/FoamTreemapPage.tsx +47 -0
- package/src/ui/app/treemap/controllers/RelatedBundlesController.tsx +41 -0
- package/src/ui/app/treemap/controllers/UrlFocusController.tsx +33 -0
- package/src/ui/app/treemap/ui/BottomPanel/BottomPanel.module.css +24 -0
- package/src/ui/app/treemap/ui/BottomPanel/BottomPanel.module.css.d.ts +5 -0
- package/src/ui/app/treemap/ui/BottomPanel/BottomPanel.tsx +24 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AdvancedSettings.module.css +13 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AdvancedSettings.module.css.d.ts +5 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AdvancedSettings.tsx +53 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/AssetTable.tsx +135 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTable.module.css +7 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTable.module.css.d.ts +3 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTable.tsx +123 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTableModel.tsx +18 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTableRow.module.css +20 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTableRow.module.css.d.ts +6 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/CollapsibleTable/CollapsibleTableRow.tsx +79 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/getFileURL.test.ts +19 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/AssetTable/getFileURL.ts +24 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/FocusedGroupInfo.module.css +20 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/FocusedGroupInfo.module.css.d.ts +5 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/FocusedGroupInfo.tsx +42 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/FocusedGroupInfoInner.module.css +29 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/FocusedGroupInfoInner.module.css.d.ts +6 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/FocusedGroupInfoInner.tsx +107 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/GraphContainer.module.css +7 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/GraphContainer.module.css.d.ts +3 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/GraphContainer.tsx +20 -0
- package/src/ui/app/treemap/ui/BottomPanel/FocusedGroupInfo/SourceCodeURL.tsx +5 -0
- package/src/ui/app/treemap/ui/BundleGraphRenderer.module.css +13 -0
- package/src/ui/app/treemap/ui/BundleGraphRenderer.module.css.d.ts +4 -0
- package/src/ui/app/treemap/ui/BundleGraphRenderer.tsx +95 -0
- package/src/ui/app/treemap/ui/FocusBreadcrumbs/FocusBreadcrumbs.module.css +6 -0
- package/src/ui/app/treemap/ui/FocusBreadcrumbs/FocusBreadcrumbs.module.css.d.ts +3 -0
- package/src/ui/app/treemap/ui/FocusBreadcrumbs/FocusBreadcrumbs.tsx +49 -0
- package/src/ui/app/treemap/ui/SigmaGraph.module.css +5 -0
- package/src/ui/app/treemap/ui/SigmaGraph.module.css.d.ts +3 -0
- package/src/ui/app/treemap/ui/SigmaGraph.tsx +80 -0
- package/src/ui/app/treemap/ui/Treemap.tsx +14 -0
- package/src/ui/app/treemap/ui/TreemapRenderer/ImpactScore.module.css +32 -0
- package/src/ui/app/treemap/ui/TreemapRenderer/ImpactScore.module.css.d.ts +5 -0
- package/src/ui/app/treemap/ui/TreemapRenderer/ImpactScore.tsx +24 -0
- package/src/ui/app/treemap/ui/TreemapRenderer/TreemapRenderer.module.css +14 -0
- package/src/ui/app/treemap/ui/TreemapRenderer/TreemapRenderer.module.css.d.ts +4 -0
- package/src/ui/app/treemap/ui/TreemapRenderer/TreemapRenderer.tsx +271 -0
- package/src/ui/app/treemap/ui/TreemapRenderer/TreemapTooltip.module.css +15 -0
- package/src/ui/app/treemap/ui/TreemapRenderer/TreemapTooltip.module.css.d.ts +4 -0
- package/src/ui/app/treemap/ui/TreemapRenderer/TreemapTooltip.tsx +111 -0
- package/src/ui/app/treemap/ui/TreemapRenderer/controllers/useStableCallback.test.ts +27 -0
- package/src/ui/app/treemap/ui/TreemapRenderer/controllers/useStableCallback.ts +21 -0
- package/src/ui/app/treemap/ui/TreemapRenderer/useMouseMoveController.ts +20 -0
- package/src/ui/globals.css +26 -0
- package/src/ui/globals.css.d.ts +1 -0
- package/src/ui/globals.d.ts +9 -0
- package/src/ui/model/ViewModel.test.ts +31 -0
- package/src/ui/model/ViewModel.ts +62 -0
- package/src/ui/not-found/NotFoundPage.module.css +7 -0
- package/src/ui/not-found/NotFoundPage.module.css.d.ts +3 -0
- package/src/ui/not-found/NotFoundPage.tsx +9 -0
- package/src/ui/types/Graph.tsx +12 -0
- package/src/ui/util/ErrorBoundary.module.css +3 -0
- package/src/ui/util/ErrorBoundary.module.css.d.ts +3 -0
- package/src/ui/util/ErrorBoundary.test.tsx +65 -0
- package/src/ui/util/ErrorBoundary.tsx +75 -0
- package/src/ui/util/colorPalette.tsx +122 -0
- package/src/ui/util/formatBytes.test.ts +13 -0
- package/src/ui/util/formatBytes.tsx +9 -0
- package/src/ui/util/getRandomDarkerColor.tsx +31 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {useNavigate, useSearchParams} from 'react-router';
|
|
2
|
+
import {useVirtualizer} from '@tanstack/react-virtual';
|
|
3
|
+
import {useEffect, useMemo, useRef} from 'react';
|
|
4
|
+
import {useInfiniteQuery} from '@tanstack/react-query';
|
|
5
|
+
import qs from 'qs';
|
|
6
|
+
import axios from 'axios';
|
|
7
|
+
import Button, {LinkButton} from '@atlaskit/button/new';
|
|
8
|
+
import {Box} from '@atlaskit/primitives';
|
|
9
|
+
|
|
10
|
+
import * as styles from './CacheFileList.module.css';
|
|
11
|
+
import * as appStyles from '../../../App.module.css';
|
|
12
|
+
|
|
13
|
+
export function CacheFileList() {
|
|
14
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
15
|
+
const sortBy = searchParams.get('sortBy') || 'invalidationCount';
|
|
16
|
+
const setSortBy = (value: string) => {
|
|
17
|
+
searchParams.set('sortBy', value);
|
|
18
|
+
setSearchParams(searchParams);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
data: cacheFiles,
|
|
23
|
+
isLoading,
|
|
24
|
+
error,
|
|
25
|
+
fetchNextPage,
|
|
26
|
+
isFetchingNextPage,
|
|
27
|
+
hasNextPage,
|
|
28
|
+
} = useInfiniteQuery<{
|
|
29
|
+
files: {
|
|
30
|
+
id: string;
|
|
31
|
+
}[];
|
|
32
|
+
count: number;
|
|
33
|
+
hasNextPage: boolean;
|
|
34
|
+
nextPageCursor: string | null;
|
|
35
|
+
}>({
|
|
36
|
+
queryFn: async ({pageParam}) => {
|
|
37
|
+
const backendUrl = process.env.ATLASPACK_INSPECTOR_BACKEND_URL;
|
|
38
|
+
const {data} = await axios.get(
|
|
39
|
+
`${backendUrl}/api/cache-invalidation-files/?` +
|
|
40
|
+
qs.stringify({cursor: pageParam, sortBy}),
|
|
41
|
+
);
|
|
42
|
+
return data;
|
|
43
|
+
},
|
|
44
|
+
queryKey: ['/api/cache-invalidation-files/?' + qs.stringify({sortBy})],
|
|
45
|
+
initialPageParam: null,
|
|
46
|
+
getNextPageParam: (lastPage) => lastPage.nextPageCursor,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const allFiles = useMemo(
|
|
50
|
+
() => cacheFiles?.pages.flatMap((page) => page.files) ?? [],
|
|
51
|
+
[cacheFiles?.pages],
|
|
52
|
+
);
|
|
53
|
+
const lastPage = cacheFiles?.pages[cacheFiles.pages.length - 1];
|
|
54
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
55
|
+
const rowVirtualizer = useVirtualizer({
|
|
56
|
+
count: lastPage?.hasNextPage ? allFiles.length + 1 : allFiles.length,
|
|
57
|
+
getScrollElement: () => parentRef.current,
|
|
58
|
+
estimateSize: () => 35,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const virtualItems = rowVirtualizer.getVirtualItems();
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const lastVirtualItem = virtualItems[virtualItems.length - 1];
|
|
64
|
+
const loaderRowIsShown =
|
|
65
|
+
allFiles[lastVirtualItem?.index] == null && lastPage?.hasNextPage;
|
|
66
|
+
|
|
67
|
+
if (hasNextPage && loaderRowIsShown && !isFetchingNextPage) {
|
|
68
|
+
fetchNextPage();
|
|
69
|
+
}
|
|
70
|
+
}, [
|
|
71
|
+
allFiles,
|
|
72
|
+
allFiles.length,
|
|
73
|
+
fetchNextPage,
|
|
74
|
+
hasNextPage,
|
|
75
|
+
isFetchingNextPage,
|
|
76
|
+
lastPage,
|
|
77
|
+
lastPage?.hasNextPage,
|
|
78
|
+
virtualItems,
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
const navigate = useNavigate();
|
|
82
|
+
|
|
83
|
+
const renderItems = () => {
|
|
84
|
+
if (isLoading) {
|
|
85
|
+
return (
|
|
86
|
+
<div className={styles.cacheFilePlaceholderContainer}>Loading...</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (error) {
|
|
91
|
+
return (
|
|
92
|
+
<div className={styles.cacheFilePlaceholderContainer}>
|
|
93
|
+
Error: {error.message}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!allFiles.length) {
|
|
99
|
+
return (
|
|
100
|
+
<div className={styles.cacheFilePlaceholderContainer}>
|
|
101
|
+
No cache files
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className={styles.cacheFileListItemsContainer} ref={parentRef}>
|
|
108
|
+
<div
|
|
109
|
+
className={styles.cacheFileListItemsContainerInner}
|
|
110
|
+
style={{
|
|
111
|
+
height: `${rowVirtualizer.getTotalSize()}px`,
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{rowVirtualizer.getVirtualItems().map((virtualItem) => {
|
|
115
|
+
const file = allFiles[virtualItem.index];
|
|
116
|
+
const isLoaderRow = file == null && lastPage?.hasNextPage;
|
|
117
|
+
const rowStyle = {
|
|
118
|
+
position: 'absolute',
|
|
119
|
+
top: 0,
|
|
120
|
+
left: 0,
|
|
121
|
+
width: '100%',
|
|
122
|
+
height: `${virtualItem.size}px`,
|
|
123
|
+
transform: `translateY(${virtualItem.start}px)`,
|
|
124
|
+
} as const;
|
|
125
|
+
|
|
126
|
+
if (isLoaderRow) {
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
key="loader"
|
|
130
|
+
className={appStyles.sidebarItem}
|
|
131
|
+
style={rowStyle}
|
|
132
|
+
>
|
|
133
|
+
Loading...
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const href = `/app/cache-invalidation/${encodeURIComponent(
|
|
138
|
+
file.id,
|
|
139
|
+
)}?${searchParams.toString()}`;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div
|
|
143
|
+
key={virtualItem.index}
|
|
144
|
+
className={appStyles.sidebarItem}
|
|
145
|
+
style={rowStyle}
|
|
146
|
+
>
|
|
147
|
+
<LinkButton
|
|
148
|
+
appearance="subtle"
|
|
149
|
+
href={href}
|
|
150
|
+
onClick={(e) => {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
navigate(href);
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
{file.id}
|
|
156
|
+
</LinkButton>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
})}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className={styles.cacheFileList}>
|
|
167
|
+
<Box>
|
|
168
|
+
<Button
|
|
169
|
+
appearance="subtle"
|
|
170
|
+
onClick={() => {
|
|
171
|
+
if (sortBy === 'order') {
|
|
172
|
+
setSortBy('invalidationCount');
|
|
173
|
+
} else {
|
|
174
|
+
setSortBy('order');
|
|
175
|
+
}
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
Sorting by {sortBy}
|
|
179
|
+
</Button>
|
|
180
|
+
</Box>
|
|
181
|
+
|
|
182
|
+
<div className={styles.cacheFileListInner}>{renderItems()}</div>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {ViewModel} from '../../model/ViewModel';
|
|
2
|
+
import {BottomPanelResizeState} from './BottomPanelResizeState';
|
|
3
|
+
|
|
4
|
+
describe('BottomPanelResizeState', () => {
|
|
5
|
+
it('should resize the bottom panel', () => {
|
|
6
|
+
const viewModel = new ViewModel();
|
|
7
|
+
viewModel.bottomPanelHeight = 100;
|
|
8
|
+
const resizeState = new BottomPanelResizeState(viewModel);
|
|
9
|
+
|
|
10
|
+
resizeState.startResize({clientY: 100} as React.MouseEvent);
|
|
11
|
+
expect(viewModel.bottomPanelHeight).toBe(100);
|
|
12
|
+
|
|
13
|
+
resizeState.onMouseMove({clientY: 50} as MouseEvent);
|
|
14
|
+
expect(viewModel.bottomPanelHeight).toBe(150);
|
|
15
|
+
|
|
16
|
+
resizeState.onMouseMove({clientY: 30} as MouseEvent);
|
|
17
|
+
expect(viewModel.bottomPanelHeight).toBe(170);
|
|
18
|
+
|
|
19
|
+
resizeState.onMouseMove({clientY: 110} as MouseEvent);
|
|
20
|
+
expect(viewModel.bottomPanelHeight).toBe(90);
|
|
21
|
+
|
|
22
|
+
resizeState.stopResize();
|
|
23
|
+
expect(viewModel.bottomPanelHeight).toBe(90);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {makeAutoObservable} from 'mobx';
|
|
2
|
+
import {ViewModel} from '../../model/ViewModel';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* View model for bottom panel UI.
|
|
6
|
+
*/
|
|
7
|
+
export class BottomPanelResizeState {
|
|
8
|
+
isResizing = false;
|
|
9
|
+
/**
|
|
10
|
+
* Last known Y mouse coordinate
|
|
11
|
+
*/
|
|
12
|
+
lastMouseY = 0;
|
|
13
|
+
isHovering = false;
|
|
14
|
+
viewModel: ViewModel;
|
|
15
|
+
|
|
16
|
+
constructor(viewModel: ViewModel) {
|
|
17
|
+
this.viewModel = viewModel;
|
|
18
|
+
makeAutoObservable(this);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
startResize = (e: React.MouseEvent) => {
|
|
22
|
+
this.isResizing = true;
|
|
23
|
+
this.lastMouseY = e.clientY;
|
|
24
|
+
document.addEventListener('mousemove', this.onMouseMove);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
mouseEnter = () => {
|
|
28
|
+
this.isHovering = true;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
mouseLeave = () => {
|
|
32
|
+
this.isHovering = false;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
stopResize = () => {
|
|
36
|
+
this.isResizing = false;
|
|
37
|
+
document.removeEventListener('mousemove', this.onMouseMove);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
onMouseMove = (e: MouseEvent) => {
|
|
41
|
+
if (this.isResizing) {
|
|
42
|
+
const deltaY = this.lastMouseY - e.clientY;
|
|
43
|
+
this.viewModel.bottomPanelHeight =
|
|
44
|
+
this.viewModel.bottomPanelHeight + deltaY;
|
|
45
|
+
this.lastMouseY = e.clientY;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
.foamTreemapPage {
|
|
2
|
+
height: 100%;
|
|
3
|
+
width: 100%;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.foamTreemapPageRenderer {
|
|
9
|
+
flex: 1;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.foamTreemapPageBottomPanel {
|
|
13
|
+
border-top: 2px solid var(--ds-border);
|
|
14
|
+
position: relative;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.foamTreemapPageBottomPanelResizeHandle {
|
|
18
|
+
position: absolute;
|
|
19
|
+
top: 0;
|
|
20
|
+
left: 0;
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: 10px;
|
|
23
|
+
cursor: row-resize;
|
|
24
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {observer} from 'mobx-react-lite';
|
|
2
|
+
import {RelatedBundlesController} from './controllers/RelatedBundlesController';
|
|
3
|
+
import {BottomPanel} from './ui/BottomPanel/BottomPanel';
|
|
4
|
+
import {FocusBreadcrumbs} from './ui/FocusBreadcrumbs/FocusBreadcrumbs';
|
|
5
|
+
import {TreemapRenderer} from './ui/TreemapRenderer/TreemapRenderer';
|
|
6
|
+
import {UrlFocusController} from './controllers/UrlFocusController';
|
|
7
|
+
|
|
8
|
+
import * as styles from './FoamTreemapPage.module.css';
|
|
9
|
+
import {viewModel} from '../../model/ViewModel';
|
|
10
|
+
import {BottomPanelResizeState} from './BottomPanelResizeState';
|
|
11
|
+
|
|
12
|
+
const bottomPanelResizeState = new BottomPanelResizeState(viewModel);
|
|
13
|
+
|
|
14
|
+
export const FoamTreemapPage = observer(() => {
|
|
15
|
+
return (
|
|
16
|
+
<div className={styles.foamTreemapPage}>
|
|
17
|
+
<FocusBreadcrumbs />
|
|
18
|
+
|
|
19
|
+
<div className={styles.foamTreemapPageRenderer}>
|
|
20
|
+
<TreemapRenderer />
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div
|
|
24
|
+
className={styles.foamTreemapPageBottomPanel}
|
|
25
|
+
style={{
|
|
26
|
+
height: `${viewModel.bottomPanelHeight}px`,
|
|
27
|
+
borderColor: bottomPanelResizeState.isHovering
|
|
28
|
+
? 'var(--ds-border-accent-blue)'
|
|
29
|
+
: 'var(--ds-border)',
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<div
|
|
33
|
+
className={styles.foamTreemapPageBottomPanelResizeHandle}
|
|
34
|
+
onMouseDown={bottomPanelResizeState.startResize}
|
|
35
|
+
onMouseUp={bottomPanelResizeState.stopResize}
|
|
36
|
+
onMouseEnter={bottomPanelResizeState.mouseEnter}
|
|
37
|
+
onMouseLeave={bottomPanelResizeState.mouseLeave}
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
<BottomPanel />
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<RelatedBundlesController />
|
|
44
|
+
<UrlFocusController />
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {useSearchParams} from 'react-router';
|
|
2
|
+
import {useQuery} from '@tanstack/react-query';
|
|
3
|
+
import {observer} from 'mobx-react-lite';
|
|
4
|
+
import {useEffect} from 'react';
|
|
5
|
+
import {runInAction} from 'mobx';
|
|
6
|
+
import {RelatedBundles, viewModel} from '../../../model/ViewModel';
|
|
7
|
+
import qs from 'qs';
|
|
8
|
+
|
|
9
|
+
export const RelatedBundlesController = observer(() => {
|
|
10
|
+
const [searchParams] = useSearchParams();
|
|
11
|
+
const focusedBundleId = searchParams.get('focusedBundleId');
|
|
12
|
+
const {data} = useQuery<RelatedBundles>({
|
|
13
|
+
queryKey: [
|
|
14
|
+
'/api/bundle-graph/related-bundles?' +
|
|
15
|
+
qs.stringify({
|
|
16
|
+
bundle: focusedBundleId,
|
|
17
|
+
}),
|
|
18
|
+
],
|
|
19
|
+
enabled: focusedBundleId != null,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (data && data.childBundles.length > 0) {
|
|
24
|
+
runInAction(() => {
|
|
25
|
+
viewModel.relatedBundles = data;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}, [data]);
|
|
29
|
+
|
|
30
|
+
const searchParamsBundle = searchParams.get('bundle');
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (searchParamsBundle != null) {
|
|
33
|
+
runInAction(() => {
|
|
34
|
+
viewModel.relatedBundles = null;
|
|
35
|
+
viewModel.hasDetails = true;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}, [searchParamsBundle]);
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {observer} from 'mobx-react-lite';
|
|
2
|
+
import {useEffect} from 'react';
|
|
3
|
+
import {useSearchParams} from 'react-router';
|
|
4
|
+
import {viewModel} from '../../../model/ViewModel';
|
|
5
|
+
import {runInAction} from 'mobx';
|
|
6
|
+
|
|
7
|
+
export const UrlFocusController = observer(() => {
|
|
8
|
+
const [searchParams] = useSearchParams();
|
|
9
|
+
const focusedBundleId = searchParams.get('focusedBundleId');
|
|
10
|
+
const focusedGroupId = searchParams.get('focusedGroupId');
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (focusedBundleId !== viewModel.focusedBundle?.id) {
|
|
14
|
+
runInAction(() => {
|
|
15
|
+
viewModel.focusedBundle = focusedBundleId
|
|
16
|
+
? (viewModel.groupsById.get(focusedBundleId) ?? null)
|
|
17
|
+
: null;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}, [focusedBundleId]);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (focusedGroupId !== viewModel.focusedGroup?.id) {
|
|
24
|
+
runInAction(() => {
|
|
25
|
+
viewModel.focusedGroup = focusedGroupId
|
|
26
|
+
? (viewModel.groupsById.get(focusedGroupId) ?? null)
|
|
27
|
+
: null;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}, [focusedGroupId]);
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
.bottomPanel {
|
|
2
|
+
border-left: 1px solid var(--border-color);
|
|
3
|
+
height: 100%;
|
|
4
|
+
display: flex;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.bottomPanelInner {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: row;
|
|
10
|
+
gap: 8px;
|
|
11
|
+
padding: 8px;
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 100%;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.loadingIndicator {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: 16px;
|
|
22
|
+
height: 100%;
|
|
23
|
+
width: 100%;
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {Suspense} from 'react';
|
|
2
|
+
import Spinner from '@atlaskit/spinner';
|
|
3
|
+
import {FocusedGroupInfo} from './FocusedGroupInfo/FocusedGroupInfo';
|
|
4
|
+
|
|
5
|
+
import * as styles from './BottomPanel.module.css';
|
|
6
|
+
|
|
7
|
+
export function BottomPanel() {
|
|
8
|
+
return (
|
|
9
|
+
<div className={styles.bottomPanel} onClick={(e) => e.stopPropagation()}>
|
|
10
|
+
<div className={styles.bottomPanelInner}>
|
|
11
|
+
<Suspense
|
|
12
|
+
fallback={
|
|
13
|
+
<div className={styles.loadingIndicator}>
|
|
14
|
+
<Spinner size="large" />
|
|
15
|
+
<h2>Loading bundle graph data...</h2>
|
|
16
|
+
</div>
|
|
17
|
+
}
|
|
18
|
+
>
|
|
19
|
+
<FocusedGroupInfo />
|
|
20
|
+
</Suspense>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {useSearchParams} from 'react-router';
|
|
2
|
+
|
|
3
|
+
import * as styles from './AdvancedSettings.module.css';
|
|
4
|
+
|
|
5
|
+
export function AdvancedSettings() {
|
|
6
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
7
|
+
const bundle = searchParams.get('bundle');
|
|
8
|
+
const isDetailView = bundle != null;
|
|
9
|
+
const maxLevels = searchParams.get('maxLevels') ?? 0;
|
|
10
|
+
const stacking = searchParams.get('stacking') ?? 'hierarchical';
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className={styles.advancedSettings}>
|
|
14
|
+
<div className={styles.advancedSettingsInner}>
|
|
15
|
+
<label className={styles.advancedSettingsLabel}>
|
|
16
|
+
Max levels: {maxLevels}
|
|
17
|
+
</label>
|
|
18
|
+
|
|
19
|
+
<input
|
|
20
|
+
disabled={!isDetailView}
|
|
21
|
+
type="range"
|
|
22
|
+
min={0}
|
|
23
|
+
max={10}
|
|
24
|
+
value={maxLevels}
|
|
25
|
+
onChange={(e) =>
|
|
26
|
+
setSearchParams((prev) => {
|
|
27
|
+
prev.set('maxLevels', e.target.value);
|
|
28
|
+
return prev;
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className={styles.advancedSettingsInner}>
|
|
35
|
+
<label className={styles.advancedSettingsLabel}>Stacking</label>
|
|
36
|
+
|
|
37
|
+
<select
|
|
38
|
+
disabled={!isDetailView}
|
|
39
|
+
value={stacking}
|
|
40
|
+
onChange={(e) =>
|
|
41
|
+
setSearchParams((prev) => {
|
|
42
|
+
prev.set('stacking', e.target.value);
|
|
43
|
+
return prev;
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
>
|
|
47
|
+
<option value="hierarchical">Hierarchical</option>
|
|
48
|
+
<option value="flattened">Flattened</option>
|
|
49
|
+
</select>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {observer} from 'mobx-react-lite';
|
|
2
|
+
import {useMemo} from 'react';
|
|
3
|
+
import {makeAutoObservable} from 'mobx';
|
|
4
|
+
import {
|
|
5
|
+
CollapsibleTableModel,
|
|
6
|
+
CollapsibleTableNode,
|
|
7
|
+
} from './CollapsibleTable/CollapsibleTableModel';
|
|
8
|
+
import {CollapsibleTable} from './CollapsibleTable/CollapsibleTable';
|
|
9
|
+
import {SourceCodeURL} from '../SourceCodeURL';
|
|
10
|
+
import {getFileURL} from './getFileURL';
|
|
11
|
+
|
|
12
|
+
export const AssetTable = observer(
|
|
13
|
+
({
|
|
14
|
+
data,
|
|
15
|
+
isBottomUp,
|
|
16
|
+
}: {
|
|
17
|
+
data: {
|
|
18
|
+
relevantPaths: string[][];
|
|
19
|
+
sourceCodeURL: SourceCodeURL | null;
|
|
20
|
+
projectRoot: string;
|
|
21
|
+
repositoryRoot: string;
|
|
22
|
+
};
|
|
23
|
+
isBottomUp: boolean;
|
|
24
|
+
}) => {
|
|
25
|
+
const model: CollapsibleTableModel = useMemo(() => {
|
|
26
|
+
// this is horrible ; but let's just hope there aren't that many children
|
|
27
|
+
// anyway things won't perform properly in that case
|
|
28
|
+
const expanded = (node: any) => {
|
|
29
|
+
if (node.isExpanded) {
|
|
30
|
+
return [
|
|
31
|
+
node,
|
|
32
|
+
...node.children.flatMap((child: any) => expanded(child)),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
return [node];
|
|
36
|
+
};
|
|
37
|
+
const model: CollapsibleTableModel = makeAutoObservable({
|
|
38
|
+
nodes: [],
|
|
39
|
+
focusedNodeId: null,
|
|
40
|
+
get flatNodeList() {
|
|
41
|
+
return this.nodes.flatMap((node) => {
|
|
42
|
+
return expanded(node);
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (isBottomUp) {
|
|
48
|
+
const seenRoots = new Set();
|
|
49
|
+
for (let path of data.relevantPaths) {
|
|
50
|
+
const node = path.slice();
|
|
51
|
+
node.reverse();
|
|
52
|
+
|
|
53
|
+
if (seenRoots.has(node[0])) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
seenRoots.add(node[0]);
|
|
57
|
+
|
|
58
|
+
const root: CollapsibleTableNode = makeAutoObservable({
|
|
59
|
+
id: node[0],
|
|
60
|
+
path: node[0],
|
|
61
|
+
sourceCodeUrl: getFileURL(node[0], data.sourceCodeURL),
|
|
62
|
+
isExpanded: false,
|
|
63
|
+
children: [],
|
|
64
|
+
parent: null,
|
|
65
|
+
level: 0,
|
|
66
|
+
});
|
|
67
|
+
let current: CollapsibleTableNode = root;
|
|
68
|
+
|
|
69
|
+
for (let i = 1; i < node.length; i++) {
|
|
70
|
+
const newNode = makeAutoObservable({
|
|
71
|
+
id: current.id + '--->>>>' + node[i],
|
|
72
|
+
path: node[i],
|
|
73
|
+
sourceCodeUrl: getFileURL(node[i], data.sourceCodeURL),
|
|
74
|
+
isExpanded: false,
|
|
75
|
+
children: [],
|
|
76
|
+
parent: current.id,
|
|
77
|
+
level: i,
|
|
78
|
+
});
|
|
79
|
+
current.children.push(newNode);
|
|
80
|
+
current = newNode;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
model.nodes.push(root);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
const roots = new Map();
|
|
87
|
+
for (let path of data.relevantPaths) {
|
|
88
|
+
const node = path.slice();
|
|
89
|
+
|
|
90
|
+
if (!roots.has(node[0])) {
|
|
91
|
+
const root = makeAutoObservable({
|
|
92
|
+
id: node[0],
|
|
93
|
+
path: node[0],
|
|
94
|
+
sourceCodeUrl: getFileURL(node[0], data.sourceCodeURL),
|
|
95
|
+
isExpanded: false,
|
|
96
|
+
children: [] as any[],
|
|
97
|
+
parent: null,
|
|
98
|
+
level: 0,
|
|
99
|
+
});
|
|
100
|
+
roots.set(node[0], root);
|
|
101
|
+
model.nodes.push(root);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const root = roots.get(node[0]);
|
|
105
|
+
let current = root;
|
|
106
|
+
|
|
107
|
+
for (let i = 1; i < node.length; i++) {
|
|
108
|
+
const existingNode = current.children.find(
|
|
109
|
+
(child: any) => child.path === node[i],
|
|
110
|
+
);
|
|
111
|
+
if (existingNode) {
|
|
112
|
+
current = existingNode as any;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const newNode = makeAutoObservable({
|
|
117
|
+
id: current.id + '--->>>>' + node[i],
|
|
118
|
+
path: node[i],
|
|
119
|
+
isExpanded: false,
|
|
120
|
+
children: [],
|
|
121
|
+
parent: current.id,
|
|
122
|
+
level: i,
|
|
123
|
+
});
|
|
124
|
+
current.children.push(newNode);
|
|
125
|
+
current = newNode;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return model;
|
|
131
|
+
}, [data, isBottomUp]);
|
|
132
|
+
|
|
133
|
+
return <CollapsibleTable model={model} />;
|
|
134
|
+
},
|
|
135
|
+
);
|