Integration Components
The Integration Components provide comprehensive third-party service integration functionality for the WorkPayCore Frontend application. These components handle external service connections, authentication flows, integration management, and service configuration with consistent UI patterns.
Overview
This document covers integration-related components that enable connections to external services like QuickBooks, Xero, Sage, WaveApps, and other third-party platforms with OAuth authentication, status management, and configuration interfaces.
Components Overview
Core Integration Components
- AccountsCard - Integration service card with connection status
- ConnectionStatusModal - Connection/disconnection modal interface
- AuthorizeCard - Service authorization and reauthorization interface
- SettingsIntegrationPageWrapper - Integration page layout wrapper
Integration Management Components
- IntegrationsHeader - Header component for integration pages
- CustomizedIntegratedApps - Hook for managing integration state
Integration Patterns
- OAuth Authentication Flow - OAuth service connection patterns
- Service Status Management - Integration status tracking
- Multi-Service Integration - Managing multiple service connections
AccountsCard
Service integration card component that displays connection status, service information, and connection controls.
Component Location
import AccountsCard from 'components/Integrations/AccountsCard';
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| softwareDetails | object | ✓ | - | Integration service details |
TypeScript Interface
interface SoftwareDetails {
id: number;
title: string;
icon: ReactElement;
status: 'Connected' | 'Disconnected';
description: string;
to?: string;
view: boolean;
application_code?: string;
oauth_client_id?: string;
oauth_client_secret?: string;
}
interface AccountsCardProps {
softwareDetails: SoftwareDetails;
}
Features
- Service Information Display: Shows service name, icon, and description
- Connection Status: Visual indicator of connection state
- Connection Toggle: Switch to connect/disconnect services
- Permission-Based Access: Respects user permissions for integration management
- Modal Integration: Triggers connection status modals
Usage Examples
Basic Integration Card
import AccountsCard from 'components/Integrations/AccountsCard';
function IntegrationGrid() {
const accountingSoftware = [
{
id: 1,
title: 'QuickBooks',
icon: <QuickBooksIcon height='40px' width='40px' />,
status: 'Disconnected',
description:
'Integrate with QuickBooks for seamless accounting data sync.',
to: '/settings/account_integrations/quick_books',
view: true,
},
{
id: 2,
title: 'Xero',
icon: <XeroIcon height='40px' width='40px' />,
status: 'Connected',
description: 'Sync payroll data with Xero accounting platform.',
to: '/settings/account_integrations/xero',
view: true,
},
];
return (
<Grid templateColumns='repeat(2, 1fr)' gap={6}>
{accountingSoftware.map(software => (
<AccountsCard key={software.id} softwareDetails={software} />
))}
</Grid>
);
}
Integration with Custom Status
function CustomIntegrationCard() {
const [integrationData, setIntegrationData] = useState({
id: 1,
title: 'Custom ERP',
icon: <CustomIcon />,
status: 'Disconnected',
description: 'Connect to your custom ERP system for data synchronization.',
view: true,
custom_config: {
api_endpoint: 'https://api.custom-erp.com',
api_key: process.env.REACT_APP_CUSTOM_ERP_KEY,
},
});
const handleConnectionToggle = async isConnected => {
setIntegrationData(prev => ({
...prev,
status: isConnected ? 'Connected' : 'Disconnected',
}));
// Handle custom connection logic
if (isConnected) {
await connectToCustomERP(integrationData.custom_config);
} else {
await disconnectFromCustomERP();
}
};
return (
<VStack spacing={4}>
<AccountsCard softwareDetails={integrationData} />
{integrationData.status === 'Connected' && (
<Alert status='success'>
<AlertIcon />
<AlertDescription>
Successfully connected to {integrationData.title}
</AlertDescription>
</Alert>
)}
</VStack>
);
}
ConnectionStatusModal
Modal component for managing service connection and disconnection flows with OAuth authentication.
Component Location
import ConnectionStatusModal from 'components/Integrations/ConnectionStatusModal';
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| isOpen | boolean | ✓ | - | Modal open state |
| onClose | function | ✓ | - | Modal close handler |
| data | object | ✓ | - | Integration service data |
Features
- OAuth Integration: Handles OAuth authentication flows
- Connect/Disconnect Actions: Manages service connection state
- External Window Handling: Opens OAuth flows in new windows
- Token Management: Includes authentication tokens in OAuth URLs
- Error Handling: Manages connection errors and failures
Usage Examples
Basic Connection Modal
import ConnectionStatusModal from 'components/Integrations/ConnectionStatusModal';
import { useDisclosure } from '@chakra-ui/react';
function ServiceConnectionManager() {
const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedService, setSelectedService] = useState(null);
const handleConnectService = serviceData => {
setSelectedService(serviceData);
onOpen();
};
const serviceData = {
id: 1,
title: 'QuickBooks',
application_code: 'QUICKBOOKS',
status: 'Disconnected',
};
return (
<>
<Button onClick={() => handleConnectService(serviceData)}>
Connect to QuickBooks
</Button>
<ConnectionStatusModal
isOpen={isOpen}
onClose={onClose}
data={selectedService}
/>
</>
);
}
Advanced Connection Flow
function AdvancedConnectionFlow() {
const [connectionState, setConnectionState] = useState('idle');
const [connectionError, setConnectionError] = useState(null);
const handleConnectionStart = () => {
setConnectionState('connecting');
setConnectionError(null);
};
const handleConnectionSuccess = () => {
setConnectionState('connected');
// Refresh integration data
queryClient.invalidateQueries(['integrations']);
};
const handleConnectionError = error => {
setConnectionState('error');
setConnectionError(error.message);
};
return (
<VStack spacing={4}>
{connectionState === 'connecting' && (
<Alert status='info'>
<Spinner size='sm' mr={2} />
<AlertDescription>
Connecting to service... You may be redirected to complete
authentication.
</AlertDescription>
</Alert>
)}
{connectionState === 'error' && (
<Alert status='error'>
<AlertIcon />
<AlertDescription>
Connection failed: {connectionError}
</AlertDescription>
</Alert>
)}
<ConnectionStatusModal
isOpen={isOpen}
onClose={onClose}
data={selectedService}
onConnectionStart={handleConnectionStart}
onConnectionSuccess={handleConnectionSuccess}
onConnectionError={handleConnectionError}
/>
</VStack>
);
}
AuthorizeCard
Authorization interface component for handling service reauthorization and OAuth flows.
Component Location
import AuthorizeCard from 'components/Integrations/AuthorizeCard';
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| action | string | ✓ | - | Action type ('view', 'connect') |
| isOpen | boolean | ✓ | - | Modal open state |
| onClose | function | ✓ | - | Modal close handler |
Features
- Reauthorization Flow: Handles expired token reauthorization
- Service Selection: Allows selection of services for authorization
- OAuth URL Generation: Creates proper OAuth authorization URLs
- Token Injection: Adds authentication tokens to OAuth flows
Usage Examples
Service Reauthorization
import AuthorizeCard from 'components/Integrations/AuthorizeCard';
function ServiceReauthorization() {
const { isOpen, onOpen, onClose } = useDisclosure();
const [needsReauth, setNeedsReauth] = useState(false);
// Check if service needs reauthorization
useEffect(() => {
checkAuthorizationStatus().then(status => {
if (status.requiresReauth) {
setNeedsReauth(true);
onOpen();
}
});
}, []);
return (
<>
{needsReauth && (
<Alert status='warning' mb={4}>
<AlertIcon />
<AlertDescription>
Your QuickBooks integration needs to be reauthorized.
<Button size='sm' ml={2} onClick={onOpen}>
Reauthorize Now
</Button>
</AlertDescription>
</Alert>
)}
<AuthorizeCard action='view' isOpen={isOpen} onClose={onClose} />
</>
);
}
SettingsIntegrationPageWrapper
Page wrapper component that provides consistent layout for integration settings pages.
Component Location
import SettingsIntegrationPageWrapper from 'containers/Settings/common/SettingsIntegrationPageWrapper';
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| data | object | ✓ | - | Page data and configuration |
| tabData | array | ✓ | - | Tab configuration data |
| handleSubmit | function | ✓ | - | Form submission handler |
| noDataMsg | string | - | - | No data message text |
| isLoading | boolean | - | false | Loading state |
Features
- Consistent Layout: Standardized integration page structure
- Tab Navigation: Vertical tab navigation for integration sections
- Header Integration: Includes integration-specific headers
- Empty State Handling: Shows appropriate messages when no data exists
- Loading States: Manages loading indicators
- Reauthorization Detection: Automatically detects and handles reauth needs
Usage Examples
Integration Settings Page
import SettingsIntegrationPageWrapper from 'containers/Settings/common/SettingsIntegrationPageWrapper';
function QuickBooksSettingsPage() {
const [isLoading, setIsLoading] = useState(true);
const [tabData, setTabData] = useState([]);
const pageData = {
pageTitle: 'QuickBooks Integration',
description:
'Manage your QuickBooks integration settings and data sync options.',
icon: <QuickBooksIcon />,
};
const integrationTabs = [
{
id: 1,
title: 'Sync Settings',
children: <SyncSettingsPanel />,
},
{
id: 2,
title: 'Field Mapping',
children: <FieldMappingPanel />,
},
{
id: 3,
title: 'Sync History',
children: <SyncHistoryPanel />,
},
];
useEffect(() => {
loadIntegrationData().then(data => {
setTabData(integrationTabs);
setIsLoading(false);
});
}, []);
const handleSettingsSubmit = settings => {
return updateIntegrationSettings(settings);
};
return (
<SettingsIntegrationPageWrapper
data={pageData}
tabData={integrationTabs}
handleSubmit={handleSettingsSubmit}
isLoading={isLoading}
noDataMsg='No integration settings available.'
/>
);
}
Custom Integration Page
function CustomIntegrationPage() {
const [configData, setConfigData] = useState(null);
const [isConfiguring, setIsConfiguring] = useState(false);
const pageData = {
pageTitle: 'Custom API Integration',
description: 'Configure custom API endpoints and authentication settings.',
customActions: (
<Button onClick={() => setIsConfiguring(true)}>Add New Endpoint</Button>
),
};
const configurationTabs = useMemo(() => {
if (!configData) return [];
return configData.endpoints.map(endpoint => ({
id: endpoint.id,
title: endpoint.name,
children: (
<EndpointConfigurationPanel
endpoint={endpoint}
onUpdate={handleEndpointUpdate}
/>
),
}));
}, [configData]);
const handleConfigurationSubmit = async config => {
setIsConfiguring(true);
try {
await saveEndpointConfiguration(config);
await refreshConfigData();
} finally {
setIsConfiguring(false);
}
};
return (
<SettingsIntegrationPageWrapper
data={pageData}
tabData={configurationTabs}
handleSubmit={handleConfigurationSubmit}
isLoading={isConfiguring}
noDataMsg='No API endpoints configured. Add your first endpoint to get started.'
/>
);
}
Integration Patterns
OAuth Authentication Flow Pattern
function OAuthIntegrationFlow() {
const [authState, setAuthState] = useState('idle');
const [authData, setAuthData] = useState(null);
const initiateOAuth = async serviceCode => {
setAuthState('initializing');
try {
// Get OAuth authorization URL
const response = await httpV2.get('/integrations/auth/url', {
params: { application_code: serviceCode },
});
if (response.data.success) {
setAuthState('authorizing');
// Open OAuth window
const authWindow = window.open(
`${response.data.data.value}&token=${USER_TOKEN}`,
'_blank',
'width=600,height=600',
);
// Monitor for OAuth completion
const checkClosed = setInterval(() => {
if (authWindow.closed) {
clearInterval(checkClosed);
checkAuthorizationResult();
}
}, 1000);
}
} catch (error) {
setAuthState('error');
console.error('OAuth initialization failed:', error);
}
};
const checkAuthorizationResult = async () => {
try {
const status = await checkIntegrationStatus();
if (status.connected) {
setAuthState('connected');
setAuthData(status.data);
} else {
setAuthState('failed');
}
} catch (error) {
setAuthState('error');
}
};
const disconnectService = async () => {
setAuthState('disconnecting');
try {
await httpV2.delete('/integrations/disconnect', {
data: { id: authData.id },
});
setAuthState('disconnected');
setAuthData(null);
} catch (error) {
setAuthState('error');
}
};
return (
<VStack spacing={4}>
<Text fontSize='lg' fontWeight='bold'>
Service Integration Status
</Text>
{authState === 'idle' && (
<Button onClick={() => initiateOAuth('QUICKBOOKS')}>
Connect to QuickBooks
</Button>
)}
{authState === 'initializing' && (
<HStack>
<Spinner size='sm' />
<Text>Initializing connection...</Text>
</HStack>
)}
{authState === 'authorizing' && (
<Alert status='info'>
<AlertIcon />
<AlertDescription>
Please complete authorization in the popup window.
</AlertDescription>
</Alert>
)}
{authState === 'connected' && (
<VStack spacing={2}>
<Alert status='success'>
<AlertIcon />
<AlertDescription>
Successfully connected to QuickBooks!
</AlertDescription>
</Alert>
<Button variant='outline' onClick={disconnectService}>
Disconnect
</Button>
</VStack>
)}
{authState === 'error' && (
<Alert status='error'>
<AlertIcon />
<AlertDescription>
Connection failed. Please try again.
</AlertDescription>
</Alert>
)}
</VStack>
);
}
Service Status Management Pattern
function ServiceStatusManager() {
const [integrations, setIntegrations] = useState([]);
const [refreshing, setRefreshing] = useState(false);
const { data: integrationsData, isLoading } = useQuery(
['integrations'],
fetchIntegratedApplications,
{
refetchInterval: 30000, // Refresh every 30 seconds
},
);
useEffect(() => {
if (integrationsData) {
const formattedIntegrations = integrationsData.map(integration => ({
...integration,
lastSync: integration.last_sync_at
? new Date(integration.last_sync_at)
: null,
status: determineIntegrationStatus(integration),
}));
setIntegrations(formattedIntegrations);
}
}, [integrationsData]);
const determineIntegrationStatus = integration => {
if (!integration.connected) return 'disconnected';
if (integration.sync_errors > 0) return 'error';
if (integration.requires_reauth) return 'reauth_needed';
return 'connected';
};
const refreshIntegrationStatus = async () => {
setRefreshing(true);
try {
await queryClient.invalidateQueries(['integrations']);
await new Promise(resolve => setTimeout(resolve, 1000)); // Brief delay for UX
} finally {
setRefreshing(false);
}
};
const getStatusColor = status => {
switch (status) {
case 'connected':
return 'green';
case 'disconnected':
return 'gray';
case 'error':
return 'red';
case 'reauth_needed':
return 'orange';
default:
return 'gray';
}
};
const getStatusText = status => {
switch (status) {
case 'connected':
return 'Connected';
case 'disconnected':
return 'Disconnected';
case 'error':
return 'Error';
case 'reauth_needed':
return 'Reauthorization Required';
default:
return 'Unknown';
}
};
return (
<VStack spacing={4} align='stretch'>
<HStack justify='space-between'>
<Text fontSize='lg' fontWeight='bold'>
Integration Status
</Text>
<Button
size='sm'
onClick={refreshIntegrationStatus}
isLoading={refreshing}
leftIcon={<RefreshIcon />}
>
Refresh
</Button>
</HStack>
{isLoading ? (
<SkeletonLoader />
) : (
<SimpleGrid columns={2} spacing={4}>
{integrations.map(integration => (
<Box
key={integration.id}
p={4}
borderWidth='1px'
borderRadius='md'
borderColor={getStatusColor(integration.status)}
>
<HStack justify='space-between' mb={2}>
<Text fontWeight='bold'>{integration.name}</Text>
<Badge colorScheme={getStatusColor(integration.status)}>
{getStatusText(integration.status)}
</Badge>
</HStack>
{integration.lastSync && (
<Text fontSize='sm' color='gray.600'>
Last sync: {formatDistanceToNow(integration.lastSync)} ago
</Text>
)}
{integration.status === 'error' && (
<Text fontSize='sm' color='red.500' mt={1}>
{integration.error_message}
</Text>
)}
</Box>
))}
</SimpleGrid>
)}
</VStack>
);
}
Multi-Service Integration Pattern
function MultiServiceIntegrationManager() {
const [selectedServices, setSelectedServices] = useState(new Set());
const [bulkActionLoading, setBulkActionLoading] = useState(false);
const availableServices = [
{ code: 'QUICKBOOKS', name: 'QuickBooks', icon: <QuickBooksIcon /> },
{ code: 'XERO', name: 'Xero', icon: <XeroIcon /> },
{ code: 'SAGE', name: 'Sage', icon: <SageIcon /> },
{ code: 'WAVEAPPS', name: 'WaveApps', icon: <WaveAppsIcon /> },
];
const handleServiceSelect = serviceCode => {
setSelectedServices(prev => {
const newSet = new Set(prev);
if (newSet.has(serviceCode)) {
newSet.delete(serviceCode);
} else {
newSet.add(serviceCode);
}
return newSet;
});
};
const handleBulkConnect = async () => {
setBulkActionLoading(true);
try {
const connectionPromises = Array.from(selectedServices).map(serviceCode =>
connectToService(serviceCode),
);
const results = await Promise.allSettled(connectionPromises);
const succeeded = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
if (succeeded > 0) {
toast({
title: `Connected to ${succeeded} service(s)`,
status: 'success',
});
}
if (failed > 0) {
toast({
title: `Failed to connect to ${failed} service(s)`,
status: 'error',
});
}
setSelectedServices(new Set());
} finally {
setBulkActionLoading(false);
}
};
const handleBulkDisconnect = async () => {
setBulkActionLoading(true);
try {
const disconnectionPromises = Array.from(selectedServices).map(
serviceCode => disconnectFromService(serviceCode),
);
await Promise.all(disconnectionPromises);
toast({
title: `Disconnected from ${selectedServices.size} service(s)`,
status: 'success',
});
setSelectedServices(new Set());
} finally {
setBulkActionLoading(false);
}
};
return (
<VStack spacing={6} align='stretch'>
<HStack justify='space-between'>
<Text fontSize='xl' fontWeight='bold'>
Service Integrations
</Text>
{selectedServices.size > 0 && (
<HStack spacing={2}>
<Button
size='sm'
onClick={handleBulkConnect}
isLoading={bulkActionLoading}
colorScheme='green'
>
Connect Selected ({selectedServices.size})
</Button>
<Button
size='sm'
variant='outline'
onClick={handleBulkDisconnect}
isLoading={bulkActionLoading}
>
Disconnect Selected
</Button>
</HStack>
)}
</HStack>
<SimpleGrid columns={2} spacing={4}>
{availableServices.map(service => (
<Box
key={service.code}
p={4}
borderWidth='2px'
borderRadius='md'
borderColor={
selectedServices.has(service.code) ? 'blue.300' : 'gray.200'
}
cursor='pointer'
onClick={() => handleServiceSelect(service.code)}
transition='all 0.2s'
_hover={{ borderColor: 'blue.300' }}
>
<VStack spacing={3}>
<Checkbox
isChecked={selectedServices.has(service.code)}
onChange={() => handleServiceSelect(service.code)}
onClick={e => e.stopPropagation()}
/>
{service.icon}
<Text fontWeight='bold'>{service.name}</Text>
<IntegrationStatusBadge serviceCode={service.code} />
</VStack>
</Box>
))}
</SimpleGrid>
</VStack>
);
}
Best Practices
Integration Security
-
Token Management
- Store OAuth tokens securely
- Implement token refresh mechanisms
- Use HTTPS for all OAuth flows
- Validate redirect URIs
-
Error Handling
- Graceful degradation for failed connections
- Clear error messages for users
- Retry mechanisms for transient failures
- Logging for debugging
-
Data Synchronization
- Implement conflict resolution strategies
- Handle partial sync failures
- Provide sync status visibility
- Rate limiting for API calls
User Experience
-
Clear Status Communication
- Visual indicators for connection states
- Progress feedback during operations
- Clear error messages with remediation steps
- Success confirmations
-
Permission Management
- Role-based access to integration features
- Clear permission requirements
- Graceful handling of insufficient permissions
-
Performance Optimization
- Lazy loading of integration data
- Caching of connection statuses
- Background sync operations
- Minimal UI blocking
Testing
Unit Tests
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import AccountsCard from 'components/Integrations/AccountsCard';
describe('AccountsCard', () => {
const mockSoftware = {
id: 1,
title: 'QuickBooks',
icon: <div>QB Icon</div>,
status: 'Disconnected',
description: 'QuickBooks integration',
view: true,
};
it('renders software information correctly', () => {
render(<AccountsCard softwareDetails={mockSoftware} />);
expect(screen.getByText('QuickBooks')).toBeInTheDocument();
expect(screen.getByText('QuickBooks integration')).toBeInTheDocument();
expect(screen.getByText('Disconnected')).toBeInTheDocument();
});
it('shows connection toggle for authorized users', () => {
render(<AccountsCard softwareDetails={mockSoftware} />);
const toggleSwitch = screen.getByRole('switch');
expect(toggleSwitch).toBeInTheDocument();
expect(toggleSwitch).not.toBeChecked();
});
it('opens connection modal when toggle is clicked', async () => {
const { user } = render(<AccountsCard softwareDetails={mockSoftware} />);
const toggleSwitch = screen.getByRole('switch');
await user.click(toggleSwitch);
await waitFor(() => {
expect(screen.getByText('Connection Settings')).toBeInTheDocument();
});
});
});
Integration Tests
describe('Integration Flow', () => {
it('completes OAuth connection flow', async () => {
const mockOAuthResponse = {
success: true,
data: { value: 'https://oauth.service.com/auth?token=abc' },
};
jest.spyOn(httpV2, 'get').mockResolvedValue({ data: mockOAuthResponse });
const { user } = render(<IntegrationPage />);
// Click connect button
await user.click(screen.getByText('Connect to QuickBooks'));
// Verify OAuth URL request
expect(httpV2.get).toHaveBeenCalledWith('/integrations/auth/url', {
params: { application_code: 'QUICKBOOKS' },
});
// Verify window.open was called
expect(window.open).toHaveBeenCalledWith(
expect.stringContaining('oauth.service.com'),
'_blank',
);
});
it('handles connection errors gracefully', async () => {
jest.spyOn(httpV2, 'get').mockRejectedValue(new Error('Network error'));
const { user } = render(<IntegrationPage />);
await user.click(screen.getByText('Connect to QuickBooks'));
await waitFor(() => {
expect(screen.getByText(/connection failed/i)).toBeInTheDocument();
});
});
});
Migration Guide
From Legacy Integration Components
-
Update Import Paths
// Old
import IntegrationCard from 'components/OldIntegration';
// New
import AccountsCard from 'components/Integrations/AccountsCard'; -
Update Props Structure
// Old
<IntegrationCard
name="QuickBooks"
connected={false}
onToggle={handleToggle}
/>
// New
<AccountsCard
softwareDetails={{
title: "QuickBooks",
status: "Disconnected",
// ... other properties
}}
/> -
Update Event Handling
// Old
const handleToggle = isConnected => {
// Handle connection state
};
// New
// Connection handling is managed internally
// Use status updates and modal callbacks
Advanced Usage
Custom OAuth Provider
function CustomOAuthProvider({ children, onTokenUpdate }) {
const [authTokens, setAuthTokens] = useState({});
const registerOAuthService = useCallback((serviceCode, config) => {
// Register custom OAuth configuration
oauthConfigs[serviceCode] = config;
}, []);
const initiateOAuth = useCallback(async serviceCode => {
const config = oauthConfigs[serviceCode];
if (!config) {
throw new Error(`OAuth config not found for ${serviceCode}`);
}
const authUrl = buildOAuthUrl(config);
const authWindow = window.open(authUrl, '_blank');
return new Promise((resolve, reject) => {
const checkWindow = setInterval(() => {
try {
if (authWindow.closed) {
clearInterval(checkWindow);
resolve(extractTokenFromCallback());
}
} catch (error) {
clearInterval(checkWindow);
reject(error);
}
}, 1000);
});
}, []);
const context = {
authTokens,
registerOAuthService,
initiateOAuth,
};
return (
<OAuthContext.Provider value={context}>{children}</OAuthContext.Provider>
);
}
Integration Monitoring Dashboard
function IntegrationMonitoringDashboard() {
const [metrics, setMetrics] = useState({});
const [alerts, setAlerts] = useState([]);
useEffect(() => {
const interval = setInterval(async () => {
const [metricsData, alertsData] = await Promise.all([
fetchIntegrationMetrics(),
fetchIntegrationAlerts(),
]);
setMetrics(metricsData);
setAlerts(alertsData);
}, 30000);
return () => clearInterval(interval);
}, []);
return (
<Grid templateColumns='repeat(3, 1fr)' gap={6}>
<GridItem colSpan={2}>
<IntegrationMetricsChart data={metrics} />
</GridItem>
<GridItem>
<IntegrationAlertsPanel alerts={alerts} />
</GridItem>
<GridItem colSpan={3}>
<IntegrationHealthStatus integrations={metrics.integrations} />
</GridItem>
</Grid>
);
}