mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-25 01:37:05 +00:00
feat(resources): show multi-stack usedByStacks on images (#1612)
Classify images with a deduped sorted stack reverse index, surface chips in the Images table and inspect sheet, and clear node-bound sheet selection on active-node change.
This commit is contained in:
@@ -431,6 +431,10 @@ describe('DockerController - pruneDanglingImages', () => {
|
||||
// ── getClassifiedResources ─────────────────────────────────────────────
|
||||
|
||||
describe('DockerController - getClassifiedResources', () => {
|
||||
beforeEach(() => {
|
||||
CacheService.getInstance().invalidate('project-name-map');
|
||||
});
|
||||
|
||||
it('classifies managed and unmanaged images', async () => {
|
||||
mockDocker.listImages.mockResolvedValue([
|
||||
{ Id: 'img1', RepoTags: ['nginx:latest'], Size: 100, Containers: 1 },
|
||||
@@ -450,12 +454,36 @@ describe('DockerController - getClassifiedResources', () => {
|
||||
const managed = result.images.find(i => i.Id === 'img1');
|
||||
expect(managed!.managedStatus).toBe('managed');
|
||||
expect(managed!.managedBy).toBe('my-stack');
|
||||
expect(managed!.usedByStacks).toEqual(['my-stack']);
|
||||
|
||||
const unmanaged = result.images.find(i => i.Id === 'img2');
|
||||
expect(unmanaged!.managedStatus).toBe('unmanaged');
|
||||
expect(unmanaged!.usedByStacks).toEqual([]);
|
||||
|
||||
const unused = result.images.find(i => i.Id === 'img3');
|
||||
expect(unused!.managedStatus).toBe('unused');
|
||||
expect(unused!.usedByStacks).toEqual([]);
|
||||
});
|
||||
|
||||
it('aggregates multi-stack usedByStacks with stable sort and managedBy first entry', async () => {
|
||||
mockDocker.listImages.mockResolvedValue([
|
||||
{ Id: 'img-shared', RepoTags: ['postgres:16'], Size: 100, Containers: 2 },
|
||||
]);
|
||||
mockDocker.listContainers.mockResolvedValue([
|
||||
{ ImageID: 'img-shared', Labels: { 'com.docker.compose.project': 'zeta' } },
|
||||
{ ImageID: 'img-shared', Labels: { 'com.docker.compose.project': 'alpha' } },
|
||||
{ ImageID: 'img-shared', Labels: { 'com.docker.compose.project': 'alpha' } },
|
||||
]);
|
||||
mockDocker.listVolumes.mockResolvedValue({ Volumes: [] });
|
||||
mockDocker.listNetworks.mockResolvedValue([]);
|
||||
|
||||
const dc = DockerController.getInstance(1);
|
||||
const result = await dc.getClassifiedResources(['alpha', 'zeta']);
|
||||
const img = result.images.find(i => i.Id === 'img-shared');
|
||||
|
||||
expect(img!.usedByStacks).toEqual(['alpha', 'zeta']);
|
||||
expect(img!.managedBy).toBe('alpha');
|
||||
expect(img!.managedStatus).toBe('managed');
|
||||
});
|
||||
|
||||
it('classifies system networks', async () => {
|
||||
|
||||
@@ -133,6 +133,9 @@ export interface ClassifiedImage {
|
||||
RepoTags: string[];
|
||||
Size: number;
|
||||
Containers: number;
|
||||
/** Deduped, localeCompare-sorted Sencho stacks that currently use this image. */
|
||||
usedByStacks: string[];
|
||||
/** First entry of usedByStacks, or null when unused / unmanaged-only. */
|
||||
managedBy: string | null;
|
||||
managedStatus: 'managed' | 'unmanaged' | 'unused';
|
||||
isSencho: boolean;
|
||||
@@ -482,29 +485,34 @@ class DockerController {
|
||||
const absDirToStack = DockerController.buildAbsDirMap(knownStackNames);
|
||||
const resolvedBase = path.resolve(COMPOSE_DIR);
|
||||
|
||||
// Build imageId → stack mapping using the full fallback resolution chain
|
||||
const imageToStack = new Map<string, string>();
|
||||
// Build imageId → set of Sencho stacks (multi-stack reverse index).
|
||||
const imageToStacks = new Map<string, Set<string>>();
|
||||
for (const c of allContainers as any[]) {
|
||||
if (!c.ImageID) continue;
|
||||
const stack = DockerController.resolveContainerStack(
|
||||
c.Labels, projectToStack, knownSet, absDirToStack, resolvedBase,
|
||||
);
|
||||
if (stack) imageToStack.set(c.ImageID, stack);
|
||||
if (!stack) continue;
|
||||
const set = imageToStacks.get(c.ImageID) ?? new Set<string>();
|
||||
set.add(stack);
|
||||
imageToStacks.set(c.ImageID, set);
|
||||
}
|
||||
|
||||
const selfIdentity = SelfIdentityService.getInstance();
|
||||
|
||||
const images: ClassifiedImage[] = this.validateApiData<any[]>(rawImages).map((img: any) => {
|
||||
const stack = imageToStack.get(img.Id) ?? null;
|
||||
const usedByStacks = [...(imageToStacks.get(img.Id) ?? [])].sort((a, b) => a.localeCompare(b));
|
||||
const managedBy = usedByStacks[0] ?? null;
|
||||
const managedStatus: ClassifiedImage['managedStatus'] =
|
||||
img.Containers === 0 ? 'unused' :
|
||||
stack ? 'managed' : 'unmanaged';
|
||||
managedBy ? 'managed' : 'unmanaged';
|
||||
return {
|
||||
Id: img.Id,
|
||||
RepoTags: img.RepoTags ?? [],
|
||||
Size: img.Size ?? 0,
|
||||
Containers: img.Containers ?? 0,
|
||||
managedBy: stack,
|
||||
usedByStacks,
|
||||
managedBy,
|
||||
managedStatus,
|
||||
isSencho: selfIdentity.isOwnImage(img.Id),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user