mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-20 07:13:05 +00:00
feat(stack-files): cap directory listings at 1000 + add file-tree filter (#1208)
* feat(stack-files): cap directory listings at 1000 + add file-tree filter
The file-tree route returned every entry in a directory unbounded.
A logs/ or data/ subfolder with rotated artifacts could produce a
multi-megabyte response and a frontend cap at 500 entries silently
hid the rest with no way for the user to find a specific file.
The list route now caps the response at 1000 entries (the audit's
recommended bound), advertises the unfiltered total via
X-Total-Count, and sets X-Truncated when truncation happened. The
service exposes both a bare-array listStackDirectory (unchanged
contract for callers that just want the array) and a paginated
listStackDirectoryPage that returns {entries, total, truncated}.
The FileTree now offers a search input above the scroll area that
filters loaded entries by name (case-insensitive substring). Clearing
the filter restores the full listing. A non-matching filter shows a
short hint instead of an empty pane. The client-side MAX_ENTRIES
matches the server cap so a perfectly-sized directory never shows
the truncation hint.
* fix(stack-files): filter keeps parent dirs when loaded descendants match
The original filter applied per-render-level inside renderEntries, so a
parent directory whose name did not match was filtered out even when one
of its already-loaded children did. The match was then unreachable: the
parent had been removed from the visible list and its children never got
a chance to render.
Compute matching-descendant once per directory by walking the loaded
dirContents map (no extra fetch, bounded by what the user already
expanded). Keep ancestors of any match in the visible list. Auto-expand
those ancestors for the duration of the filter so the match comes into
view without a manual click on every parent.
Filter scope is still 'what is already loaded'; unexpanded subtrees do
not contribute to ancestor-keep until the user expands them. Two new
tests pin both behaviours.
This commit is contained in:
@@ -141,6 +141,41 @@ describe('GET /api/stacks/:stackName/files', () => {
|
||||
.set('Cookie', adminCookie);
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('truncates a directory with more than 1000 entries and advertises totals in headers', async () => {
|
||||
const subdir = path.join(stacksDir, STACK, 'huge');
|
||||
await fs.mkdir(subdir, { recursive: true });
|
||||
// Seed 1100 small files. Names sort lexicographically so we can pin the
|
||||
// truncation boundary by inspecting the last returned entry.
|
||||
const targetCount = 1100;
|
||||
for (let i = 0; i < targetCount; i++) {
|
||||
const name = `f${String(i).padStart(5, '0')}.txt`;
|
||||
await fs.writeFile(path.join(subdir, name), '');
|
||||
}
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/api/stacks/${STACK}/files`)
|
||||
.query({ path: 'huge' })
|
||||
.set('Cookie', adminCookie);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(Array.isArray(res.body)).toBe(true);
|
||||
expect(res.body.length).toBe(1000);
|
||||
expect(res.headers['x-total-count']).toBe(String(targetCount));
|
||||
expect(res.headers['x-returned-count']).toBe('1000');
|
||||
expect(res.headers['x-truncated']).toBe('true');
|
||||
|
||||
await fs.rm(subdir, { recursive: true, force: true });
|
||||
}, 30000);
|
||||
|
||||
it('does not set X-Truncated when the directory fits under the limit', async () => {
|
||||
const res = await request(app)
|
||||
.get(`/api/stacks/${STACK}/files`)
|
||||
.set('Cookie', adminCookie);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers['x-truncated']).toBeUndefined();
|
||||
expect(res.headers['x-total-count']).toBe(String(res.body.length));
|
||||
});
|
||||
});
|
||||
|
||||
// ── GET /:stackName/files/content ─────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user