Skip to content

Commit

Permalink
add tfvars table changes
Browse files Browse the repository at this point in the history
  • Loading branch information
psiddharthdesign committed Jul 24, 2024
1 parent d732e62 commit 33d141c
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 76 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
'use client'

import { T } from "@/components/ui/Typography";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
import { EnvVar } from "@/types/userTypes";
import { Copy, Edit, Trash } from 'lucide-react';
import { motion } from 'framer-motion';
import { Copy, Edit, Plus, Save, Trash } from 'lucide-react';
import moment from 'moment';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'sonner';

type TFVarTableProps = {
envVars: EnvVar[];
Expand All @@ -18,71 +24,138 @@ type TFVarTableProps = {
onBulkUpdate: (vars: EnvVar[]) => Promise<EnvVar[]>;
};

const EmptyState: React.FC<{ onAddVariable: () => void }> = ({ onAddVariable }) => {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Card className="mt-6 border-none bg-transparent shadow-none">
<CardContent className="flex flex-col items-center justify-center py-12">
<h3 className="text-2xl font-semibold mb-2">No Environment Variables Yet</h3>
<p className="text-muted-foreground mb-6 text-center max-w-md">
Add your first environment variable to get started. These variables will be available in your project's runtime.
</p>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Button onClick={onAddVariable} size="lg">
<Plus className="mr-2 h-4 w-4" /> Add Environment Variable
</Button>
</motion.div>
</CardContent>
</Card>
</motion.div>
);
};

