feat(CC-083): add API Connections page (external REST/GraphQL connections with auth config)

This commit is contained in:
admin-valentin 2026-08-02 17:22:13 +00:00
parent b71cb4a88a
commit 5a124fd015

View file

@ -0,0 +1,138 @@
'use client';
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import Link from 'next/link';
import { apiFetch } from '../../../../lib/api';
import { useSession } from '../../../../components/session-provider';
interface DataSource {
id: string; name: string; type: string; status: string;
config: Record<string, unknown> | null; lastSyncAt: string | null; createdAt: string;
}
const API_TYPES = ['rest', 'graphql', 'grpc', 'soap', 'openapi'];
export default function APIConnectionsPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const qc = useQueryClient();
const [showCreate, setShowCreate] = useState(false);
const [form, setForm] = useState({ name: '', type: 'rest', baseUrl: '', authType: 'bearer', authValue: '', scope: '' });
const { data: sources = [], isLoading } = useQuery({
queryKey: ['api-conn', tenantId],
queryFn: () => apiFetch<DataSource[]>('/v1/data-sources', { tenantId }),
enabled: Boolean(tenantId), staleTime: 60_000,
});
const apiConns = sources.filter((s) => API_TYPES.includes(s.type) || (s.config as Record<string,string>)?.authType);
const createMut = useMutation({
mutationFn: () => apiFetch('/v1/data-sources', { tenantId, method: 'POST', body: {
name: form.name, type: form.type,
config: { baseUrl: form.baseUrl, authType: form.authType, scope: form.scope || undefined },
}}),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['api-conn', tenantId] }); setShowCreate(false); setForm({ name: '', type: 'rest', baseUrl: '', authType: 'bearer', authValue: '', scope: '' }); },
});
const testMut = useMutation({
mutationFn: (id: string) => apiFetch(`/v1/data-sources/${id}/test`, { tenantId, method: 'POST', body: {} }),
});
return (
<div className="max-w-3xl space-y-6 p-6">
<div className="flex items-start justify-between flex-wrap gap-3">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Conexiuni API</h1>
<p className="text-sm text-ink-faint mt-1">
{isLoading ? 'Se încarcă…' : `${apiConns.length} API-uri externe conectate`}
</p>
</div>
<button onClick={() => setShowCreate(true)}
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary/90">
+ Conexiune nouă
</button>
</div>
{showCreate && (
<div className="card p-5 space-y-3">
<p className="text-sm font-semibold text-ink">Conexiune API nouă</p>
<div className="grid gap-3 sm:grid-cols-2">
<input placeholder="Nume (ex: Stripe API)" value={form.name}
onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
<select value={form.type} onChange={(e) => setForm((p) => ({ ...p, type: e.target.value }))}
className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring">
{API_TYPES.map((t) => <option key={t} value={t}>{t.toUpperCase()}</option>)}
</select>
<input placeholder="Base URL (https://api.example.com)" value={form.baseUrl}
onChange={(e) => setForm((p) => ({ ...p, baseUrl: e.target.value }))}
className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring sm:col-span-2 font-mono text-xs" />
<select value={form.authType} onChange={(e) => setForm((p) => ({ ...p, authType: e.target.value }))}
className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring">
<option value="bearer">Bearer Token</option>
<option value="apikey">API Key (header)</option>
<option value="oauth2">OAuth2</option>
<option value="basic">Basic Auth</option>
<option value="none">Fără autentificare</option>
</select>
<input placeholder="Scope (opțional)" value={form.scope}
onChange={(e) => setForm((p) => ({ ...p, scope: e.target.value }))}
className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<div className="card p-3 bg-warn/5 border-warn/30">
<p className="text-[10px] text-warn">Cheile și token-urile se stochează criptat. Nu le introduce în câmpul URL.</p>
</div>
<div className="flex gap-2">
<button onClick={() => createMut.mutate()} disabled={!form.name || !form.baseUrl || createMut.isPending}
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white disabled:opacity-50">
{createMut.isPending ? 'Se salvează…' : 'Salvează'}
</button>
<button onClick={() => setShowCreate(false)} className="rounded-lg border px-4 py-2 text-sm text-ink">Anulează</button>
</div>
</div>
)}
{isLoading ? (
<div className="text-center text-sm text-ink-faint py-8">Se încarcă</div>
) : apiConns.length === 0 ? (
<div className="card p-8 text-center space-y-2">
<p className="text-2xl">🔌</p>
<p className="text-sm text-ink-faint">Nicio conexiune API externă configurată.</p>
<p className="text-xs text-ink-faint">Adaugă API-uri externe (Stripe, HubSpot, etc.) pentru a le folosi în automatizări.</p>
</div>
) : (
<div className="card divide-y divide-border/50">
{apiConns.map((conn) => {
const cfg = conn.config as Record<string, string> ?? {};
return (
<div key={conn.id} className="flex items-center gap-4 p-4">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-ink">{conn.name}</p>
<div className="flex flex-wrap gap-2 mt-0.5 text-[10px] text-ink-faint">
<span className="uppercase font-mono">{conn.type}</span>
{cfg.baseUrl && <span className="font-mono truncate max-w-48">{cfg.baseUrl}</span>}
{cfg.authType && <span>{cfg.authType}</span>}
{conn.lastSyncAt && <span>Testat: {new Date(conn.lastSyncAt).toLocaleDateString('ro-RO')}</span>}
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<span className={`rounded-full px-2 py-0.5 text-[9px] font-bold ${conn.status === 'active' ? 'bg-signal-ok/10 text-signal-ok' : 'bg-muted text-ink-faint'}`}>
{conn.status}
</span>
<button onClick={() => testMut.mutate(conn.id)} disabled={testMut.isPending}
className="rounded border px-2 py-1 text-[10px] text-ink hover:bg-muted/50 disabled:opacity-50">
Test
</button>
</div>
</div>
);
})}
</div>
)}
<Link href="/dashboard/integrations" className="text-xs text-primary hover:underline"> Toate integrările</Link>
</div>
);
}