feat(docker): add endpoint filtering and streamline setup assistant (#6625)
* feat(docker): add endpoint filtering and setup assistant * fix(docker): simplify environment setup * test(docker): cover demo endpoint filtering * refactor(docker): use accordion for setup suggestions * fix(docker): use docker icon for setup assistant * refactor(docker): simplify assisted service setup * refactor(docker): reuse shared translations * fix(docker): address review feedback
This commit is contained in:
1 parent
315245e048
commit
150d38be62
24 files changed
+772
-409
No files matched your search
@@ -195,9 +195,24 @@ const mockContainers: {
|
||||
},
|
||||
];
|
||||
|
||||
const demoDockerEndpoint = {
|
||||
id: "demo",
|
||||
name: "Demo Docker",
|
||||
status: "available",
|
||||
kind: "docker",
|
||||
transport: "socket",
|
||||
capabilities: ["inventory"],
|
||||
source: "default",
|
||||
scope: "admin",
|
||||
} satisfies DockerEndpointStatus;
|
||||
|
||||
export const dockerContainersRequestHandler = createWidgetRequestHandler({
|
||||
async requestAsync() {
|
||||
async requestAsync({ endpointIds }: { endpointIds?: string[] }) {
|
||||
if (isDemoMode()) {
|
||||
const includesDemoEndpoint =
|
||||
endpointIds === undefined || endpointIds.length === 0 || endpointIds.includes("demo");
|
||||
if (!includesDemoEndpoint) return { containers: [], endpoints: [] };
|
||||
|
||||
return {
|
||||
containers: mockContainers.map((container) => ({
|
||||
...container,
|
||||
@@ -205,21 +220,10 @@ export const dockerContainersRequestHandler = createWidgetRequestHandler({
|
||||
endpointName: "Demo Docker",
|
||||
resourceId: `demo:${container.id}`,
|
||||
})),
|
||||
endpoints: [
|
||||
{
|
||||
id: "demo",
|
||||
name: "Demo Docker",
|
||||
status: "available",
|
||||
kind: "docker",
|
||||
transport: "socket",
|
||||
capabilities: ["inventory"],
|
||||
source: "default",
|
||||
scope: "admin",
|
||||
},
|
||||
] satisfies DockerEndpointStatus[],
|
||||
endpoints: [demoDockerEndpoint],
|
||||
};
|
||||
}
|
||||
return await getContainersWithStatsAsync();
|
||||
return await getContainersWithStatsAsync(dockerWidgetEndpointTimeoutMs, endpointIds);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -304,6 +308,8 @@ export const streamContainerLogsAsync = async (
|
||||
};
|
||||
|
||||
export const getDockerEndpointsAsync = (): DockerEndpointStatus[] => {
|
||||
if (isDemoMode()) return [demoDockerEndpoint];
|
||||
|
||||
const dockerInstances = DockerSingleton.getInstances();
|
||||
const initializationFailures = DockerSingleton.getInitializationFailures();
|
||||
|
||||
@@ -331,9 +337,18 @@ export const getDockerEndpointsAsync = (): DockerEndpointStatus[] => {
|
||||
];
|
||||
};
|
||||
|
||||
export async function getContainersWithStatsAsync(timeoutMs = dockerWidgetEndpointTimeoutMs) {
|
||||
const dockerInstances = DockerSingleton.getInstances();
|
||||
const initializationFailures = DockerSingleton.getInitializationFailures();
|
||||
export async function getContainersWithStatsAsync(
|
||||
timeoutMs = dockerWidgetEndpointTimeoutMs,
|
||||
endpointIds: string[] = [],
|
||||
) {
|
||||
const selectedEndpointIds = new Set(endpointIds);
|
||||
const includesAllEndpoints = selectedEndpointIds.size === 0;
|
||||
const dockerInstances = DockerSingleton.getInstances().filter(
|
||||
({ endpointId }) => includesAllEndpoints || selectedEndpointIds.has(endpointId),
|
||||
);
|
||||
const initializationFailures = DockerSingleton.getInitializationFailures().filter(
|
||||
({ descriptor }) => includesAllEndpoints || selectedEndpointIds.has(descriptor.id),
|
||||
);
|
||||
const results = await Promise.allSettled(
|
||||
dockerInstances.map(async ({ instance, host, endpointId, endpointName }) => {
|
||||
const instanceContainers = await withDockerTimeoutAsync(
|
||||
|
||||
@@ -7,6 +7,8 @@ import { DockerSingleton } from "@homarr/docker";
|
||||
import {
|
||||
calculateCpuUsage,
|
||||
calculateMemoryUsage,
|
||||
dockerContainersRequestHandler,
|
||||
getDockerEndpointsAsync,
|
||||
getContainersWithStatsAsync,
|
||||
hasDockerEndpointCapability,
|
||||
} from "../docker";
|
||||
@@ -127,6 +129,31 @@ describe("calculateMemoryUsage", () => {
|
||||
});
|
||||
|
||||
describe("getContainersWithStatsAsync", () => {
|
||||
test("queries only selected Docker endpoints and leaves an empty selection as all", async () => {
|
||||
const firstListContainers = vi.fn(async () => []);
|
||||
const secondListContainers = vi.fn(async () => []);
|
||||
vi.spyOn(DockerSingleton, "getInstances").mockReturnValue([
|
||||
createDockerInstance("first", firstListContainers),
|
||||
createDockerInstance("second", secondListContainers),
|
||||
] as never);
|
||||
vi.spyOn(DockerSingleton, "getInitializationFailures").mockReturnValue([]);
|
||||
|
||||
try {
|
||||
const selectedResult = await getContainersWithStatsAsync(50, ["second"]);
|
||||
|
||||
expect(firstListContainers).not.toHaveBeenCalled();
|
||||
expect(secondListContainers).toHaveBeenCalledOnce();
|
||||
expect(selectedResult.endpoints.map(({ id }) => id)).toEqual(["second"]);
|
||||
|
||||
await getContainersWithStatsAsync(50, []);
|
||||
|
||||
expect(firstListContainers).toHaveBeenCalledOnce();
|
||||
expect(secondListContainers).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
});
|
||||
|
||||
test("marks a timed-out endpoint unavailable without blocking healthy endpoints", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
@@ -211,14 +238,26 @@ describe("getContainersWithStatsAsync", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("demo capabilities match the advertised inventory-only endpoint", () => {
|
||||
test("demo mode advertises and filters the same inventory-only endpoint", async () => {
|
||||
const previousDemoMode = process.env.DEMO_MODE;
|
||||
process.env.DEMO_MODE = "true";
|
||||
try {
|
||||
expect(getDockerEndpointsAsync()).toEqual([
|
||||
expect.objectContaining({ id: "demo", name: "Demo Docker", capabilities: ["inventory"] }),
|
||||
]);
|
||||
|
||||
const selected = await dockerContainersRequestHandler.handler({ endpointIds: ["demo"] }).getDataAsync();
|
||||
expect(selected.data.endpoints).toEqual([expect.objectContaining({ id: "demo" })]);
|
||||
expect(selected.data.containers.length).toBeGreaterThan(0);
|
||||
|
||||
const excluded = await dockerContainersRequestHandler.handler({ endpointIds: ["other"] }).getDataAsync();
|
||||
expect(excluded.data).toEqual({ containers: [], endpoints: [] });
|
||||
|
||||
expect(hasDockerEndpointCapability("demo", "inventory")).toBe(true);
|
||||
expect(hasDockerEndpointCapability("demo", "logs")).toBe(false);
|
||||
expect(hasDockerEndpointCapability("other", "inventory")).toBe(false);
|
||||
} finally {
|
||||
dockerContainersRequestHandler.invalidateCache();
|
||||
if (previousDemoMode === undefined) delete process.env.DEMO_MODE;
|
||||
else process.env.DEMO_MODE = previousDemoMode;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user