echos-ocr/src/app/(dashboard)/settings/integrations/new/page.tsx

296 lines
9.6 KiB
TypeScript
Raw Normal View History

"use client";
import * as React from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { toast } from "sonner";
import { Loader2, ArrowLeft, Plug, Plus, Check, Zap } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
type ProviderInfo = {
id: string;
name: string;
description: string;
configFields: ConfigField[];
supportsOAuth: boolean;
};
type ConfigField = {
key: string;
label: string;
type: string;
placeholder?: string;
required?: boolean;
helpText?: string;
options?: { value: string; label: string }[];
};
export default function NewIntegrationPage() {
return (
<React.Suspense fallback={<div className="flex items-center justify-center py-20"><Loader2 className="size-6 animate-spin text-muted-foreground" /></div>}>
<NewIntegrationForm />
</React.Suspense>
);
}
function NewIntegrationForm() {
const router = useRouter();
const searchParams = useSearchParams();
const preselectedProvider = searchParams.get("provider") || "";
const [providers, setProviders] = React.useState<ProviderInfo[]>([]);
const [selectedProvider, setSelectedProvider] = React.useState(preselectedProvider);
const [name, setName] = React.useState("");
const [config, setConfig] = React.useState<Record<string, string>>({});
const [loading, setLoading] = React.useState(true);
const [creating, setCreating] = React.useState(false);
const [testStatus, setTestStatus] = React.useState<"idle" | "testing" | "success" | "error">("idle");
React.useEffect(() => {
fetch("/api/integrations")
.then((r) => r.json())
.then((data) => {
setProviders(data.providers || []);
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
React.useEffect(() => {
if (selectedProvider) {
const p = providers.find((pr) => pr.id === selectedProvider);
if (p && !name) {
setName(p.name);
}
}
}, [selectedProvider, providers, name]);
const provider = providers.find((p) => p.id === selectedProvider);
const handleTest = async () => {
if (!selectedProvider) return;
setTestStatus("testing");
try {
const res = await fetch("/api/integrations/test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider: selectedProvider, config }),
});
const data = await res.json();
if (res.ok && data.ok) {
setTestStatus("success");
toast.success(data.message || "Connection successful");
} else {
setTestStatus("error");
toast.error(data.error || "Connection failed");
}
} catch {
setTestStatus("error");
toast.error("Connection test failed");
}
};
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedProvider) {
toast.error("Select a provider");
return;
}
setCreating(true);
try {
const res = await fetch("/api/integrations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
provider: selectedProvider,
name: name || provider?.name || selectedProvider,
config,
}),
});
if (res.ok) {
const data = await res.json();
toast.success("Integration created");
if (provider?.supportsOAuth) {
window.location.href = `/api/integrations/oauth/${selectedProvider}/authorize?integrationId=${data.integration.id}`;
} else {
router.push(`/settings/integrations/${data.integration.id}`);
}
} else {
const data = await res.json();
toast.error(data.error || "Failed to create integration");
}
} catch {
toast.error("Failed to create integration");
} finally {
setCreating(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="space-y-6">
<Button
variant="ghost"
size="sm"
onClick={() => router.push("/settings/integrations")}
>
<ArrowLeft className="mr-1.5 size-3.5" />
Back to Integrations
</Button>
{!selectedProvider ? (
<div className="space-y-4">
<h2 className="text-lg font-semibold">Choose a provider</h2>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{providers.map((p) => (
<button
key={p.id}
onClick={() => setSelectedProvider(p.id)}
className="flex flex-col items-center gap-2 rounded-xl border border-border/50 p-5 text-center transition-colors hover:border-primary/50 hover:bg-primary/5"
>
<Plug className="size-6 text-muted-foreground" />
<span className="text-sm font-medium">{p.name}</span>
<span className="text-xs text-muted-foreground">
{p.description}
</span>
</button>
))}
</div>
</div>
) : (
<Card className="glass-card">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Plug className="size-4" />
Configure {provider?.name}
</CardTitle>
<CardDescription>{provider?.description}</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleCreate} className="space-y-4">
<div className="space-y-2">
<Label>Integration Name</Label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={provider?.name || "My Integration"}
/>
</div>
{provider?.configFields.map((field) => (
<div key={field.key} className="space-y-2">
<Label>
{field.label}
{field.required && (
<span className="text-destructive"> *</span>
)}
</Label>
{field.type === "select" && field.options ? (
<select
value={config[field.key] || ""}
onChange={(e) =>
setConfig((c) => ({
...c,
[field.key]: e.target.value,
}))
}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
>
<option value="">Select...</option>
{field.options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
) : (
<Input
type={field.type === "password" ? "password" : "text"}
value={config[field.key] || ""}
onChange={(e) =>
setConfig((c) => ({
...c,
[field.key]: e.target.value,
}))
}
placeholder={field.placeholder}
required={field.required}
/>
)}
{field.helpText && (
<p className="text-xs text-muted-foreground">
{field.helpText}
</p>
)}
</div>
))}
{provider?.supportsOAuth && (
<p className="text-sm text-muted-foreground">
After creating, you&apos;ll be redirected to authorize with{" "}
{provider.name}.
</p>
)}
<div className="flex gap-3">
<Button
type="button"
variant="outline"
onClick={() => setSelectedProvider("")}
>
Change Provider
</Button>
{!provider?.supportsOAuth && (
<Button
type="button"
variant="outline"
onClick={handleTest}
disabled={testStatus === "testing"}
>
{testStatus === "testing" ? (
<Loader2 className="mr-2 size-4 animate-spin" />
) : testStatus === "success" ? (
<Check className="mr-2 size-4 text-emerald-500" />
) : (
<Zap className="mr-2 size-4" />
)}
Test Connection
</Button>
)}
<Button type="submit" disabled={creating} className="flex-1">
{creating ? (
<Loader2 className="mr-2 size-4 animate-spin" />
) : (
<Plus className="mr-2 size-4" />
)}
{provider?.supportsOAuth
? "Create & Connect"
: "Create Integration"}
</Button>
</div>
</form>
</CardContent>
</Card>
)}
</div>
);
}