diff --git a/packages/common/src/string.ts b/packages/common/src/string.ts index 75f757a78..178cb9a47 100644 --- a/packages/common/src/string.ts +++ b/packages/common/src/string.ts @@ -5,3 +5,26 @@ export const capitalize = (str: T) => { export const isNullOrWhitespace = (value: string | null): value is null => { return value == null || value.trim() === ""; }; + +export const bestMatch = >( + search: string, + options: TItem[], + by: (item: TItem) => string, +) => { + if (options.length === 0) return null; + + const normalizedSearch = search.toLowerCase().trim(); + + return options.reduce((best, current) => { + const currentMatchIndex = by(current).toLowerCase().indexOf(normalizedSearch); + + if (currentMatchIndex === -1) return best; + if (best === null) return current; + + const bestMatchIndex = by(best).toLowerCase().indexOf(normalizedSearch); + + if (currentMatchIndex < bestMatchIndex) return current; + if (by(current).length < by(best).length && currentMatchIndex === bestMatchIndex) return current; + return best; + }, null); +}; diff --git a/packages/common/src/test/string.spec.ts b/packages/common/src/test/string.spec.ts index df8b4461a..d1bc1f9d0 100644 --- a/packages/common/src/test/string.spec.ts +++ b/packages/common/src/test/string.spec.ts @@ -1,6 +1,6 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; -import { capitalize } from "../string"; +import { bestMatch, capitalize } from "../string"; const capitalizeTestCases = [ ["hello", "Hello"], @@ -12,8 +12,45 @@ const capitalizeTestCases = [ describe("capitalize should capitalize the first letter of a string", () => { capitalizeTestCases.forEach(([input, expected]) => { - it(`should capitalize ${input} to ${expected}`, () => { + test(`should capitalize ${input} to ${expected}`, () => { expect(capitalize(input)).toEqual(expected); }); }); }); + +describe("bestMatch should find the best match in an array of options", () => { + test("should find exact match for 'nginx'", () => { + const options = [ + { name: "my-nginx" }, + { name: "nginx-proxy" }, + { name: "nginx" }, + { name: "redis" }, + { name: "postgres" }, + ]; + + const result = bestMatch("nginx", options, ({ name }) => name); + + expect(result).toEqual({ name: "nginx" }); + }); + test("should find closest match for 'nginx' when exact match is not available", () => { + const options = [{ name: "redis" }, { name: "nginx-proxy" }, { name: "my-nginx" }, { name: "postgres" }]; + + const result = bestMatch("nginx", options, ({ name }) => name); + + expect(result).toEqual({ name: "nginx-proxy" }); + }); + test("should return null if no match is found", () => { + const options = [{ name: "redis" }, { name: "postgres" }]; + + const result = bestMatch("nginx", options, ({ name }) => name); + + expect(result).toBeNull(); + }); + test("should return null if options array is empty", () => { + const options: { name: string }[] = []; + + const result = bestMatch("nginx", options, ({ name }) => name); + + expect(result).toBeNull(); + }); +}); diff --git a/packages/request-handler/src/docker.ts b/packages/request-handler/src/docker.ts index 6610aba7e..a31eed56c 100644 --- a/packages/request-handler/src/docker.ts +++ b/packages/request-handler/src/docker.ts @@ -2,6 +2,7 @@ import dayjs from "dayjs"; import type { ContainerInfo, ContainerStats } from "dockerode"; import type Dockerode from "dockerode"; +import { bestMatch } from "@homarr/common"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { db, like, or } from "@homarr/db"; @@ -82,12 +83,7 @@ async function getContainersWithStatsAsync() { name: container.Names[0]?.split("/")[1] ?? "Unknown", host: container.instance, state: container.State as ContainerState, - iconUrl: - dbIcons.find((icon) => { - const extractedImage = extractImage(container); - if (!extractedImage) return false; - return icon.name.toLowerCase().includes(extractedImage.toLowerCase()); - })?.url ?? null, + iconUrl: bestMatch(extractImage(container), dbIcons, (icon) => icon.name)?.url ?? null, cpuUsage, memoryUsage, image: container.Image,