mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 18:57:09 +00:00
feat: sortable resource tables and richer dashboard stack-health columns (#1498)
Make the Resources Images, Volumes, and Networks tables sortable with a shared useTableSort hook and SortableTableHead, move the Images scan-history control into the Images tab header, and keep the network List/Topology toggle anchored with Create Network visible in both modes. Rework the dashboard Stack health table: drop the redundant Host column, add sortable Stack/Up/CPU/Mem headers, and add Source (local/git) and Port columns. The status endpoint now labels each stack with its git/local source, computed outside the cache so linking changes show immediately. Extract a reusable CreateNetworkDialog and add a create-network action to the stack-detail Networking tab.
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, vi, beforeEach } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import { setupTestDb, cleanupTestDb, TEST_USERNAME, TEST_PASSWORD } from './helpers/setupTestDb';
|
||||
import { GitSourceService } from '../services/GitSourceService';
|
||||
import type { PublicGitSource } from '../services/GitSourceService';
|
||||
|
||||
// ── Hoisted mocks (must come before importing the app) ─────────────────
|
||||
|
||||
@@ -219,6 +221,45 @@ describe('GET /api/stacks/statuses caching', () => {
|
||||
await request(app).get('/api/stacks/statuses').set('Cookie', authCookie);
|
||||
expect(mockGetStacks).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('labels each stack with its git/local source, computed outside the cache', async () => {
|
||||
mockGetStacks.mockResolvedValue(['web.yml', 'db.yml']);
|
||||
mockGetBulkStackStatuses.mockResolvedValue({
|
||||
web: { status: 'running' },
|
||||
db: { status: 'running' },
|
||||
});
|
||||
// Only `web` is linked to a Git source.
|
||||
const listSpy = vi
|
||||
.spyOn(GitSourceService.getInstance(), 'list')
|
||||
.mockReturnValue([{ stack_name: 'web' } as PublicGitSource]);
|
||||
|
||||
const first = await request(app).get('/api/stacks/statuses').set('Cookie', authCookie);
|
||||
expect(first.body['web.yml'].source).toBe('git');
|
||||
expect(first.body['db.yml'].source).toBe('local');
|
||||
|
||||
// Source is recomputed live even when the Docker-status payload is cached:
|
||||
// unlinking `web` flips it to local on the next request without a cache flush.
|
||||
listSpy.mockReturnValue([]);
|
||||
const second = await request(app).get('/api/stacks/statuses').set('Cookie', authCookie);
|
||||
expect(second.body['web.yml'].source).toBe('local');
|
||||
expect(mockGetBulkStackStatuses).toHaveBeenCalledTimes(1); // status portion served from cache
|
||||
|
||||
listSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('falls back to local labels (200, not 500) when the git-source lookup throws', async () => {
|
||||
mockGetStacks.mockResolvedValue(['web.yml']);
|
||||
mockGetBulkStackStatuses.mockResolvedValue({ web: { status: 'running' } });
|
||||
const listSpy = vi
|
||||
.spyOn(GitSourceService.getInstance(), 'list')
|
||||
.mockImplementation(() => { throw new Error('db locked'); });
|
||||
|
||||
const res = await request(app).get('/api/stacks/statuses').set('Cookie', authCookie);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body['web.yml'].source).toBe('local');
|
||||
|
||||
listSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// ── /api/system/cache-stats ────────────────────────────────────────────
|
||||
|
||||
@@ -252,7 +252,23 @@ stacksRouter.get('/statuses', async (req: Request, res: Response) => {
|
||||
return data;
|
||||
},
|
||||
);
|
||||
res.json(result);
|
||||
// Git-source labels are computed live, outside the cache, so linking or
|
||||
// unlinking a stack's Git source is reflected immediately. The Docker
|
||||
// status portion keeps its short TTL; only the cheap source label is fresh.
|
||||
// The label is cosmetic, so a lookup failure must not take down the primary
|
||||
// status payload: fall back to labeling everything 'local'.
|
||||
let gitStackNames = new Set<string>();
|
||||
try {
|
||||
gitStackNames = new Set(GitSourceService.getInstance().list().map((s) => s.stack_name));
|
||||
} catch (sourceError) {
|
||||
console.error('Failed to load git sources for status labels; defaulting to local:', sourceError);
|
||||
}
|
||||
const withSource: Record<string, BulkStackInfo & { source: 'local' | 'git' }> = {};
|
||||
for (const [stack, info] of Object.entries(result)) {
|
||||
const name = stack.replace(/\.(yml|yaml)$/, '');
|
||||
withSource[stack] = { ...info, source: gitStackNames.has(name) ? 'git' : 'local' };
|
||||
}
|
||||
res.json(withSource);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch stack statuses:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch stack statuses' });
|
||||
|
||||
Reference in New Issue
Block a user