From 20d560e5dd460d19343257ab735558539eaeaaf1 Mon Sep 17 00:00:00 2001 From: psiddharthdesign <107192927+psiddharthdesign@users.noreply.github.com> Date: Tue, 23 Jul 2024 22:42:57 +0530 Subject: [PATCH] feature/tentative tfvars table implementation --- .../EmptyTFVarState.tsx | 38 +++ .../ProjectDetails.tsx | 40 +++ .../ProjectSettings.tsx | 37 +++ .../(specific-project-pages)/RunsTable.tsx | 57 ++++ .../(specific-project-pages)/TFVarTable.tsx | 303 ++++++++++++++++++ .../TFVarsDetails.tsx | 76 +++++ .../(specific-project-pages)/layout.tsx | 30 +- .../(specific-project-pages)/page.tsx | 116 ++++--- .../(specific-project-pages)/runs/page.tsx | 7 + .../settings/page.tsx | 11 +- .../(specific-project-pages)/tfvars/page.tsx | 35 ++ .../onboarding/OrganizationCreation.tsx | 4 - .../onboarding/ProfileUpdate.tsx | 4 - .../onboarding/TermsAcceptance.tsx | 2 - src/components/TabsNavigation/Tab.tsx | 20 +- .../TabsNavigation/TabsNavigation.tsx | 19 +- src/components/TabsNavigation/types.ts | 6 + src/data/user/runs.ts | 73 +++++ src/data/user/user.tsx | 6 - src/lib/database.types.ts | 21 ++ src/middleware.ts | 19 -- .../20240723171014_add_tfvars_table.sql | 11 + 22 files changed, 837 insertions(+), 98 deletions(-) create mode 100644 src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/EmptyTFVarState.tsx create mode 100644 src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ProjectDetails.tsx create mode 100644 src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ProjectSettings.tsx create mode 100644 src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/RunsTable.tsx create mode 100644 src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/TFVarTable.tsx create mode 100644 src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/TFVarsDetails.tsx create mode 100644 src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/runs/page.tsx create mode 100644 src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/tfvars/page.tsx create mode 100644 src/data/user/runs.ts create mode 100644 supabase/migrations/20240723171014_add_tfvars_table.sql diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/EmptyTFVarState.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/EmptyTFVarState.tsx new file mode 100644 index 00000000..0482b43d --- /dev/null +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/EmptyTFVarState.tsx @@ -0,0 +1,38 @@ +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { motion } from 'framer-motion'; +import { Plus } from 'lucide-react'; +import React from 'react'; + +interface EmptyStateProps { + onAddVariable: () => void; +} + +const EmptyState: React.FC = ({ onAddVariable }) => { + return ( + + + +

No Environment Variables Yet

+

+ Add your first environment variable to get started. These variables will be available in your project's runtime. +

+ + + +
+
+
+ ); +}; + +export default EmptyState; \ No newline at end of file diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ProjectDetails.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ProjectDetails.tsx new file mode 100644 index 00000000..d975ec9d --- /dev/null +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ProjectDetails.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { motion } from "framer-motion"; +import { Run, RunsTable } from "./RunsTable"; + +export default function RunsDetails({ runs }: { runs: Run[] }) { + return ( + + + + + + Project Runs + View all runs for this project + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ProjectSettings.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ProjectSettings.tsx new file mode 100644 index 00000000..e40914c4 --- /dev/null +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ProjectSettings.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { motion } from "framer-motion"; + +export default function ProjectSettings() { + return ( + + + + + Project Settings + Manage settings for your project + + + + + {/* Add your project settings management component here */} + + + + + ); +} \ No newline at end of file diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/RunsTable.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/RunsTable.tsx new file mode 100644 index 00000000..83c5dcfa --- /dev/null +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/RunsTable.tsx @@ -0,0 +1,57 @@ +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; + +export type Run = { + runId: string; + commitId: string; + status: string; + date: string; + user: string; +}; + +type StatusColor = { + [key: string]: string; +}; + +const statusColors: StatusColor = { + queued: 'bg-yellow-200/50 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-200', + 'pending approval': 'bg-blue-200/50 text-blue-800 dark:bg-blue-900/50 dark:text-blue-200', + running: 'bg-purple-200/50 text-purple-800 dark:bg-purple-900/50 dark:text-purple-200', + approved: 'bg-green-200/50 text-green-800 dark:bg-green-900/50 dark:text-green-200', + succeeded: 'bg-green-200/50 text-green-800 dark:bg-green-900/50 dark:text-green-200', + failed: 'bg-red-200/50 text-red-800 dark:bg-red-900/50 dark:text-red-200', +}; + +export const RunsTable = ({ runs }: { runs: Run[] }) => ( + + + + Run ID + Commit ID + Status + Date + User + + + + {runs.length > 0 ? ( + runs.map((run) => ( + + {run.runId} + {run.commitId} + + + {run.status.toUpperCase()} + + + {run.date} + {run.user} + + )) + ) : ( + + No runs available + + )} + +
+); \ No newline at end of file diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/TFVarTable.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/TFVarTable.tsx new file mode 100644 index 00000000..9e5727fa --- /dev/null +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/TFVarTable.tsx @@ -0,0 +1,303 @@ +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { useSAToastMutation } from '@/hooks/useSAToastMutation'; +import { AnimatePresence, motion } from "framer-motion"; +import { Copy, Edit, Eye, EyeOff, Plus, Trash } from 'lucide-react'; +import moment from 'moment'; +import React, { useCallback, useState } from 'react'; +import { toast } from 'sonner'; +import EmptyState from "./EmptyTFVarState"; + +type TFVar = { + name: string; + value: string; + updated_at: string; +} + +type TFVarTableProps = { + initialVariables: string; + onUpdate: (variables: string) => Promise; +} + +const TFVarTable: React.FC = ({ initialVariables, onUpdate }) => { + const [tfvars, setTfvars] = useState(() => { + try { + return JSON.parse(initialVariables); + } catch (error) { + console.error("Failed to parse initial variables:", error); + return []; + } + }); + const [editingState, setEditingState] = useState<{ + index: number | null; + var: TFVar | null; + }>({ + index: null, + var: null, + }); + const [showSecrets, setShowSecrets] = useState<{ [key: string]: boolean }>({}); + const [bulkEditMode, setBulkEditMode] = useState(false); + const [bulkEditValue, setBulkEditValue] = useState(initialVariables); + + const { mutate: updateTFVar } = useSAToastMutation( + async (updatedVars) => { + const jsonString = JSON.stringify(updatedVars); + await onUpdate(jsonString); + return { status: 'success', data: jsonString }; + }, + { + loadingMessage: 'Updating TF variables...', + successMessage: 'Variables updated successfully', + errorMessage: 'Failed to update TF variables', + onSuccess: (response) => { + const updatedVars = JSON.parse(response.data) as TFVar[]; + setTfvars(updatedVars); + }, + } + ); + + const handleEdit = (index: number) => { + setEditingState({ + index, + var: { ...tfvars[index] }, + }); + }; + + const handleSave = () => { + if (editingState.index === null || editingState.var === null) return; + + const updatedVar = { + ...editingState.var, + updated_at: new Date().toISOString(), + }; + + const isDuplicate = tfvars.some((v, i) => + i !== editingState.index && v.name === updatedVar.name + ); + + if (isDuplicate) { + toast.error('Variable with this name already exists'); + return; + } + + const newTFVars = [...tfvars]; + newTFVars[editingState.index] = updatedVar; + updateTFVar(newTFVars); + setEditingState({ index: null, var: null }); + }; + + const handleDelete = (index: number) => { + const updatedTFVars = tfvars.filter((_, i) => i !== index); + updateTFVar(updatedTFVars); + toast.success(`Variable ${tfvars[index].name} deleted`); + }; + + const handleAddVariable = () => { + const now = new Date().toISOString(); + const newVar: TFVar = { + name: `NEW_VARIABLE_${tfvars.length + 1}`, + value: 'ENV_VALUE', + updated_at: now, + }; + const newIndex = tfvars.length; + setTfvars([...tfvars, newVar]); + setEditingState({ + index: newIndex, + var: newVar, + }); + }; + const handleBulkEdit = () => { + try { + const parsedVars = JSON.parse(bulkEditValue); + const validatedVars = parsedVars.map(({ name, value }) => { + if (!name || typeof value === 'undefined') { + throw new Error(`Invalid variable: ${JSON.stringify({ name, value })}`); + } + const existingVar = tfvars.find(v => v.name === name); + return { + name, + value, + updated_at: existingVar ? existingVar.updated_at : new Date().toISOString() + }; + }); + + const names = new Set(); + validatedVars.forEach(v => { + if (names.has(v.name)) { + throw new Error(`Duplicate variable name: ${v.name}`); + } + names.add(v.name); + }); + + updateTFVar(validatedVars); + setBulkEditMode(false); + toast.success('Bulk edit successful'); + } catch (error) { + toast.error(`Invalid JSON format: ${error.message}`); + } + }; + + const toggleBulkEdit = () => { + if (!bulkEditMode) { + const filteredVars = tfvars.map(({ name, value }) => ({ name, value })); + setBulkEditValue(JSON.stringify(filteredVars, null, 2)); + } + setBulkEditMode(!bulkEditMode); + }; + + const copyToClipboard = useCallback((tfvar: TFVar) => { + navigator.clipboard.writeText(JSON.stringify(tfvar, null, 2)); + toast.success('Copied to clipboard'); + }, []); + + const handleInputChange = (field: 'name' | 'value', value: string) => { + if (editingState.var) { + setEditingState(prev => ({ + ...prev, + var: { + ...prev.var!, + [field]: field === 'name' ? value.toUpperCase() : value + } + })); + } + }; + + return ( + + {!bulkEditMode ? ( + <> + + + + Name + Value + Secret + Last Updated + Copy + Edit + Delete + + + {tfvars.length > 0 ? ( + + + {tfvars.map((tfvar, index) => ( + + + {editingState.index === index ? ( + handleInputChange('name', e.target.value)} + onBlur={(e) => handleInputChange('name', e.target.value.toUpperCase())} + /> + ) : tfvar.name} + + + {editingState.index === index ? ( + handleInputChange('value', e.target.value)} + /> + ) : ( + + {tfvar.value} + + )} + + + + + {moment(tfvar.updated_at).fromNow()} + + + + + {editingState.index === index ? ( + + ) : ( + + )} + + + + + + ))} + + + ) : ( + + + + + + )} +
+ {tfvars.length > 0 && ( +
+ +
+ )} + + ) : ( +
+