diff --git a/packages/api/src/router/onboard/onboard-router.ts b/packages/api/src/router/onboard/onboard-router.ts index 4135f7cc6..e4eb293e1 100644 --- a/packages/api/src/router/onboard/onboard-router.ts +++ b/packages/api/src/router/onboard/onboard-router.ts @@ -43,7 +43,7 @@ import { widgetIntegrationLimits, } from "@homarr/definitions"; import type { IntegrationKind, OnboardingLayoutPreset, WidgetKind } from "@homarr/definitions"; -import { onboardingCompleteSetupSchema } from "@homarr/validation/onboarding"; +import { onboardingCompleteSetupSchema, onboardingIntegrationDraftSchema } from "@homarr/validation/onboarding"; import { onboardingClaimSettingKey } from "../../onboarding-claim"; import { env } from "../../env"; @@ -148,6 +148,24 @@ export const onboardRouter = createTRPCRouter({ await nextOnboardingStepAsync(ctx.db); }), + testIntegration: onboardingProcedure + .requiresStep("setup") + .input(onboardingIntegrationDraftSchema) + .mutation(async ({ input }) => { + const result = await testConnectionAsync({ + id: stableOnboardingId("integration", input.sourceId), + name: input.name, + url: input.url, + kind: input.kind, + secrets: input.secrets, + }).catch((error) => { + if (!(error instanceof MissingSecretError)) throw error; + throw new TRPCError({ code: "BAD_REQUEST", message: error.message }); + }); + + return { success: result.success }; + }), + detectRuntimeCapabilities: onboardingProcedure .requiresStep("setup") .output(runtimeCapabilitiesSchema) diff --git a/packages/onboarding/src/setup-studio.tsx b/packages/onboarding/src/setup-studio.tsx index a75b35d41..81df8f134 100644 --- a/packages/onboarding/src/setup-studio.tsx +++ b/packages/onboarding/src/setup-studio.tsx @@ -88,7 +88,7 @@ import { getWidgetKindsForIntegration, generalWidgets, } from "@homarr/definitions"; -import { showErrorNotification, showWarningNotification } from "@homarr/notifications"; +import { showErrorNotification, showSuccessNotification, showWarningNotification } from "@homarr/notifications"; import type { SupportedLanguage } from "@homarr/translation"; import { useCurrentLocale, useScopedI18n } from "@homarr/translation/client"; import { BoardColorInput, ColorSchemeCombobox, IntegrationAvatar, LanguageCombobox, Link } from "@homarr/ui"; @@ -1222,6 +1222,22 @@ const IntegrationEditor = ({ const apiKeyUrl = getIntegrationApiKeyUrl(draft.url, draft.kind); const documentationUrl = getIntegrationDocumentationUrl(draft.kind); const needsConfiguration = !isIntegrationDraftComplete(draft); + const testConnection = clientApi.onboard.testIntegration.useMutation({ + onSuccess(result) { + if (result.success) { + showSuccessNotification({ title: t("testSuccess"), message: t("testSuccessDescription") }); + } else { + showErrorNotification({ title: t("testError"), message: t("testErrorDescription") }); + } + }, + onError() { + showErrorNotification({ title: t("testError"), message: t("testErrorDescription") }); + }, + }); + const updateAndResetTest = (patch: Partial) => { + testConnection.reset(); + update(patch); + }; return ( update({ name: event.currentTarget.value })} + onChange={(event) => updateAndResetTest({ name: event.currentTarget.value })} /> update({ url: event.currentTarget.value, urlOverridden: true })} + onChange={(event) => updateAndResetTest({ url: event.currentTarget.value, urlOverridden: true })} /> {options.length > 1 ? ( selectSecretOption(Number(value))} + onChange={(value) => { + testConnection.reset(); + selectSecretOption(Number(value)); + }} options={options.map((secretKinds, index) => ({ value: String(index), label: secretKinds.length === 0 ? t("noCredentials") : secretKinds.map(formatSecretKind).join(" + "), @@ -1280,7 +1299,7 @@ const IntegrationEditor = ({ label={formatSecretKind(secret.kind)} value={secret.value} onChange={(event) => - update({ + updateAndResetTest({ secrets: draft.secrets.map((item, itemIndex) => itemIndex === index ? { ...item, value: event.currentTarget.value } : item, ), @@ -1293,6 +1312,29 @@ const IntegrationEditor = ({ {t("noCredentialsDescription")} ) : null} + + + {apiKeyUrl ? ( diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 372629d76..15623df0d 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -127,6 +127,11 @@ "credentialMethod": "Credential method", "getApiKey": "Open the API key settings for this service", "openGuide": "Open Homarr's setup guide for this service", + "testConnection": "Test connection", + "testSuccess": "Connection works", + "testSuccessDescription": "Homarr connected with the current address and credentials.", + "testError": "Connection failed", + "testErrorDescription": "Check the service address and credentials, then try again.", "needsConfiguration": "Needs configuration", "configured": "Configured", "incompleteConfirmation": "Unfinished integrations will be skipped. Continue without them?"