feat(onboarding): test integration connections
This commit is contained in:
1 parent
276431ef4c
commit
dd707bed7e
3 files changed
+71
-6
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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<IntegrationDraft>) => {
|
||||
testConnection.reset();
|
||||
update(patch);
|
||||
};
|
||||
return (
|
||||
<Accordion.Item value={draft.id}>
|
||||
<Accordion.Control
|
||||
@@ -1255,19 +1271,22 @@ const IntegrationEditor = ({
|
||||
<TextInput
|
||||
label={t("name")}
|
||||
value={draft.name}
|
||||
onChange={(event) => update({ name: event.currentTarget.value })}
|
||||
onChange={(event) => updateAndResetTest({ name: event.currentTarget.value })}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("url")}
|
||||
value={draft.url}
|
||||
onChange={(event) => update({ url: event.currentTarget.value, urlOverridden: true })}
|
||||
onChange={(event) => updateAndResetTest({ url: event.currentTarget.value, urlOverridden: true })}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
{options.length > 1 ? (
|
||||
<OnboardingFloatingControl
|
||||
ariaLabel={t("credentialMethod")}
|
||||
value={String(draft.secretOption)}
|
||||
onChange={(value) => 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")}
|
||||
</Text>
|
||||
) : null}
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-sm"
|
||||
disabled={needsConfiguration}
|
||||
loading={testConnection.isPending}
|
||||
color={
|
||||
testConnection.data?.success ? "green" : testConnection.data?.success === false ? "red" : undefined
|
||||
}
|
||||
leftSection={testConnection.data?.success ? <IconCheck size={15} /> : <IconPlugConnected size={15} />}
|
||||
onClick={() =>
|
||||
testConnection.mutate({
|
||||
sourceId: draft.sourceId ?? draft.id,
|
||||
name: draft.name,
|
||||
url: normalizeServiceUrl(draft.url) ?? draft.url,
|
||||
kind: draft.kind,
|
||||
secrets: draft.secrets,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("testConnection")}
|
||||
</Button>
|
||||
</Group>
|
||||
{apiKeyUrl ? (
|
||||
<Anchor href={apiKeyUrl} target="_blank" rel="noopener noreferrer" size="sm">
|
||||
<Group gap={4}>
|
||||
|
||||
@@ -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?"
|
||||
|
||||
Reference in new issue
Block a user