frontend_nyla/src/hooks/useConnections.ts

870 lines
No EOL
28 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { useApiRequest } from './useApi';
import { getApiBaseUrl } from '../../config/config';
import api from '../api';
import { usePermissions, type UserPermissions } from './usePermissions';
import {
fetchConnections as fetchConnectionsApi,
createConnection as createConnectionApi,
connectService as connectServiceApi,
disconnectService as disconnectServiceApi,
deleteConnection as deleteConnectionApi,
updateConnection as updateConnectionApi,
refreshMicrosoftToken as refreshMicrosoftTokenApi,
refreshGoogleToken as refreshGoogleTokenApi,
type Connection,
type AttributeDefinition,
type PaginationParams,
type CreateConnectionData,
type ConnectResponse
} from '../api/connectionApi';
// Re-export types for backward compatibility
export type { Connection, AttributeDefinition, PaginationParams, CreateConnectionData, ConnectResponse };
// Hook for managing connections
export function useConnections() {
const [connections, setConnections] = useState<Connection[]>([]);
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
const [pagination, setPagination] = useState<{
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
} | null>(null);
const [isConnecting, setIsConnecting] = useState(false);
const [connectError, setConnectError] = useState<string | null>(null);
const { request, isLoading, error } = useApiRequest<any, any>();
const { checkPermission } = usePermissions();
// Fetch attributes from backend
const fetchAttributes = useCallback(async () => {
try {
const response = await api.get('/api/attributes/UserConnection');
// Extract attributes from response
let attrs: AttributeDefinition[] = [];
if (response.data?.attributes && Array.isArray(response.data.attributes)) {
attrs = response.data.attributes;
} else if (Array.isArray(response.data)) {
attrs = response.data;
} else if (response.data && typeof response.data === 'object') {
const keys = Object.keys(response.data);
for (const key of keys) {
if (Array.isArray(response.data[key])) {
attrs = response.data[key];
break;
}
}
}
setAttributes(attrs);
return attrs;
} catch (error: any) {
console.error('Error fetching attributes:', error);
setAttributes([]);
return [];
}
}, []);
// Fetch permissions from backend
const fetchPermissions = useCallback(async () => {
try {
const perms = await checkPermission('DATA', 'UserConnection');
setPermissions(perms);
return perms;
} catch (error: any) {
console.error('Error fetching permissions:', error);
const defaultPerms: UserPermissions = {
view: false,
read: 'n',
create: 'n',
update: 'n',
delete: 'n',
};
setPermissions(defaultPerms);
return defaultPerms;
}
}, [checkPermission]);
// Fetch connections with pagination support
const fetchConnections = useCallback(async (params?: PaginationParams): Promise<Connection[]> => {
try {
const data = await fetchConnectionsApi(request, params);
// Handle paginated response
if (data && typeof data === 'object' && 'items' in data) {
const items = Array.isArray(data.items) ? data.items : [];
setConnections(items);
if (data.pagination) {
setPagination(data.pagination);
}
} else {
// Handle non-paginated response (backward compatibility)
const items = Array.isArray(data) ? data : [];
setConnections(items);
setPagination(null);
}
return Array.isArray(data) ? data : (data?.items || []);
} catch (error) {
console.error('Error fetching connections:', error);
setConnections([]);
setPagination(null);
throw error;
}
}, [request]);
// Create a new connection
const createConnection = async (connectionData: CreateConnectionData): Promise<Connection> => {
try {
const data = await createConnectionApi(request, connectionData);
// Update local state
setConnections(prev => {
const existing = prev.find(conn => conn.id === data.id);
if (existing) {
return prev.map(conn => conn.id === data.id ? data : conn);
} else {
return [...prev, data];
}
});
return data;
} catch (error) {
console.error('Error creating connection:', error);
throw error;
}
};
// Connect to a service (initiate OAuth)
const connectService = async (connectionId: string): Promise<ConnectResponse> => {
try {
const data = await connectServiceApi(request, connectionId);
return data;
} catch (error) {
console.error('Error connecting service:', error);
throw error;
}
};
// Disconnect from a service
const disconnectService = async (connectionId: string): Promise<{ message: string }> => {
try {
const data = await disconnectServiceApi(request, connectionId);
// Update local state
setConnections(prev =>
prev.map(conn =>
conn.id === connectionId
? { ...conn, status: 'inactive' as any, lastChecked: Math.floor(Date.now() / 1000) }
: conn
)
);
return data;
} catch (error) {
console.error('Error disconnecting service:', error);
throw error;
}
};
// Delete a connection
const deleteConnection = async (connectionId: string): Promise<{ message: string }> => {
try {
const data = await deleteConnectionApi(request, connectionId);
// Update local state
setConnections(prev => prev.filter(conn => conn.id !== connectionId));
return data;
} catch (error) {
console.error('Error deleting connection:', error);
throw error;
}
};
// Update a connection
const updateConnection = async (connectionId: string, updateData: Partial<Connection>): Promise<Connection> => {
try {
const data = await updateConnectionApi(request, connectionId, updateData);
// Update local state
setConnections(prev =>
prev.map(conn => conn.id === connectionId ? { ...conn, ...data } : conn)
);
return data;
} catch (error) {
console.error('Error updating connection:', error);
throw error;
}
};
// Refresh Microsoft token
const refreshMicrosoftToken = async (connectionId: string): Promise<Connection> => {
try {
const data = await refreshMicrosoftTokenApi(request, connectionId);
// Update local state
setConnections(prev =>
prev.map(conn => conn.id === connectionId ? { ...conn, ...data } : conn)
);
return data;
} catch (error) {
console.error('Error refreshing Microsoft token:', error);
throw error;
}
};
// Refresh Google token
const refreshGoogleToken = async (connectionId: string): Promise<Connection> => {
try {
const data = await refreshGoogleTokenApi(request, connectionId);
// Update local state
setConnections(prev =>
prev.map(conn => conn.id === connectionId ? { ...conn, ...data } : conn)
);
return data;
} catch (error) {
console.error('Error refreshing Google token:', error);
throw error;
}
};
// Connect with popup (OAuth flow)
const connectWithPopup = async (connectionId: string): Promise<void> => {
setIsConnecting(true);
setConnectError(null);
try {
// Get the OAuth URL from backend
const response = await connectService(connectionId);
if (!response.authUrl) {
throw new Error('No OAuth URL received from backend');
}
console.log('OAuth URL from backend:', response.authUrl);
return new Promise((resolve, reject) => {
// Convert relative URL to absolute URL if needed
let authUrl = response.authUrl;
if (authUrl.startsWith('/')) {
authUrl = `${getApiBaseUrl()}${authUrl}`;
}
// Open popup
const popup = window.open(
authUrl,
'oauth-connect',
'width=500,height=600,scrollbars=yes,resizable=yes'
);
if (!popup) {
setConnectError('Popup was blocked. Please allow popups and try again.');
setIsConnecting(false);
reject(new Error('Popup was blocked'));
return;
}
// Handle popup closing without completing auth
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
setIsConnecting(false);
console.log('OAuth popup closed');
// Refresh connections anyway in case it succeeded
fetchConnections();
resolve(); // Resolve instead of reject to avoid error on normal close
}
}, 1000);
// Listen for messages from the popup
const messageListener = (event: MessageEvent) => {
// Verify origin for security
const apiUrl = new URL(getApiBaseUrl());
if (event.origin !== apiUrl.origin) {
return;
}
if (
event.data.type === 'msft_connection_success' ||
event.data.type === 'google_connection_success' ||
event.data.type === 'clickup_connection_success'
) {
// Clean up
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
setIsConnecting(false);
console.log('OAuth connection successful');
// Refresh connections
fetchConnections();
resolve();
} else if (
event.data.type === 'msft_connection_error' ||
event.data.type === 'google_connection_error' ||
event.data.type === 'clickup_connection_error'
) {
// Handle error
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
setIsConnecting(false);
setConnectError(event.data.error || 'OAuth connection failed');
reject(new Error(event.data.error || 'OAuth connection failed'));
}
};
// Add message listener
window.addEventListener('message', messageListener);
});
} catch (error: any) {
setConnectError(error.message || 'OAuth connection failed');
setIsConnecting(false);
throw error;
}
};
// Create Google connection and open OAuth popup
const createGoogleConnectionAndAuth = async (): Promise<void> => {
try {
// Step 1: Create a Google connection
const newConnection = await createConnection({
type: 'google',
authority: 'google'
});
// Step 2: Get the OAuth URL from backend for this specific connection
const connectResponse = await connectServiceApi(request, newConnection.id);
if (!connectResponse.authUrl) {
throw new Error('No OAuth URL received from backend');
}
// Step 3: Open popup to the OAuth URL
const apiBaseUrl = getApiBaseUrl();
let authUrl = connectResponse.authUrl;
if (authUrl.startsWith('/')) {
authUrl = `${apiBaseUrl}${authUrl}`;
}
return new Promise((resolve, reject) => {
const popup = window.open(
authUrl,
'google-connection',
'width=500,height=600,scrollbars=yes,resizable=yes'
);
if (!popup) {
reject(new Error('Popup was blocked. Please allow popups and try again.'));
return;
}
// Handle popup closing without completing auth
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
console.log('Google OAuth popup closed');
// Refresh connections in case it succeeded
fetchConnections();
resolve();
}
}, 1000);
// Listen for messages from the popup
const messageListener = (event: MessageEvent) => {
// Verify origin for security
const apiUrl = new URL(apiBaseUrl);
if (event.origin !== apiUrl.origin) {
return;
}
if (event.data.type === 'google_connection_success' || event.data.type === 'google_auth_success') {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
console.log('Google connection successful');
// Refresh connections
fetchConnections();
resolve();
} else if (event.data.type === 'google_connection_error') {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
reject(new Error(event.data.error || 'Google connection failed'));
}
};
window.addEventListener('message', messageListener);
});
} catch (error) {
console.error('Error creating Google connection:', error);
throw error;
}
};
// Create ClickUp connection and open OAuth popup
const createClickupConnectionAndAuth = async (): Promise<void> => {
try {
const newConnection = await createConnection({
type: 'clickup',
authority: 'clickup',
});
const connectResponse = await connectServiceApi(request, newConnection.id);
if (!connectResponse.authUrl) {
throw new Error('No OAuth URL received from backend');
}
const apiBaseUrl = getApiBaseUrl();
let authUrl = connectResponse.authUrl;
if (authUrl.startsWith('/')) {
authUrl = `${apiBaseUrl}${authUrl}`;
}
return new Promise((resolve, reject) => {
const popup = window.open(
authUrl,
'clickup-connection',
'width=500,height=600,scrollbars=yes,resizable=yes'
);
if (!popup) {
reject(new Error('Popup was blocked. Please allow popups and try again.'));
return;
}
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
console.log('ClickUp OAuth popup closed');
fetchConnections();
resolve();
}
}, 1000);
const messageListener = (event: MessageEvent) => {
const apiUrl = new URL(apiBaseUrl);
if (event.origin !== apiUrl.origin) {
return;
}
if (event.data.type === 'clickup_connection_success') {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
console.log('ClickUp connection successful');
fetchConnections();
resolve();
} else if (event.data.type === 'clickup_connection_error') {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
reject(new Error(event.data.error || 'ClickUp connection failed'));
}
};
window.addEventListener('message', messageListener);
});
} catch (error) {
console.error('Error creating ClickUp connection:', error);
throw error;
}
};
// Create Microsoft connection and open OAuth popup
const createMicrosoftConnectionAndAuth = async (): Promise<void> => {
try {
// Step 1: Create a Microsoft connection
const newConnection = await createConnection({
type: 'msft',
authority: 'msft'
});
// Step 2: Get the OAuth URL from backend for this specific connection
const connectResponse = await connectServiceApi(request, newConnection.id);
if (!connectResponse.authUrl) {
throw new Error('No OAuth URL received from backend');
}
// Step 3: Open popup to the OAuth URL
const apiBaseUrl = getApiBaseUrl();
let authUrl = connectResponse.authUrl;
if (authUrl.startsWith('/')) {
authUrl = `${apiBaseUrl}${authUrl}`;
}
return new Promise((resolve, reject) => {
const popup = window.open(
authUrl,
'msft-connection',
'width=500,height=600,scrollbars=yes,resizable=yes'
);
if (!popup) {
reject(new Error('Popup was blocked. Please allow popups and try again.'));
return;
}
// Handle popup closing without completing auth
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
console.log('Microsoft OAuth popup closed');
// Refresh connections in case it succeeded
fetchConnections();
resolve();
}
}, 1000);
// Listen for messages from the popup
const messageListener = (event: MessageEvent) => {
// Verify origin for security
const apiUrl = new URL(apiBaseUrl);
if (event.origin !== apiUrl.origin) {
return;
}
if (event.data.type === 'msft_connection_success' || event.data.type === 'msft_auth_success') {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
console.log('Microsoft connection successful');
// Refresh connections
fetchConnections();
resolve();
} else if (event.data.type === 'msft_connection_error') {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
reject(new Error(event.data.error || 'Microsoft connection failed'));
}
};
window.addEventListener('message', messageListener);
});
} catch (error) {
console.error('Error creating Microsoft connection:', error);
throw error;
}
};
// Generate edit fields from attributes dynamically
const generateEditFieldsFromAttributes = useCallback((): Array<{
key: string;
label: string;
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'readonly';
editable?: boolean;
required?: boolean;
validator?: (value: any) => string | null;
minRows?: number;
maxRows?: number;
}> => {
if (!attributes || attributes.length === 0) {
return [];
}
const editableFields = attributes
.filter(attr => {
// Filter out non-editable fields
const nonEditableFields = ['id', 'userId', 'connectedAt', 'lastChecked'];
return !nonEditableFields.includes(attr.name);
})
.map(attr => {
// Map attribute type to form field type
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'readonly' = 'string';
if (attr.type === 'boolean') {
fieldType = 'boolean';
} else if (attr.type === 'date') {
fieldType = 'date';
} else if (attr.type === 'enum' && attr.filterOptions) {
fieldType = 'enum';
} else if (attr.name.toLowerCase().includes('email')) {
fieldType = 'email';
}
return {
key: attr.name,
label: attr.label || attr.name,
type: fieldType,
editable: true,
required: false
};
});
return editableFields;
}, [attributes]);
// Ensure attributes are loaded
const ensureAttributesLoaded = useCallback(async () => {
if (attributes && attributes.length > 0) {
return attributes;
}
const fetchedAttributes = await fetchAttributes();
return fetchedAttributes;
}, [attributes, fetchAttributes]);
// Fetch attributes and permissions on mount
useEffect(() => {
fetchAttributes();
fetchPermissions();
}, [fetchAttributes, fetchPermissions]);
// Initial fetch
useEffect(() => {
fetchConnections();
}, [fetchConnections]);
// Optimistically update a connection in local state
const updateOptimistically = useCallback((connectionId: string, updateData: Partial<Connection>) => {
setConnections(prev =>
prev.map(conn => conn.id === connectionId ? { ...conn, ...updateData } : conn)
);
}, []);
// Generic inline update handler for FormGeneratorTable
const handleInlineUpdate = useCallback(async (connectionId: string, changes: Partial<Connection>, existingRow?: any) => {
if (!existingRow) {
throw new Error('Existing row data required for inline update');
}
try {
const result = await updateConnection(connectionId, changes);
return { success: true, data: result };
} catch (error: any) {
throw new Error(error.message || 'Failed to update');
}
}, [updateConnection]);
// Fetch connection by ID
const fetchConnectionById = useCallback(async (connectionId: string): Promise<Connection | null> => {
try {
// Since there's no individual connection endpoint, find from current list or fetch all
const existing = connections.find(c => c.id === connectionId);
if (existing) return existing;
const data = await fetchConnectionsApi(request);
const items = Array.isArray(data) ? data : (data?.items || []);
return items.find((c: Connection) => c.id === connectionId) || null;
} catch (error) {
console.error('Error fetching connection by ID:', error);
return null;
}
}, [connections, request]);
return {
connections,
data: connections, // Alias for FormGenerator compatibility
fetchConnections,
refetch: fetchConnections, // Alias for FormGenerator compatibility
createConnection,
updateConnection,
connectService,
disconnectService,
deleteConnection,
refreshMicrosoftToken,
refreshGoogleToken,
connectWithPopup,
createGoogleConnectionAndAuth,
createMicrosoftConnectionAndAuth,
createClickupConnectionAndAuth,
isLoading,
loading: isLoading, // Alias for FormGenerator compatibility
isConnecting,
error: error || connectError,
// Attributes and permissions for dynamic column/button generation
attributes,
permissions,
pagination,
generateEditFieldsFromAttributes,
ensureAttributesLoaded,
fetchAttributes,
fetchPermissions,
// Additional methods for FormGenerator
updateOptimistically,
handleInlineUpdate,
fetchConnectionById
};
}
// Hook for OAuth connection popup flow (similar to useMsalAuth)
export function useOAuthConnect() {
const { connectService, fetchConnections } = useConnections();
const [isConnecting, setIsConnecting] = useState(false);
const [connectError, setConnectError] = useState<string | null>(null);
const connectWithPopup = async (connectionId: string): Promise<void> => {
setIsConnecting(true);
setConnectError(null);
try {
// Get the OAuth URL from backend
const response = await connectService(connectionId);
if (!response.authUrl) {
throw new Error('No OAuth URL received from backend');
}
console.log('OAuth URL from backend:', response.authUrl);
return new Promise((resolve, reject) => {
// Convert relative URL to absolute URL if needed
let authUrl = response.authUrl;
if (authUrl.startsWith('/')) {
authUrl = `${getApiBaseUrl()}${authUrl}`;
}
// Open popup using the same pattern as useAuthentication.ts
const popup = window.open(
authUrl,
'oauth-connect',
'width=500,height=600,scrollbars=yes,resizable=yes'
);
if (!popup) {
setConnectError('Popup was blocked. Please allow popups and try again.');
setIsConnecting(false);
reject(new Error('Popup was blocked'));
return;
}
// Handle popup closing without completing auth
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
setIsConnecting(false);
console.log('OAuth popup closed');
// Refresh connections anyway in case it succeeded
fetchConnections();
setConnectError('Authentication was cancelled');
reject(new Error('Authentication was cancelled'));
}
}, 1000);
// Listen for messages from the popup (similar to useMsalAuth)
const messageListener = (event: MessageEvent) => {
// Verify origin for security
const apiUrl = new URL(getApiBaseUrl());
if (event.origin !== apiUrl.origin) {
return;
}
if (
event.data.type === 'msft_connection_success' ||
event.data.type === 'google_connection_success' ||
event.data.type === 'clickup_connection_success'
) {
// Clean up - IMPORTANT: clear the checkClosed interval first
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
setIsConnecting(false);
console.log('OAuth connection successful');
// Refresh connections
fetchConnections();
resolve();
} else if (
event.data.type === 'msft_connection_error' ||
event.data.type === 'google_connection_error' ||
event.data.type === 'clickup_connection_error'
) {
// Handle error - also clear the checkClosed interval
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
setIsConnecting(false);
setConnectError(event.data.error || 'OAuth connection failed');
reject(new Error(event.data.error || 'OAuth connection failed'));
}
};
// Add message listener
window.addEventListener('message', messageListener);
});
} catch (error: any) {
setConnectError(error.message || 'OAuth connection failed');
setIsConnecting(false);
throw error;
}
};
return {
connectWithPopup,
isConnecting,
error: connectError
};
}
// Hook for disconnecting services
export function useDisconnect() {
const { disconnectService, fetchConnections } = useConnections();
const [isDisconnecting, setIsDisconnecting] = useState(false);
const [disconnectError, setDisconnectError] = useState<string | null>(null);
const disconnect = async (connectionId: string): Promise<void> => {
setIsDisconnecting(true);
setDisconnectError(null);
try {
await disconnectService(connectionId);
console.log('Service disconnected successfully');
// Refresh connections to update the status
await fetchConnections();
} catch (error: any) {
setDisconnectError(error.message || 'Disconnect failed');
console.error('Error disconnecting service:', error);
throw error;
} finally {
setIsDisconnecting(false);
}
};
return {
disconnect,
isDisconnecting,
error: disconnectError
};
}
// Hook for individual connection operations
export function useConnection(connectionId?: string) {
const [connection, setConnection] = useState<Connection | null>(null);
const { request, isLoading, error } = useApiRequest<any, Connection[]>();
const fetchConnection = async (id: string = connectionId!): Promise<Connection | null> => {
if (!id) return null;
try {
// Since there's no individual connection endpoint, fetch all and filter
const data = await fetchConnectionsApi(request);
const connections = Array.isArray(data) ? data : (data?.items || []);
const foundConnection = connections.find((conn: Connection) => conn.id === id);
setConnection(foundConnection || null);
return foundConnection || null;
} catch (error) {
console.error('Error fetching connection:', error);
setConnection(null);
throw error;
}
};
return {
connection,
fetchConnection,
isLoading,
error
};
}