2025-07-28 15:37:21 +02:00

622 lines
21 KiB
TypeScript

"use client";
import { GraphQLClient, gql } from "graphql-request";
import Image from "next/image";
import { useEffect, useState } from "react";
const GET_CONNECTORS_QUERY = gql`
query GetConnectors {
connectors {
success
message
connectors {
id
type
enabled
config
metadata {
id
target
platform
name
description
logo
logoDark
}
createdAt
updatedAt
}
availableConnectors {
id
target
platform
name
description
logo
logoDark
configTemplate
formItems {
key
type
label
placeholder
required
defaultValue
description
}
isAdded
}
}
}
`;
const CREATE_CONNECTOR_MUTATION = gql`
mutation CreateConnector($input: CreateConnectorInput!) {
createConnector(input: $input) {
success
message
connector {
id
type
enabled
metadata {
name
platform
}
}
}
}
`;
const UPDATE_CONNECTOR_MUTATION = gql`
mutation UpdateConnector($input: UpdateConnectorInput!) {
updateConnector(input: $input) {
success
message
connector {
id
enabled
}
}
}
`;
const DELETE_CONNECTOR_MUTATION = gql`
mutation DeleteConnector($id: ID!) {
deleteConnector(id: $id) {
success
message
}
}
`;
const TOGGLE_CONNECTOR_MUTATION = gql`
mutation ToggleConnector($id: ID!, $enabled: Boolean!) {
toggleConnector(id: $id, enabled: $enabled) {
success
message
connector {
id
enabled
}
}
}
`;
const TEST_CONNECTOR_MUTATION = gql`
mutation TestConnector($id: ID!) {
testConnector(id: $id) {
success
message
}
}
`;
interface Connector {
id: string;
type: string;
enabled: boolean;
config: Record<string, unknown>;
metadata: {
id: string;
target: string;
platform?: string;
name: string | Record<string, string>;
description: string | Record<string, string>;
logo: string;
logoDark: string | null;
};
createdAt: string;
updatedAt: string;
}
interface FactoryConnector {
id: string;
target: string;
platform?: string;
name: string | Record<string, string>;
description: string | Record<string, string>;
logo: string;
logoDark: string | null;
configTemplate: Record<string, unknown> | null;
formItems: FormItem[];
isAdded: boolean;
}
interface FormItem {
key: string;
type: string;
label: string | Record<string, string>;
placeholder?: string | Record<string, string>;
required?: boolean;
defaultValue?: string | boolean | number | string[];
description?: string | Record<string, string>;
}
interface ConnectorsResponse {
connectors: {
success: boolean;
message?: string;
connectors?: Connector[];
availableConnectors?: FactoryConnector[];
};
}
export default function ConnectorsPage() {
const [connectors, setConnectors] = useState<Connector[]>([]);
const [availableConnectors, setAvailableConnectors] = useState<FactoryConnector[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedFactory, setSelectedFactory] = useState<FactoryConnector | null>(null);
const [showConfig, setShowConfig] = useState(false);
const [configForm, setConfigForm] = useState<Record<string, string | boolean | number | string[]>>({});
useEffect(() => {
fetchConnectors();
}, []);
const fetchConnectors = async () => {
setLoading(true);
setError(null);
try {
const storedToken = localStorage.getItem("accessToken");
if (!storedToken) {
setError("Aucun token trouvé. Veuillez vous connecter.");
return;
}
const authenticatedClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql", {
headers: {
Authorization: `Bearer ${storedToken}`,
},
});
const response = await authenticatedClient.request<ConnectorsResponse>(GET_CONNECTORS_QUERY);
if (response.connectors.success) {
setConnectors(response.connectors.connectors || []);
setAvailableConnectors(response.connectors.availableConnectors || []);
} else {
setError(response.connectors.message || "Erreur lors du chargement");
}
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : "Une erreur est survenue";
setError(errorMessage);
console.error("Erreur lors du chargement des connecteurs:", err);
} finally {
setLoading(false);
}
};
const handleCreateConnector = async () => {
if (!selectedFactory) return;
try {
const storedToken = localStorage.getItem("accessToken");
if (!storedToken) {
setError("Aucun token trouvé. Veuillez vous connecter.");
return;
}
const authenticatedClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql", {
headers: {
Authorization: `Bearer ${storedToken}`,
},
});
const response = (await authenticatedClient.request(CREATE_CONNECTOR_MUTATION, {
input: {
connectorId: selectedFactory.id,
config: configForm,
},
})) as {
createConnector: {
success: boolean;
message?: string;
connector?: {
id: string;
enabled: boolean;
metadata: {
name: string | Record<string, string>;
platform?: string;
};
};
};
};
if (response.createConnector.success) {
await fetchConnectors();
setShowConfig(false);
setSelectedFactory(null);
setConfigForm({});
} else {
setError(response.createConnector.message || "Erreur lors de la création");
}
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : "Une erreur est survenue";
setError(errorMessage);
}
};
const handleToggleConnector = async (id: string, enabled: boolean) => {
try {
const storedToken = localStorage.getItem("accessToken");
if (!storedToken) return;
const authenticatedClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql", {
headers: {
Authorization: `Bearer ${storedToken}`,
},
});
await authenticatedClient.request(TOGGLE_CONNECTOR_MUTATION, {
id,
enabled,
});
await fetchConnectors();
} catch (err: unknown) {
console.error("Erreur lors du basculement:", err);
}
};
const handleDeleteConnector = async (id: string) => {
if (!confirm("Êtes-vous sûr de vouloir supprimer ce connecteur ?")) return;
try {
const storedToken = localStorage.getItem("accessToken");
if (!storedToken) return;
const authenticatedClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql", {
headers: {
Authorization: `Bearer ${storedToken}`,
},
});
await authenticatedClient.request(DELETE_CONNECTOR_MUTATION, { id });
await fetchConnectors();
} catch (err: unknown) {
console.error("Erreur lors de la suppression:", err);
}
};
const handleTestConnector = async (id: string) => {
try {
const storedToken = localStorage.getItem("accessToken");
if (!storedToken) return;
const authenticatedClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql", {
headers: {
Authorization: `Bearer ${storedToken}`,
},
});
const response = (await authenticatedClient.request(TEST_CONNECTOR_MUTATION, { id })) as {
testConnector: {
success: boolean;
message?: string;
};
};
alert(response.testConnector.message || "Test terminé");
} catch (err: unknown) {
console.error("Erreur lors du test:", err);
}
};
const openConfigDialog = (factory: FactoryConnector) => {
setSelectedFactory(factory);
setShowConfig(true);
// Initialiser le formulaire avec les valeurs par défaut
const initialForm: Record<string, string | boolean | number | string[]> = {};
factory.formItems.forEach(item => {
if (item.defaultValue !== undefined) {
initialForm[item.key] = item.defaultValue;
}
});
setConfigForm(initialForm);
};
const getConnectorName = (nameObj: string | Record<string, string>): string => {
if (typeof nameObj === "object" && nameObj !== null) {
return nameObj.en || nameObj.fr || Object.values(nameObj)[0] || "Connecteur";
}
return nameObj || "Connecteur";
};
const getConnectorDescription = (descObj: string | Record<string, string>): string => {
if (typeof descObj === "object" && descObj !== null) {
return descObj.en || descObj.fr || Object.values(descObj)[0] || "";
}
return descObj || "";
};
return (
<div className="min-h-screen bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
<div className="mx-auto max-w-6xl">
<div className="rounded-lg bg-white p-8 shadow-lg">
<h1 className="mb-6 text-center text-2xl font-bold text-gray-900">Gestion des Connecteurs Logto</h1>
{error && (
<div className="mb-6 rounded-md border border-red-200 bg-red-50 p-4">
<p className="text-sm text-red-700">{error}</p>
</div>
)}
{loading ? (
<div className="text-center">
<p>Chargement des connecteurs...</p>
</div>
) : (
<div className="space-y-8">
{/* Connecteurs configurés */}
<div>
<h2 className="mb-4 text-xl font-semibold text-gray-800">
Connecteurs configurés ({connectors.length})
</h2>
{connectors.length === 0 ? (
<p className="text-gray-500">Aucun connecteur configuré</p>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{connectors.map(connector => (
<div key={connector.id} className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
<div className="flex items-center justify-between">
<Image
src={connector.metadata.logo}
alt={getConnectorName(connector.metadata.name)}
width={32}
height={32}
className="h-8 w-8"
/>
<span
className={`rounded-full px-2 py-1 text-xs ${
connector.enabled ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"
}`}
>
{connector.enabled ? "Activé" : "Désactivé"}
</span>
</div>
<h3 className="mt-2 font-medium text-gray-900">{getConnectorName(connector.metadata.name)}</h3>
<p className="text-sm text-gray-600">{connector.metadata.platform || connector.type}</p>
<div className="mt-4 flex space-x-2">
<button
onClick={() => handleToggleConnector(connector.id, !connector.enabled)}
className={`rounded px-3 py-1 text-xs ${
connector.enabled
? "bg-red-100 text-red-700 hover:bg-red-200"
: "bg-green-100 text-green-700 hover:bg-green-200"
}`}
>
{connector.enabled ? "Désactiver" : "Activer"}
</button>
<button
onClick={() => handleTestConnector(connector.id)}
className="rounded bg-blue-100 px-3 py-1 text-xs text-blue-700 hover:bg-blue-200"
>
Tester
</button>
<button
onClick={() => handleDeleteConnector(connector.id)}
className="rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
>
Supprimer
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Connecteurs disponibles */}
<div>
<h2 className="mb-4 text-xl font-semibold text-gray-800">Connecteurs disponibles</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{availableConnectors.map(factory => (
<div
key={factory.id}
className={`rounded-lg border p-4 shadow-sm ${
factory.isAdded ? "border-gray-300 bg-gray-50" : "border-gray-200 bg-white"
}`}
>
<div className="flex items-center justify-between">
<Image
src={factory.logo}
alt={getConnectorName(factory.name)}
width={32}
height={32}
className="h-8 w-8"
/>
{factory.isAdded && (
<span className="rounded-full bg-green-100 px-2 py-1 text-xs text-green-800">Ajouté</span>
)}
</div>
<h3 className="mt-2 font-medium text-gray-900">{getConnectorName(factory.name)}</h3>
<p className="text-sm text-gray-600">{getConnectorDescription(factory.description)}</p>
{!factory.isAdded && (
<button
onClick={() => openConfigDialog(factory)}
className="mt-4 w-full rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700"
>
Configurer
</button>
)}
</div>
))}
</div>
</div>
</div>
)}
{/* Dialog de configuration */}
{showConfig && selectedFactory && (
<div className="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
<div className="max-h-[80vh] w-full max-w-md overflow-y-auto rounded-lg bg-white p-6">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-medium">Configurer {getConnectorName(selectedFactory.name)}</h3>
<button onClick={() => setShowConfig(false)} className="text-gray-400 hover:text-gray-600">
</button>
</div>
<form
onSubmit={e => {
e.preventDefault();
handleCreateConnector();
}}
className="space-y-4"
>
{selectedFactory.formItems.map(item => (
<div key={item.key}>
<label className="block text-sm font-medium text-gray-700">
{typeof item.label === "object" && item.label !== null
? item.label.en || item.label.fr
: item.label}
{item.required && <span className="text-red-500">*</span>}
</label>
{item.type === "Password" ? (
<input
type="password"
required={item.required}
value={String(configForm[item.key] || "")}
onChange={e => setConfigForm({ ...configForm, [item.key]: e.target.value })}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
) : item.type === "Switch" ? (
<input
type="checkbox"
checked={Boolean(configForm[item.key])}
onChange={e => setConfigForm({ ...configForm, [item.key]: e.target.checked })}
className="mt-1"
/>
) : item.type === "MultiSelect" ? (
<select
multiple
required={item.required}
value={Array.isArray(configForm[item.key]) ? (configForm[item.key] as string[]) : []}
onChange={e => {
const values = Array.from(e.target.selectedOptions, option => option.value);
setConfigForm({ ...configForm, [item.key]: values });
}}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
{/* Options would come from the form item configuration */}
</select>
) : item.type === "Number" ? (
<input
type="number"
required={item.required}
value={Number(configForm[item.key] || 0)}
onChange={e => setConfigForm({ ...configForm, [item.key]: Number(e.target.value) })}
placeholder={
typeof item.placeholder === "object" && item.placeholder !== null
? item.placeholder.en || item.placeholder.fr
: item.placeholder || ""
}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
) : (
<input
type="text"
required={item.required}
value={String(configForm[item.key] || "")}
onChange={e => setConfigForm({ ...configForm, [item.key]: e.target.value })}
placeholder={
typeof item.placeholder === "object" && item.placeholder !== null
? item.placeholder.en || item.placeholder.fr
: item.placeholder || ""
}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
)}
{item.description && (
<p className="mt-1 text-xs text-gray-500">
{typeof item.description === "object" && item.description !== null
? item.description.en || item.description.fr
: item.description}
</p>
)}
</div>
))}
<div className="flex space-x-4 pt-4">
<button type="submit" className="flex-1 rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">
Créer
</button>
<button
type="button"
onClick={() => setShowConfig(false)}
className="flex-1 rounded border border-gray-300 px-4 py-2 text-gray-700 hover:bg-gray-50"
>
Annuler
</button>
</div>
</form>
</div>
</div>
)}
{/* Navigation */}
<div className="mt-8 rounded-md border border-blue-200 bg-blue-50 p-4">
<h3 className="text-sm font-medium text-blue-800">Navigation</h3>
<div className="mt-2 space-y-1">
<div>
<a href="/login" className="text-sm text-blue-600 underline hover:text-blue-500">
🔐 Page de connexion
</a>
</div>
<div>
<a href="/signup" className="text-sm text-blue-600 underline hover:text-blue-500">
📝 Page d&apos;inscription
</a>
</div>
<div>
<a href="/profile" className="text-sm text-blue-600 underline hover:text-blue-500">
👤 Voir votre profil
</a>
</div>
<div>
<a href="/test-users" className="text-sm text-blue-600 underline hover:text-blue-500">
👥 Liste des utilisateurs
</a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}