1
1
'use client'
2
2
3
+ import { T } from "@/components/ui/Typography" ;
3
4
import { Button } from "@/components/ui/button" ;
5
+ import { Card , CardContent } from "@/components/ui/card" ;
4
6
import { Input } from "@/components/ui/input" ;
5
- import { Switch } from "@/components/ui/switch" ;
7
+ import { Label } from "@/components/ui/label" ;
8
+ import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from "@/components/ui/select" ;
9
+ import { Separator } from "@/components/ui/separator" ;
6
10
import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from "@/components/ui/table" ;
7
11
import { Textarea } from "@/components/ui/textarea" ;
8
12
import { EnvVar } from "@/types/userTypes" ;
9
- import { Copy , Edit , Trash } from 'lucide-react' ;
13
+ import { motion } from 'framer-motion' ;
14
+ import { Copy , Edit , Plus , Save , Trash } from 'lucide-react' ;
10
15
import moment from 'moment' ;
11
16
import { useRouter } from 'next/navigation' ;
12
17
import { useState } from 'react' ;
18
+ import { toast } from 'sonner' ;
13
19
14
20
type TFVarTableProps = {
15
21
envVars : EnvVar [ ] ;
@@ -18,71 +24,138 @@ type TFVarTableProps = {
18
24
onBulkUpdate : ( vars : EnvVar [ ] ) => Promise < EnvVar [ ] > ;
19
25
} ;
20
26
27
+ const EmptyState : React . FC < { onAddVariable : ( ) => void } > = ( { onAddVariable } ) => {
28
+ return (
29
+ < motion . div
30
+ initial = { { opacity : 0 , y : 20 } }
31
+ animate = { { opacity : 1 , y : 0 } }
32
+ transition = { { duration : 0.5 } }
33
+ >
34
+ < Card className = "mt-6 border-none bg-transparent shadow-none" >
35
+ < CardContent className = "flex flex-col items-center justify-center py-12" >
36
+ < h3 className = "text-2xl font-semibold mb-2" > No Environment Variables Yet</ h3 >
37
+ < p className = "text-muted-foreground mb-6 text-center max-w-md" >
38
+ Add your first environment variable to get started. These variables will be available in your project's runtime.
39
+ </ p >
40
+ < motion . div
41
+ whileHover = { { scale : 1.05 } }
42
+ whileTap = { { scale : 0.95 } }
43
+ >
44
+ < Button onClick = { onAddVariable } size = "lg" >
45
+ < Plus className = "mr-2 h-4 w-4" /> Add Environment Variable
46
+ </ Button >
47
+ </ motion . div >
48
+ </ CardContent >
49
+ </ Card >
50
+ </ motion . div >
51
+ ) ;
52
+ } ;
53
+
21
54
export default function TFVarTable ( { envVars, onUpdate, onDelete, onBulkUpdate } : TFVarTableProps ) {
22
55
const [ editingVar , setEditingVar ] = useState < EnvVar | null > ( null ) ;
23
56
const [ newVar , setNewVar ] = useState < Omit < EnvVar , 'updated_at' > > ( { name : '' , value : '' , is_secret : false } ) ;
24
57
const [ bulkEditMode , setBulkEditMode ] = useState ( false ) ;
25
58
const [ bulkEditValue , setBulkEditValue ] = useState < string > ( '' ) ;
26
59
const [ isLoading , setIsLoading ] = useState ( false ) ;
60
+ const [ showAddForm , setShowAddForm ] = useState ( false ) ;
27
61
const router = useRouter ( ) ;
28
62
29
63
const handleEdit = ( envVar : EnvVar ) => {
30
- if ( ! envVar . is_secret ) {
31
- setEditingVar ( envVar ) ;
32
- }
64
+ setEditingVar ( { ...envVar , value : envVar . is_secret ? '' : envVar . value } ) ;
33
65
} ;
34
66
35
67
const handleSave = async ( ) => {
36
68
if ( editingVar ) {
37
69
setIsLoading ( true ) ;
38
- await onUpdate ( editingVar . name , editingVar . value , editingVar . is_secret ) ;
39
- setIsLoading ( false ) ;
40
- setEditingVar ( null ) ;
41
- router . refresh ( ) ;
70
+ try {
71
+ await onUpdate ( editingVar . name , editingVar . value , editingVar . is_secret ) ;
72
+ toast . success ( 'Variable updated successfully' ) ;
73
+ setEditingVar ( null ) ;
74
+ router . refresh ( ) ;
75
+ } catch ( error ) {
76
+ toast . error ( 'Failed to update variable' ) ;
77
+ } finally {
78
+ setIsLoading ( false ) ;
79
+ }
42
80
}
43
81
} ;
44
82
45
83
const handleAddNew = async ( ) => {
46
84
if ( newVar . name && newVar . value ) {
85
+ if ( envVars . some ( v => v . name === newVar . name ) ) {
86
+ toast . error ( 'A variable with this name already exists' ) ;
87
+ return ;
88
+ }
47
89
setIsLoading ( true ) ;
48
- await onUpdate ( newVar . name , newVar . value , newVar . is_secret ) ;
49
- setIsLoading ( false ) ;
50
- setNewVar ( { name : '' , value : '' , is_secret : false } ) ;
51
- router . refresh ( ) ;
90
+ try {
91
+ await onUpdate ( newVar . name , newVar . value , newVar . is_secret ) ;
92
+ toast . success ( 'New variable added successfully' ) ;
93
+ setNewVar ( { name : '' , value : '' , is_secret : false } ) ;
94
+ setShowAddForm ( false ) ;
95
+ router . refresh ( ) ;
96
+ } catch ( error ) {
97
+ toast . error ( 'Failed to add new variable' ) ;
98
+ } finally {
99
+ setIsLoading ( false ) ;
100
+ }
52
101
}
53
102
} ;
54
103
55
104
const handleDeleteVar = async ( name : string ) => {
56
105
setIsLoading ( true ) ;
57
- await onDelete ( name ) ;
58
- setIsLoading ( false ) ;
59
- router . refresh ( ) ;
106
+ try {
107
+ await onDelete ( name ) ;
108
+ toast . success ( 'Variable deleted successfully' ) ;
109
+ router . refresh ( ) ;
110
+ } catch ( error ) {
111
+ toast . error ( 'Failed to delete variable' ) ;
112
+ } finally {
113
+ setIsLoading ( false ) ;
114
+ }
60
115
} ;
61
116
62
117
const handleBulkEdit = async ( ) => {
63
118
try {
64
119
const parsedVars = JSON . parse ( bulkEditValue ) ;
65
120
if ( Array . isArray ( parsedVars ) ) {
66
121
setIsLoading ( true ) ;
67
- await onBulkUpdate ( parsedVars . filter ( v => ! v . is_secret ) ) ;
68
- setIsLoading ( false ) ;
122
+ await onBulkUpdate ( parsedVars ) ;
123
+ toast . success ( 'Bulk update successful' ) ;
69
124
setBulkEditMode ( false ) ;
70
125
router . refresh ( ) ;
71
126
}
72
127
} catch ( error ) {
73
- console . error ( 'Error parsing JSON:' , error ) ;
128
+ toast . error ( 'Error parsing JSON or updating variables' ) ;
129
+ } finally {
130
+ setIsLoading ( false ) ;
74
131
}
75
132
} ;
76
133
77
134
const toggleBulkEdit = ( ) => {
78
135
if ( ! bulkEditMode ) {
79
- setBulkEditValue ( JSON . stringify ( envVars . filter ( v => ! v . is_secret ) , null , 2 ) ) ;
136
+ const nonSecretVars = envVars . filter ( v => ! v . is_secret ) . map ( ( { name, value } ) => ( { name, value } ) ) ;
137
+ setBulkEditValue ( JSON . stringify ( nonSecretVars , null , 2 ) ) ;
80
138
}
81
139
setBulkEditMode ( ! bulkEditMode ) ;
82
140
} ;
83
141
84
- const handleCopy = ( value : string ) => {
85
- navigator . clipboard . writeText ( value ) ;
142
+ const handleCopy = ( envVar : EnvVar ) => {
143
+ const copyText = JSON . stringify ( {
144
+ name : envVar . name ,
145
+ value : envVar . is_secret ? '********' : envVar . value ,
146
+ updated_at : envVar . updated_at
147
+ } , null , 2 ) ;
148
+ navigator . clipboard . writeText ( copyText ) ;
149
+ toast . success ( 'Copied to clipboard' ) ;
150
+ } ;
151
+
152
+ const handleCopyAll = ( ) => {
153
+ const copyText = JSON . stringify ( envVars . map ( v => ( {
154
+ ...v ,
155
+ value : v . is_secret ? '********' : v . value
156
+ } ) ) , null , 2 ) ;
157
+ navigator . clipboard . writeText ( copyText ) ;
158
+ toast . success ( 'All variables copied to clipboard' ) ;
86
159
} ;
87
160
88
161
if ( bulkEditMode ) {
@@ -91,7 +164,7 @@ export default function TFVarTable({ envVars, onUpdate, onDelete, onBulkUpdate }
91
164
< Textarea
92
165
value = { bulkEditValue }
93
166
onChange = { ( e ) => setBulkEditValue ( e . target . value ) }
94
- rows = { 10 }
167
+ rows = { 24 }
95
168
className = "font-mono"
96
169
/>
97
170
< div className = "space-x-2" >
@@ -104,72 +177,61 @@ export default function TFVarTable({ envVars, onUpdate, onDelete, onBulkUpdate }
104
177
) ;
105
178
}
106
179
180
+ if ( envVars . length === 0 && ! showAddForm ) {
181
+ return < EmptyState onAddVariable = { ( ) => setShowAddForm ( true ) } /> ;
182
+ }
183
+
107
184
return (
108
185
< div className = "space-y-4" >
109
- < div className = "flex gap-2" >
110
- < Input
111
- placeholder = "New variable name"
112
- value = { newVar . name }
113
- onChange = { ( e ) => setNewVar ( { ...newVar , name : e . target . value } ) }
114
- />
115
- < Input
116
- placeholder = "New variable value"
117
- type = { newVar . is_secret ? "password" : "text" }
118
- value = { newVar . value }
119
- onChange = { ( e ) => setNewVar ( { ...newVar , value : e . target . value } ) }
120
- />
121
- < Switch
122
- checked = { newVar . is_secret }
123
- onCheckedChange = { ( checked ) => setNewVar ( { ...newVar , is_secret : checked } ) }
124
- />
125
- < Button onClick = { handleAddNew } disabled = { isLoading || ! newVar . name || ! newVar . value } >
126
- { isLoading ? 'Adding...' : 'Add' }
127
- </ Button >
128
- </ div >
129
186
< Table >
130
- < TableHeader >
131
- < TableRow >
132
- < TableHead > Name</ TableHead >
133
- < TableHead > Value</ TableHead >
134
- < TableHead > Secret</ TableHead >
135
- < TableHead > Last Updated</ TableHead >
136
- < TableHead > Actions</ TableHead >
137
- </ TableRow >
138
- </ TableHeader >
187
+ { envVars . length > 0 && (
188
+ < TableHeader >
189
+ < TableRow >
190
+ < TableHead > Name</ TableHead >
191
+ < TableHead > Value</ TableHead >
192
+ < TableHead > Secret</ TableHead >
193
+ < TableHead > Last Updated</ TableHead >
194
+ < TableHead > Copy</ TableHead >
195
+ < TableHead > Edit</ TableHead >
196
+ < TableHead > Delete</ TableHead >
197
+ </ TableRow >
198
+ </ TableHeader >
199
+ ) }
139
200
< TableBody >
140
201
{ envVars . map ( ( envVar ) => (
141
202
< TableRow key = { envVar . name } >
142
203
< TableCell > { envVar . name } </ TableCell >
143
204
< TableCell >
144
205
{ editingVar && editingVar . name === envVar . name ? (
145
206
< Input
207
+ type = { envVar . is_secret ? "password" : "text" }
146
208
value = { editingVar . value }
147
209
onChange = { ( e ) => setEditingVar ( { ...editingVar , value : e . target . value } ) }
210
+ placeholder = { envVar . is_secret ? "Enter new secret value" : "" }
148
211
/>
149
212
) : (
150
213
< span > { envVar . is_secret ? '********' : envVar . value } </ span >
151
214
) }
152
215
</ TableCell >
153
- < TableCell >
154
- < Switch
155
- checked = { envVar . is_secret }
156
- onCheckedChange = { async ( checked ) => {
157
- await onUpdate ( envVar . name , envVar . value , checked ) ;
158
- router . refresh ( ) ;
159
- } }
160
- disabled = { isLoading }
161
- />
162
- </ TableCell >
216
+ < TableCell > { envVar . is_secret ? 'Yes' : 'No' } </ TableCell >
163
217
< TableCell > { moment ( envVar . updated_at ) . fromNow ( ) } </ TableCell >
164
218
< TableCell >
165
- < Button variant = "ghost" size = "icon" onClick = { ( ) => handleCopy ( envVar . value ) } >
219
+ < Button variant = "ghost" size = "icon" onClick = { ( ) => handleCopy ( envVar ) } >
166
220
< Copy className = "h-4 w-4" />
167
221
</ Button >
168
- { ! envVar . is_secret && (
222
+ </ TableCell >
223
+ < TableCell >
224
+ { editingVar && editingVar . name === envVar . name ? (
225
+ < Button variant = "ghost" size = "icon" onClick = { handleSave } disabled = { isLoading } >
226
+ < Save className = "h-4 w-4" />
227
+ </ Button >
228
+ ) : (
169
229
< Button variant = "ghost" size = "icon" onClick = { ( ) => handleEdit ( envVar ) } >
170
230
< Edit className = "h-4 w-4" />
171
231
</ Button >
172
232
) }
233
+ </ TableCell >
234
+ < TableCell >
173
235
< Button variant = "ghost" size = "icon" onClick = { ( ) => handleDeleteVar ( envVar . name ) } disabled = { isLoading } >
174
236
< Trash className = "h-4 w-4 text-destructive" />
175
237
</ Button >
@@ -178,9 +240,79 @@ export default function TFVarTable({ envVars, onUpdate, onDelete, onBulkUpdate }
178
240
) ) }
179
241
</ TableBody >
180
242
</ Table >
181
- < div className = "flex justify-end" >
182
- < Button onClick = { toggleBulkEdit } > Bulk Edit</ Button >
183
- </ div >
243
+
244
+ { ( showAddForm || envVars . length === 0 ) && (
245
+ < Card className = "p-5 mt-4 bg-muted/50 rounded-lg" >
246
+ < T . H4 className = " mt-1" > Add a new environment variable</ T . H4 >
247
+ < T . P className = "mt-0 pt-0 mb-4" > Enter the environment variable details. These variables will be assigned to your project</ T . P >
248
+ < div className = "grid gap-4 grid-cols-1 md:grid-cols-3" >
249
+ < div >
250
+ < Label htmlFor = "varName" > Variable Name</ Label >
251
+ < Input
252
+ id = "varName"
253
+ placeholder = "e.g., API_KEY"
254
+ value = { newVar . name }
255
+ onChange = { ( e ) => setNewVar ( { ...newVar , name : e . target . value . toUpperCase ( ) } ) }
256
+ />
257
+ </ div >
258
+ < div >
259
+ < Label htmlFor = "varValue" > Variable Value</ Label >
260
+ < Input
261
+ id = "varValue"
262
+ type = { newVar . is_secret ? "password" : "text" }
263
+ placeholder = "Enter value"
264
+ value = { newVar . value }
265
+ onChange = { ( e ) => setNewVar ( { ...newVar , value : e . target . value } ) }
266
+ />
267
+ </ div >
268
+ < div >
269
+ < Label htmlFor = "varType" > Variable Type</ Label >
270
+ < Select
271
+ value = { newVar . is_secret ? "secret" : "default" }
272
+ onValueChange = { ( value ) => setNewVar ( { ...newVar , is_secret : value === "secret" } ) }
273
+ >
274
+ < SelectTrigger >
275
+ < SelectValue placeholder = "Select type" />
276
+ </ SelectTrigger >
277
+ < SelectContent >
278
+ < SelectItem value = "default" > Default</ SelectItem >
279
+ < SelectItem value = "secret" > Secret</ SelectItem >
280
+ </ SelectContent >
281
+ </ Select >
282
+ </ div >
283
+ </ div >
284
+ < div className = "mt-4 flex justify-end" >
285
+ < Button onClick = { handleAddNew } disabled = { isLoading || ! newVar . name || ! newVar . value } >
286
+ < Plus className = "h-4 w-4 mr-2" />
287
+ { isLoading ? 'Adding...' : 'Add Variable' }
288
+ </ Button >
289
+ </ div >
290
+ </ Card >
291
+ ) }
292
+
293
+ { envVars . length > 0 && (
294
+ < >
295
+ < div className = "mt-4 flex justify-end" >
296
+ < Button variant = "outline" onClick = { ( ) => setShowAddForm ( ! showAddForm ) } >
297
+ < Plus className = "h-4 w-4 mr-2" />
298
+ { showAddForm ? 'Cancel' : 'Add New Variable' }
299
+ </ Button >
300
+ </ div >
301
+ < Separator className = "my-4" />
302
+ < div className = "mt-8" >
303
+ < h3 className = "text-lg font-semibold" > Bulk Edit Environment Variables</ h3 >
304
+ < p className = "text-sm text-gray-600 mb-4" >
305
+ Edit all environment variables at once in JSON format. Be careful with this operation.
306
+ </ p >
307
+ < div className = "flex gap-2" >
308
+ { /* <Button variant="outline" className='w-full' onClick={handleCopyAll}>Copy All</Button> */ }
309
+ < Button variant = "secondary" className = "w-full" onClick = { toggleBulkEdit } >
310
+ { bulkEditMode ? 'Cancel Bulk Edit' : 'Bulk Edit' }
311
+ </ Button >
312
+ </ div >
313
+ </ div >
314
+ </ >
315
+ ) }
184
316
</ div >
185
317
) ;
186
318
}
0 commit comments