Skip to content

Commit 33d141c

Browse files
add tfvars table changes
1 parent d732e62 commit 33d141c

File tree

2 files changed

+217
-76
lines changed
  • src
    • app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)
    • data/admin

2 files changed

+217
-76
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
'use client'
22

3+
import { T } from "@/components/ui/Typography";
34
import { Button } from "@/components/ui/button";
5+
import { Card, CardContent } from "@/components/ui/card";
46
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";
610
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
711
import { Textarea } from "@/components/ui/textarea";
812
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';
1015
import moment from 'moment';
1116
import { useRouter } from 'next/navigation';
1217
import { useState } from 'react';
18+
import { toast } from 'sonner';
1319

1420
type TFVarTableProps = {
1521
envVars: EnvVar[];
@@ -18,71 +24,138 @@ type TFVarTableProps = {
1824
onBulkUpdate: (vars: EnvVar[]) => Promise<EnvVar[]>;
1925
};
2026

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+
2154
export default function TFVarTable({ envVars, onUpdate, onDelete, onBulkUpdate }: TFVarTableProps) {
2255
const [editingVar, setEditingVar] = useState<EnvVar | null>(null);
2356
const [newVar, setNewVar] = useState<Omit<EnvVar, 'updated_at'>>({ name: '', value: '', is_secret: false });
2457
const [bulkEditMode, setBulkEditMode] = useState(false);
2558
const [bulkEditValue, setBulkEditValue] = useState<string>('');
2659
const [isLoading, setIsLoading] = useState(false);
60+
const [showAddForm, setShowAddForm] = useState(false);
2761
const router = useRouter();
2862

2963
const handleEdit = (envVar: EnvVar) => {
30-
if (!envVar.is_secret) {
31-
setEditingVar(envVar);
32-
}
64+
setEditingVar({ ...envVar, value: envVar.is_secret ? '' : envVar.value });
3365
};
3466

3567
const handleSave = async () => {
3668
if (editingVar) {
3769
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+
}
4280
}
4381
};
4482

4583
const handleAddNew = async () => {
4684
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+
}
4789
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+
}
52101
}
53102
};
54103

55104
const handleDeleteVar = async (name: string) => {
56105
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+
}
60115
};
61116

62117
const handleBulkEdit = async () => {
63118
try {
64119
const parsedVars = JSON.parse(bulkEditValue);
65120
if (Array.isArray(parsedVars)) {
66121
setIsLoading(true);
67-
await onBulkUpdate(parsedVars.filter(v => !v.is_secret));
68-
setIsLoading(false);
122+
await onBulkUpdate(parsedVars);
123+
toast.success('Bulk update successful');
69124
setBulkEditMode(false);
70125
router.refresh();
71126
}
72127
} catch (error) {
73-
console.error('Error parsing JSON:', error);
128+
toast.error('Error parsing JSON or updating variables');
129+
} finally {
130+
setIsLoading(false);
74131
}
75132
};
76133

77134
const toggleBulkEdit = () => {
78135
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));
80138
}
81139
setBulkEditMode(!bulkEditMode);
82140
};
83141

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');
86159
};
87160

88161
if (bulkEditMode) {
@@ -91,7 +164,7 @@ export default function TFVarTable({ envVars, onUpdate, onDelete, onBulkUpdate }
91164
<Textarea
92165
value={bulkEditValue}
93166
onChange={(e) => setBulkEditValue(e.target.value)}
94-
rows={10}
167+
rows={24}
95168
className="font-mono"
96169
/>
97170
<div className="space-x-2">
@@ -104,72 +177,61 @@ export default function TFVarTable({ envVars, onUpdate, onDelete, onBulkUpdate }
104177
);
105178
}
106179

180+
if (envVars.length === 0 && !showAddForm) {
181+
return <EmptyState onAddVariable={() => setShowAddForm(true)} />;
182+
}
183+
107184
return (
108185
<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>
129186
<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+
)}
139200
<TableBody>
140201
{envVars.map((envVar) => (
141202
<TableRow key={envVar.name}>
142203
<TableCell>{envVar.name}</TableCell>
143204
<TableCell>
144205
{editingVar && editingVar.name === envVar.name ? (
145206
<Input
207+
type={envVar.is_secret ? "password" : "text"}
146208
value={editingVar.value}
147209
onChange={(e) => setEditingVar({ ...editingVar, value: e.target.value })}
210+
placeholder={envVar.is_secret ? "Enter new secret value" : ""}
148211
/>
149212
) : (
150213
<span>{envVar.is_secret ? '********' : envVar.value}</span>
151214
)}
152215
</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>
163217
<TableCell>{moment(envVar.updated_at).fromNow()}</TableCell>
164218
<TableCell>
165-
<Button variant="ghost" size="icon" onClick={() => handleCopy(envVar.value)}>
219+
<Button variant="ghost" size="icon" onClick={() => handleCopy(envVar)}>
166220
<Copy className="h-4 w-4" />
167221
</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+
) : (
169229
<Button variant="ghost" size="icon" onClick={() => handleEdit(envVar)}>
170230
<Edit className="h-4 w-4" />
171231
</Button>
172232
)}
233+
</TableCell>
234+
<TableCell>
173235
<Button variant="ghost" size="icon" onClick={() => handleDeleteVar(envVar.name)} disabled={isLoading}>
174236
<Trash className="h-4 w-4 text-destructive" />
175237
</Button>
@@ -178,9 +240,79 @@ export default function TFVarTable({ envVars, onUpdate, onDelete, onBulkUpdate }
178240
))}
179241
</TableBody>
180242
</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+
)}
184316
</div>
185317
);
186318
}

0 commit comments

Comments
 (0)