@friggframework/devtools 2.0.0-next.29 → 2.0.0-next.30
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,469 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react'
|
|
2
|
+
import api from '../services/api'
|
|
3
|
+
|
|
4
|
+
const EnvironmentImportExport = ({ environment = 'local', onImport, onExport }) => {
|
|
5
|
+
const [importData, setImportData] = useState('')
|
|
6
|
+
const [importFormat, setImportFormat] = useState('env') // env, json
|
|
7
|
+
const [importPreview, setImportPreview] = useState(null)
|
|
8
|
+
const [importErrors, setImportErrors] = useState([])
|
|
9
|
+
const [exportOptions, setExportOptions] = useState({
|
|
10
|
+
format: 'env', // env, json
|
|
11
|
+
excludeSecrets: false,
|
|
12
|
+
includeDescriptions: true
|
|
13
|
+
})
|
|
14
|
+
const [isImporting, setIsImporting] = useState(false)
|
|
15
|
+
const [dragActive, setDragActive] = useState(false)
|
|
16
|
+
const fileInputRef = useRef(null)
|
|
17
|
+
|
|
18
|
+
// Parse import data
|
|
19
|
+
const parseImportData = (data, format) => {
|
|
20
|
+
const errors = []
|
|
21
|
+
const variables = []
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
if (format === 'json') {
|
|
25
|
+
// Parse JSON format
|
|
26
|
+
const parsed = typeof data === 'string' ? JSON.parse(data) : data
|
|
27
|
+
|
|
28
|
+
if (typeof parsed !== 'object') {
|
|
29
|
+
errors.push({ message: 'Invalid JSON format' })
|
|
30
|
+
return { variables, errors }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Object.entries(parsed).forEach(([key, value]) => {
|
|
34
|
+
if (typeof key !== 'string') {
|
|
35
|
+
errors.push({ message: `Invalid key: ${key}` })
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
variables.push({
|
|
40
|
+
key: key.toUpperCase(),
|
|
41
|
+
value: String(value),
|
|
42
|
+
isSecret: isSecretVariable(key)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
} else {
|
|
46
|
+
// Parse .env format
|
|
47
|
+
const lines = data.split('\n')
|
|
48
|
+
let currentDescription = ''
|
|
49
|
+
|
|
50
|
+
lines.forEach((line, index) => {
|
|
51
|
+
const trimmed = line.trim()
|
|
52
|
+
|
|
53
|
+
// Skip empty lines
|
|
54
|
+
if (!trimmed) return
|
|
55
|
+
|
|
56
|
+
// Handle comments
|
|
57
|
+
if (trimmed.startsWith('#')) {
|
|
58
|
+
currentDescription = trimmed.substring(1).trim()
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Parse variable
|
|
63
|
+
const equalIndex = trimmed.indexOf('=')
|
|
64
|
+
if (equalIndex > 0) {
|
|
65
|
+
const key = trimmed.substring(0, equalIndex).trim()
|
|
66
|
+
let value = trimmed.substring(equalIndex + 1).trim()
|
|
67
|
+
|
|
68
|
+
// Remove quotes if present
|
|
69
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
70
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
71
|
+
value = value.slice(1, -1)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Validate key format
|
|
75
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) {
|
|
76
|
+
errors.push({
|
|
77
|
+
line: index + 1,
|
|
78
|
+
message: `Invalid variable name: ${key}`
|
|
79
|
+
})
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
variables.push({
|
|
84
|
+
key: key.toUpperCase(),
|
|
85
|
+
value,
|
|
86
|
+
description: currentDescription,
|
|
87
|
+
isSecret: isSecretVariable(key),
|
|
88
|
+
line: index + 1
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
currentDescription = ''
|
|
92
|
+
} else {
|
|
93
|
+
errors.push({
|
|
94
|
+
line: index + 1,
|
|
95
|
+
message: `Invalid line format: ${trimmed}`
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
errors.push({ message: error.message })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { variables, errors }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check if variable is sensitive
|
|
108
|
+
const isSecretVariable = (key) => {
|
|
109
|
+
const patterns = ['PASSWORD', 'SECRET', 'KEY', 'TOKEN', 'PRIVATE', 'CREDENTIAL']
|
|
110
|
+
return patterns.some(pattern => key.toUpperCase().includes(pattern))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Handle file drop
|
|
114
|
+
const handleDrop = (e) => {
|
|
115
|
+
e.preventDefault()
|
|
116
|
+
e.stopPropagation()
|
|
117
|
+
setDragActive(false)
|
|
118
|
+
|
|
119
|
+
const file = e.dataTransfer.files[0]
|
|
120
|
+
if (file) {
|
|
121
|
+
handleFileSelect(file)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle file select
|
|
126
|
+
const handleFileSelect = (file) => {
|
|
127
|
+
const reader = new FileReader()
|
|
128
|
+
|
|
129
|
+
reader.onload = (e) => {
|
|
130
|
+
const content = e.target.result
|
|
131
|
+
setImportData(content)
|
|
132
|
+
|
|
133
|
+
// Auto-detect format
|
|
134
|
+
const detectedFormat = file.name.endsWith('.json') ? 'json' : 'env'
|
|
135
|
+
setImportFormat(detectedFormat)
|
|
136
|
+
|
|
137
|
+
// Generate preview
|
|
138
|
+
const { variables, errors } = parseImportData(content, detectedFormat)
|
|
139
|
+
setImportPreview(variables)
|
|
140
|
+
setImportErrors(errors)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
reader.readAsText(file)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Handle manual input change
|
|
147
|
+
const handleImportDataChange = (e) => {
|
|
148
|
+
const data = e.target.value
|
|
149
|
+
setImportData(data)
|
|
150
|
+
|
|
151
|
+
if (data) {
|
|
152
|
+
const { variables, errors } = parseImportData(data, importFormat)
|
|
153
|
+
setImportPreview(variables)
|
|
154
|
+
setImportErrors(errors)
|
|
155
|
+
} else {
|
|
156
|
+
setImportPreview(null)
|
|
157
|
+
setImportErrors([])
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Perform import
|
|
162
|
+
const handleImport = async () => {
|
|
163
|
+
if (!importPreview || importPreview.length === 0) return
|
|
164
|
+
|
|
165
|
+
setIsImporting(true)
|
|
166
|
+
try {
|
|
167
|
+
const response = await api.post(`/api/environment/import/${environment}`, {
|
|
168
|
+
data: importData,
|
|
169
|
+
format: importFormat,
|
|
170
|
+
merge: true
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
if (onImport) {
|
|
174
|
+
onImport({
|
|
175
|
+
imported: response.data.imported,
|
|
176
|
+
total: response.data.total
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Reset form
|
|
181
|
+
setImportData('')
|
|
182
|
+
setImportPreview(null)
|
|
183
|
+
setImportErrors([])
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error('Import error:', error)
|
|
186
|
+
setImportErrors([{ message: error.response?.data?.message || error.message }])
|
|
187
|
+
} finally {
|
|
188
|
+
setIsImporting(false)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Perform export
|
|
193
|
+
const handleExport = async () => {
|
|
194
|
+
try {
|
|
195
|
+
const params = new URLSearchParams({
|
|
196
|
+
format: exportOptions.format,
|
|
197
|
+
excludeSecrets: exportOptions.excludeSecrets
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const response = await api.get(
|
|
201
|
+
`/api/environment/export/${environment}?${params}`,
|
|
202
|
+
{ responseType: 'blob' }
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// Create download link
|
|
206
|
+
const url = window.URL.createObjectURL(new Blob([response.data]))
|
|
207
|
+
const link = document.createElement('a')
|
|
208
|
+
link.href = url
|
|
209
|
+
|
|
210
|
+
const filename = exportOptions.format === 'json'
|
|
211
|
+
? `${environment}-env.json`
|
|
212
|
+
: environment === 'local' ? '.env' : `.env.${environment}`
|
|
213
|
+
|
|
214
|
+
link.setAttribute('download', filename)
|
|
215
|
+
document.body.appendChild(link)
|
|
216
|
+
link.click()
|
|
217
|
+
link.remove()
|
|
218
|
+
|
|
219
|
+
if (onExport) {
|
|
220
|
+
onExport({ format: exportOptions.format })
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error('Export error:', error)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<div className="environment-import-export">
|
|
229
|
+
{/* Import Section */}
|
|
230
|
+
<div className="bg-white rounded-lg shadow mb-6">
|
|
231
|
+
<div className="p-4 border-b border-gray-200">
|
|
232
|
+
<h3 className="text-lg font-medium text-gray-900">Import Variables</h3>
|
|
233
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
234
|
+
Import environment variables from .env or JSON files
|
|
235
|
+
</p>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div className="p-4">
|
|
239
|
+
{/* Format Selection */}
|
|
240
|
+
<div className="mb-4">
|
|
241
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
242
|
+
Import Format
|
|
243
|
+
</label>
|
|
244
|
+
<div className="flex space-x-4">
|
|
245
|
+
<label className="flex items-center">
|
|
246
|
+
<input
|
|
247
|
+
type="radio"
|
|
248
|
+
value="env"
|
|
249
|
+
checked={importFormat === 'env'}
|
|
250
|
+
onChange={(e) => setImportFormat(e.target.value)}
|
|
251
|
+
className="mr-2"
|
|
252
|
+
/>
|
|
253
|
+
<span className="text-sm">.env format</span>
|
|
254
|
+
</label>
|
|
255
|
+
<label className="flex items-center">
|
|
256
|
+
<input
|
|
257
|
+
type="radio"
|
|
258
|
+
value="json"
|
|
259
|
+
checked={importFormat === 'json'}
|
|
260
|
+
onChange={(e) => setImportFormat(e.target.value)}
|
|
261
|
+
className="mr-2"
|
|
262
|
+
/>
|
|
263
|
+
<span className="text-sm">JSON format</span>
|
|
264
|
+
</label>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{/* File Drop Zone */}
|
|
269
|
+
<div
|
|
270
|
+
onDrop={handleDrop}
|
|
271
|
+
onDragOver={(e) => { e.preventDefault(); setDragActive(true) }}
|
|
272
|
+
onDragLeave={() => setDragActive(false)}
|
|
273
|
+
className={`border-2 border-dashed rounded-lg p-6 text-center mb-4 transition-colors ${
|
|
274
|
+
dragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
|
|
275
|
+
}`}
|
|
276
|
+
>
|
|
277
|
+
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 48 48">
|
|
278
|
+
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
279
|
+
</svg>
|
|
280
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
281
|
+
Drop a file here, or{' '}
|
|
282
|
+
<button
|
|
283
|
+
onClick={() => fileInputRef.current?.click()}
|
|
284
|
+
className="text-blue-600 hover:text-blue-500"
|
|
285
|
+
>
|
|
286
|
+
browse
|
|
287
|
+
</button>
|
|
288
|
+
</p>
|
|
289
|
+
<p className="text-xs text-gray-500 mt-1">
|
|
290
|
+
Supports .env and .json files
|
|
291
|
+
</p>
|
|
292
|
+
<input
|
|
293
|
+
ref={fileInputRef}
|
|
294
|
+
type="file"
|
|
295
|
+
accept=".env,.json"
|
|
296
|
+
onChange={(e) => e.target.files[0] && handleFileSelect(e.target.files[0])}
|
|
297
|
+
className="hidden"
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{/* Manual Input */}
|
|
302
|
+
<div className="mb-4">
|
|
303
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
304
|
+
Or paste content directly:
|
|
305
|
+
</label>
|
|
306
|
+
<textarea
|
|
307
|
+
value={importData}
|
|
308
|
+
onChange={handleImportDataChange}
|
|
309
|
+
placeholder={importFormat === 'json'
|
|
310
|
+
? '{\n "API_KEY": "your-api-key",\n "DATABASE_URL": "postgresql://..."\n}'
|
|
311
|
+
: '# Database configuration\nDATABASE_URL=postgresql://...\n\n# API settings\nAPI_KEY=your-api-key'
|
|
312
|
+
}
|
|
313
|
+
className="w-full h-32 px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:border-blue-500"
|
|
314
|
+
/>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{/* Import Errors */}
|
|
318
|
+
{importErrors.length > 0 && (
|
|
319
|
+
<div className="mb-4 bg-red-50 border border-red-200 rounded p-3">
|
|
320
|
+
<h4 className="text-sm font-medium text-red-800 mb-2">Import Errors:</h4>
|
|
321
|
+
<ul className="text-sm text-red-600 space-y-1">
|
|
322
|
+
{importErrors.map((error, index) => (
|
|
323
|
+
<li key={index}>
|
|
324
|
+
{error.line && `Line ${error.line}: `}{error.message}
|
|
325
|
+
</li>
|
|
326
|
+
))}
|
|
327
|
+
</ul>
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
|
|
331
|
+
{/* Import Preview */}
|
|
332
|
+
{importPreview && importPreview.length > 0 && (
|
|
333
|
+
<div className="mb-4">
|
|
334
|
+
<h4 className="text-sm font-medium text-gray-700 mb-2">
|
|
335
|
+
Preview ({importPreview.length} variables)
|
|
336
|
+
</h4>
|
|
337
|
+
<div className="max-h-48 overflow-y-auto border border-gray-200 rounded">
|
|
338
|
+
<table className="min-w-full text-sm">
|
|
339
|
+
<thead className="bg-gray-50">
|
|
340
|
+
<tr>
|
|
341
|
+
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Key</th>
|
|
342
|
+
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Value</th>
|
|
343
|
+
</tr>
|
|
344
|
+
</thead>
|
|
345
|
+
<tbody className="divide-y divide-gray-200">
|
|
346
|
+
{importPreview.map((variable, index) => (
|
|
347
|
+
<tr key={index}>
|
|
348
|
+
<td className="px-3 py-2 font-mono text-gray-900">{variable.key}</td>
|
|
349
|
+
<td className="px-3 py-2 font-mono text-gray-600 truncate max-w-xs">
|
|
350
|
+
{variable.isSecret ? '•••••••' : variable.value}
|
|
351
|
+
</td>
|
|
352
|
+
</tr>
|
|
353
|
+
))}
|
|
354
|
+
</tbody>
|
|
355
|
+
</table>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
{/* Import Button */}
|
|
361
|
+
<button
|
|
362
|
+
onClick={handleImport}
|
|
363
|
+
disabled={!importPreview || importPreview.length === 0 || importErrors.length > 0 || isImporting}
|
|
364
|
+
className={`w-full py-2 px-4 rounded font-medium ${
|
|
365
|
+
importPreview && importPreview.length > 0 && importErrors.length === 0
|
|
366
|
+
? 'bg-blue-600 text-white hover:bg-blue-700'
|
|
367
|
+
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
|
368
|
+
}`}
|
|
369
|
+
>
|
|
370
|
+
{isImporting ? 'Importing...' : `Import ${importPreview?.length || 0} Variables`}
|
|
371
|
+
</button>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
{/* Export Section */}
|
|
376
|
+
<div className="bg-white rounded-lg shadow">
|
|
377
|
+
<div className="p-4 border-b border-gray-200">
|
|
378
|
+
<h3 className="text-lg font-medium text-gray-900">Export Variables</h3>
|
|
379
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
380
|
+
Export current environment variables to a file
|
|
381
|
+
</p>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<div className="p-4">
|
|
385
|
+
{/* Export Options */}
|
|
386
|
+
<div className="space-y-4 mb-4">
|
|
387
|
+
<div>
|
|
388
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
389
|
+
Export Format
|
|
390
|
+
</label>
|
|
391
|
+
<div className="flex space-x-4">
|
|
392
|
+
<label className="flex items-center">
|
|
393
|
+
<input
|
|
394
|
+
type="radio"
|
|
395
|
+
value="env"
|
|
396
|
+
checked={exportOptions.format === 'env'}
|
|
397
|
+
onChange={(e) => setExportOptions({ ...exportOptions, format: e.target.value })}
|
|
398
|
+
className="mr-2"
|
|
399
|
+
/>
|
|
400
|
+
<span className="text-sm">.env format</span>
|
|
401
|
+
</label>
|
|
402
|
+
<label className="flex items-center">
|
|
403
|
+
<input
|
|
404
|
+
type="radio"
|
|
405
|
+
value="json"
|
|
406
|
+
checked={exportOptions.format === 'json'}
|
|
407
|
+
onChange={(e) => setExportOptions({ ...exportOptions, format: e.target.value })}
|
|
408
|
+
className="mr-2"
|
|
409
|
+
/>
|
|
410
|
+
<span className="text-sm">JSON format</span>
|
|
411
|
+
</label>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<label className="flex items-center">
|
|
416
|
+
<input
|
|
417
|
+
type="checkbox"
|
|
418
|
+
checked={exportOptions.excludeSecrets}
|
|
419
|
+
onChange={(e) => setExportOptions({ ...exportOptions, excludeSecrets: e.target.checked })}
|
|
420
|
+
className="mr-2"
|
|
421
|
+
/>
|
|
422
|
+
<span className="text-sm">Exclude secret values (replace with **REDACTED**)</span>
|
|
423
|
+
</label>
|
|
424
|
+
|
|
425
|
+
{exportOptions.format === 'env' && (
|
|
426
|
+
<label className="flex items-center">
|
|
427
|
+
<input
|
|
428
|
+
type="checkbox"
|
|
429
|
+
checked={exportOptions.includeDescriptions}
|
|
430
|
+
onChange={(e) => setExportOptions({ ...exportOptions, includeDescriptions: e.target.checked })}
|
|
431
|
+
className="mr-2"
|
|
432
|
+
/>
|
|
433
|
+
<span className="text-sm">Include variable descriptions as comments</span>
|
|
434
|
+
</label>
|
|
435
|
+
)}
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
{/* Export Info */}
|
|
439
|
+
<div className="bg-blue-50 border border-blue-200 rounded p-3 mb-4">
|
|
440
|
+
<div className="flex">
|
|
441
|
+
<svg className="h-5 w-5 text-blue-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
442
|
+
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
|
443
|
+
</svg>
|
|
444
|
+
<div className="text-sm text-blue-700">
|
|
445
|
+
<p>Export will create a file named:</p>
|
|
446
|
+
<code className="font-mono">
|
|
447
|
+
{exportOptions.format === 'json'
|
|
448
|
+
? `${environment}-env.json`
|
|
449
|
+
: environment === 'local' ? '.env' : `.env.${environment}`
|
|
450
|
+
}
|
|
451
|
+
</code>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
{/* Export Button */}
|
|
457
|
+
<button
|
|
458
|
+
onClick={handleExport}
|
|
459
|
+
className="w-full py-2 px-4 bg-green-600 text-white rounded font-medium hover:bg-green-700"
|
|
460
|
+
>
|
|
461
|
+
Export Variables
|
|
462
|
+
</button>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export default EnvironmentImportExport
|