@friggframework/devtools 2.0.0--canary.398.dd443c7.0 → 2.0.0--canary.402.d2f4ae6.0
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/frigg-cli/.eslintrc.js +141 -0
- package/frigg-cli/__tests__/jest.config.js +102 -0
- package/frigg-cli/__tests__/unit/commands/build.test.js +483 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +418 -0
- package/frigg-cli/__tests__/unit/commands/ui.test.js +592 -0
- package/frigg-cli/__tests__/utils/command-tester.js +170 -0
- package/frigg-cli/__tests__/utils/mock-factory.js +270 -0
- package/frigg-cli/__tests__/utils/test-fixtures.js +463 -0
- package/frigg-cli/__tests__/utils/test-setup.js +286 -0
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +312 -0
- package/frigg-cli/generate-command/azure-generator.js +43 -0
- package/frigg-cli/generate-command/gcp-generator.js +47 -0
- package/frigg-cli/generate-command/index.js +332 -0
- package/frigg-cli/generate-command/terraform-generator.js +555 -0
- package/frigg-cli/index.js +19 -1
- package/frigg-cli/init-command/backend-first-handler.js +756 -0
- package/frigg-cli/init-command/index.js +93 -0
- package/frigg-cli/init-command/template-handler.js +143 -0
- package/frigg-cli/package.json +51 -0
- package/frigg-cli/test/init-command.test.js +180 -0
- package/frigg-cli/test/npm-registry.test.js +319 -0
- package/frigg-cli/ui-command/index.js +154 -0
- package/frigg-cli/utils/app-resolver.js +319 -0
- package/frigg-cli/utils/backend-path.js +25 -0
- package/frigg-cli/utils/npm-registry.js +167 -0
- package/frigg-cli/utils/process-manager.js +199 -0
- package/frigg-cli/utils/repo-detection.js +405 -0
- package/infrastructure/serverless-template.js +177 -292
- package/management-ui/.eslintrc.js +22 -0
- package/management-ui/README.md +203 -0
- package/management-ui/components.json +21 -0
- package/management-ui/docs/phase2-integration-guide.md +320 -0
- package/management-ui/{dist/index.html → index.html} +1 -2
- package/management-ui/package-lock.json +16517 -0
- package/management-ui/package.json +76 -0
- package/management-ui/packages/devtools/frigg-cli/ui-command/index.js +302 -0
- package/management-ui/postcss.config.js +6 -0
- package/management-ui/server/api/backend.js +256 -0
- package/management-ui/server/api/cli.js +315 -0
- package/management-ui/server/api/codegen.js +663 -0
- package/management-ui/server/api/connections.js +857 -0
- package/management-ui/server/api/discovery.js +185 -0
- package/management-ui/server/api/environment/index.js +1 -0
- package/management-ui/server/api/environment/router.js +378 -0
- package/management-ui/server/api/environment.js +328 -0
- package/management-ui/server/api/integrations.js +876 -0
- package/management-ui/server/api/logs.js +248 -0
- package/management-ui/server/api/monitoring.js +282 -0
- package/management-ui/server/api/open-ide.js +31 -0
- package/management-ui/server/api/project.js +1029 -0
- package/management-ui/server/api/users/sessions.js +371 -0
- package/management-ui/server/api/users/simulation.js +254 -0
- package/management-ui/server/api/users.js +362 -0
- package/management-ui/server/api-contract.md +275 -0
- package/management-ui/server/index.js +873 -0
- package/management-ui/server/middleware/errorHandler.js +93 -0
- package/management-ui/server/middleware/security.js +32 -0
- package/management-ui/server/processManager.js +296 -0
- package/management-ui/server/server.js +346 -0
- package/management-ui/server/services/aws-monitor.js +413 -0
- package/management-ui/server/services/npm-registry.js +347 -0
- package/management-ui/server/services/template-engine.js +538 -0
- package/management-ui/server/utils/cliIntegration.js +220 -0
- package/management-ui/server/utils/environment/auditLogger.js +471 -0
- package/management-ui/server/utils/environment/awsParameterStore.js +264 -0
- package/management-ui/server/utils/environment/encryption.js +278 -0
- package/management-ui/server/utils/environment/envFileManager.js +286 -0
- package/management-ui/server/utils/import-commonjs.js +28 -0
- package/management-ui/server/utils/response.js +83 -0
- package/management-ui/server/websocket/handler.js +325 -0
- package/management-ui/src/App.jsx +109 -0
- package/management-ui/src/components/AppRouter.jsx +65 -0
- package/management-ui/src/components/Button.jsx +70 -0
- package/management-ui/src/components/Card.jsx +97 -0
- package/management-ui/src/components/EnvironmentCompare.jsx +400 -0
- package/management-ui/src/components/EnvironmentEditor.jsx +372 -0
- package/management-ui/src/components/EnvironmentImportExport.jsx +469 -0
- package/management-ui/src/components/EnvironmentSchema.jsx +491 -0
- package/management-ui/src/components/EnvironmentSecurity.jsx +463 -0
- package/management-ui/src/components/ErrorBoundary.jsx +73 -0
- package/management-ui/src/components/IntegrationCard.jsx +481 -0
- package/management-ui/src/components/IntegrationCardEnhanced.jsx +770 -0
- package/management-ui/src/components/IntegrationExplorer.jsx +379 -0
- package/management-ui/src/components/IntegrationStatus.jsx +336 -0
- package/management-ui/src/components/Layout.jsx +716 -0
- package/management-ui/src/components/LoadingSpinner.jsx +113 -0
- package/management-ui/src/components/RepositoryPicker.jsx +248 -0
- package/management-ui/src/components/SessionMonitor.jsx +350 -0
- package/management-ui/src/components/StatusBadge.jsx +208 -0
- package/management-ui/src/components/UserContextSwitcher.jsx +212 -0
- package/management-ui/src/components/UserSimulation.jsx +327 -0
- package/management-ui/src/components/Welcome.jsx +434 -0
- package/management-ui/src/components/codegen/APIEndpointGenerator.jsx +637 -0
- package/management-ui/src/components/codegen/APIModuleSelector.jsx +227 -0
- package/management-ui/src/components/codegen/CodeGenerationWizard.jsx +247 -0
- package/management-ui/src/components/codegen/CodePreviewEditor.jsx +316 -0
- package/management-ui/src/components/codegen/DynamicModuleForm.jsx +271 -0
- package/management-ui/src/components/codegen/FormBuilder.jsx +737 -0
- package/management-ui/src/components/codegen/IntegrationGenerator.jsx +855 -0
- package/management-ui/src/components/codegen/ProjectScaffoldWizard.jsx +797 -0
- package/management-ui/src/components/codegen/SchemaBuilder.jsx +303 -0
- package/management-ui/src/components/codegen/TemplateSelector.jsx +586 -0
- package/management-ui/src/components/codegen/index.js +10 -0
- package/management-ui/src/components/connections/ConnectionConfigForm.jsx +362 -0
- package/management-ui/src/components/connections/ConnectionHealthMonitor.jsx +182 -0
- package/management-ui/src/components/connections/ConnectionTester.jsx +200 -0
- package/management-ui/src/components/connections/EntityRelationshipMapper.jsx +292 -0
- package/management-ui/src/components/connections/OAuthFlow.jsx +204 -0
- package/management-ui/src/components/connections/index.js +5 -0
- package/management-ui/src/components/index.js +21 -0
- package/management-ui/src/components/monitoring/APIGatewayMetrics.jsx +222 -0
- package/management-ui/src/components/monitoring/LambdaMetrics.jsx +169 -0
- package/management-ui/src/components/monitoring/MetricsChart.jsx +197 -0
- package/management-ui/src/components/monitoring/MonitoringDashboard.jsx +393 -0
- package/management-ui/src/components/monitoring/SQSMetrics.jsx +246 -0
- package/management-ui/src/components/monitoring/index.js +6 -0
- package/management-ui/src/components/monitoring/monitoring.css +218 -0
- package/management-ui/src/components/theme-provider.jsx +52 -0
- package/management-ui/src/components/theme-toggle.jsx +39 -0
- package/management-ui/src/components/ui/badge.tsx +36 -0
- package/management-ui/src/components/ui/button.test.jsx +56 -0
- package/management-ui/src/components/ui/button.tsx +57 -0
- package/management-ui/src/components/ui/card.tsx +76 -0
- package/management-ui/src/components/ui/dropdown-menu.tsx +199 -0
- package/management-ui/src/components/ui/select.tsx +157 -0
- package/management-ui/src/components/ui/skeleton.jsx +15 -0
- package/management-ui/src/hooks/useFrigg.jsx +601 -0
- package/management-ui/src/hooks/useSocket.jsx +58 -0
- package/management-ui/src/index.css +193 -0
- package/management-ui/src/lib/utils.ts +6 -0
- package/management-ui/src/main.jsx +10 -0
- package/management-ui/src/pages/CodeGeneration.jsx +14 -0
- package/management-ui/src/pages/Connections.jsx +252 -0
- package/management-ui/src/pages/ConnectionsEnhanced.jsx +633 -0
- package/management-ui/src/pages/Dashboard.jsx +311 -0
- package/management-ui/src/pages/Environment.jsx +314 -0
- package/management-ui/src/pages/IntegrationConfigure.jsx +669 -0
- package/management-ui/src/pages/IntegrationDiscovery.jsx +567 -0
- package/management-ui/src/pages/IntegrationTest.jsx +742 -0
- package/management-ui/src/pages/Integrations.jsx +253 -0
- package/management-ui/src/pages/Monitoring.jsx +17 -0
- package/management-ui/src/pages/Simulation.jsx +155 -0
- package/management-ui/src/pages/Users.jsx +492 -0
- package/management-ui/src/services/api.js +41 -0
- package/management-ui/src/services/apiModuleService.js +193 -0
- package/management-ui/src/services/websocket-handlers.js +120 -0
- package/management-ui/src/test/api/project.test.js +273 -0
- package/management-ui/src/test/components/Welcome.test.jsx +378 -0
- package/management-ui/src/test/mocks/server.js +178 -0
- package/management-ui/src/test/setup.js +61 -0
- package/management-ui/src/test/utils/test-utils.jsx +134 -0
- package/management-ui/src/utils/repository.js +98 -0
- package/management-ui/src/utils/repository.test.js +118 -0
- package/management-ui/src/workflows/phase2-integration-workflows.js +884 -0
- package/management-ui/tailwind.config.js +63 -0
- package/management-ui/tsconfig.json +37 -0
- package/management-ui/tsconfig.node.json +10 -0
- package/management-ui/vite.config.js +26 -0
- package/management-ui/vitest.config.js +38 -0
- package/package.json +5 -5
- package/management-ui/dist/assets/index-BA21WgFa.js +0 -1221
- package/management-ui/dist/assets/index-CbM64Oba.js +0 -1221
- package/management-ui/dist/assets/index-CkvseXTC.css +0 -1
- /package/management-ui/{dist/assets/FriggLogo-B7Xx8ZW1.svg → src/assets/FriggLogo.svg} +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { Button } from '../Button'
|
|
3
|
+
import LoadingSpinner from '../LoadingSpinner'
|
|
4
|
+
import StatusBadge from '../StatusBadge'
|
|
5
|
+
import api from '../../services/api'
|
|
6
|
+
|
|
7
|
+
const ConnectionTester = ({ connection, onTestComplete }) => {
|
|
8
|
+
const [testing, setTesting] = useState(false)
|
|
9
|
+
const [testResult, setTestResult] = useState(null)
|
|
10
|
+
const [testDetails, setTestDetails] = useState([])
|
|
11
|
+
|
|
12
|
+
const runConnectionTest = async () => {
|
|
13
|
+
setTesting(true)
|
|
14
|
+
setTestResult(null)
|
|
15
|
+
setTestDetails([])
|
|
16
|
+
|
|
17
|
+
const steps = [
|
|
18
|
+
{ id: 'auth', name: 'Validating authentication', status: 'pending' },
|
|
19
|
+
{ id: 'api', name: 'Testing API connectivity', status: 'pending' },
|
|
20
|
+
{ id: 'permissions', name: 'Checking permissions', status: 'pending' },
|
|
21
|
+
{ id: 'data', name: 'Fetching sample data', status: 'pending' }
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
setTestDetails(steps)
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Run comprehensive connection test
|
|
28
|
+
const response = await api.post(`/api/connections/${connection.id}/test`, {
|
|
29
|
+
comprehensive: true
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const { results, summary } = response.data
|
|
33
|
+
|
|
34
|
+
// Update test details with results
|
|
35
|
+
const updatedSteps = steps.map(step => {
|
|
36
|
+
const result = results[step.id]
|
|
37
|
+
return {
|
|
38
|
+
...step,
|
|
39
|
+
status: result?.success ? 'success' : 'failed',
|
|
40
|
+
message: result?.message,
|
|
41
|
+
latency: result?.latency,
|
|
42
|
+
error: result?.error
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
setTestDetails(updatedSteps)
|
|
47
|
+
setTestResult(summary)
|
|
48
|
+
|
|
49
|
+
if (onTestComplete) {
|
|
50
|
+
onTestComplete(summary)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
} catch (error) {
|
|
54
|
+
setTestResult({
|
|
55
|
+
success: false,
|
|
56
|
+
error: error.response?.data?.error || 'Connection test failed'
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Mark all steps as failed
|
|
60
|
+
setTestDetails(steps.map(step => ({
|
|
61
|
+
...step,
|
|
62
|
+
status: 'failed',
|
|
63
|
+
error: 'Test aborted due to error'
|
|
64
|
+
})))
|
|
65
|
+
} finally {
|
|
66
|
+
setTesting(false)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const getStatusColor = (status) => {
|
|
71
|
+
switch (status) {
|
|
72
|
+
case 'success':
|
|
73
|
+
return 'text-green-600'
|
|
74
|
+
case 'failed':
|
|
75
|
+
return 'text-red-600'
|
|
76
|
+
case 'pending':
|
|
77
|
+
return 'text-gray-400'
|
|
78
|
+
default:
|
|
79
|
+
return 'text-gray-600'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const getStatusIcon = (status) => {
|
|
84
|
+
switch (status) {
|
|
85
|
+
case 'success':
|
|
86
|
+
return (
|
|
87
|
+
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
88
|
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
|
89
|
+
</svg>
|
|
90
|
+
)
|
|
91
|
+
case 'failed':
|
|
92
|
+
return (
|
|
93
|
+
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
94
|
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
95
|
+
</svg>
|
|
96
|
+
)
|
|
97
|
+
case 'pending':
|
|
98
|
+
return <LoadingSpinner size="sm" />
|
|
99
|
+
default:
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="bg-white rounded-lg shadow p-6">
|
|
106
|
+
<div className="flex items-center justify-between mb-6">
|
|
107
|
+
<h3 className="text-lg font-semibold text-gray-900">Connection Test</h3>
|
|
108
|
+
{testResult && (
|
|
109
|
+
<StatusBadge
|
|
110
|
+
status={testResult.success ? 'success' : 'error'}
|
|
111
|
+
text={testResult.success ? 'Passed' : 'Failed'}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{!testing && !testResult && (
|
|
117
|
+
<div>
|
|
118
|
+
<p className="text-sm text-gray-600 mb-4">
|
|
119
|
+
Run a comprehensive test to validate this connection's authentication,
|
|
120
|
+
API access, and permissions.
|
|
121
|
+
</p>
|
|
122
|
+
<Button onClick={runConnectionTest} variant="primary">
|
|
123
|
+
Run Connection Test
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{(testing || testDetails.length > 0) && (
|
|
129
|
+
<div className="space-y-3">
|
|
130
|
+
{testDetails.map((step) => (
|
|
131
|
+
<div key={step.id} className="flex items-start space-x-3">
|
|
132
|
+
<div className={`flex-shrink-0 ${getStatusColor(step.status)}`}>
|
|
133
|
+
{getStatusIcon(step.status)}
|
|
134
|
+
</div>
|
|
135
|
+
<div className="flex-1">
|
|
136
|
+
<p className="text-sm font-medium text-gray-900">{step.name}</p>
|
|
137
|
+
{step.message && (
|
|
138
|
+
<p className="text-sm text-gray-600 mt-1">{step.message}</p>
|
|
139
|
+
)}
|
|
140
|
+
{step.error && (
|
|
141
|
+
<p className="text-sm text-red-600 mt-1">{step.error}</p>
|
|
142
|
+
)}
|
|
143
|
+
{step.latency && (
|
|
144
|
+
<p className="text-xs text-gray-500 mt-1">
|
|
145
|
+
Response time: {step.latency}ms
|
|
146
|
+
</p>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{testResult && (
|
|
155
|
+
<div className="mt-6 pt-6 border-t border-gray-200">
|
|
156
|
+
<h4 className="text-sm font-semibold text-gray-900 mb-3">Test Summary</h4>
|
|
157
|
+
|
|
158
|
+
{testResult.success ? (
|
|
159
|
+
<div className="bg-green-50 border border-green-200 rounded-md p-4">
|
|
160
|
+
<p className="text-sm text-green-800">
|
|
161
|
+
All tests passed successfully. The connection is working properly.
|
|
162
|
+
</p>
|
|
163
|
+
{testResult.avgLatency && (
|
|
164
|
+
<p className="text-xs text-green-700 mt-2">
|
|
165
|
+
Average response time: {testResult.avgLatency}ms
|
|
166
|
+
</p>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
) : (
|
|
170
|
+
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
|
171
|
+
<p className="text-sm text-red-800">
|
|
172
|
+
{testResult.error || 'One or more tests failed. Please check the connection configuration.'}
|
|
173
|
+
</p>
|
|
174
|
+
{testResult.suggestion && (
|
|
175
|
+
<p className="text-xs text-red-700 mt-2">
|
|
176
|
+
Suggestion: {testResult.suggestion}
|
|
177
|
+
</p>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{!testing && (
|
|
183
|
+
<div className="mt-4 flex space-x-3">
|
|
184
|
+
<Button onClick={runConnectionTest} variant="secondary" size="sm">
|
|
185
|
+
Run Again
|
|
186
|
+
</Button>
|
|
187
|
+
{testResult.success && testResult.canRefreshToken && (
|
|
188
|
+
<Button variant="secondary" size="sm">
|
|
189
|
+
Refresh Token
|
|
190
|
+
</Button>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export default ConnectionTester
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react'
|
|
2
|
+
import { Button } from '../Button'
|
|
3
|
+
import LoadingSpinner from '../LoadingSpinner'
|
|
4
|
+
import api from '../../services/api'
|
|
5
|
+
|
|
6
|
+
const EntityRelationshipMapper = ({ connectionId }) => {
|
|
7
|
+
const [entities, setEntities] = useState([])
|
|
8
|
+
const [relationships, setRelationships] = useState([])
|
|
9
|
+
const [loading, setLoading] = useState(true)
|
|
10
|
+
const [selectedEntity, setSelectedEntity] = useState(null)
|
|
11
|
+
const [viewMode, setViewMode] = useState('graph') // graph or list
|
|
12
|
+
const canvasRef = useRef(null)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
fetchEntityData()
|
|
16
|
+
}, [connectionId])
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (viewMode === 'graph' && entities.length > 0) {
|
|
20
|
+
drawEntityGraph()
|
|
21
|
+
}
|
|
22
|
+
}, [entities, relationships, viewMode, selectedEntity])
|
|
23
|
+
|
|
24
|
+
const fetchEntityData = async () => {
|
|
25
|
+
setLoading(true)
|
|
26
|
+
try {
|
|
27
|
+
const [entitiesRes, relationshipsRes] = await Promise.all([
|
|
28
|
+
api.get(`/api/connections/${connectionId}/entities`),
|
|
29
|
+
api.get(`/api/connections/${connectionId}/relationships`)
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
setEntities(entitiesRes.data.entities || [])
|
|
33
|
+
setRelationships(relationshipsRes.data.relationships || [])
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Failed to fetch entity data:', error)
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const drawEntityGraph = () => {
|
|
42
|
+
const canvas = canvasRef.current
|
|
43
|
+
if (!canvas) return
|
|
44
|
+
|
|
45
|
+
const ctx = canvas.getContext('2d')
|
|
46
|
+
const width = canvas.width = canvas.offsetWidth
|
|
47
|
+
const height = canvas.height = canvas.offsetHeight
|
|
48
|
+
|
|
49
|
+
// Clear canvas
|
|
50
|
+
ctx.clearRect(0, 0, width, height)
|
|
51
|
+
|
|
52
|
+
// Calculate positions for entities (simple circular layout)
|
|
53
|
+
const centerX = width / 2
|
|
54
|
+
const centerY = height / 2
|
|
55
|
+
const radius = Math.min(width, height) * 0.35
|
|
56
|
+
|
|
57
|
+
const entityPositions = {}
|
|
58
|
+
entities.forEach((entity, index) => {
|
|
59
|
+
const angle = (index / entities.length) * 2 * Math.PI
|
|
60
|
+
entityPositions[entity.id] = {
|
|
61
|
+
x: centerX + radius * Math.cos(angle),
|
|
62
|
+
y: centerY + radius * Math.sin(angle),
|
|
63
|
+
entity
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Draw relationships (lines)
|
|
68
|
+
ctx.strokeStyle = '#e5e7eb'
|
|
69
|
+
ctx.lineWidth = 1
|
|
70
|
+
relationships.forEach(rel => {
|
|
71
|
+
const from = entityPositions[rel.fromId]
|
|
72
|
+
const to = entityPositions[rel.toId]
|
|
73
|
+
if (from && to) {
|
|
74
|
+
ctx.beginPath()
|
|
75
|
+
ctx.moveTo(from.x, from.y)
|
|
76
|
+
ctx.lineTo(to.x, to.y)
|
|
77
|
+
ctx.stroke()
|
|
78
|
+
|
|
79
|
+
// Draw relationship label
|
|
80
|
+
const midX = (from.x + to.x) / 2
|
|
81
|
+
const midY = (from.y + to.y) / 2
|
|
82
|
+
ctx.fillStyle = '#6b7280'
|
|
83
|
+
ctx.font = '10px sans-serif'
|
|
84
|
+
ctx.textAlign = 'center'
|
|
85
|
+
ctx.fillText(rel.type, midX, midY)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Draw entities (circles)
|
|
90
|
+
Object.values(entityPositions).forEach(({ x, y, entity }) => {
|
|
91
|
+
const isSelected = selectedEntity?.id === entity.id
|
|
92
|
+
|
|
93
|
+
// Draw circle
|
|
94
|
+
ctx.beginPath()
|
|
95
|
+
ctx.arc(x, y, 30, 0, 2 * Math.PI)
|
|
96
|
+
ctx.fillStyle = isSelected ? '#2563eb' : '#ffffff'
|
|
97
|
+
ctx.fill()
|
|
98
|
+
ctx.strokeStyle = isSelected ? '#2563eb' : '#d1d5db'
|
|
99
|
+
ctx.lineWidth = 2
|
|
100
|
+
ctx.stroke()
|
|
101
|
+
|
|
102
|
+
// Draw entity name
|
|
103
|
+
ctx.fillStyle = isSelected ? '#ffffff' : '#111827'
|
|
104
|
+
ctx.font = '12px sans-serif'
|
|
105
|
+
ctx.textAlign = 'center'
|
|
106
|
+
ctx.textBaseline = 'middle'
|
|
107
|
+
ctx.fillText(entity.name || entity.type, x, y)
|
|
108
|
+
|
|
109
|
+
// Draw entity type
|
|
110
|
+
ctx.fillStyle = isSelected ? '#dbeafe' : '#6b7280'
|
|
111
|
+
ctx.font = '10px sans-serif'
|
|
112
|
+
ctx.fillText(entity.type, x, y + 40)
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const handleCanvasClick = (e) => {
|
|
117
|
+
const canvas = canvasRef.current
|
|
118
|
+
const rect = canvas.getBoundingClientRect()
|
|
119
|
+
const x = e.clientX - rect.left
|
|
120
|
+
const y = e.clientY - rect.top
|
|
121
|
+
|
|
122
|
+
// Check if click is on an entity
|
|
123
|
+
const centerX = canvas.width / 2
|
|
124
|
+
const centerY = canvas.height / 2
|
|
125
|
+
const radius = Math.min(canvas.width, canvas.height) * 0.35
|
|
126
|
+
|
|
127
|
+
entities.forEach((entity, index) => {
|
|
128
|
+
const angle = (index / entities.length) * 2 * Math.PI
|
|
129
|
+
const entityX = centerX + radius * Math.cos(angle)
|
|
130
|
+
const entityY = centerY + radius * Math.sin(angle)
|
|
131
|
+
|
|
132
|
+
const distance = Math.sqrt(Math.pow(x - entityX, 2) + Math.pow(y - entityY, 2))
|
|
133
|
+
if (distance <= 30) {
|
|
134
|
+
setSelectedEntity(entity)
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const syncEntities = async () => {
|
|
140
|
+
setLoading(true)
|
|
141
|
+
try {
|
|
142
|
+
await api.post(`/api/connections/${connectionId}/sync`)
|
|
143
|
+
await fetchEntityData()
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('Failed to sync entities:', error)
|
|
146
|
+
} finally {
|
|
147
|
+
setLoading(false)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (loading) {
|
|
152
|
+
return (
|
|
153
|
+
<div className="flex items-center justify-center p-8">
|
|
154
|
+
<LoadingSpinner size="lg" />
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className="bg-white rounded-lg shadow p-6">
|
|
161
|
+
<div className="flex items-center justify-between mb-6">
|
|
162
|
+
<h3 className="text-lg font-semibold text-gray-900">Entity Relationships</h3>
|
|
163
|
+
<div className="flex space-x-2">
|
|
164
|
+
<div className="flex rounded-md shadow-sm">
|
|
165
|
+
<button
|
|
166
|
+
onClick={() => setViewMode('graph')}
|
|
167
|
+
className={`px-3 py-1 text-sm font-medium rounded-l-md ${
|
|
168
|
+
viewMode === 'graph'
|
|
169
|
+
? 'bg-blue-600 text-white'
|
|
170
|
+
: 'bg-white text-gray-700 hover:bg-gray-50'
|
|
171
|
+
}`}
|
|
172
|
+
>
|
|
173
|
+
Graph
|
|
174
|
+
</button>
|
|
175
|
+
<button
|
|
176
|
+
onClick={() => setViewMode('list')}
|
|
177
|
+
className={`px-3 py-1 text-sm font-medium rounded-r-md ${
|
|
178
|
+
viewMode === 'list'
|
|
179
|
+
? 'bg-blue-600 text-white'
|
|
180
|
+
: 'bg-white text-gray-700 hover:bg-gray-50'
|
|
181
|
+
}`}
|
|
182
|
+
>
|
|
183
|
+
List
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
<Button onClick={syncEntities} size="sm" variant="secondary">
|
|
187
|
+
Sync Entities
|
|
188
|
+
</Button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{entities.length === 0 ? (
|
|
193
|
+
<div className="text-center py-8">
|
|
194
|
+
<p className="text-gray-500 mb-4">No entities found for this connection.</p>
|
|
195
|
+
<Button onClick={syncEntities} variant="primary">
|
|
196
|
+
Sync Entities Now
|
|
197
|
+
</Button>
|
|
198
|
+
</div>
|
|
199
|
+
) : (
|
|
200
|
+
<>
|
|
201
|
+
{viewMode === 'graph' ? (
|
|
202
|
+
<div className="relative">
|
|
203
|
+
<canvas
|
|
204
|
+
ref={canvasRef}
|
|
205
|
+
width={600}
|
|
206
|
+
height={400}
|
|
207
|
+
className="w-full h-96 border border-gray-200 rounded-lg cursor-pointer"
|
|
208
|
+
onClick={handleCanvasClick}
|
|
209
|
+
/>
|
|
210
|
+
|
|
211
|
+
{selectedEntity && (
|
|
212
|
+
<div className="mt-4 p-4 bg-gray-50 rounded-lg">
|
|
213
|
+
<h4 className="font-medium text-gray-900 mb-2">
|
|
214
|
+
{selectedEntity.name || selectedEntity.id}
|
|
215
|
+
</h4>
|
|
216
|
+
<dl className="grid grid-cols-2 gap-2 text-sm">
|
|
217
|
+
<dt className="text-gray-500">Type:</dt>
|
|
218
|
+
<dd className="text-gray-900">{selectedEntity.type}</dd>
|
|
219
|
+
<dt className="text-gray-500">External ID:</dt>
|
|
220
|
+
<dd className="text-gray-900 font-mono text-xs">
|
|
221
|
+
{selectedEntity.externalId}
|
|
222
|
+
</dd>
|
|
223
|
+
<dt className="text-gray-500">Created:</dt>
|
|
224
|
+
<dd className="text-gray-900">
|
|
225
|
+
{new Date(selectedEntity.createdAt).toLocaleDateString()}
|
|
226
|
+
</dd>
|
|
227
|
+
</dl>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
) : (
|
|
232
|
+
<div className="space-y-4">
|
|
233
|
+
{entities.map((entity) => (
|
|
234
|
+
<div
|
|
235
|
+
key={entity.id}
|
|
236
|
+
className="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 cursor-pointer"
|
|
237
|
+
onClick={() => setSelectedEntity(entity)}
|
|
238
|
+
>
|
|
239
|
+
<div className="flex items-center justify-between">
|
|
240
|
+
<div>
|
|
241
|
+
<h4 className="font-medium text-gray-900">
|
|
242
|
+
{entity.name || entity.id}
|
|
243
|
+
</h4>
|
|
244
|
+
<p className="text-sm text-gray-500">
|
|
245
|
+
Type: {entity.type} | External ID: {entity.externalId}
|
|
246
|
+
</p>
|
|
247
|
+
</div>
|
|
248
|
+
<div className="text-sm text-gray-500">
|
|
249
|
+
{relationships.filter(r =>
|
|
250
|
+
r.fromId === entity.id || r.toId === entity.id
|
|
251
|
+
).length} relationships
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{selectedEntity?.id === entity.id && (
|
|
256
|
+
<div className="mt-4 pt-4 border-t border-gray-200">
|
|
257
|
+
<h5 className="text-sm font-medium text-gray-900 mb-2">
|
|
258
|
+
Relationships:
|
|
259
|
+
</h5>
|
|
260
|
+
<div className="space-y-1">
|
|
261
|
+
{relationships
|
|
262
|
+
.filter(r => r.fromId === entity.id || r.toId === entity.id)
|
|
263
|
+
.map((rel, index) => (
|
|
264
|
+
<p key={index} className="text-sm text-gray-600">
|
|
265
|
+
{rel.fromId === entity.id ? 'Has' : 'Is'} {rel.type}
|
|
266
|
+
{' '}
|
|
267
|
+
{rel.fromId === entity.id
|
|
268
|
+
? entities.find(e => e.id === rel.toId)?.name || rel.toId
|
|
269
|
+
: entities.find(e => e.id === rel.fromId)?.name || rel.fromId
|
|
270
|
+
}
|
|
271
|
+
</p>
|
|
272
|
+
))}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
))}
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
<div className="mt-6 pt-4 border-t border-gray-200">
|
|
282
|
+
<p className="text-sm text-gray-500">
|
|
283
|
+
Total: {entities.length} entities, {relationships.length} relationships
|
|
284
|
+
</p>
|
|
285
|
+
</div>
|
|
286
|
+
</>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export default EntityRelationshipMapper
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { Button } from '../Button'
|
|
3
|
+
import LoadingSpinner from '../LoadingSpinner'
|
|
4
|
+
import api from '../../services/api'
|
|
5
|
+
|
|
6
|
+
const OAuthFlow = ({ integration, onSuccess, onCancel }) => {
|
|
7
|
+
const [loading, setLoading] = useState(false)
|
|
8
|
+
const [error, setError] = useState(null)
|
|
9
|
+
const [authUrl, setAuthUrl] = useState(null)
|
|
10
|
+
const [pollingForToken, setPollingForToken] = useState(false)
|
|
11
|
+
|
|
12
|
+
// OAuth configuration for different providers
|
|
13
|
+
const oauthConfigs = {
|
|
14
|
+
slack: {
|
|
15
|
+
authEndpoint: 'https://slack.com/oauth/v2/authorize',
|
|
16
|
+
scopes: ['channels:read', 'chat:write', 'users:read'],
|
|
17
|
+
responseType: 'code'
|
|
18
|
+
},
|
|
19
|
+
google: {
|
|
20
|
+
authEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
21
|
+
scopes: ['https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/drive.readonly'],
|
|
22
|
+
responseType: 'code'
|
|
23
|
+
},
|
|
24
|
+
salesforce: {
|
|
25
|
+
authEndpoint: 'https://login.salesforce.com/services/oauth2/authorize',
|
|
26
|
+
scopes: ['api', 'refresh_token'],
|
|
27
|
+
responseType: 'code'
|
|
28
|
+
},
|
|
29
|
+
hubspot: {
|
|
30
|
+
authEndpoint: 'https://app.hubspot.com/oauth/authorize',
|
|
31
|
+
scopes: ['contacts', 'oauth'],
|
|
32
|
+
responseType: 'code'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const startOAuthFlow = async () => {
|
|
37
|
+
setLoading(true)
|
|
38
|
+
setError(null)
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Get OAuth initialization data from server
|
|
42
|
+
const response = await api.post(`/api/connections/oauth/init`, {
|
|
43
|
+
integration: integration.name,
|
|
44
|
+
provider: integration.provider || integration.name.toLowerCase()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const { authUrl: serverAuthUrl, state, codeVerifier } = response.data
|
|
48
|
+
|
|
49
|
+
// Store state and code verifier for later verification
|
|
50
|
+
sessionStorage.setItem('oauth_state', state)
|
|
51
|
+
if (codeVerifier) {
|
|
52
|
+
sessionStorage.setItem('oauth_verifier', codeVerifier)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Open OAuth window
|
|
56
|
+
const authWindow = window.open(
|
|
57
|
+
serverAuthUrl,
|
|
58
|
+
'OAuth Authorization',
|
|
59
|
+
'width=600,height=700,left=200,top=100'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
// Start polling for completion
|
|
63
|
+
setPollingForToken(true)
|
|
64
|
+
pollForAuthCompletion(state, authWindow)
|
|
65
|
+
|
|
66
|
+
} catch (err) {
|
|
67
|
+
setError(err.response?.data?.error || 'Failed to initialize OAuth flow')
|
|
68
|
+
setLoading(false)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pollForAuthCompletion = async (state, authWindow) => {
|
|
73
|
+
const pollInterval = setInterval(async () => {
|
|
74
|
+
// Check if window was closed
|
|
75
|
+
if (authWindow && authWindow.closed) {
|
|
76
|
+
clearInterval(pollInterval)
|
|
77
|
+
setPollingForToken(false)
|
|
78
|
+
setLoading(false)
|
|
79
|
+
setError('Authorization window was closed')
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Check if auth is complete
|
|
85
|
+
const response = await api.get(`/api/connections/oauth/status/${state}`)
|
|
86
|
+
|
|
87
|
+
if (response.data.status === 'completed') {
|
|
88
|
+
clearInterval(pollInterval)
|
|
89
|
+
setPollingForToken(false)
|
|
90
|
+
setLoading(false)
|
|
91
|
+
|
|
92
|
+
if (authWindow && !authWindow.closed) {
|
|
93
|
+
authWindow.close()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Clean up session storage
|
|
97
|
+
sessionStorage.removeItem('oauth_state')
|
|
98
|
+
sessionStorage.removeItem('oauth_verifier')
|
|
99
|
+
|
|
100
|
+
onSuccess(response.data.connection)
|
|
101
|
+
} else if (response.data.status === 'error') {
|
|
102
|
+
clearInterval(pollInterval)
|
|
103
|
+
setPollingForToken(false)
|
|
104
|
+
setLoading(false)
|
|
105
|
+
setError(response.data.error || 'OAuth authorization failed')
|
|
106
|
+
|
|
107
|
+
if (authWindow && !authWindow.closed) {
|
|
108
|
+
authWindow.close()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
// Continue polling on network errors
|
|
113
|
+
console.error('Polling error:', err)
|
|
114
|
+
}
|
|
115
|
+
}, 1500)
|
|
116
|
+
|
|
117
|
+
// Stop polling after 5 minutes
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
clearInterval(pollInterval)
|
|
120
|
+
setPollingForToken(false)
|
|
121
|
+
setLoading(false)
|
|
122
|
+
setError('OAuth authorization timed out')
|
|
123
|
+
|
|
124
|
+
if (authWindow && !authWindow.closed) {
|
|
125
|
+
authWindow.close()
|
|
126
|
+
}
|
|
127
|
+
}, 300000)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const handleManualEntry = () => {
|
|
131
|
+
// TODO: Implement manual credential entry
|
|
132
|
+
console.log('Manual entry not yet implemented')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className="p-6 bg-white rounded-lg shadow-lg max-w-md mx-auto">
|
|
137
|
+
<h3 className="text-lg font-semibold mb-4">
|
|
138
|
+
Connect to {integration.displayName || integration.name}
|
|
139
|
+
</h3>
|
|
140
|
+
|
|
141
|
+
{error && (
|
|
142
|
+
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
|
|
143
|
+
<p className="text-sm text-red-800">{error}</p>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{!loading && !pollingForToken && (
|
|
148
|
+
<>
|
|
149
|
+
<p className="text-sm text-gray-600 mb-6">
|
|
150
|
+
Click the button below to authorize access to your {integration.displayName || integration.name} account.
|
|
151
|
+
You'll be redirected to {integration.displayName || integration.name} to complete the authorization.
|
|
152
|
+
</p>
|
|
153
|
+
|
|
154
|
+
<div className="space-y-3">
|
|
155
|
+
<Button
|
|
156
|
+
onClick={startOAuthFlow}
|
|
157
|
+
className="w-full"
|
|
158
|
+
variant="primary"
|
|
159
|
+
>
|
|
160
|
+
Authorize with {integration.displayName || integration.name}
|
|
161
|
+
</Button>
|
|
162
|
+
|
|
163
|
+
<Button
|
|
164
|
+
onClick={onCancel}
|
|
165
|
+
className="w-full"
|
|
166
|
+
variant="secondary"
|
|
167
|
+
>
|
|
168
|
+
Cancel
|
|
169
|
+
</Button>
|
|
170
|
+
|
|
171
|
+
{integration.supportsApiKey && (
|
|
172
|
+
<button
|
|
173
|
+
onClick={handleManualEntry}
|
|
174
|
+
className="w-full text-sm text-gray-600 hover:text-gray-800 underline"
|
|
175
|
+
>
|
|
176
|
+
Enter credentials manually
|
|
177
|
+
</button>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{(loading || pollingForToken) && (
|
|
184
|
+
<div className="text-center py-8">
|
|
185
|
+
<LoadingSpinner size="lg" />
|
|
186
|
+
<p className="mt-4 text-sm text-gray-600">
|
|
187
|
+
{pollingForToken
|
|
188
|
+
? 'Waiting for authorization... Please complete the process in the popup window.'
|
|
189
|
+
: 'Initializing OAuth flow...'}
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
<div className="mt-6 pt-4 border-t border-gray-200">
|
|
195
|
+
<p className="text-xs text-gray-500">
|
|
196
|
+
By connecting, you agree to share the requested permissions with this application.
|
|
197
|
+
Your credentials are securely stored and can be revoked at any time.
|
|
198
|
+
</p>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export default OAuthFlow
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as OAuthFlow } from './OAuthFlow'
|
|
2
|
+
export { default as ConnectionTester } from './ConnectionTester'
|
|
3
|
+
export { default as ConnectionHealthMonitor } from './ConnectionHealthMonitor'
|
|
4
|
+
export { default as EntityRelationshipMapper } from './EntityRelationshipMapper'
|
|
5
|
+
export { default as ConnectionConfigForm } from './ConnectionConfigForm'
|