feat(releases-widget): add import from docker functionality (#3130)
This commit is contained in:
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "Repositories",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": "Tilføj repository"
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "Repositories",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": "Repository hinzufügen"
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "מאגרים",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": "הוסף מאגר"
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "Depolar",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": "Depo Ekle"
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": ""
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -2223,7 +2223,7 @@
|
||||
},
|
||||
"repositories": {
|
||||
"label": "儲存庫",
|
||||
"addRRepository": {
|
||||
"addRepository": {
|
||||
"label": "新增儲存庫"
|
||||
},
|
||||
"provider": {
|
||||
|
||||
@@ -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 (
|
||||
<Fieldset legend={t("label")}>
|
||||
<Stack gap="5">
|
||||
<Button onClick={addNewItem}>{tRepository("addRRepository.label")}</Button>
|
||||
<Group grow>
|
||||
<Button leftSection={<IconPlus />} onClick={addNewRepository}>
|
||||
{tRepository("addRepository.label")}
|
||||
</Button>
|
||||
<Tooltip label={tRepository("importRepositories.onlyAdminCanImport")} disabled={isAdmin} withArrow>
|
||||
<Button
|
||||
disabled={!isAdmin}
|
||||
leftSection={<IconBrandDocker stroke={1.25} />}
|
||||
onClick={() =>
|
||||
openImportModal({
|
||||
repositories,
|
||||
versionFilterPrecisionOptions,
|
||||
onConfirm: (selectedRepositories) => {
|
||||
if (!selectedRepositories.length) return;
|
||||
|
||||
form.setValues((previous) => {
|
||||
const previousValues = previous.options?.[property] as ReleasesRepository[];
|
||||
return {
|
||||
...previous,
|
||||
options: {
|
||||
...previous.options,
|
||||
[property]: [...previousValues, ...selectedRepositories],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
isAdmin,
|
||||
})
|
||||
}
|
||||
>
|
||||
{tRepository("importRepositories.label")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Divider my="sm" />
|
||||
|
||||
{repositories.map((repository, index) => {
|
||||
return (
|
||||
<Stack key={`${repository.providerKey}.${repository.identifier}`} gap={5}>
|
||||
<Group align="center" gap="xs">
|
||||
<MaskedOrNormalImage
|
||||
hasColor={false}
|
||||
imageUrl={repository.iconUrl ?? Providers[repository.providerKey]?.iconUrl}
|
||||
<Image
|
||||
src={repository.iconUrl ?? Providers[repository.providerKey].iconUrl}
|
||||
style={{
|
||||
height: "1em",
|
||||
width: "1em",
|
||||
height: "1.2em",
|
||||
width: "1.2em",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Text c="dimmed" fw={100} size="xs">
|
||||
{Providers[repository.providerKey]?.name}
|
||||
{Providers[repository.providerKey].name}
|
||||
</Text>
|
||||
|
||||
<Group justify="space-between" align="center" style={{ flex: 1 }} gap={5}>
|
||||
@@ -135,7 +197,7 @@ export const WidgetMultiReleasesRepositoriesInput = ({
|
||||
|
||||
<Button
|
||||
onClick={() =>
|
||||
openModal({
|
||||
openEditModal({
|
||||
fieldPath: `options.${property}.${index}`,
|
||||
repository,
|
||||
onRepositorySave: (saved) => onRepositorySave(saved, index),
|
||||
@@ -185,7 +247,7 @@ const formatIdentifierName = (identifier: string) => {
|
||||
return unformattedName?.replace(/[-_]/g, " ").replace(/(?:^\w|[A-Z]|\b\w)/g, (char) => char.toUpperCase()) ?? "";
|
||||
};
|
||||
|
||||
interface ReleaseEditProps {
|
||||
interface RepositoryEditProps {
|
||||
fieldPath: string;
|
||||
repository: ReleasesRepository;
|
||||
onRepositorySave: (repository: ReleasesRepository) => FormValidation;
|
||||
@@ -193,7 +255,7 @@ interface ReleaseEditProps {
|
||||
versionFilterPrecisionOptions: string[];
|
||||
}
|
||||
|
||||
const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions }) => {
|
||||
const RepositoryEditModal = createModal<RepositoryEditProps>(({ innerProps, actions }) => {
|
||||
const tRepository = useScopedI18n("widget.releases.option.repositories");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tempRepository, setTempRepository] = useState(() => ({ ...innerProps.repository }));
|
||||
@@ -262,7 +324,7 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions })
|
||||
value={tempRepository.providerKey}
|
||||
error={formErrors[`${innerProps.fieldPath}.providerKey`]}
|
||||
onChange={(value) => {
|
||||
if (value && Providers[value]) {
|
||||
if (value && isProviderKey(value)) {
|
||||
handleChange({ providerKey: value });
|
||||
}
|
||||
}}
|
||||
@@ -386,7 +448,7 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions })
|
||||
{tRepository("editForm.cancel.label")}
|
||||
</Button>
|
||||
|
||||
<Button data-autofocus onClick={handleConfirm} color="red.9" loading={loading}>
|
||||
<Button data-autofocus onClick={handleConfirm} loading={loading}>
|
||||
{tRepository("editForm.confirm.label")}
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -398,3 +460,247 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ 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 (
|
||||
<Group
|
||||
key={`${Providers[containerImage.providerKey].name}/${containerImage.identifier}`}
|
||||
gap="xl"
|
||||
justify="space-between"
|
||||
>
|
||||
<Group gap="md">
|
||||
<Checkbox
|
||||
label={
|
||||
<Group>
|
||||
<Image
|
||||
src={containerImage.iconUrl}
|
||||
style={{
|
||||
height: "1.2em",
|
||||
width: "1.2em",
|
||||
}}
|
||||
/>
|
||||
<Text>{containerImage.identifier}</Text>
|
||||
</Group>
|
||||
}
|
||||
{...checkBoxProps}
|
||||
/>
|
||||
|
||||
{containerImage.versionFilter && (
|
||||
<Group gap={5}>
|
||||
<Text c="dimmed" size="xs">
|
||||
{tRepository("versionFilter.label")}:
|
||||
</Text>
|
||||
|
||||
<Code>{containerImage.versionFilter.prefix && containerImage.versionFilter.prefix}</Code>
|
||||
<Code color="var(--mantine-primary-color-light)" fw={700}>
|
||||
{versionFilterPrecisionOptions[containerImage.versionFilter.precision]}
|
||||
</Code>
|
||||
<Code>{containerImage.versionFilter.suffix && containerImage.versionFilter.suffix}</Code>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
<MaskedImage
|
||||
color="dimmed"
|
||||
imageUrl={Providers[containerImage.providerKey].iconUrl}
|
||||
style={{
|
||||
height: "1em",
|
||||
width: "1em",
|
||||
}}
|
||||
/>
|
||||
<Text ff="monospace" c="dimmed" size="sm">
|
||||
{Providers[containerImage.providerKey].name}
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
interface RepositoryImportProps {
|
||||
repositories: ReleasesRepository[];
|
||||
versionFilterPrecisionOptions: string[];
|
||||
onConfirm: (selectedRepositories: ReleasesRepositoryImport[]) => void;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const RepositoryImportModal = createModal<RepositoryImportProps>(({ 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<ReleasesRepositoryImport[]>((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 (
|
||||
<Stack>
|
||||
{docker.isPending ? (
|
||||
<Stack justify="center" align="center">
|
||||
<Loader size="xl" />
|
||||
<Title order={3}>{tRepository("importRepositories.loading")}</Title>
|
||||
</Stack>
|
||||
) : containersImages.length === 0 ? (
|
||||
<Stack justify="center" align="center">
|
||||
<IconBrandDocker stroke={1} size={128} />
|
||||
<Title order={3}>{tRepository("importRepositories.noImagesFound")}</Title>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack>
|
||||
<Accordion defaultValue={!allImagesImported ? "foundImages" : anyImagesImported ? "alreadyImported" : ""}>
|
||||
<Accordion.Item value="foundImages">
|
||||
<Accordion.Control disabled={allImagesImported} icon={<IconSquare stroke={1.25} />}>
|
||||
<Group>
|
||||
{tRepository("importRepositories.listFoundImages")}
|
||||
{allImagesImported && (
|
||||
<Text c="dimmed" size="sm">
|
||||
{tRepository("importRepositories.allImagesAlreadyImported")}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
{!allImagesImported &&
|
||||
containersImages
|
||||
.filter((containerImage) => !containerImage.alreadyImported)
|
||||
.map((containerImage) => {
|
||||
return (
|
||||
<ContainerImageSelector
|
||||
key={`${containerImage.providerKey}/${containerImage.identifier}`}
|
||||
containerImage={containerImage}
|
||||
versionFilterPrecisionOptions={innerProps.versionFilterPrecisionOptions}
|
||||
onImageSelectionChanged={(isSelected) =>
|
||||
isSelected
|
||||
? setSelectedImages([...selectedImages, containerImage])
|
||||
: setSelectedImages(selectedImages.filter((img) => img !== containerImage))
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="alreadyImported">
|
||||
<Accordion.Control disabled={!anyImagesImported} icon={<IconSquareCheck stroke={1.25} />}>
|
||||
{tRepository("importRepositories.listAlreadyImportedImages")}
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
{anyImagesImported &&
|
||||
containersImages
|
||||
.filter((containerImage) => containerImage.alreadyImported)
|
||||
.map((containerImage) => {
|
||||
return (
|
||||
<ContainerImageSelector
|
||||
key={`${containerImage.providerKey}/${containerImage.identifier}`}
|
||||
containerImage={containerImage}
|
||||
versionFilterPrecisionOptions={innerProps.versionFilterPrecisionOptions}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={actions.closeModal} color="gray.5">
|
||||
{tRepository("editForm.cancel.label")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleConfirm} loading={loading} disabled={selectedImages.length === 0}>
|
||||
{tRepository("editForm.confirm.label")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}).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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -156,7 +156,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
||||
>
|
||||
<MaskedOrNormalImage
|
||||
className="releases-repository-header-icon"
|
||||
imageUrl={repository.iconUrl ?? Providers[repository.providerKey]?.iconUrl}
|
||||
imageUrl={repository.iconUrl ?? Providers[repository.providerKey].iconUrl}
|
||||
hasColor={hasIconColor}
|
||||
style={{
|
||||
width: "1em",
|
||||
@@ -474,7 +474,7 @@ const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) =>
|
||||
<Group className="releases-repository-expanded-header-provider-wrapper" gap={5} align="center">
|
||||
<MaskedOrNormalImage
|
||||
className="releases-repository-expanded-header-provider-icon"
|
||||
imageUrl={Providers[repository.providerKey]?.iconUrl}
|
||||
imageUrl={Providers[repository.providerKey].iconUrl}
|
||||
hasColor={hasIconColor}
|
||||
style={{
|
||||
width: "1em",
|
||||
@@ -482,7 +482,7 @@ const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) =>
|
||||
}}
|
||||
/>
|
||||
<Text className="releases-repository-expanded-header-provider-name" size="xs" c="iconColor" ff="monospace">
|
||||
{Providers[repository.providerKey]?.name}
|
||||
{Providers[repository.providerKey].name}
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
@@ -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<string, ReleasesProvider>;
|
||||
|
||||
export type ProviderKey = keyof typeof Providers;
|
||||
|
||||
export const isProviderKey = (key: string): key is ProviderKey => {
|
||||
return key in Providers;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user