From 7071d76c526f1b59a955abcc595894975d574218 Mon Sep 17 00:00:00 2001 From: Andre Silva <32734153+Aandree5@users.noreply.github.com> Date: Fri, 6 Jun 2025 18:59:46 +0100 Subject: [PATCH] feat(releases-widget): add `import from docker` functionality (#3130) --- packages/translation/src/lang/ca.json | 2 +- packages/translation/src/lang/cn.json | 2 +- packages/translation/src/lang/cs.json | 2 +- packages/translation/src/lang/da.json | 2 +- packages/translation/src/lang/de-CH.json | 2 +- packages/translation/src/lang/de.json | 2 +- packages/translation/src/lang/el.json | 2 +- packages/translation/src/lang/en-gb.json | 2 +- packages/translation/src/lang/en.json | 14 +- packages/translation/src/lang/es.json | 2 +- packages/translation/src/lang/et.json | 2 +- packages/translation/src/lang/fr.json | 2 +- packages/translation/src/lang/he.json | 2 +- packages/translation/src/lang/hr.json | 2 +- packages/translation/src/lang/hu.json | 2 +- packages/translation/src/lang/it.json | 2 +- packages/translation/src/lang/ja.json | 2 +- packages/translation/src/lang/ko.json | 2 +- packages/translation/src/lang/lt.json | 2 +- packages/translation/src/lang/lv.json | 2 +- packages/translation/src/lang/nl.json | 2 +- packages/translation/src/lang/no.json | 2 +- packages/translation/src/lang/pl.json | 2 +- packages/translation/src/lang/pt.json | 2 +- packages/translation/src/lang/ro.json | 2 +- packages/translation/src/lang/ru.json | 2 +- packages/translation/src/lang/sk.json | 2 +- packages/translation/src/lang/sl.json | 2 +- packages/translation/src/lang/sv.json | 2 +- packages/translation/src/lang/tr.json | 2 +- packages/translation/src/lang/uk.json | 2 +- packages/translation/src/lang/vi.json | 2 +- packages/translation/src/lang/zh.json | 2 +- ...widget-multiReleasesRepositories-input.tsx | 350 ++++++++++++++++-- packages/widgets/src/releases/component.tsx | 6 +- .../src/releases/releases-providers.ts | 17 +- .../src/releases/releases-repository.ts | 4 +- 37 files changed, 386 insertions(+), 69 deletions(-) diff --git a/packages/translation/src/lang/ca.json b/packages/translation/src/lang/ca.json index 8ac477747..9eb2b7ed7 100644 --- a/packages/translation/src/lang/ca.json +++ b/packages/translation/src/lang/ca.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/cn.json b/packages/translation/src/lang/cn.json index a10f16e85..4ee71e94e 100644 --- a/packages/translation/src/lang/cn.json +++ b/packages/translation/src/lang/cn.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/cs.json b/packages/translation/src/lang/cs.json index 3736de65d..336a33da1 100644 --- a/packages/translation/src/lang/cs.json +++ b/packages/translation/src/lang/cs.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/da.json b/packages/translation/src/lang/da.json index c2d6469a7..c47a494d8 100644 --- a/packages/translation/src/lang/da.json +++ b/packages/translation/src/lang/da.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "Repositories", - "addRRepository": { + "addRepository": { "label": "Tilføj repository" }, "provider": { diff --git a/packages/translation/src/lang/de-CH.json b/packages/translation/src/lang/de-CH.json index d4c75942e..09c0f43e0 100644 --- a/packages/translation/src/lang/de-CH.json +++ b/packages/translation/src/lang/de-CH.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/de.json b/packages/translation/src/lang/de.json index bd587f013..7d2e31827 100644 --- a/packages/translation/src/lang/de.json +++ b/packages/translation/src/lang/de.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "Repositories", - "addRRepository": { + "addRepository": { "label": "Repository hinzufügen" }, "provider": { diff --git a/packages/translation/src/lang/el.json b/packages/translation/src/lang/el.json index b19d2a903..f29b3d029 100644 --- a/packages/translation/src/lang/el.json +++ b/packages/translation/src/lang/el.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/en-gb.json b/packages/translation/src/lang/en-gb.json index 7240e20a9..1956235f1 100644 --- a/packages/translation/src/lang/en-gb.json +++ b/packages/translation/src/lang/en-gb.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 50239cbac..1534ea16e 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -2223,9 +2223,18 @@ }, "repositories": { "label": "Repositories", - "addRRepository": { + "addRepository": { "label": "Add repository" }, + "importRepositories": { + "label": "Import from docker", + "loading": "Loading docker images", + "noImagesFound": "No docker images found", + "listFoundImages": "List of found images", + "listAlreadyImportedImages": "List of already imported images", + "allImagesAlreadyImported": "All images already imported", + "onlyAdminCanImport": "Only administrators can import from docker" + }, "provider": { "label": "Provider" }, @@ -2266,6 +2275,9 @@ "label": "Confirm" } }, + "importForm": { + "title": "Import from Docker" + }, "example": { "label": "Example" }, diff --git a/packages/translation/src/lang/es.json b/packages/translation/src/lang/es.json index 33a8a16d1..89361c63a 100644 --- a/packages/translation/src/lang/es.json +++ b/packages/translation/src/lang/es.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/et.json b/packages/translation/src/lang/et.json index 41bd93b6e..3d9369e2d 100644 --- a/packages/translation/src/lang/et.json +++ b/packages/translation/src/lang/et.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/fr.json b/packages/translation/src/lang/fr.json index cbeefa0dd..0b6b678cf 100644 --- a/packages/translation/src/lang/fr.json +++ b/packages/translation/src/lang/fr.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/he.json b/packages/translation/src/lang/he.json index 9d35cb5b3..fcd660792 100644 --- a/packages/translation/src/lang/he.json +++ b/packages/translation/src/lang/he.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "מאגרים", - "addRRepository": { + "addRepository": { "label": "הוסף מאגר" }, "provider": { diff --git a/packages/translation/src/lang/hr.json b/packages/translation/src/lang/hr.json index cf8d876d8..c05ec2959 100644 --- a/packages/translation/src/lang/hr.json +++ b/packages/translation/src/lang/hr.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/hu.json b/packages/translation/src/lang/hu.json index 83f3f6010..08b458b18 100644 --- a/packages/translation/src/lang/hu.json +++ b/packages/translation/src/lang/hu.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/it.json b/packages/translation/src/lang/it.json index bacc5a02c..8a14b40b6 100644 --- a/packages/translation/src/lang/it.json +++ b/packages/translation/src/lang/it.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/ja.json b/packages/translation/src/lang/ja.json index 17b5e35af..589993a5f 100644 --- a/packages/translation/src/lang/ja.json +++ b/packages/translation/src/lang/ja.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/ko.json b/packages/translation/src/lang/ko.json index dd88b63dd..a054e9c6e 100644 --- a/packages/translation/src/lang/ko.json +++ b/packages/translation/src/lang/ko.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/lt.json b/packages/translation/src/lang/lt.json index d28a9d410..c600d9429 100644 --- a/packages/translation/src/lang/lt.json +++ b/packages/translation/src/lang/lt.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/lv.json b/packages/translation/src/lang/lv.json index e1b2b2932..3a4933dfd 100644 --- a/packages/translation/src/lang/lv.json +++ b/packages/translation/src/lang/lv.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/nl.json b/packages/translation/src/lang/nl.json index f50f0865f..3649eecce 100644 --- a/packages/translation/src/lang/nl.json +++ b/packages/translation/src/lang/nl.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/no.json b/packages/translation/src/lang/no.json index d2f803d86..24c9562b3 100644 --- a/packages/translation/src/lang/no.json +++ b/packages/translation/src/lang/no.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/pl.json b/packages/translation/src/lang/pl.json index b1b5e3c4b..5408bb567 100644 --- a/packages/translation/src/lang/pl.json +++ b/packages/translation/src/lang/pl.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/pt.json b/packages/translation/src/lang/pt.json index aca267d8a..e52753e11 100644 --- a/packages/translation/src/lang/pt.json +++ b/packages/translation/src/lang/pt.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/ro.json b/packages/translation/src/lang/ro.json index 34501246a..6314da301 100644 --- a/packages/translation/src/lang/ro.json +++ b/packages/translation/src/lang/ro.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/ru.json b/packages/translation/src/lang/ru.json index f903ac075..fb0f31c5f 100644 --- a/packages/translation/src/lang/ru.json +++ b/packages/translation/src/lang/ru.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/sk.json b/packages/translation/src/lang/sk.json index bbddc405e..fc08897de 100644 --- a/packages/translation/src/lang/sk.json +++ b/packages/translation/src/lang/sk.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/sl.json b/packages/translation/src/lang/sl.json index 7b6b779b9..bc3442cf2 100644 --- a/packages/translation/src/lang/sl.json +++ b/packages/translation/src/lang/sl.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/sv.json b/packages/translation/src/lang/sv.json index a1db32096..f9c675c20 100644 --- a/packages/translation/src/lang/sv.json +++ b/packages/translation/src/lang/sv.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/tr.json b/packages/translation/src/lang/tr.json index b48b1e00a..067073e96 100644 --- a/packages/translation/src/lang/tr.json +++ b/packages/translation/src/lang/tr.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "Depolar", - "addRRepository": { + "addRepository": { "label": "Depo Ekle" }, "provider": { diff --git a/packages/translation/src/lang/uk.json b/packages/translation/src/lang/uk.json index a3562f9e3..b8be66d2e 100644 --- a/packages/translation/src/lang/uk.json +++ b/packages/translation/src/lang/uk.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/vi.json b/packages/translation/src/lang/vi.json index f7f79efcd..4c09f5a6e 100644 --- a/packages/translation/src/lang/vi.json +++ b/packages/translation/src/lang/vi.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "", - "addRRepository": { + "addRepository": { "label": "" }, "provider": { diff --git a/packages/translation/src/lang/zh.json b/packages/translation/src/lang/zh.json index b36d01dfc..7e6531834 100644 --- a/packages/translation/src/lang/zh.json +++ b/packages/translation/src/lang/zh.json @@ -2223,7 +2223,7 @@ }, "repositories": { "label": "儲存庫", - "addRRepository": { + "addRepository": { "label": "新增儲存庫" }, "provider": { diff --git a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx index 24313c67e..22c9ad253 100644 --- a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx +++ b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx @@ -1,19 +1,46 @@ "use client"; import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { ActionIcon, Button, Divider, Fieldset, Group, Select, Stack, Text, TextInput } from "@mantine/core"; +import { + Accordion, + ActionIcon, + Button, + Checkbox, + Code, + Divider, + Fieldset, + Group, + Image, + Loader, + Select, + Stack, + Text, + TextInput, + Title, + Tooltip, +} from "@mantine/core"; +import type { CheckboxProps } from "@mantine/core"; import type { FormErrors } from "@mantine/form"; import { useDebouncedValue } from "@mantine/hooks"; -import { IconEdit, IconTrash, IconTriangleFilled } from "@tabler/icons-react"; +import { + IconBrandDocker, + IconEdit, + IconPlus, + IconSquare, + IconSquareCheck, + IconTrash, + IconTriangleFilled, +} from "@tabler/icons-react"; import { escapeForRegEx } from "@tiptap/react"; import { clientApi } from "@homarr/api/client"; +import { useSession } from "@homarr/auth/client"; import { findBestIconMatch, IconPicker } from "@homarr/forms-collection"; import { createModal, useModalAction } from "@homarr/modals"; import { useScopedI18n } from "@homarr/translation/client"; -import { MaskedOrNormalImage } from "@homarr/ui"; +import { MaskedImage } from "@homarr/ui"; -import { Providers } from "../releases/releases-providers"; +import { isProviderKey, Providers } from "../releases/releases-providers"; import type { ReleasesRepository, ReleasesVersionFilter } from "../releases/releases-repository"; import type { CommonWidgetInputProps } from "./common"; import { useWidgetInputTranslation } from "./common"; @@ -32,11 +59,14 @@ export const WidgetMultiReleasesRepositoriesInput = ({ const tRepository = useScopedI18n("widget.releases.option.repositories"); const form = useFormContext(); const repositories = form.values.options[property] as ReleasesRepository[]; - const { openModal } = useModalAction(ReleaseEditModal); + const { openModal: openEditModal } = useModalAction(RepositoryEditModal); + const { openModal: openImportModal } = useModalAction(RepositoryImportModal); const versionFilterPrecisionOptions = useMemo( () => [tRepository("versionFilter.precision.options.none"), "#", "#.#", "#.#.#", "#.#.#.#", "#.#.#.#.#"], [tRepository], ); + const { data: session } = useSession(); + const isAdmin = session?.user.permissions.includes("admin") ?? false; const onRepositorySave = useCallback( (repository: ReleasesRepository, index: number): FormValidation => { @@ -62,8 +92,8 @@ export const WidgetMultiReleasesRepositoriesInput = ({ [form, property], ); - const addNewItem = () => { - const item = { + const addNewRepository = () => { + const repository: ReleasesRepository = { providerKey: "DockerHub", identifier: "", }; @@ -74,16 +104,16 @@ export const WidgetMultiReleasesRepositoriesInput = ({ ...previous, options: { ...previous.options, - [property]: [...previousValues, item], + [property]: [...previousValues, repository], }, }; }); const index = repositories.length; - openModal({ + openEditModal({ fieldPath: `options.${property}.${index}`, - repository: item, + repository, onRepositorySave: (saved) => onRepositorySave(saved, index), onRepositoryCancel: () => onRepositoryRemove(index), versionFilterPrecisionOptions, @@ -106,24 +136,56 @@ export const WidgetMultiReleasesRepositoriesInput = ({ return (
- + + + + + + {repositories.map((repository, index) => { return ( - - {Providers[repository.providerKey]?.name} + {Providers[repository.providerKey].name} @@ -135,7 +197,7 @@ export const WidgetMultiReleasesRepositoriesInput = ({ - @@ -398,3 +460,247 @@ const ReleaseEditModal = createModal(({ innerProps, actions }) }, size: "xl", }); + +interface ReleasesRepositoryImport extends ReleasesRepository { + alreadyImported: boolean; +} + +interface ContainerImageSelectorProps { + containerImage: ReleasesRepositoryImport; + versionFilterPrecisionOptions: string[]; + onImageSelectionChanged?: (isSelected: boolean) => void; +} + +const ContainerImageSelector = ({ + containerImage, + versionFilterPrecisionOptions, + onImageSelectionChanged, +}: ContainerImageSelectorProps) => { + const tRepository = useScopedI18n("widget.releases.option.repositories"); + const checkBoxProps: CheckboxProps = !onImageSelectionChanged + ? { + disabled: true, + checked: true, + } + : { + onChange: (event) => onImageSelectionChanged(event.currentTarget.checked), + }; + + return ( + + + + + {containerImage.identifier} + + } + {...checkBoxProps} + /> + + {containerImage.versionFilter && ( + + + {tRepository("versionFilter.label")}: + + + {containerImage.versionFilter.prefix && containerImage.versionFilter.prefix} + + {versionFilterPrecisionOptions[containerImage.versionFilter.precision]} + + {containerImage.versionFilter.suffix && containerImage.versionFilter.suffix} + + )} + + + + + + {Providers[containerImage.providerKey].name} + + + + ); +}; + +interface RepositoryImportProps { + repositories: ReleasesRepository[]; + versionFilterPrecisionOptions: string[]; + onConfirm: (selectedRepositories: ReleasesRepositoryImport[]) => void; + isAdmin: boolean; +} + +const RepositoryImportModal = createModal(({ innerProps, actions }) => { + const tRepository = useScopedI18n("widget.releases.option.repositories"); + const [loading, setLoading] = useState(false); + const [selectedImages, setSelectedImages] = useState([] as ReleasesRepositoryImport[]); + + const docker = clientApi.docker.getContainers.useQuery(undefined, { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + enabled: innerProps.isAdmin, + }); + + const containersImages: ReleasesRepositoryImport[] = useMemo( + () => + docker.data?.containers.reduce((acc, containerImage) => { + const providerKey = containerImage.image.startsWith("ghcr.io/") ? "Github" : "DockerHub"; + const [identifier, version] = containerImage.image.replace(/^(ghcr\.io\/|docker\.io\/)/, "").split(":"); + + if (!identifier) return acc; + + if (acc.some((item) => item.providerKey === providerKey && item.identifier === identifier)) return acc; + + acc.push({ + providerKey, + identifier, + iconUrl: containerImage.iconUrl ?? undefined, + name: formatIdentifierName(identifier), + versionFilter: version ? parseImageVersionToVersionFilter(version) : undefined, + alreadyImported: innerProps.repositories.some( + (item) => item.providerKey === providerKey && item.identifier === identifier, + ), + }); + return acc; + }, []) ?? [], + [docker.data, innerProps.repositories], + ); + + const handleConfirm = useCallback(() => { + setLoading(true); + + innerProps.onConfirm(selectedImages); + + setLoading(false); + actions.closeModal(); + }, [innerProps, selectedImages, actions]); + + const allImagesImported = useMemo( + () => containersImages.every((containerImage) => containerImage.alreadyImported), + [containersImages], + ); + + const anyImagesImported = useMemo( + () => containersImages.some((containerImage) => containerImage.alreadyImported), + [containersImages], + ); + + return ( + + {docker.isPending ? ( + + + {tRepository("importRepositories.loading")} + + ) : containersImages.length === 0 ? ( + + + {tRepository("importRepositories.noImagesFound")} + + ) : ( + + + + }> + + {tRepository("importRepositories.listFoundImages")} + {allImagesImported && ( + + {tRepository("importRepositories.allImagesAlreadyImported")} + + )} + + + + {!allImagesImported && + containersImages + .filter((containerImage) => !containerImage.alreadyImported) + .map((containerImage) => { + return ( + + isSelected + ? setSelectedImages([...selectedImages, containerImage]) + : setSelectedImages(selectedImages.filter((img) => img !== containerImage)) + } + /> + ); + })} + + + + }> + {tRepository("importRepositories.listAlreadyImportedImages")} + + + {anyImagesImported && + containersImages + .filter((containerImage) => containerImage.alreadyImported) + .map((containerImage) => { + return ( + + ); + })} + + + + + )} + + + + + + + + ); +}).withOptions({ + defaultTitle(t) { + return t("widget.releases.option.repositories.importForm.title"); + }, + size: "xl", +}); + +const parseImageVersionToVersionFilter = (imageVersion: string): ReleasesVersionFilter | undefined => { + const version = /(?<=\D|^)\d+(?:\.\d+)*(?![\d.])/.exec(imageVersion)?.[0]; + + if (!version) return undefined; + + const [prefix, suffix] = imageVersion.split(version); + + return { + prefix, + precision: version.split(".").length, + suffix, + }; +}; diff --git a/packages/widgets/src/releases/component.tsx b/packages/widgets/src/releases/component.tsx index e26a89609..c5461b7c3 100644 --- a/packages/widgets/src/releases/component.tsx +++ b/packages/widgets/src/releases/component.tsx @@ -156,7 +156,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas > }} /> - {Providers[repository.providerKey]?.name} + {Providers[repository.providerKey].name} diff --git a/packages/widgets/src/releases/releases-providers.ts b/packages/widgets/src/releases/releases-providers.ts index a30c16908..34fdeef83 100644 --- a/packages/widgets/src/releases/releases-providers.ts +++ b/packages/widgets/src/releases/releases-providers.ts @@ -3,16 +3,7 @@ export interface ReleasesProvider { iconUrl: string; } -interface ProvidersProps { - [key: string]: ReleasesProvider; - DockerHub: ReleasesProvider; - Github: ReleasesProvider; - Gitlab: ReleasesProvider; - Npm: ReleasesProvider; - Codeberg: ReleasesProvider; -} - -export const Providers: ProvidersProps = { +export const Providers = { DockerHub: { name: "Docker Hub", iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/docker.svg", @@ -33,4 +24,10 @@ export const Providers: ProvidersProps = { name: "Codeberg", iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/codeberg.svg", }, +} as const satisfies Record; + +export type ProviderKey = keyof typeof Providers; + +export const isProviderKey = (key: string): key is ProviderKey => { + return key in Providers; }; diff --git a/packages/widgets/src/releases/releases-repository.ts b/packages/widgets/src/releases/releases-repository.ts index 825b6b0c0..0435f098c 100644 --- a/packages/widgets/src/releases/releases-repository.ts +++ b/packages/widgets/src/releases/releases-repository.ts @@ -1,3 +1,5 @@ +import type { ProviderKey } from "./releases-providers"; + export interface ReleasesVersionFilter { prefix?: string; precision: number; @@ -5,7 +7,7 @@ export interface ReleasesVersionFilter { } export interface ReleasesRepository { - providerKey: string; + providerKey: ProviderKey; identifier: string; name?: string; versionFilter?: ReleasesVersionFilter;