/** * ConnectionsPage * * Page for managing OAuth connections (Google, Microsoft) using FormGeneratorTable. * Follows the pattern established in AdminUsersPage/WorkflowsPage. */ import React, { useState, useMemo, useEffect } from 'react'; import { useConnections, type Connection } from '../../hooks/useConnections'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm'; import { FaSync, FaPlug, FaGoogle, FaMicrosoft, FaLink, FaRedo, FaShieldAlt, FaTasks } from 'react-icons/fa'; import { getApiBaseUrl } from '../../../config/config'; import styles from '../admin/Admin.module.css'; export const ConnectionsPage: React.FC = () => { // Use the consolidated hook const { data: connections, attributes, permissions, pagination, loading, error, refetch, fetchConnectionById, updateOptimistically, deleteConnection, handleInlineUpdate, createGoogleConnectionAndAuth, createMicrosoftConnectionAndAuth, createClickupConnectionAndAuth, connectWithPopup, refreshMicrosoftToken, refreshGoogleToken, isConnecting, } = useConnections(); const [editingConnection, setEditingConnection] = useState(null); const [deletingConnections, setDeletingConnections] = useState>(new Set()); const [refreshingConnections, setRefreshingConnections] = useState>(new Set()); const [adminConsentPending, setAdminConsentPending] = useState(false); // Initial fetch useEffect(() => { refetch(); }, []); // Generate columns from attributes - hide internal/redundant fields const columns = useMemo(() => { const hiddenColumns = ['id', 'externalId', 'tokenStatus', 'tokenExpiresAt', 'grantedScopes']; return (attributes || []) .filter(attr => !hiddenColumns.includes(attr.name)) .map(attr => { const col: any = { key: attr.name, label: attr.label || attr.name, type: attr.type as any, sortable: attr.sortable !== false, filterable: attr.filterable !== false, searchable: attr.searchable !== false, width: attr.width || 150, minWidth: attr.minWidth || 100, maxWidth: attr.maxWidth || 400, fkSource: (attr as any).fkSource, fkDisplayField: (attr as any).fkDisplayField, }; // Resolve userId to username via FK if (attr.name === 'userId') { col.fkSource = '/api/users/'; col.fkDisplayField = 'username'; col.label = 'User'; } return col; }); }, [attributes]); // Check permissions const canCreate = permissions?.create !== 'n'; const canUpdate = permissions?.update !== 'n'; const canDelete = permissions?.delete !== 'n'; // Handle edit click const handleEditClick = async (connection: Connection) => { const fullConnection = await fetchConnectionById(connection.id); if (fullConnection) { setEditingConnection(fullConnection as Connection); } }; // Handle edit submit const handleEditSubmit = async (data: Partial) => { if (!editingConnection) return; // Note: updateConnection is handled through the hook try { // Ensure authority is properly typed - filter and validate authority value const updateData: Partial = { ...data }; // Validate and set authority if present if (data.authority) { if ( data.authority === 'local' || data.authority === 'google' || data.authority === 'msft' || data.authority === 'clickup' ) { updateData.authority = data.authority; } else { // Remove invalid authority value delete (updateData as any).authority; } } await handleInlineUpdate(editingConnection.id, updateData, editingConnection); setEditingConnection(null); refetch(); } catch (error) { console.error('Error updating connection:', error); } }; // Handle delete (confirmation handled by DeleteActionButton) const handleDelete = async (connection: Connection) => { setDeletingConnections(prev => new Set(prev).add(connection.id)); try { await deleteConnection(connection.id); refetch(); } catch (error) { console.error('Error deleting connection:', error); } finally { setDeletingConnections(prev => { const newSet = new Set(prev); newSet.delete(connection.id); return newSet; }); } }; // Handle connect const handleConnect = async (connection: Connection) => { try { await connectWithPopup(connection.id); refetch(); } catch (error) { console.error('Error connecting:', error); } }; // Handle refresh token const handleRefresh = async (connection: Connection) => { setRefreshingConnections(prev => new Set(prev).add(connection.id)); try { if (connection.authority === 'msft') { await refreshMicrosoftToken(connection.id); } else if (connection.authority === 'google') { await refreshGoogleToken(connection.id); } refetch(); } catch (error) { console.error('Error refreshing token:', error); } finally { setRefreshingConnections(prev => { const newSet = new Set(prev); newSet.delete(connection.id); return newSet; }); } }; // Handle create Google connection const handleCreateGoogle = async () => { try { await createGoogleConnectionAndAuth(); refetch(); } catch (error) { console.error('Error creating Google connection:', error); } }; // Handle create Microsoft connection const handleCreateMicrosoft = async () => { try { await createMicrosoftConnectionAndAuth(); refetch(); } catch (error) { console.error('Error creating Microsoft connection:', error); } }; // Handle create ClickUp connection const handleCreateClickup = async () => { try { await createClickupConnectionAndAuth(); refetch(); } catch (error) { console.error('Error creating ClickUp connection:', error); } }; // Open Microsoft Admin Consent flow in a popup const handleAdminConsent = () => { setAdminConsentPending(true); const consentUrl = `${getApiBaseUrl()}/api/msft/adminconsent`; const popup = window.open(consentUrl, 'msft-admin-consent', 'width=600,height=700,scrollbars=yes,resizable=yes'); if (!popup) { setAdminConsentPending(false); return; } const checkClosed = setInterval(() => { if (popup.closed) { clearInterval(checkClosed); setAdminConsentPending(false); refetch(); } }, 1000); }; // Form attributes for edit modal const formAttributes = useMemo(() => { const excludedFields = ['id', 'mandateId', 'userId', 'sysCreatedBy', 'sysCreatedAt', 'sysModifiedAt', 'connectedAt', 'lastChecked']; return (attributes || []) .filter(attr => !excludedFields.includes(attr.name)); }, [attributes]); if (error) { return (
⚠️

Fehler beim Laden der Verbindungen: {error}

); } return (

Verbindungen

Persönliche Datenanbindungen verwalten (OAuth: Google, Microsoft, ClickUp)

{canCreate && ( <> )}
{loading && (!connections || connections.length === 0) ? (
Lade Verbindungen...
) : !connections || connections.length === 0 ? (

Keine Verbindungen vorhanden

Verbinden Sie Ihr Google-, Microsoft- oder ClickUp-Konto, um loszulegen.

{canCreate && (
)}
) : ( deletingConnections.has(row.id), }] : []), ]} customActions={[ { id: 'connect', icon: , onClick: handleConnect, title: 'Verbinden', visible: (row: Connection) => row.status !== 'active', loading: () => isConnecting, }, { id: 'refresh', icon: , onClick: handleRefresh, title: 'Token erneuern', visible: (row: Connection) => row.status === 'active', loading: (row: Connection) => refreshingConnections.has(row.id), }, ]} onDelete={handleDelete} hookData={{ refetch, permissions, pagination, handleDelete: deleteConnection, handleInlineUpdate, updateOptimistically, }} emptyMessage="Keine Verbindungen gefunden" /> )}
{/* Edit Modal */} {editingConnection && (
setEditingConnection(null)}>
e.stopPropagation()}>

Verbindung bearbeiten

{formAttributes.length === 0 ? (
Lade Formular...
) : ( setEditingConnection(null)} submitButtonText="Speichern" cancelButtonText="Abbrechen" /> )}
)}
); }; export default ConnectionsPage;