mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-27 08:50:56 +01:00
feat: add support for multiple integration kind options (#127)
* feat: add support for multiple integration kind options * fix: deepsource issue JS-0417 missing use callback
This commit is contained in:
@@ -5,7 +5,10 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { getSecretKinds } from "@homarr/definitions";
|
||||
import {
|
||||
getAllSecretKindOptions,
|
||||
getDefaultSecretKinds,
|
||||
} from "@homarr/definitions";
|
||||
import { useForm, zodResolver } from "@homarr/form";
|
||||
import {
|
||||
showErrorNotification,
|
||||
@@ -32,7 +35,10 @@ interface EditIntegrationForm {
|
||||
|
||||
export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
const t = useI18n();
|
||||
const secretsKinds = getSecretKinds(integration.kind);
|
||||
const secretsKinds =
|
||||
getAllSecretKindOptions(integration.kind).find((x) =>
|
||||
integration.secrets.every((y) => x.includes(y.kind)),
|
||||
) ?? getDefaultSecretKinds(integration.kind);
|
||||
const initialFormValues = {
|
||||
name: integration.name,
|
||||
url: integration.url,
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import { getSecretKinds } from "@homarr/definitions";
|
||||
import type {
|
||||
IntegrationKind,
|
||||
IntegrationSecretKind,
|
||||
} from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions } from "@homarr/definitions";
|
||||
import type { UseFormReturnType } from "@homarr/form";
|
||||
import { useForm, zodResolver } from "@homarr/form";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Button, Fieldset, Group, Stack, TextInput } from "@homarr/ui";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import {
|
||||
Button,
|
||||
Fieldset,
|
||||
Group,
|
||||
SegmentedControl,
|
||||
Stack,
|
||||
TextInput,
|
||||
} from "@homarr/ui";
|
||||
import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
@@ -34,11 +46,11 @@ export const NewIntegrationForm = ({
|
||||
searchParams,
|
||||
}: NewIntegrationFormProps) => {
|
||||
const t = useI18n();
|
||||
const secretKinds = getSecretKinds(searchParams.kind);
|
||||
const secretKinds = getAllSecretKindOptions(searchParams.kind);
|
||||
const initialFormValues = {
|
||||
name: searchParams.name ?? "",
|
||||
url: searchParams.url ?? "",
|
||||
secrets: secretKinds.map((kind) => ({
|
||||
secrets: secretKinds[0].map((kind) => ({
|
||||
kind,
|
||||
value: "",
|
||||
})),
|
||||
@@ -99,7 +111,13 @@ export const NewIntegrationForm = ({
|
||||
|
||||
<Fieldset legend={t("integration.secrets.title")}>
|
||||
<Stack gap="sm">
|
||||
{secretKinds.map((kind, index) => (
|
||||
{secretKinds.length > 1 && (
|
||||
<SecretKindsSegmentedControl
|
||||
secretKinds={secretKinds}
|
||||
form={form}
|
||||
/>
|
||||
)}
|
||||
{form.values.secrets.map(({ kind }, index) => (
|
||||
<IntegrationSecretInput
|
||||
key={kind}
|
||||
kind={kind}
|
||||
@@ -134,4 +152,41 @@ export const NewIntegrationForm = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface SecretKindsSegmentedControlProps {
|
||||
secretKinds: IntegrationSecretKind[][];
|
||||
form: UseFormReturnType<FormType, (values: FormType) => FormType>;
|
||||
}
|
||||
|
||||
const SecretKindsSegmentedControl = ({
|
||||
secretKinds,
|
||||
form,
|
||||
}: SecretKindsSegmentedControlProps) => {
|
||||
const t = useScopedI18n("integration.secrets");
|
||||
|
||||
const secretKindGroups = secretKinds.map((kinds) => ({
|
||||
label: kinds.map((kind) => t(`kind.${kind}.label`)).join(" & "),
|
||||
value: kinds.join("-"),
|
||||
}));
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: string) => {
|
||||
const kinds = value.split("-") as IntegrationSecretKind[];
|
||||
const secrets = kinds.map((kind) => ({
|
||||
kind,
|
||||
value: "",
|
||||
}));
|
||||
form.setFieldValue("secrets", secrets);
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
return (
|
||||
<SegmentedControl
|
||||
fullWidth
|
||||
data={secretKindGroups}
|
||||
onChange={onChange}
|
||||
></SegmentedControl>
|
||||
);
|
||||
};
|
||||
|
||||
type FormType = Omit<z.infer<typeof validation.integration.create>, "kind">;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { and, createId, eq } from "@homarr/db";
|
||||
import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite";
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
import {
|
||||
getSecretKinds,
|
||||
getAllSecretKindOptions,
|
||||
integrationKinds,
|
||||
integrationSecretKindObject,
|
||||
} from "@homarr/definitions";
|
||||
@@ -165,22 +165,27 @@ export const integrationRouter = createTRPCRouter({
|
||||
testConnection: publicProcedure
|
||||
.input(validation.integration.testConnection)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const secretKinds = getSecretKinds(input.kind);
|
||||
const secrets = input.secrets.filter(
|
||||
(secret): secret is { kind: IntegrationSecretKind; value: string } =>
|
||||
Boolean(secret.value),
|
||||
);
|
||||
const everyInputSecretDefined = secretKinds.every((secretKind) =>
|
||||
secrets.some((secret) => secret.kind === secretKind),
|
||||
|
||||
// Find any matching secret kinds
|
||||
let secretKinds = getAllSecretKindOptions(input.kind).find(
|
||||
(secretKinds) =>
|
||||
secretKinds.every((secretKind) =>
|
||||
secrets.some((secret) => secret.kind === secretKind),
|
||||
),
|
||||
);
|
||||
if (!everyInputSecretDefined && input.id === null) {
|
||||
|
||||
if (!secretKinds && input.id === null) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "SECRETS_NOT_DEFINED",
|
||||
});
|
||||
}
|
||||
|
||||
if (!everyInputSecretDefined && input.id !== null) {
|
||||
if (!secretKinds && input.id !== null) {
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
where: eq(integrations.id, input.id),
|
||||
with: {
|
||||
@@ -208,11 +213,13 @@ export const integrationRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
const everySecretDefined = secretKinds.every((secretKind) =>
|
||||
secrets.some((secret) => secret.kind === secretKind),
|
||||
secretKinds = getAllSecretKindOptions(input.kind).find((secretKinds) =>
|
||||
secretKinds.every((secretKind) =>
|
||||
secrets.some((secret) => secret.kind === secretKind),
|
||||
),
|
||||
);
|
||||
|
||||
if (!everySecretDefined) {
|
||||
if (!secretKinds) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "SECRETS_NOT_DEFINED",
|
||||
|
||||
@@ -11,112 +11,112 @@ export const integrationSecretKinds = objectKeys(integrationSecretKindObject);
|
||||
export const integrationDefs = {
|
||||
sabNzbd: {
|
||||
name: "SABnzbd",
|
||||
secretKinds: ["apiKey"],
|
||||
secretKinds: [["apiKey"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/sabnzbd.png",
|
||||
category: ["useNetClient"],
|
||||
},
|
||||
nzbGet: {
|
||||
name: "NZBGet",
|
||||
secretKinds: ["username", "password"],
|
||||
secretKinds: [["username", "password"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/nzbget.png",
|
||||
category: ["useNetClient"],
|
||||
},
|
||||
deluge: {
|
||||
name: "Deluge",
|
||||
secretKinds: ["password"],
|
||||
secretKinds: [["password"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/deluge.png",
|
||||
category: ["downloadClient"],
|
||||
},
|
||||
transmission: {
|
||||
name: "Transmission",
|
||||
secretKinds: ["username", "password"],
|
||||
secretKinds: [["username", "password"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/transmission.png",
|
||||
category: ["downloadClient"],
|
||||
},
|
||||
qBittorrent: {
|
||||
name: "qBittorrent",
|
||||
secretKinds: ["username", "password"],
|
||||
secretKinds: [["username", "password"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/qbittorrent.png",
|
||||
category: ["downloadClient"],
|
||||
},
|
||||
sonarr: {
|
||||
name: "Sonarr",
|
||||
secretKinds: ["apiKey"],
|
||||
secretKinds: [["apiKey"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/sonarr.png",
|
||||
category: ["calendar"],
|
||||
},
|
||||
radarr: {
|
||||
name: "Radarr",
|
||||
secretKinds: ["apiKey"],
|
||||
secretKinds: [["apiKey"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/radarr.png",
|
||||
category: ["calendar"],
|
||||
},
|
||||
lidarr: {
|
||||
name: "Lidarr",
|
||||
secretKinds: ["apiKey"],
|
||||
secretKinds: [["apiKey"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/lidarr.png",
|
||||
category: ["calendar"],
|
||||
},
|
||||
readarr: {
|
||||
name: "Readarr",
|
||||
secretKinds: ["apiKey"],
|
||||
secretKinds: [["apiKey"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/readarr.png",
|
||||
category: ["calendar"],
|
||||
},
|
||||
jellyfin: {
|
||||
name: "Jellyfin",
|
||||
secretKinds: ["username", "password"],
|
||||
secretKinds: [["username", "password"], ["apiKey"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyfin.png",
|
||||
category: ["mediaService"],
|
||||
},
|
||||
plex: {
|
||||
name: "Plex",
|
||||
secretKinds: ["apiKey"],
|
||||
secretKinds: [["apiKey"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/plex.png",
|
||||
category: ["mediaService"],
|
||||
},
|
||||
jellyseerr: {
|
||||
name: "Jellyseerr",
|
||||
secretKinds: ["apiKey"],
|
||||
secretKinds: [["apiKey"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyseerr.png",
|
||||
category: ["mediaSearch", "mediaRequest"],
|
||||
},
|
||||
overseerr: {
|
||||
name: "Overseerr",
|
||||
secretKinds: ["apiKey"],
|
||||
secretKinds: [["apiKey"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/overseerr.png",
|
||||
category: ["mediaSearch", "mediaRequest"],
|
||||
},
|
||||
piHole: {
|
||||
name: "Pi-hole",
|
||||
secretKinds: ["apiKey"],
|
||||
secretKinds: [["apiKey"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/pi-hole.png",
|
||||
category: ["dnsHole"],
|
||||
},
|
||||
adGuardHome: {
|
||||
name: "AdGuard Home",
|
||||
secretKinds: ["username", "password"],
|
||||
secretKinds: [["username", "password"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/adguard-home.png",
|
||||
category: ["dnsHole"],
|
||||
},
|
||||
homeAssistant: {
|
||||
name: "Home Assistant",
|
||||
secretKinds: ["apiKey"],
|
||||
secretKinds: [["apiKey"]],
|
||||
iconUrl:
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/home-assistant.png",
|
||||
category: [],
|
||||
@@ -126,7 +126,7 @@ export const integrationDefs = {
|
||||
{
|
||||
name: string;
|
||||
iconUrl: string;
|
||||
secretKinds: IntegrationSecretKind[];
|
||||
secretKinds: [IntegrationSecretKind[], ...IntegrationSecretKind[][]]; // at least one secret kind set is required
|
||||
category: IntegrationCategory[];
|
||||
}
|
||||
>;
|
||||
@@ -137,9 +137,14 @@ export const getIconUrl = (integration: IntegrationKind) =>
|
||||
export const getIntegrationName = (integration: IntegrationKind) =>
|
||||
integrationDefs[integration].name;
|
||||
|
||||
export const getSecretKinds = (
|
||||
export const getDefaultSecretKinds = (
|
||||
integration: IntegrationKind,
|
||||
): IntegrationSecretKind[] => integrationDefs[integration]?.secretKinds ?? null;
|
||||
): IntegrationSecretKind[] => integrationDefs[integration]?.secretKinds[0];
|
||||
|
||||
export const getAllSecretKindOptions = (
|
||||
integration: IntegrationKind,
|
||||
): [IntegrationSecretKind[], ...IntegrationSecretKind[][]] =>
|
||||
integrationDefs[integration]?.secretKinds;
|
||||
|
||||
export const integrationKinds = objectKeys(integrationDefs);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user