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:
Thomas authored and GitHub committed 2026-08-19 08:45:07 +02:00
1 parent 315245e048
commit 150d38be62
24 files changed
+772 -409

No files matched your search

@@ -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;
}