@friggframework/devtools 2.0.0-next.4 → 2.0.0-next.41

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.
Files changed (198) hide show
  1. package/frigg-cli/.eslintrc.js +141 -0
  2. package/frigg-cli/__tests__/jest.config.js +102 -0
  3. package/frigg-cli/__tests__/unit/commands/build.test.js +483 -0
  4. package/frigg-cli/__tests__/unit/commands/install.test.js +418 -0
  5. package/frigg-cli/__tests__/unit/commands/ui.test.js +592 -0
  6. package/frigg-cli/__tests__/utils/command-tester.js +170 -0
  7. package/frigg-cli/__tests__/utils/mock-factory.js +270 -0
  8. package/frigg-cli/__tests__/utils/test-fixtures.js +463 -0
  9. package/frigg-cli/__tests__/utils/test-setup.js +286 -0
  10. package/frigg-cli/build-command/index.js +54 -0
  11. package/frigg-cli/deploy-command/index.js +175 -0
  12. package/frigg-cli/generate-command/__tests__/generate-command.test.js +312 -0
  13. package/frigg-cli/generate-command/azure-generator.js +43 -0
  14. package/frigg-cli/generate-command/gcp-generator.js +47 -0
  15. package/frigg-cli/generate-command/index.js +332 -0
  16. package/frigg-cli/generate-command/terraform-generator.js +555 -0
  17. package/frigg-cli/generate-iam-command.js +115 -0
  18. package/frigg-cli/index.js +47 -1
  19. package/frigg-cli/index.test.js +1 -4
  20. package/frigg-cli/init-command/backend-first-handler.js +756 -0
  21. package/frigg-cli/init-command/index.js +93 -0
  22. package/frigg-cli/init-command/template-handler.js +143 -0
  23. package/frigg-cli/install-command/index.js +1 -4
  24. package/frigg-cli/package.json +51 -0
  25. package/frigg-cli/start-command/index.js +30 -4
  26. package/frigg-cli/start-command/start-command.test.js +155 -0
  27. package/frigg-cli/test/init-command.test.js +180 -0
  28. package/frigg-cli/test/npm-registry.test.js +319 -0
  29. package/frigg-cli/ui-command/index.js +154 -0
  30. package/frigg-cli/utils/app-resolver.js +319 -0
  31. package/frigg-cli/utils/backend-path.js +16 -17
  32. package/frigg-cli/utils/npm-registry.js +167 -0
  33. package/frigg-cli/utils/process-manager.js +199 -0
  34. package/frigg-cli/utils/repo-detection.js +405 -0
  35. package/infrastructure/DEPLOYMENT-INSTRUCTIONS.md +268 -0
  36. package/infrastructure/GENERATE-IAM-DOCS.md +278 -0
  37. package/infrastructure/IAM-POLICY-TEMPLATES.md +176 -0
  38. package/infrastructure/README.md +443 -0
  39. package/infrastructure/WEBSOCKET-CONFIGURATION.md +105 -0
  40. package/infrastructure/__tests__/fixtures/mock-aws-resources.js +391 -0
  41. package/infrastructure/__tests__/helpers/test-utils.js +277 -0
  42. package/infrastructure/aws-discovery.js +1176 -0
  43. package/infrastructure/aws-discovery.test.js +1220 -0
  44. package/infrastructure/build-time-discovery.js +206 -0
  45. package/infrastructure/build-time-discovery.test.js +378 -0
  46. package/infrastructure/create-frigg-infrastructure.js +3 -5
  47. package/infrastructure/env-validator.js +77 -0
  48. package/infrastructure/frigg-deployment-iam-stack.yaml +401 -0
  49. package/infrastructure/iam-generator.js +836 -0
  50. package/infrastructure/iam-generator.test.js +172 -0
  51. package/infrastructure/iam-policy-basic.json +218 -0
  52. package/infrastructure/iam-policy-full.json +288 -0
  53. package/infrastructure/integration.test.js +383 -0
  54. package/infrastructure/run-discovery.js +110 -0
  55. package/infrastructure/serverless-template.js +1493 -138
  56. package/infrastructure/serverless-template.test.js +1804 -0
  57. package/management-ui/.eslintrc.js +22 -0
  58. package/management-ui/README.md +203 -0
  59. package/management-ui/components.json +21 -0
  60. package/management-ui/docs/phase2-integration-guide.md +320 -0
  61. package/management-ui/index.html +13 -0
  62. package/management-ui/package-lock.json +16517 -0
  63. package/management-ui/package.json +76 -0
  64. package/management-ui/packages/devtools/frigg-cli/ui-command/index.js +302 -0
  65. package/management-ui/postcss.config.js +6 -0
  66. package/management-ui/server/api/backend.js +256 -0
  67. package/management-ui/server/api/cli.js +315 -0
  68. package/management-ui/server/api/codegen.js +663 -0
  69. package/management-ui/server/api/connections.js +857 -0
  70. package/management-ui/server/api/discovery.js +185 -0
  71. package/management-ui/server/api/environment/index.js +1 -0
  72. package/management-ui/server/api/environment/router.js +378 -0
  73. package/management-ui/server/api/environment.js +328 -0
  74. package/management-ui/server/api/integrations.js +876 -0
  75. package/management-ui/server/api/logs.js +248 -0
  76. package/management-ui/server/api/monitoring.js +282 -0
  77. package/management-ui/server/api/open-ide.js +31 -0
  78. package/management-ui/server/api/project.js +1029 -0
  79. package/management-ui/server/api/users/sessions.js +371 -0
  80. package/management-ui/server/api/users/simulation.js +254 -0
  81. package/management-ui/server/api/users.js +362 -0
  82. package/management-ui/server/api-contract.md +275 -0
  83. package/management-ui/server/index.js +873 -0
  84. package/management-ui/server/middleware/errorHandler.js +93 -0
  85. package/management-ui/server/middleware/security.js +32 -0
  86. package/management-ui/server/processManager.js +296 -0
  87. package/management-ui/server/server.js +346 -0
  88. package/management-ui/server/services/aws-monitor.js +413 -0
  89. package/management-ui/server/services/npm-registry.js +347 -0
  90. package/management-ui/server/services/template-engine.js +538 -0
  91. package/management-ui/server/utils/cliIntegration.js +220 -0
  92. package/management-ui/server/utils/environment/auditLogger.js +471 -0
  93. package/management-ui/server/utils/environment/awsParameterStore.js +264 -0
  94. package/management-ui/server/utils/environment/encryption.js +278 -0
  95. package/management-ui/server/utils/environment/envFileManager.js +286 -0
  96. package/management-ui/server/utils/import-commonjs.js +28 -0
  97. package/management-ui/server/utils/response.js +83 -0
  98. package/management-ui/server/websocket/handler.js +325 -0
  99. package/management-ui/src/App.jsx +109 -0
  100. package/management-ui/src/assets/FriggLogo.svg +1 -0
  101. package/management-ui/src/components/AppRouter.jsx +65 -0
  102. package/management-ui/src/components/Button.jsx +70 -0
  103. package/management-ui/src/components/Card.jsx +97 -0
  104. package/management-ui/src/components/EnvironmentCompare.jsx +400 -0
  105. package/management-ui/src/components/EnvironmentEditor.jsx +372 -0
  106. package/management-ui/src/components/EnvironmentImportExport.jsx +469 -0
  107. package/management-ui/src/components/EnvironmentSchema.jsx +491 -0
  108. package/management-ui/src/components/EnvironmentSecurity.jsx +463 -0
  109. package/management-ui/src/components/ErrorBoundary.jsx +73 -0
  110. package/management-ui/src/components/IntegrationCard.jsx +481 -0
  111. package/management-ui/src/components/IntegrationCardEnhanced.jsx +770 -0
  112. package/management-ui/src/components/IntegrationExplorer.jsx +379 -0
  113. package/management-ui/src/components/IntegrationStatus.jsx +336 -0
  114. package/management-ui/src/components/Layout.jsx +716 -0
  115. package/management-ui/src/components/LoadingSpinner.jsx +113 -0
  116. package/management-ui/src/components/RepositoryPicker.jsx +248 -0
  117. package/management-ui/src/components/SessionMonitor.jsx +350 -0
  118. package/management-ui/src/components/StatusBadge.jsx +208 -0
  119. package/management-ui/src/components/UserContextSwitcher.jsx +212 -0
  120. package/management-ui/src/components/UserSimulation.jsx +327 -0
  121. package/management-ui/src/components/Welcome.jsx +434 -0
  122. package/management-ui/src/components/codegen/APIEndpointGenerator.jsx +637 -0
  123. package/management-ui/src/components/codegen/APIModuleSelector.jsx +227 -0
  124. package/management-ui/src/components/codegen/CodeGenerationWizard.jsx +247 -0
  125. package/management-ui/src/components/codegen/CodePreviewEditor.jsx +316 -0
  126. package/management-ui/src/components/codegen/DynamicModuleForm.jsx +271 -0
  127. package/management-ui/src/components/codegen/FormBuilder.jsx +737 -0
  128. package/management-ui/src/components/codegen/IntegrationGenerator.jsx +855 -0
  129. package/management-ui/src/components/codegen/ProjectScaffoldWizard.jsx +797 -0
  130. package/management-ui/src/components/codegen/SchemaBuilder.jsx +303 -0
  131. package/management-ui/src/components/codegen/TemplateSelector.jsx +586 -0
  132. package/management-ui/src/components/codegen/index.js +10 -0
  133. package/management-ui/src/components/connections/ConnectionConfigForm.jsx +362 -0
  134. package/management-ui/src/components/connections/ConnectionHealthMonitor.jsx +182 -0
  135. package/management-ui/src/components/connections/ConnectionTester.jsx +200 -0
  136. package/management-ui/src/components/connections/EntityRelationshipMapper.jsx +292 -0
  137. package/management-ui/src/components/connections/OAuthFlow.jsx +204 -0
  138. package/management-ui/src/components/connections/index.js +5 -0
  139. package/management-ui/src/components/index.js +21 -0
  140. package/management-ui/src/components/monitoring/APIGatewayMetrics.jsx +222 -0
  141. package/management-ui/src/components/monitoring/LambdaMetrics.jsx +169 -0
  142. package/management-ui/src/components/monitoring/MetricsChart.jsx +197 -0
  143. package/management-ui/src/components/monitoring/MonitoringDashboard.jsx +393 -0
  144. package/management-ui/src/components/monitoring/SQSMetrics.jsx +246 -0
  145. package/management-ui/src/components/monitoring/index.js +6 -0
  146. package/management-ui/src/components/monitoring/monitoring.css +218 -0
  147. package/management-ui/src/components/theme-provider.jsx +52 -0
  148. package/management-ui/src/components/theme-toggle.jsx +39 -0
  149. package/management-ui/src/components/ui/badge.tsx +36 -0
  150. package/management-ui/src/components/ui/button.test.jsx +56 -0
  151. package/management-ui/src/components/ui/button.tsx +57 -0
  152. package/management-ui/src/components/ui/card.tsx +76 -0
  153. package/management-ui/src/components/ui/dropdown-menu.tsx +199 -0
  154. package/management-ui/src/components/ui/select.tsx +157 -0
  155. package/management-ui/src/components/ui/skeleton.jsx +15 -0
  156. package/management-ui/src/hooks/useFrigg.jsx +601 -0
  157. package/management-ui/src/hooks/useSocket.jsx +58 -0
  158. package/management-ui/src/index.css +193 -0
  159. package/management-ui/src/lib/utils.ts +6 -0
  160. package/management-ui/src/main.jsx +10 -0
  161. package/management-ui/src/pages/CodeGeneration.jsx +14 -0
  162. package/management-ui/src/pages/Connections.jsx +252 -0
  163. package/management-ui/src/pages/ConnectionsEnhanced.jsx +633 -0
  164. package/management-ui/src/pages/Dashboard.jsx +311 -0
  165. package/management-ui/src/pages/Environment.jsx +314 -0
  166. package/management-ui/src/pages/IntegrationConfigure.jsx +669 -0
  167. package/management-ui/src/pages/IntegrationDiscovery.jsx +567 -0
  168. package/management-ui/src/pages/IntegrationTest.jsx +742 -0
  169. package/management-ui/src/pages/Integrations.jsx +253 -0
  170. package/management-ui/src/pages/Monitoring.jsx +17 -0
  171. package/management-ui/src/pages/Simulation.jsx +155 -0
  172. package/management-ui/src/pages/Users.jsx +492 -0
  173. package/management-ui/src/services/api.js +41 -0
  174. package/management-ui/src/services/apiModuleService.js +193 -0
  175. package/management-ui/src/services/websocket-handlers.js +120 -0
  176. package/management-ui/src/test/api/project.test.js +273 -0
  177. package/management-ui/src/test/components/Welcome.test.jsx +378 -0
  178. package/management-ui/src/test/mocks/server.js +178 -0
  179. package/management-ui/src/test/setup.js +61 -0
  180. package/management-ui/src/test/utils/test-utils.jsx +134 -0
  181. package/management-ui/src/utils/repository.js +98 -0
  182. package/management-ui/src/utils/repository.test.js +118 -0
  183. package/management-ui/src/workflows/phase2-integration-workflows.js +884 -0
  184. package/management-ui/tailwind.config.js +63 -0
  185. package/management-ui/tsconfig.json +37 -0
  186. package/management-ui/tsconfig.node.json +10 -0
  187. package/management-ui/vite.config.js +26 -0
  188. package/management-ui/vitest.config.js +38 -0
  189. package/package.json +20 -9
  190. package/infrastructure/app-handler-helpers.js +0 -57
  191. package/infrastructure/backend-utils.js +0 -90
  192. package/infrastructure/routers/auth.js +0 -26
  193. package/infrastructure/routers/integration-defined-routers.js +0 -37
  194. package/infrastructure/routers/middleware/loadUser.js +0 -15
  195. package/infrastructure/routers/middleware/requireLoggedInUser.js +0 -12
  196. package/infrastructure/routers/user.js +0 -41
  197. package/infrastructure/routers/websocket.js +0 -55
  198. package/infrastructure/workers/integration-defined-workers.js +0 -24
@@ -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'