export default function TFVarTable({ envVars, onUpdate, onDelete, onBulkUpdate }: TFVarTableProps) {
const [editingVar, setEditingVar] = useState<EnvVar | null>(null);
const [newVar, setNewVar] = useState<Omit<EnvVar, 'updated_at'>>({ name: '', value: '', is_secret: false });
const [bulkEditMode, setBulkEditMode] = useState(false);
const [bulkEditValue, setBulkEditValue] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const [showAddForm, setShowAddForm] = useState(false);
const router = useRouter();

const handleEdit = (envVar: EnvVar) => {
if (!envVar.is_secret) {
setEditingVar(envVar);
}
setEditingVar({ ...envVar, value: envVar.is_secret ? '' : envVar.value });
};

const handleSave = async () => {
if (editingVar) {
setIsLoading(true);
await onUpdate(editingVar.name, editingVar.value, editingVar.is_secret);
setIsLoading(false);
setEditingVar(null);
router.refresh();
try {
await onUpdate(editingVar.name, editingVar.value, editingVar.is_secret);
toast.success('Variable updated successfully');
setEditingVar(null);
router.refresh();
} catch (error) {
toast.error('Failed to update variable');
} finally {
setIsLoading(false);
}
}
};

const handleAddNew = async () => {
if (newVar.name && newVar.value) {
if (envVars.some(v => v.name === newVar.name)) {
toast.error('A variable with this name already exists');
return;
}
setIsLoading(true);
await onUpdate(newVar.name, newVar.value, newVar.is_secret);
setIsLoading(false);
setNewVar({ name: '', value: '', is_secret: false });
router.refresh();
try {
await onUpdate(newVar.name, newVar.value, newVar.is_secret);
toast.success('New variable added successfully');
setNewVar({ name: '', value: '', is_secret: false });
setShowAddForm(false);
router.refresh();
} catch (error) {
toast.error('Failed to add new variable');
} finally {
setIsLoading(false);
}
}
};

const handleDeleteVar = async (name: string) => {
setIsLoading(true);
await onDelete(name);
setIsLoading(false);
router.refresh();
try {
await onDelete(name);
toast.success('Variable deleted successfully');
router.refresh();
} catch (error) {
toast.error('Failed to delete variable');
} finally {
setIsLoading(false);
}
};

const handleBulkEdit = async () => {
try {
const parsedVars = JSON.parse(bulkEditValue);
if (Array.isArray(parsedVars)) {
setIsLoading(true);
await onBulkUpdate(parsedVars.filter(v => !v.is_secret));
setIsLoading(false);
await onBulkUpdate(parsedVars);
toast.success('Bulk update successful');
setBulkEditMode(false);
router.refresh();
}
} catch (error) {
console.error('Error parsing JSON:', error);
toast.error('Error parsing JSON or updating variables');
} finally {
setIsLoading(false);
}
};

const toggleBulkEdit = () => {
if (!bulkEditMode) {
setBulkEditValue(JSON.stringify(envVars.filter(v => !v.is_secret), null, 2));
const nonSecretVars = envVars.filter(v => !v.is_secret).map(({ name, value }) => ({ name, value }));
setBulkEditValue(JSON.stringify(nonSecretVars, null, 2));
}
setBulkEditMode(!bulkEditMode);
};

const handleCopy = (value: string) => {
navigator.clipboard.writeText(value);
const handleCopy = (envVar: EnvVar) => {
const copyText = JSON.stringify({
name: envVar.name,
value: envVar.is_secret ? '********' : envVar.value,
updated_at: envVar.updated_at
}, null, 2);
navigator.clipboard.writeText(copyText);
toast.success('Copied to clipboard');
};

const handleCopyAll = () => {
const copyText = JSON.stringify(envVars.map(v => ({
...v,
value: v.is_secret ? '********' : v.value
})), null, 2);
navigator.clipboard.writeText(copyText);
toast.success('All variables copied to clipboard');
};

if (bulkEditMode) {
Expand All @@ -91,7 +164,7 @@ export default function TFVarTable({ envVars, onUpdate, onDelete, onBulkUpdate }
<Textarea
value={bulkEditValue}
onChange={(e) => setBulkEditValue(e.target.value)}
rows={10}
rows={24}
className="font-mono"
/>
<div className="space-x-2">
Expand All @@ -104,72 +177,61 @@ export default function TFVarTable({ envVars, onUpdate, onDelete, onBulkUpdate }
);
}

if (envVars.length === 0 && !showAddForm) {
return <EmptyState onAddVariable={() => setShowAddForm(true)} />;
}

return (
<div className="space-y-4">
<div className="flex gap-2">
<Input
placeholder="New variable name"
value={newVar.name}
onChange={(e) => setNewVar({ ...newVar, name: e.target.value })}
/>
<Input
placeholder="New variable value"
type={newVar.is_secret ? "password" : "text"}
value={newVar.value}
onChange={(e) => setNewVar({ ...newVar, value: e.target.value })}
/>
<Switch
checked={newVar.is_secret}
onCheckedChange={(checked) => setNewVar({ ...newVar, is_secret: checked })}
/>
<Button onClick={handleAddNew} disabled={isLoading || !newVar.name || !newVar.value}>
{isLoading ? 'Adding...' : 'Add'}
</Button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Value</TableHead>
<TableHead>Secret</TableHead>
<TableHead>Last Updated</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
{envVars.length > 0 && (
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Value</TableHead>
<TableHead>Secret</TableHead>
<TableHead>Last Updated</TableHead>
<TableHead>Copy</TableHead>
<TableHead>Edit</TableHead>
<TableHead>Delete</TableHead>
</TableRow>
</TableHeader>
)}
<TableBody>
{envVars.map((envVar) => (
<TableRow key={envVar.name}>
<TableCell>{envVar.name}</TableCell>
<TableCell>
{editingVar && editingVar.name === envVar.name ? (
<Input
type={envVar.is_secret ? "password" : "text"}
value={editingVar.value}
onChange={(e) => setEditingVar({ ...editingVar, value: e.target.value })}
placeholder={envVar.is_secret ? "Enter new secret value" : ""}
/>
) : (
<span>{envVar.is_secret ? '********' : envVar.value}</span>
)}
</TableCell>
<TableCell>
<Switch
checked={envVar.is_secret}
onCheckedChange={async (checked) => {
await onUpdate(envVar.name, envVar.value, checked);
router.refresh();
}}
disabled={isLoading}
/>
</TableCell>
<TableCell>{envVar.is_secret ? 'Yes' : 'No'}</TableCell>
<TableCell>{moment(envVar.updated_at).fromNow()}</TableCell>
<TableCell>
<Button variant="ghost" size="icon" onClick={() => handleCopy(envVar.value)}>
<Button variant="ghost" size="icon" onClick={() => handleCopy(envVar)}>
<Copy className="h-4 w-4" />
</Button>
{!envVar.is_secret && (
</TableCell>
<TableCell>
{editingVar && editingVar.name === envVar.name ? (
<Button variant="ghost" size="icon" onClick={handleSave} disabled={isLoading}>
<Save className="h-4 w-4" />
</Button>
) : (
<Button variant="ghost" size="icon" onClick={() => handleEdit(envVar)}>
<Edit className="h-4 w-4" />
</Button>
)}
</TableCell>
<TableCell>
<Button variant="ghost" size="icon" onClick={() => handleDeleteVar(envVar.name)} disabled={isLoading}>
<Trash className="h-4 w-4 text-destructive" />
</Button>
Expand All @@ -178,9 +240,79 @@ export default function TFVarTable({ envVars, onUpdate, onDelete, onBulkUpdate }
))}
</TableBody>
</Table>
<div className="flex justify-end">
<Button onClick={toggleBulkEdit}>Bulk Edit</Button>
</div>

{(showAddForm || envVars.length === 0) && (
<Card className="p-5 mt-4 bg-muted/50 rounded-lg">
<T.H4 className=" mt-1">Add a new environment variable</T.H4>
<T.P className="mt-0 pt-0 mb-4">Enter the environment variable details. These variables will be assigned to your project</T.P>
<div className="grid gap-4 grid-cols-1 md:grid-cols-3">
<div>
<Label htmlFor="varName">Variable Name</Label>
<Input
id="varName"
placeholder="e.g., API_KEY"
value={newVar.name}
onChange={(e) => setNewVar({ ...newVar, name: e.target.value.toUpperCase() })}
/>
</div>
<div>
<Label htmlFor="varValue">Variable Value</Label>
<Input
id="varValue"
type={newVar.is_secret ? "password" : "text"}
placeholder="Enter value"
value={newVar.value}
onChange={(e) => setNewVar({ ...newVar, value: e.target.value })}
/>
</div>
<div>
<Label htmlFor="varType">Variable Type</Label>
<Select
value={newVar.is_secret ? "secret" : "default"}
onValueChange={(value) => setNewVar({ ...newVar, is_secret: value === "secret" })}
>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="secret">Secret</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="mt-4 flex justify-end">
<Button onClick={handleAddNew} disabled={isLoading || !newVar.name || !newVar.value}>
<Plus className="h-4 w-4 mr-2" />
{isLoading ? 'Adding...' : 'Add Variable'}
</Button>
</div>
</Card>
)}

{envVars.length > 0 && (
<>
<div className="mt-4 flex justify-end">
<Button variant="outline" onClick={() => setShowAddForm(!showAddForm)}>
<Plus className="h-4 w-4 mr-2" />
{showAddForm ? 'Cancel' : 'Add New Variable'}
</Button>
</div>
<Separator className="my-4" />
<div className="mt-8">
<h3 className="text-lg font-semibold">Bulk Edit Environment Variables</h3>
<p className="text-sm text-gray-600 mb-4">
Edit all environment variables at once in JSON format. Be careful with this operation.
</p>
<div className="flex gap-2">
{/* <Button variant="outline" className='w-full' onClick={handleCopyAll}>Copy All</Button> */}
<Button variant="secondary" className="w-full" onClick={toggleBulkEdit}>
{bulkEditMode ? 'Cancel Bulk Edit' : 'Bulk Edit'}
</Button>
</div>
</div>
</>
)}
</div>
);
}
Loading

0 comments on commit 33d141c

Please sign in to comment.