feat(docker): add endpoint filtering and setup assistant

This commit is contained in:
ajnart committed 2026-08-18 17:47:31 +02:00
1 parent 315245e048
commit f9e62a4932
13 files changed
+426 -190

No files matched your search

+2
View File
@@ -4,6 +4,7 @@ import { WidgetAppInput } from "./widget-app-input";
import { WidgetCustomWidgetSelectInput } from "./widget-custom-widget-select-input";
import { WidgetCustomWidgetConfigurationInput } from "./widget-custom-widget-configuration-input";
import { WidgetDynamicSelectInput } from "./widget-dynamic-select-input";
import { WidgetDynamicMultiSelectInput } from "./widget-dynamic-multiselect-input";
import { WidgetLocationInput } from "./widget-location-input";
import { WidgetMultiTextInput } from "./widget-multi-text-input";
import { WidgetMultiReleasesRepositoriesInput } from "./widget-multiReleasesRepositories-input";
@@ -36,6 +37,7 @@ const mapping = {
sortableItemList: WidgetSortedItemListInput,
multiReleasesRepositories: WidgetMultiReleasesRepositoriesInput,
dynamicSelect: WidgetDynamicSelectInput,
dynamicMultiSelect: WidgetDynamicMultiSelectInput,
umamiEventName: WidgetUmamiEventInput,
umamiEventNames: WidgetUmamiEventNamesInput,
umamiWebsite: WidgetUmamiWebsiteInput,
@@ -0,0 +1,44 @@
"use client";
import { Loader, MultiSelect } from "@mantine/core";
import { useScopedI18n } from "@homarr/translation/client";
import type { CommonWidgetInputProps } from "./common";
import { useWidgetInputTranslation } from "./common";
import { useFormContext } from "./form";
export const WidgetDynamicMultiSelectInput = ({
property,
kind,
options,
}: CommonWidgetInputProps<"dynamicMultiSelect">) => {
const t = useScopedI18n("widget.dynamicSelect");
const tInput = useWidgetInputTranslation(kind, property);
const form = useFormContext();
const inputProps = form.getInputProps(`options.${property}`);
const selectedValues = (inputProps.value as string[] | undefined) ?? [];
const { data: selectData, isPending, isError } = options.useOptions();
const data = [
...selectData,
...selectedValues
.filter((selectedValue) => !selectData.some((option) => option.value === selectedValue))
.map((selectedValue) => ({ value: selectedValue, label: selectedValue })),
];
return (
<MultiSelect
{...inputProps}
label={tInput("label")}
description={options.withDescription ? tInput("description") : undefined}
placeholder={isPending ? t("loading") : tInput("placeholder")}
leftSection={isPending ? <Loader size="xs" /> : undefined}
searchable
clearable
nothingFoundMessage={t("noResults")}
data={data}
disabled={isError}
error={isError ? t("loadError") : inputProps.error}
/>
);
};
+1 -1
View File
@@ -248,7 +248,7 @@ export default function DockerWidget({
const isAdvanced = displayMode === "advanced";
const utils = clientApi.useUtils();
const containersQuery = clientApi.docker.getContainers.useQuery();
const containersQuery = clientApi.docker.getContainers.useQuery({ endpointIds: options.endpointIds });
const data = getUsableWidgetQueryData(containersQuery);
const { isFetching } = containersQuery;
const refreshInventory = clientApi.docker.refreshInventory.useMutation({
+13
View File
@@ -1,6 +1,7 @@
import { IconBrandDocker, IconServerOff } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
@@ -30,6 +31,18 @@ export const { definition, componentLoader } = createWidgetDefinition("dockerCon
createOptions() {
return optionsBuilder.from(
(factory) => ({
endpointIds: factory.dynamicMultiSelect({
defaultValue: [],
withDescription: true,
useOptions: () => {
const { data = [], isPending, isError } = clientApi.docker.getEndpoints.useQuery();
return {
data: data.map((endpoint) => ({ value: endpoint.id, label: endpoint.name })),
isPending,
isError,
};
},
}),
columns: factory.multiSelect({
defaultValue: [...allColumnsList],
options: allColumnsList.map((value) => ({
+14
View File
@@ -69,6 +69,14 @@ interface DynamicSelectInput extends CommonInput<DynamicSelectOption | null> {
};
}
interface DynamicMultiSelectInput extends CommonInput<string[]> {
useOptions: () => {
data: DynamicSelectOption[];
isPending: boolean;
isError: boolean;
};
}
interface IntegrationSelectInput extends CommonInput<string> {
clearable?: boolean;
searchable?: boolean;
@@ -141,6 +149,12 @@ const optionsFactory = {
useOptions: input.useOptions,
withDescription: input.withDescription ?? false,
}),
dynamicMultiSelect: (input: DynamicMultiSelectInput) => ({
type: "dynamicMultiSelect" as const,
defaultValue: input.defaultValue ?? [],
useOptions: input.useOptions,
withDescription: input.withDescription ?? false,
}),
select: <const TOptions extends SelectOption[]>(input: SelectInput<TOptions>) => ({
type: "select" as const,
defaultValue: (input.defaultValue ?? input.options[0]) as inferSelectOptionValue<TOptions[number]>,