@visns-studio/visns-components 5.15.10 → 5.15.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/Breadcrumb.jsx +19 -11
- package/src/components/Form.jsx +240 -95
- package/src/components/oauth/DataPreviewModal.jsx +372 -0
- package/src/components/oauth/DataSyncWizard.jsx +850 -0
- package/src/components/oauth/OAuthConnectionStatus.jsx +85 -0
- package/src/components/oauth/OAuthIntegrationsPage.jsx +185 -0
- package/src/components/oauth/OAuthManager.jsx +260 -0
- package/src/components/oauth/OAuthProviderCard.jsx +291 -0
- package/src/components/styles/DataPreviewModal.module.scss +658 -0
- package/src/components/styles/DataSyncWizard.module.scss +1211 -0
- package/src/components/styles/OAuthConnectionStatus.module.scss +143 -0
- package/src/components/styles/OAuthManager.module.scss +119 -0
- package/src/components/styles/OAuthProviderCard.module.scss +760 -0
- package/src/index.js +12 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from '../styles/OAuthConnectionStatus.module.scss';
|
|
3
|
+
|
|
4
|
+
const OAuthConnectionStatus = ({ provider, compact = false }) => {
|
|
5
|
+
const { connection_status: status } = provider;
|
|
6
|
+
|
|
7
|
+
const formatDate = (dateString) => {
|
|
8
|
+
if (!dateString) return null;
|
|
9
|
+
return new Date(dateString).toLocaleString();
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const getStatusIcon = () => {
|
|
13
|
+
switch (status?.status) {
|
|
14
|
+
case 'connected':
|
|
15
|
+
return '✅';
|
|
16
|
+
case 'expired':
|
|
17
|
+
return '⚠️';
|
|
18
|
+
case 'error':
|
|
19
|
+
return '❌';
|
|
20
|
+
default:
|
|
21
|
+
return '⚪';
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getStatusClass = () => {
|
|
26
|
+
switch (status?.status) {
|
|
27
|
+
case 'connected':
|
|
28
|
+
return styles.connected;
|
|
29
|
+
case 'expired':
|
|
30
|
+
return styles.expired;
|
|
31
|
+
case 'error':
|
|
32
|
+
return styles.error;
|
|
33
|
+
default:
|
|
34
|
+
return styles.disconnected;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (compact) {
|
|
39
|
+
return (
|
|
40
|
+
<div className={`${styles.connectionStatus} ${styles.compact} ${getStatusClass()}`}>
|
|
41
|
+
<span className={styles.icon}>{getStatusIcon()}</span>
|
|
42
|
+
<span className={styles.provider}>{provider.display_name}</span>
|
|
43
|
+
<span className={styles.status}>{status?.message || 'Not connected'}</span>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className={`${styles.connectionStatus} ${getStatusClass()}`}>
|
|
50
|
+
<div className={styles.statusHeader}>
|
|
51
|
+
<span className={styles.icon}>{getStatusIcon()}</span>
|
|
52
|
+
<span className={styles.message}>{status?.message || 'Not connected'}</span>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{status && (
|
|
56
|
+
<div className={styles.statusDetails}>
|
|
57
|
+
{status.expires_at && (
|
|
58
|
+
<div className={styles.detail}>
|
|
59
|
+
<span className={styles.label}>Expires:</span>
|
|
60
|
+
<span className={styles.value}>{formatDate(status.expires_at)}</span>
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
{status.last_sync_at && (
|
|
65
|
+
<div className={styles.detail}>
|
|
66
|
+
<span className={styles.label}>Last Sync:</span>
|
|
67
|
+
<span className={styles.value}>{formatDate(status.last_sync_at)}</span>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{status.sync_status && (
|
|
72
|
+
<div className={styles.detail}>
|
|
73
|
+
<span className={styles.label}>Sync Status:</span>
|
|
74
|
+
<span className={`${styles.value} ${styles[status.sync_status]}`}>
|
|
75
|
+
{status.sync_status}
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default OAuthConnectionStatus;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuthIntegrationsPage Component
|
|
3
|
+
*
|
|
4
|
+
* A complete OAuth integrations page component that provides:
|
|
5
|
+
* - Consistent breadcrumb navigation using project styling patterns
|
|
6
|
+
* - Full OAuth provider management (connect, sync, disconnect)
|
|
7
|
+
* - Step-by-step sync wizard with preview functionality
|
|
8
|
+
* - Toast notification integration (optional)
|
|
9
|
+
* - Configurable via props for different projects
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ```jsx
|
|
13
|
+
* import { OAuthIntegrationsPage } from '@visns-studio/visns-components';
|
|
14
|
+
* import { toast } from 'react-toastify';
|
|
15
|
+
*
|
|
16
|
+
* const MyOAuthPage = () => (
|
|
17
|
+
* <OAuthIntegrationsPage
|
|
18
|
+
* breadcrumbConfig={{
|
|
19
|
+
* title: 'OAuth Integrations',
|
|
20
|
+
* parentTitle: 'Admin',
|
|
21
|
+
* parentUrl: '/admin'
|
|
22
|
+
* }}
|
|
23
|
+
* showSuccessToast={toast.success}
|
|
24
|
+
* showErrorToast={toast.error}
|
|
25
|
+
* showInfoToast={toast.info}
|
|
26
|
+
* />
|
|
27
|
+
* );
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import React from 'react';
|
|
32
|
+
import Breadcrumb from '../Breadcrumb';
|
|
33
|
+
import OAuthManager from './OAuthManager';
|
|
34
|
+
import styles from '../styles/GenericDetail.module.scss';
|
|
35
|
+
|
|
36
|
+
const OAuthIntegrationsPage = ({
|
|
37
|
+
// Breadcrumb configuration
|
|
38
|
+
breadcrumbConfig = {
|
|
39
|
+
title: 'OAuth Integrations',
|
|
40
|
+
parentTitle: 'Admin',
|
|
41
|
+
parentUrl: '/admin'
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// Page configuration
|
|
45
|
+
pageTitle = 'OAuth Integrations',
|
|
46
|
+
pageDescription = 'Connect your CRM with third-party services to automatically sync data and streamline your workflow.',
|
|
47
|
+
|
|
48
|
+
// Event handlers - these can be passed from the consuming app
|
|
49
|
+
onAuthComplete,
|
|
50
|
+
onSyncComplete,
|
|
51
|
+
onError,
|
|
52
|
+
|
|
53
|
+
// Toast notification function (optional) - pass toast.success, toast.error etc.
|
|
54
|
+
showSuccessToast,
|
|
55
|
+
showErrorToast,
|
|
56
|
+
showInfoToast,
|
|
57
|
+
|
|
58
|
+
// Optional custom styling
|
|
59
|
+
className = '',
|
|
60
|
+
layout = 'full'
|
|
61
|
+
}) => {
|
|
62
|
+
|
|
63
|
+
// Enhanced event handlers that integrate with toast notifications
|
|
64
|
+
const handleAuthComplete = (provider, action) => {
|
|
65
|
+
console.log(`OAuth ${action} completed for ${provider}`);
|
|
66
|
+
|
|
67
|
+
if (action === 'connected' && showSuccessToast) {
|
|
68
|
+
showSuccessToast(`Successfully connected to ${provider}`, {
|
|
69
|
+
position: "top-right",
|
|
70
|
+
autoClose: 5000,
|
|
71
|
+
hideProgressBar: false,
|
|
72
|
+
closeOnClick: true,
|
|
73
|
+
pauseOnHover: true,
|
|
74
|
+
draggable: true,
|
|
75
|
+
});
|
|
76
|
+
} else if (action === 'disconnected' && showInfoToast) {
|
|
77
|
+
showInfoToast(`Successfully disconnected from ${provider}`, {
|
|
78
|
+
position: "top-right",
|
|
79
|
+
autoClose: 5000,
|
|
80
|
+
hideProgressBar: false,
|
|
81
|
+
closeOnClick: true,
|
|
82
|
+
pauseOnHover: true,
|
|
83
|
+
draggable: true,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Call custom handler if provided
|
|
88
|
+
if (onAuthComplete) {
|
|
89
|
+
onAuthComplete(provider, action);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleSyncComplete = (provider, dataType, result) => {
|
|
94
|
+
console.log(`Sync completed for ${provider}.${dataType}`, result);
|
|
95
|
+
|
|
96
|
+
if (result.success && showSuccessToast) {
|
|
97
|
+
const totalSynced = result.result?.total || 0;
|
|
98
|
+
showSuccessToast(`Successfully synced ${totalSynced} ${dataType} from ${provider}`, {
|
|
99
|
+
position: "top-right",
|
|
100
|
+
autoClose: 5000,
|
|
101
|
+
hideProgressBar: false,
|
|
102
|
+
closeOnClick: true,
|
|
103
|
+
pauseOnHover: true,
|
|
104
|
+
draggable: true,
|
|
105
|
+
});
|
|
106
|
+
} else if (!result.success && showErrorToast) {
|
|
107
|
+
showErrorToast(`Sync failed: ${result.message || 'Unknown error'}`, {
|
|
108
|
+
position: "top-right",
|
|
109
|
+
autoClose: 7000,
|
|
110
|
+
hideProgressBar: false,
|
|
111
|
+
closeOnClick: true,
|
|
112
|
+
pauseOnHover: true,
|
|
113
|
+
draggable: true,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Call custom handler if provided
|
|
118
|
+
if (onSyncComplete) {
|
|
119
|
+
onSyncComplete(provider, dataType, result);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const handleError = (error) => {
|
|
124
|
+
console.error('OAuth Integration Error:', error);
|
|
125
|
+
|
|
126
|
+
if (showErrorToast) {
|
|
127
|
+
showErrorToast(`Error: ${error.message || 'An unexpected error occurred'}`, {
|
|
128
|
+
position: "top-right",
|
|
129
|
+
autoClose: 7000,
|
|
130
|
+
hideProgressBar: false,
|
|
131
|
+
closeOnClick: true,
|
|
132
|
+
pauseOnHover: true,
|
|
133
|
+
draggable: true,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Call custom handler if provided
|
|
138
|
+
if (onError) {
|
|
139
|
+
onError(error);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className={className}>
|
|
145
|
+
{/* Header Section with Breadcrumb */}
|
|
146
|
+
<div className={styles.grid}>
|
|
147
|
+
<div className={styles.grid__row}>
|
|
148
|
+
<div className={`${styles.grid__full} ${
|
|
149
|
+
layout === 'full'
|
|
150
|
+
? styles.crmtitle
|
|
151
|
+
: styles['crmtitle--alternate']
|
|
152
|
+
}`}>
|
|
153
|
+
<Breadcrumb
|
|
154
|
+
data={{}}
|
|
155
|
+
page={breadcrumbConfig}
|
|
156
|
+
/>
|
|
157
|
+
{/* Description positioned on the right side */}
|
|
158
|
+
{pageDescription && (
|
|
159
|
+
<div style={{
|
|
160
|
+
color: '#666',
|
|
161
|
+
fontSize: '0.95rem',
|
|
162
|
+
fontWeight: '400',
|
|
163
|
+
maxWidth: '60%',
|
|
164
|
+
textAlign: 'right'
|
|
165
|
+
}}>
|
|
166
|
+
{pageDescription}
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* OAuth Manager */}
|
|
174
|
+
<div style={{ padding: '32px 0 20px 0' }}>
|
|
175
|
+
<OAuthManager
|
|
176
|
+
onAuthComplete={handleAuthComplete}
|
|
177
|
+
onSyncComplete={handleSyncComplete}
|
|
178
|
+
onError={handleError}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export default OAuthIntegrationsPage;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import OAuthProviderCard from './OAuthProviderCard';
|
|
3
|
+
import OAuthConnectionStatus from './OAuthConnectionStatus';
|
|
4
|
+
import DataSyncWizard from './DataSyncWizard';
|
|
5
|
+
import styles from '../styles/OAuthManager.module.scss';
|
|
6
|
+
|
|
7
|
+
const OAuthManager = ({
|
|
8
|
+
onAuthComplete = () => {},
|
|
9
|
+
onSyncComplete = () => {},
|
|
10
|
+
onError = () => {},
|
|
11
|
+
className = ''
|
|
12
|
+
}) => {
|
|
13
|
+
const [providers, setProviders] = useState([]);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const [error, setError] = useState(null);
|
|
16
|
+
const [syncingProviders, setSyncingProviders] = useState(new Set());
|
|
17
|
+
const [wizardState, setWizardState] = useState({
|
|
18
|
+
isOpen: false,
|
|
19
|
+
provider: null,
|
|
20
|
+
dataType: null
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
loadProviders();
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const loadProviders = async () => {
|
|
28
|
+
try {
|
|
29
|
+
setLoading(true);
|
|
30
|
+
const response = await fetch('/integrations/oauth/providers', {
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
34
|
+
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
|
35
|
+
},
|
|
36
|
+
credentials: 'same-origin'
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
|
|
45
|
+
if (data.success) {
|
|
46
|
+
setProviders(Object.values(data.providers));
|
|
47
|
+
setError(null);
|
|
48
|
+
} else {
|
|
49
|
+
throw new Error(data.message || 'Failed to load providers');
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
setError(err.message);
|
|
53
|
+
onError(err);
|
|
54
|
+
} finally {
|
|
55
|
+
setLoading(false);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleConnect = (provider) => {
|
|
60
|
+
window.location.href = `/integrations/oauth/${provider.name}/authorize`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleDisconnect = async (provider) => {
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(`/integrations/oauth/${provider.name}/disconnect`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
70
|
+
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'),
|
|
71
|
+
},
|
|
72
|
+
credentials: 'same-origin'
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const data = await response.json();
|
|
80
|
+
|
|
81
|
+
if (data.success) {
|
|
82
|
+
await loadProviders(); // Refresh the list
|
|
83
|
+
onAuthComplete(provider.name, 'disconnected');
|
|
84
|
+
} else {
|
|
85
|
+
throw new Error(data.message || 'Failed to disconnect');
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
setError(err.message);
|
|
89
|
+
onError(err);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleTest = async (provider) => {
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch(`/integrations/oauth/${provider.name}/test`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
100
|
+
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'),
|
|
101
|
+
},
|
|
102
|
+
credentials: 'same-origin'
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const data = await response.json();
|
|
110
|
+
|
|
111
|
+
if (data.success) {
|
|
112
|
+
await loadProviders(); // Refresh status
|
|
113
|
+
} else {
|
|
114
|
+
throw new Error(data.message || 'Connection test failed');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return data;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
setError(err.message);
|
|
120
|
+
onError(err);
|
|
121
|
+
return { success: false, message: err.message };
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleSync = async (provider, dataType, options = {}) => {
|
|
126
|
+
const providerId = provider.name;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
setSyncingProviders(prev => new Set([...prev, providerId]));
|
|
130
|
+
|
|
131
|
+
const response = await fetch(`/integrations/oauth/${providerId}/sync`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: {
|
|
134
|
+
'Content-Type': 'application/json',
|
|
135
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
136
|
+
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'),
|
|
137
|
+
},
|
|
138
|
+
credentials: 'same-origin',
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
type: dataType,
|
|
141
|
+
options: options
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const data = await response.json();
|
|
150
|
+
|
|
151
|
+
if (data.success) {
|
|
152
|
+
await loadProviders(); // Refresh status
|
|
153
|
+
onSyncComplete(providerId, dataType, data.result || data);
|
|
154
|
+
} else {
|
|
155
|
+
throw new Error(data.message || 'Sync failed');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return data;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
setError(err.message);
|
|
161
|
+
onError(err);
|
|
162
|
+
return { success: false, message: err.message };
|
|
163
|
+
} finally {
|
|
164
|
+
setSyncingProviders(prev => {
|
|
165
|
+
const newSet = new Set(prev);
|
|
166
|
+
newSet.delete(providerId);
|
|
167
|
+
return newSet;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (loading) {
|
|
173
|
+
return (
|
|
174
|
+
<div className={`${styles.oauthManager} ${className}`}>
|
|
175
|
+
<div className={styles.loading}>
|
|
176
|
+
<div className={styles.spinner}></div>
|
|
177
|
+
<p>Loading integrations...</p>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (error) {
|
|
184
|
+
return (
|
|
185
|
+
<div className={`${styles.oauthManager} ${className}`}>
|
|
186
|
+
<div className={styles.error}>
|
|
187
|
+
<p>Error loading integrations: {error}</p>
|
|
188
|
+
<button onClick={loadProviders} className={styles.retryButton}>
|
|
189
|
+
Retry
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const handleWizardOpen = (provider, dataType) => {
|
|
197
|
+
setWizardState({
|
|
198
|
+
isOpen: true,
|
|
199
|
+
provider: provider,
|
|
200
|
+
dataType: dataType
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const handleWizardClose = () => {
|
|
205
|
+
setWizardState({
|
|
206
|
+
isOpen: false,
|
|
207
|
+
provider: null,
|
|
208
|
+
dataType: null
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const handleWizardComplete = async (dataType, options) => {
|
|
213
|
+
if (wizardState.provider) {
|
|
214
|
+
await handleSync(wizardState.provider, dataType, options);
|
|
215
|
+
}
|
|
216
|
+
handleWizardClose();
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// If wizard is open, show it full screen
|
|
220
|
+
if (wizardState.isOpen && wizardState.provider) {
|
|
221
|
+
return (
|
|
222
|
+
<DataSyncWizard
|
|
223
|
+
provider={wizardState.provider}
|
|
224
|
+
dataType={wizardState.dataType}
|
|
225
|
+
onCancel={handleWizardClose}
|
|
226
|
+
onComplete={handleWizardComplete}
|
|
227
|
+
/>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div className={`${styles.oauthManager} ${className}`}>
|
|
233
|
+
{providers.length === 0 ? (
|
|
234
|
+
<div className={styles.empty}>
|
|
235
|
+
<p>No integrations available.</p>
|
|
236
|
+
</div>
|
|
237
|
+
) : (
|
|
238
|
+
<div className={styles.providersGrid}>
|
|
239
|
+
{providers.map((provider) => (
|
|
240
|
+
<OAuthProviderCard
|
|
241
|
+
key={provider.name}
|
|
242
|
+
provider={provider}
|
|
243
|
+
isConnected={provider.is_connected}
|
|
244
|
+
connectionStatus={provider.connection_status}
|
|
245
|
+
isSyncing={syncingProviders.has(provider.name)}
|
|
246
|
+
onConnect={() => handleConnect(provider)}
|
|
247
|
+
onDisconnect={() => handleDisconnect(provider)}
|
|
248
|
+
onTest={() => handleTest(provider)}
|
|
249
|
+
onSync={(dataType, options) => handleSync(provider, dataType, options)}
|
|
250
|
+
onOpenWizard={handleWizardOpen}
|
|
251
|
+
compact={true}
|
|
252
|
+
/>
|
|
253
|
+
))}
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export default OAuthManager;
|