@principal-ai/file-city-react 0.4.2 → 0.4.3
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.
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CityData } from '@principal-ai/file-city-builder';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a large number of file paths with realistic nested directory structures
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateLargeFilePaths(fileCount: number): string[];
|
|
6
|
+
/**
|
|
7
|
+
* Create CityData with a large number of files for stress testing subdirectory zoom
|
|
8
|
+
*
|
|
9
|
+
* @param fileCount - Number of files to generate (default: 8000)
|
|
10
|
+
* @param useCache - Whether to cache results (default: true)
|
|
11
|
+
*/
|
|
12
|
+
export declare function createStressTestCityData(fileCount?: number, useCache?: boolean): CityData;
|
|
13
|
+
/**
|
|
14
|
+
* Clear the stress test cache (useful for testing memory)
|
|
15
|
+
*/
|
|
16
|
+
export declare function clearStressTestCache(): void;
|
|
17
|
+
//# sourceMappingURL=stress-test-data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stress-test-data.d.ts","sourceRoot":"","sources":["../../src/stories/stress-test-data.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EAGT,MAAM,iCAAiC,CAAC;AA2CzC;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAoElE;AAoBD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,GAAE,MAAa,EAAE,QAAQ,GAAE,OAAc,GAAG,QAAQ,CAwBrG;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateLargeFilePaths = generateLargeFilePaths;
|
|
4
|
+
exports.createStressTestCityData = createStressTestCityData;
|
|
5
|
+
exports.clearStressTestCache = clearStressTestCache;
|
|
6
|
+
const file_city_builder_1 = require("@principal-ai/file-city-builder");
|
|
7
|
+
// Common file extensions for realistic distribution
|
|
8
|
+
const FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.css', '.json', '.md', '.test.ts', '.spec.tsx'];
|
|
9
|
+
// Top-level source directories
|
|
10
|
+
const TOP_LEVEL_DIRS = ['src', 'lib', 'packages', 'modules'];
|
|
11
|
+
// Second-level directories (domain areas)
|
|
12
|
+
const DOMAIN_DIRS = ['components', 'utils', 'services', 'hooks', 'types', 'helpers', 'core', 'api', 'features', 'pages'];
|
|
13
|
+
// Third-level directories (categories within domains)
|
|
14
|
+
const CATEGORY_DIRS = ['ui', 'forms', 'layout', 'navigation', 'data', 'auth', 'common', 'shared', 'internal', 'public'];
|
|
15
|
+
// Fourth-level directories (specific feature areas)
|
|
16
|
+
const FEATURE_DIRS = ['Button', 'Modal', 'Table', 'Card', 'Input', 'Select', 'Dialog', 'Menu', 'Tabs', 'List', 'Grid', 'Panel'];
|
|
17
|
+
// Fifth-level directories (variants/subfeatures)
|
|
18
|
+
const VARIANT_DIRS = ['variants', 'styles', 'hooks', 'utils', 'types', 'tests', '__tests__', 'stories', 'docs'];
|
|
19
|
+
// File name prefixes for realistic naming
|
|
20
|
+
const FILE_PREFIXES = ['index', 'main', 'handler', 'controller', 'service', 'helper', 'util', 'config', 'constants', 'types'];
|
|
21
|
+
/**
|
|
22
|
+
* Seeded random number generator for consistent results
|
|
23
|
+
*/
|
|
24
|
+
function seededRandom(seed) {
|
|
25
|
+
return () => {
|
|
26
|
+
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
|
27
|
+
return seed / 0x7fffffff;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate a large number of file paths with realistic nested directory structures
|
|
32
|
+
*/
|
|
33
|
+
function generateLargeFilePaths(fileCount) {
|
|
34
|
+
const files = [];
|
|
35
|
+
const random = seededRandom(42); // Consistent seed for reproducible results
|
|
36
|
+
for (let i = 0; i < fileCount; i++) {
|
|
37
|
+
const extension = FILE_EXTENSIONS[i % FILE_EXTENSIONS.length];
|
|
38
|
+
// Determine nesting depth (0-5 levels) with weighted distribution
|
|
39
|
+
// More files at medium depths (2-3), fewer at extremes
|
|
40
|
+
const depthRoll = random();
|
|
41
|
+
let depth;
|
|
42
|
+
if (depthRoll < 0.1)
|
|
43
|
+
depth = 1; // 10% at depth 1
|
|
44
|
+
else if (depthRoll < 0.25)
|
|
45
|
+
depth = 2; // 15% at depth 2
|
|
46
|
+
else if (depthRoll < 0.50)
|
|
47
|
+
depth = 3; // 25% at depth 3
|
|
48
|
+
else if (depthRoll < 0.75)
|
|
49
|
+
depth = 4; // 25% at depth 4
|
|
50
|
+
else if (depthRoll < 0.90)
|
|
51
|
+
depth = 5; // 15% at depth 5
|
|
52
|
+
else
|
|
53
|
+
depth = 6; // 10% at depth 6
|
|
54
|
+
const pathParts = [];
|
|
55
|
+
// Level 1: Top-level directory
|
|
56
|
+
// Weight 'src' heavily so it contains ~50% of files (for stress testing large subdirectory zoom)
|
|
57
|
+
const topLevelRoll = random();
|
|
58
|
+
if (topLevelRoll < 0.5) {
|
|
59
|
+
pathParts.push('src'); // 50% of files in src
|
|
60
|
+
}
|
|
61
|
+
else if (topLevelRoll < 0.7) {
|
|
62
|
+
pathParts.push('lib'); // 20% in lib
|
|
63
|
+
}
|
|
64
|
+
else if (topLevelRoll < 0.85) {
|
|
65
|
+
pathParts.push('packages'); // 15% in packages
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
pathParts.push('modules'); // 15% in modules
|
|
69
|
+
}
|
|
70
|
+
// Level 2: Domain directory
|
|
71
|
+
if (depth >= 2) {
|
|
72
|
+
pathParts.push(DOMAIN_DIRS[Math.floor(random() * DOMAIN_DIRS.length)]);
|
|
73
|
+
}
|
|
74
|
+
// Level 3: Category directory
|
|
75
|
+
if (depth >= 3) {
|
|
76
|
+
pathParts.push(CATEGORY_DIRS[Math.floor(random() * CATEGORY_DIRS.length)]);
|
|
77
|
+
}
|
|
78
|
+
// Level 4: Feature directory
|
|
79
|
+
if (depth >= 4) {
|
|
80
|
+
pathParts.push(FEATURE_DIRS[Math.floor(random() * FEATURE_DIRS.length)]);
|
|
81
|
+
}
|
|
82
|
+
// Level 5: Variant directory
|
|
83
|
+
if (depth >= 5) {
|
|
84
|
+
pathParts.push(VARIANT_DIRS[Math.floor(random() * VARIANT_DIRS.length)]);
|
|
85
|
+
}
|
|
86
|
+
// Level 6: Additional nesting for very deep files
|
|
87
|
+
if (depth >= 6) {
|
|
88
|
+
pathParts.push(`nested${Math.floor(random() * 5)}`);
|
|
89
|
+
}
|
|
90
|
+
// Generate filename
|
|
91
|
+
const prefix = FILE_PREFIXES[Math.floor(random() * FILE_PREFIXES.length)];
|
|
92
|
+
const suffix = Math.floor(i / 10); // Group files by suffix number
|
|
93
|
+
const fileName = `${prefix}${suffix}${extension}`;
|
|
94
|
+
pathParts.push(fileName);
|
|
95
|
+
files.push(pathParts.join('/'));
|
|
96
|
+
}
|
|
97
|
+
return files;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Convert file paths to FileInfo objects
|
|
101
|
+
*/
|
|
102
|
+
function createFileInfoList(paths) {
|
|
103
|
+
return paths.map(path => ({
|
|
104
|
+
name: path.split('/').pop() || path,
|
|
105
|
+
path: path,
|
|
106
|
+
relativePath: path,
|
|
107
|
+
size: 500 + Math.floor(Math.random() * 5000), // 500-5500 bytes
|
|
108
|
+
extension: path.includes('.') ? '.' + (path.split('.').pop() || '') : '',
|
|
109
|
+
lastModified: new Date(),
|
|
110
|
+
isDirectory: false,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
// Cache for stress test data to avoid regenerating
|
|
114
|
+
const stressTestCache = new Map();
|
|
115
|
+
/**
|
|
116
|
+
* Create CityData with a large number of files for stress testing subdirectory zoom
|
|
117
|
+
*
|
|
118
|
+
* @param fileCount - Number of files to generate (default: 8000)
|
|
119
|
+
* @param useCache - Whether to cache results (default: true)
|
|
120
|
+
*/
|
|
121
|
+
function createStressTestCityData(fileCount = 8000, useCache = true) {
|
|
122
|
+
if (useCache && stressTestCache.has(fileCount)) {
|
|
123
|
+
return stressTestCache.get(fileCount);
|
|
124
|
+
}
|
|
125
|
+
const filePaths = generateLargeFilePaths(fileCount);
|
|
126
|
+
const fileInfos = createFileInfoList(filePaths);
|
|
127
|
+
const fileTree = (0, file_city_builder_1.buildFileSystemTreeFromFileInfoList)(fileInfos, `stress-test-${fileCount}`);
|
|
128
|
+
const builder = new file_city_builder_1.CodeCityBuilderWithGrid();
|
|
129
|
+
const cityData = builder.buildCityFromFileSystem(fileTree, '', {
|
|
130
|
+
paddingTop: 2,
|
|
131
|
+
paddingBottom: 2,
|
|
132
|
+
paddingLeft: 2,
|
|
133
|
+
paddingRight: 2,
|
|
134
|
+
paddingInner: 1,
|
|
135
|
+
paddingOuter: 3,
|
|
136
|
+
});
|
|
137
|
+
if (useCache) {
|
|
138
|
+
stressTestCache.set(fileCount, cityData);
|
|
139
|
+
}
|
|
140
|
+
return cityData;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Clear the stress test cache (useful for testing memory)
|
|
144
|
+
*/
|
|
145
|
+
function clearStressTestCache() {
|
|
146
|
+
stressTestCache.clear();
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
|
|
4
|
+
import { ArchitectureMapHighlightLayers } from '../components/ArchitectureMapHighlightLayers';
|
|
5
|
+
import { HighlightLayer } from '../render/client/drawLayeredBuildings';
|
|
6
|
+
import { createStressTestCityData } from './stress-test-data';
|
|
7
|
+
|
|
8
|
+
// Wrapper component that regenerates cityData when fileCount changes
|
|
9
|
+
// and includes the click-to-zoom panel
|
|
10
|
+
function StressTestWrapper({
|
|
11
|
+
fileCount,
|
|
12
|
+
...props
|
|
13
|
+
}: {
|
|
14
|
+
fileCount: number;
|
|
15
|
+
} & Omit<React.ComponentProps<typeof ArchitectureMapHighlightLayers>, 'cityData'>) {
|
|
16
|
+
const [zoomToPath, setZoomToPath] = useState<string | null>(null);
|
|
17
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
18
|
+
|
|
19
|
+
const cityData = useMemo(() => {
|
|
20
|
+
console.log(`Generating city data for ${fileCount} files...`);
|
|
21
|
+
const start = performance.now();
|
|
22
|
+
const data = createStressTestCityData(fileCount, true);
|
|
23
|
+
const elapsed = performance.now() - start;
|
|
24
|
+
console.log(`Generated in ${elapsed.toFixed(2)}ms`);
|
|
25
|
+
return data;
|
|
26
|
+
}, [fileCount]);
|
|
27
|
+
|
|
28
|
+
// Get unique top-level directories for navigation buttons
|
|
29
|
+
const topLevelDirs = useMemo(() => {
|
|
30
|
+
return Array.from(
|
|
31
|
+
new Set(
|
|
32
|
+
cityData.districts
|
|
33
|
+
.map(d => d.path.split('/')[0])
|
|
34
|
+
.filter(Boolean),
|
|
35
|
+
),
|
|
36
|
+
).sort();
|
|
37
|
+
}, [cityData]);
|
|
38
|
+
|
|
39
|
+
// Get nested directories at different levels for navigation
|
|
40
|
+
const nestedDirs = useMemo(() => {
|
|
41
|
+
const allPaths = cityData.districts.map(d => d.path);
|
|
42
|
+
|
|
43
|
+
// Get depth-2 directories (e.g., src/components)
|
|
44
|
+
const depth2 = Array.from(
|
|
45
|
+
new Set(allPaths.filter(p => p.split('/').length === 2)),
|
|
46
|
+
).sort().slice(0, 6);
|
|
47
|
+
|
|
48
|
+
// Get depth-3 directories (e.g., src/components/ui)
|
|
49
|
+
const depth3 = Array.from(
|
|
50
|
+
new Set(allPaths.filter(p => p.split('/').length === 3)),
|
|
51
|
+
).sort().slice(0, 6);
|
|
52
|
+
|
|
53
|
+
// Get depth-4 directories (e.g., src/components/ui/Button)
|
|
54
|
+
const depth4 = Array.from(
|
|
55
|
+
new Set(allPaths.filter(p => p.split('/').length === 4)),
|
|
56
|
+
).sort().slice(0, 4);
|
|
57
|
+
|
|
58
|
+
return { depth2, depth3, depth4 };
|
|
59
|
+
}, [cityData]);
|
|
60
|
+
|
|
61
|
+
const handleZoomTo = (path: string | null) => {
|
|
62
|
+
setIsAnimating(true);
|
|
63
|
+
setZoomToPath(path);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleZoomComplete = () => {
|
|
67
|
+
setIsAnimating(false);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Create highlight layer for the focused directory
|
|
71
|
+
const highlightLayers: HighlightLayer[] = zoomToPath
|
|
72
|
+
? [
|
|
73
|
+
{
|
|
74
|
+
id: 'zoom-focus',
|
|
75
|
+
name: 'Zoom Focus',
|
|
76
|
+
enabled: true,
|
|
77
|
+
color: '#3b82f6',
|
|
78
|
+
priority: 1,
|
|
79
|
+
items: [{ path: zoomToPath, type: 'directory' }],
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
: [];
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
|
86
|
+
<ArchitectureMapHighlightLayers
|
|
87
|
+
cityData={cityData}
|
|
88
|
+
zoomToPath={zoomToPath}
|
|
89
|
+
onZoomComplete={handleZoomComplete}
|
|
90
|
+
zoomAnimationSpeed={0.1}
|
|
91
|
+
highlightLayers={highlightLayers}
|
|
92
|
+
onFileClick={(path, type) => {
|
|
93
|
+
if (type === 'directory') {
|
|
94
|
+
handleZoomTo(path);
|
|
95
|
+
}
|
|
96
|
+
}}
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
{/* Navigation Controls */}
|
|
101
|
+
<div
|
|
102
|
+
style={{
|
|
103
|
+
position: 'absolute',
|
|
104
|
+
top: 20,
|
|
105
|
+
left: 20,
|
|
106
|
+
zIndex: 100,
|
|
107
|
+
display: 'flex',
|
|
108
|
+
flexDirection: 'column',
|
|
109
|
+
gap: '8px',
|
|
110
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
111
|
+
padding: '16px',
|
|
112
|
+
borderRadius: '8px',
|
|
113
|
+
maxWidth: '220px',
|
|
114
|
+
maxHeight: 'calc(100vh - 100px)',
|
|
115
|
+
overflowY: 'auto',
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<div
|
|
119
|
+
style={{
|
|
120
|
+
color: '#3b82f6',
|
|
121
|
+
fontFamily: 'monospace',
|
|
122
|
+
fontSize: '12px',
|
|
123
|
+
fontWeight: 'bold',
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
Stress Test: {fileCount.toLocaleString()} files
|
|
127
|
+
</div>
|
|
128
|
+
<div
|
|
129
|
+
style={{
|
|
130
|
+
color: '#9ca3af',
|
|
131
|
+
fontFamily: 'monospace',
|
|
132
|
+
fontSize: '10px',
|
|
133
|
+
marginBottom: '8px',
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{isAnimating ? 'Animating...' : 'Click to zoom'}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Reset button */}
|
|
140
|
+
<button
|
|
141
|
+
onClick={() => handleZoomTo(null)}
|
|
142
|
+
disabled={isAnimating}
|
|
143
|
+
style={{
|
|
144
|
+
padding: '8px 12px',
|
|
145
|
+
backgroundColor: zoomToPath === null ? '#3b82f6' : '#374151',
|
|
146
|
+
color: 'white',
|
|
147
|
+
border: 'none',
|
|
148
|
+
borderRadius: '4px',
|
|
149
|
+
cursor: isAnimating ? 'not-allowed' : 'pointer',
|
|
150
|
+
fontFamily: 'monospace',
|
|
151
|
+
fontSize: '12px',
|
|
152
|
+
opacity: isAnimating ? 0.6 : 1,
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
Reset View
|
|
156
|
+
</button>
|
|
157
|
+
|
|
158
|
+
{/* Top-level directory buttons */}
|
|
159
|
+
{topLevelDirs.map(dir => (
|
|
160
|
+
<button
|
|
161
|
+
key={dir}
|
|
162
|
+
onClick={() => handleZoomTo(dir)}
|
|
163
|
+
disabled={isAnimating}
|
|
164
|
+
style={{
|
|
165
|
+
padding: '8px 12px',
|
|
166
|
+
backgroundColor: zoomToPath === dir ? '#3b82f6' : '#374151',
|
|
167
|
+
color: 'white',
|
|
168
|
+
border: 'none',
|
|
169
|
+
borderRadius: '4px',
|
|
170
|
+
cursor: isAnimating ? 'not-allowed' : 'pointer',
|
|
171
|
+
fontFamily: 'monospace',
|
|
172
|
+
fontSize: '12px',
|
|
173
|
+
textAlign: 'left',
|
|
174
|
+
opacity: isAnimating ? 0.6 : 1,
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
{dir}
|
|
178
|
+
</button>
|
|
179
|
+
))}
|
|
180
|
+
|
|
181
|
+
{/* Depth 2 directories (e.g., src/components) */}
|
|
182
|
+
{nestedDirs.depth2.length > 0 && (
|
|
183
|
+
<div
|
|
184
|
+
style={{
|
|
185
|
+
borderTop: '1px solid #4b5563',
|
|
186
|
+
paddingTop: '8px',
|
|
187
|
+
marginTop: '4px',
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
<div
|
|
191
|
+
style={{
|
|
192
|
+
color: '#9ca3af',
|
|
193
|
+
fontFamily: 'monospace',
|
|
194
|
+
fontSize: '10px',
|
|
195
|
+
marginBottom: '4px',
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
Depth 2:
|
|
199
|
+
</div>
|
|
200
|
+
{nestedDirs.depth2.map(dir => (
|
|
201
|
+
<button
|
|
202
|
+
key={dir}
|
|
203
|
+
onClick={() => handleZoomTo(dir)}
|
|
204
|
+
disabled={isAnimating}
|
|
205
|
+
style={{
|
|
206
|
+
padding: '6px 10px',
|
|
207
|
+
backgroundColor: zoomToPath === dir ? '#3b82f6' : '#1f2937',
|
|
208
|
+
color: 'white',
|
|
209
|
+
border: 'none',
|
|
210
|
+
borderRadius: '4px',
|
|
211
|
+
cursor: isAnimating ? 'not-allowed' : 'pointer',
|
|
212
|
+
fontFamily: 'monospace',
|
|
213
|
+
fontSize: '10px',
|
|
214
|
+
textAlign: 'left',
|
|
215
|
+
marginBottom: '4px',
|
|
216
|
+
width: '100%',
|
|
217
|
+
opacity: isAnimating ? 0.6 : 1,
|
|
218
|
+
overflow: 'hidden',
|
|
219
|
+
textOverflow: 'ellipsis',
|
|
220
|
+
whiteSpace: 'nowrap',
|
|
221
|
+
}}
|
|
222
|
+
title={dir}
|
|
223
|
+
>
|
|
224
|
+
{dir}
|
|
225
|
+
</button>
|
|
226
|
+
))}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{/* Depth 3 directories (e.g., src/components/ui) */}
|
|
231
|
+
{nestedDirs.depth3.length > 0 && (
|
|
232
|
+
<div
|
|
233
|
+
style={{
|
|
234
|
+
borderTop: '1px solid #4b5563',
|
|
235
|
+
paddingTop: '8px',
|
|
236
|
+
marginTop: '4px',
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
<div
|
|
240
|
+
style={{
|
|
241
|
+
color: '#9ca3af',
|
|
242
|
+
fontFamily: 'monospace',
|
|
243
|
+
fontSize: '10px',
|
|
244
|
+
marginBottom: '4px',
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
Depth 3:
|
|
248
|
+
</div>
|
|
249
|
+
{nestedDirs.depth3.map(dir => (
|
|
250
|
+
<button
|
|
251
|
+
key={dir}
|
|
252
|
+
onClick={() => handleZoomTo(dir)}
|
|
253
|
+
disabled={isAnimating}
|
|
254
|
+
style={{
|
|
255
|
+
padding: '6px 10px',
|
|
256
|
+
backgroundColor: zoomToPath === dir ? '#10b981' : '#1f2937',
|
|
257
|
+
color: 'white',
|
|
258
|
+
border: 'none',
|
|
259
|
+
borderRadius: '4px',
|
|
260
|
+
cursor: isAnimating ? 'not-allowed' : 'pointer',
|
|
261
|
+
fontFamily: 'monospace',
|
|
262
|
+
fontSize: '10px',
|
|
263
|
+
textAlign: 'left',
|
|
264
|
+
marginBottom: '4px',
|
|
265
|
+
width: '100%',
|
|
266
|
+
opacity: isAnimating ? 0.6 : 1,
|
|
267
|
+
overflow: 'hidden',
|
|
268
|
+
textOverflow: 'ellipsis',
|
|
269
|
+
whiteSpace: 'nowrap',
|
|
270
|
+
}}
|
|
271
|
+
title={dir}
|
|
272
|
+
>
|
|
273
|
+
{dir}
|
|
274
|
+
</button>
|
|
275
|
+
))}
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
|
|
279
|
+
{/* Depth 4 directories (e.g., src/components/ui/Button) */}
|
|
280
|
+
{nestedDirs.depth4.length > 0 && (
|
|
281
|
+
<div
|
|
282
|
+
style={{
|
|
283
|
+
borderTop: '1px solid #4b5563',
|
|
284
|
+
paddingTop: '8px',
|
|
285
|
+
marginTop: '4px',
|
|
286
|
+
}}
|
|
287
|
+
>
|
|
288
|
+
<div
|
|
289
|
+
style={{
|
|
290
|
+
color: '#9ca3af',
|
|
291
|
+
fontFamily: 'monospace',
|
|
292
|
+
fontSize: '10px',
|
|
293
|
+
marginBottom: '4px',
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
Depth 4:
|
|
297
|
+
</div>
|
|
298
|
+
{nestedDirs.depth4.map(dir => (
|
|
299
|
+
<button
|
|
300
|
+
key={dir}
|
|
301
|
+
onClick={() => handleZoomTo(dir)}
|
|
302
|
+
disabled={isAnimating}
|
|
303
|
+
style={{
|
|
304
|
+
padding: '6px 10px',
|
|
305
|
+
backgroundColor: zoomToPath === dir ? '#f59e0b' : '#1f2937',
|
|
306
|
+
color: 'white',
|
|
307
|
+
border: 'none',
|
|
308
|
+
borderRadius: '4px',
|
|
309
|
+
cursor: isAnimating ? 'not-allowed' : 'pointer',
|
|
310
|
+
fontFamily: 'monospace',
|
|
311
|
+
fontSize: '10px',
|
|
312
|
+
textAlign: 'left',
|
|
313
|
+
marginBottom: '4px',
|
|
314
|
+
width: '100%',
|
|
315
|
+
opacity: isAnimating ? 0.6 : 1,
|
|
316
|
+
overflow: 'hidden',
|
|
317
|
+
textOverflow: 'ellipsis',
|
|
318
|
+
whiteSpace: 'nowrap',
|
|
319
|
+
}}
|
|
320
|
+
title={dir}
|
|
321
|
+
>
|
|
322
|
+
{dir}
|
|
323
|
+
</button>
|
|
324
|
+
))}
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
{/* Status info */}
|
|
330
|
+
<div
|
|
331
|
+
style={{
|
|
332
|
+
position: 'absolute',
|
|
333
|
+
bottom: 20,
|
|
334
|
+
left: 20,
|
|
335
|
+
zIndex: 100,
|
|
336
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
337
|
+
padding: '12px',
|
|
338
|
+
borderRadius: '8px',
|
|
339
|
+
color: 'white',
|
|
340
|
+
fontFamily: 'monospace',
|
|
341
|
+
fontSize: '11px',
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
<div>Zoomed to: {zoomToPath || '(root)'}</div>
|
|
345
|
+
<div style={{ color: '#9ca3af', marginTop: '4px' }}>
|
|
346
|
+
Click directories in the map or use buttons above
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const meta = {
|
|
354
|
+
title: 'Performance/Stress Test',
|
|
355
|
+
component: StressTestWrapper,
|
|
356
|
+
parameters: {
|
|
357
|
+
layout: 'fullscreen',
|
|
358
|
+
},
|
|
359
|
+
decorators: [
|
|
360
|
+
Story => (
|
|
361
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
362
|
+
<Story />
|
|
363
|
+
</div>
|
|
364
|
+
),
|
|
365
|
+
],
|
|
366
|
+
argTypes: {
|
|
367
|
+
fileCount: {
|
|
368
|
+
control: { type: 'number', min: 100, max: 20000, step: 100 },
|
|
369
|
+
description: 'Number of files to generate for stress testing',
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
} satisfies Meta<typeof StressTestWrapper>;
|
|
373
|
+
|
|
374
|
+
export default meta;
|
|
375
|
+
type Story = StoryObj<typeof meta>;
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Test subdirectory zoom with a large number of files.
|
|
379
|
+
* Use the fileCount control to adjust the number of files.
|
|
380
|
+
*
|
|
381
|
+
* Try clicking on directories to zoom in and test performance.
|
|
382
|
+
*/
|
|
383
|
+
export const SubdirectoryZoom: Story = {
|
|
384
|
+
args: {
|
|
385
|
+
fileCount: 8000,
|
|
386
|
+
enableZoom: true,
|
|
387
|
+
showGrid: true,
|
|
388
|
+
showFileNames: false,
|
|
389
|
+
fullSize: true,
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Smaller stress test starting point (1000 files)
|
|
395
|
+
*/
|
|
396
|
+
export const SmallStressTest: Story = {
|
|
397
|
+
args: {
|
|
398
|
+
fileCount: 1000,
|
|
399
|
+
enableZoom: true,
|
|
400
|
+
showGrid: true,
|
|
401
|
+
showFileNames: false,
|
|
402
|
+
fullSize: true,
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Medium stress test (5000 files)
|
|
408
|
+
*/
|
|
409
|
+
export const MediumStressTest: Story = {
|
|
410
|
+
args: {
|
|
411
|
+
fileCount: 5000,
|
|
412
|
+
enableZoom: true,
|
|
413
|
+
showGrid: true,
|
|
414
|
+
showFileNames: false,
|
|
415
|
+
fullSize: true,
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Large stress test (10000 files)
|
|
421
|
+
*/
|
|
422
|
+
export const LargeStressTest: Story = {
|
|
423
|
+
args: {
|
|
424
|
+
fileCount: 10000,
|
|
425
|
+
enableZoom: true,
|
|
426
|
+
showGrid: true,
|
|
427
|
+
showFileNames: false,
|
|
428
|
+
fullSize: true,
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Extreme stress test (20000 files) - may be slow!
|
|
434
|
+
*/
|
|
435
|
+
export const ExtremeStressTest: Story = {
|
|
436
|
+
args: {
|
|
437
|
+
fileCount: 20000,
|
|
438
|
+
enableZoom: true,
|
|
439
|
+
showGrid: true,
|
|
440
|
+
showFileNames: false,
|
|
441
|
+
fullSize: true,
|
|
442
|
+
},
|
|
443
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CityData,
|
|
3
|
+
CodeCityBuilderWithGrid,
|
|
4
|
+
buildFileSystemTreeFromFileInfoList,
|
|
5
|
+
} from '@principal-ai/file-city-builder';
|
|
6
|
+
|
|
7
|
+
interface FileInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
path: string;
|
|
10
|
+
relativePath: string;
|
|
11
|
+
size: number;
|
|
12
|
+
extension: string;
|
|
13
|
+
lastModified: Date;
|
|
14
|
+
isDirectory: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Common file extensions for realistic distribution
|
|
18
|
+
const FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.css', '.json', '.md', '.test.ts', '.spec.tsx'];
|
|
19
|
+
|
|
20
|
+
// Top-level source directories
|
|
21
|
+
const TOP_LEVEL_DIRS = ['src', 'lib', 'packages', 'modules'];
|
|
22
|
+
|
|
23
|
+
// Second-level directories (domain areas)
|
|
24
|
+
const DOMAIN_DIRS = ['components', 'utils', 'services', 'hooks', 'types', 'helpers', 'core', 'api', 'features', 'pages'];
|
|
25
|
+
|
|
26
|
+
// Third-level directories (categories within domains)
|
|
27
|
+
const CATEGORY_DIRS = ['ui', 'forms', 'layout', 'navigation', 'data', 'auth', 'common', 'shared', 'internal', 'public'];
|
|
28
|
+
|
|
29
|
+
// Fourth-level directories (specific feature areas)
|
|
30
|
+
const FEATURE_DIRS = ['Button', 'Modal', 'Table', 'Card', 'Input', 'Select', 'Dialog', 'Menu', 'Tabs', 'List', 'Grid', 'Panel'];
|
|
31
|
+
|
|
32
|
+
// Fifth-level directories (variants/subfeatures)
|
|
33
|
+
const VARIANT_DIRS = ['variants', 'styles', 'hooks', 'utils', 'types', 'tests', '__tests__', 'stories', 'docs'];
|
|
34
|
+
|
|
35
|
+
// File name prefixes for realistic naming
|
|
36
|
+
const FILE_PREFIXES = ['index', 'main', 'handler', 'controller', 'service', 'helper', 'util', 'config', 'constants', 'types'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Seeded random number generator for consistent results
|
|
40
|
+
*/
|
|
41
|
+
function seededRandom(seed: number): () => number {
|
|
42
|
+
return () => {
|
|
43
|
+
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
|
44
|
+
return seed / 0x7fffffff;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate a large number of file paths with realistic nested directory structures
|
|
50
|
+
*/
|
|
51
|
+
export function generateLargeFilePaths(fileCount: number): string[] {
|
|
52
|
+
const files: string[] = [];
|
|
53
|
+
const random = seededRandom(42); // Consistent seed for reproducible results
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < fileCount; i++) {
|
|
56
|
+
const extension = FILE_EXTENSIONS[i % FILE_EXTENSIONS.length];
|
|
57
|
+
|
|
58
|
+
// Determine nesting depth (0-5 levels) with weighted distribution
|
|
59
|
+
// More files at medium depths (2-3), fewer at extremes
|
|
60
|
+
const depthRoll = random();
|
|
61
|
+
let depth: number;
|
|
62
|
+
if (depthRoll < 0.1) depth = 1; // 10% at depth 1
|
|
63
|
+
else if (depthRoll < 0.25) depth = 2; // 15% at depth 2
|
|
64
|
+
else if (depthRoll < 0.50) depth = 3; // 25% at depth 3
|
|
65
|
+
else if (depthRoll < 0.75) depth = 4; // 25% at depth 4
|
|
66
|
+
else if (depthRoll < 0.90) depth = 5; // 15% at depth 5
|
|
67
|
+
else depth = 6; // 10% at depth 6
|
|
68
|
+
|
|
69
|
+
const pathParts: string[] = [];
|
|
70
|
+
|
|
71
|
+
// Level 1: Top-level directory
|
|
72
|
+
// Weight 'src' heavily so it contains ~50% of files (for stress testing large subdirectory zoom)
|
|
73
|
+
const topLevelRoll = random();
|
|
74
|
+
if (topLevelRoll < 0.5) {
|
|
75
|
+
pathParts.push('src'); // 50% of files in src
|
|
76
|
+
} else if (topLevelRoll < 0.7) {
|
|
77
|
+
pathParts.push('lib'); // 20% in lib
|
|
78
|
+
} else if (topLevelRoll < 0.85) {
|
|
79
|
+
pathParts.push('packages'); // 15% in packages
|
|
80
|
+
} else {
|
|
81
|
+
pathParts.push('modules'); // 15% in modules
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Level 2: Domain directory
|
|
85
|
+
if (depth >= 2) {
|
|
86
|
+
pathParts.push(DOMAIN_DIRS[Math.floor(random() * DOMAIN_DIRS.length)]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Level 3: Category directory
|
|
90
|
+
if (depth >= 3) {
|
|
91
|
+
pathParts.push(CATEGORY_DIRS[Math.floor(random() * CATEGORY_DIRS.length)]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Level 4: Feature directory
|
|
95
|
+
if (depth >= 4) {
|
|
96
|
+
pathParts.push(FEATURE_DIRS[Math.floor(random() * FEATURE_DIRS.length)]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Level 5: Variant directory
|
|
100
|
+
if (depth >= 5) {
|
|
101
|
+
pathParts.push(VARIANT_DIRS[Math.floor(random() * VARIANT_DIRS.length)]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Level 6: Additional nesting for very deep files
|
|
105
|
+
if (depth >= 6) {
|
|
106
|
+
pathParts.push(`nested${Math.floor(random() * 5)}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Generate filename
|
|
110
|
+
const prefix = FILE_PREFIXES[Math.floor(random() * FILE_PREFIXES.length)];
|
|
111
|
+
const suffix = Math.floor(i / 10); // Group files by suffix number
|
|
112
|
+
const fileName = `${prefix}${suffix}${extension}`;
|
|
113
|
+
|
|
114
|
+
pathParts.push(fileName);
|
|
115
|
+
files.push(pathParts.join('/'));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return files;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Convert file paths to FileInfo objects
|
|
123
|
+
*/
|
|
124
|
+
function createFileInfoList(paths: string[]): FileInfo[] {
|
|
125
|
+
return paths.map(path => ({
|
|
126
|
+
name: path.split('/').pop() || path,
|
|
127
|
+
path: path,
|
|
128
|
+
relativePath: path,
|
|
129
|
+
size: 500 + Math.floor(Math.random() * 5000), // 500-5500 bytes
|
|
130
|
+
extension: path.includes('.') ? '.' + (path.split('.').pop() || '') : '',
|
|
131
|
+
lastModified: new Date(),
|
|
132
|
+
isDirectory: false,
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Cache for stress test data to avoid regenerating
|
|
137
|
+
const stressTestCache = new Map<number, CityData>();
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create CityData with a large number of files for stress testing subdirectory zoom
|
|
141
|
+
*
|
|
142
|
+
* @param fileCount - Number of files to generate (default: 8000)
|
|
143
|
+
* @param useCache - Whether to cache results (default: true)
|
|
144
|
+
*/
|
|
145
|
+
export function createStressTestCityData(fileCount: number = 8000, useCache: boolean = true): CityData {
|
|
146
|
+
if (useCache && stressTestCache.has(fileCount)) {
|
|
147
|
+
return stressTestCache.get(fileCount)!;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const filePaths = generateLargeFilePaths(fileCount);
|
|
151
|
+
const fileInfos = createFileInfoList(filePaths);
|
|
152
|
+
const fileTree = buildFileSystemTreeFromFileInfoList(fileInfos as any, `stress-test-${fileCount}`);
|
|
153
|
+
|
|
154
|
+
const builder = new CodeCityBuilderWithGrid();
|
|
155
|
+
const cityData = builder.buildCityFromFileSystem(fileTree, '', {
|
|
156
|
+
paddingTop: 2,
|
|
157
|
+
paddingBottom: 2,
|
|
158
|
+
paddingLeft: 2,
|
|
159
|
+
paddingRight: 2,
|
|
160
|
+
paddingInner: 1,
|
|
161
|
+
paddingOuter: 3,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (useCache) {
|
|
165
|
+
stressTestCache.set(fileCount, cityData);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return cityData;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Clear the stress test cache (useful for testing memory)
|
|
173
|
+
*/
|
|
174
|
+
export function clearStressTestCache(): void {
|
|
175
|
+
stressTestCache.clear();
|
|
176
|
+
}
|