From c3a08ccb9f653671b7200b9e0429257a5e517f72 Mon Sep 17 00:00:00 2001 From: Sameer Poswal <106386145+poswalsameer@users.noreply.github.com> Date: Sat, 11 Jan 2025 13:15:09 +0530 Subject: [PATCH] feat(platform): Add a new secret and added loader on project screen (#603) Co-authored-by: kriptonian1 Co-authored-by: rajdip-b --- apps/platform/.eslintrc.cjs | 3 +- apps/platform/package.json | 1 + apps/platform/public/svg/shared/index.ts | 6 +- apps/platform/public/svg/shared/trash.svg | 6 + .../svg/shared/{Vector.svg => vector.svg} | 0 apps/platform/src/app/(main)/page.tsx | 10 +- .../project/[project]/@variable/page.tsx | 285 ++++++++++++----- .../app/(main)/project/[project]/layout.tsx | 136 ++++---- .../common/online-status-handler.tsx | 8 +- .../src/components/ui/add-secret-dialog.tsx | 234 ++++++++++++++ .../src/components/ui/alert-dialog.tsx | 140 +++++++++ .../src/components/ui/collapsible.tsx | 8 +- .../src/components/ui/confirm-delete.tsx | 102 ++++++ .../components/ui/edit-variable-dialog.tsx | 128 ++++++++ .../components/ui/project-screen-loader.tsx | 29 ++ apps/platform/src/hooks/use-online-status.ts | 54 ++-- apps/web/src/components/hero/index.tsx | 2 +- .../web/src/components/pricing/card/index.tsx | 2 +- pnpm-lock.yaml | 293 ++++++++++++++++++ 19 files changed, 1259 insertions(+), 188 deletions(-) create mode 100644 apps/platform/public/svg/shared/trash.svg rename apps/platform/public/svg/shared/{Vector.svg => vector.svg} (100%) create mode 100644 apps/platform/src/components/ui/add-secret-dialog.tsx create mode 100644 apps/platform/src/components/ui/alert-dialog.tsx create mode 100644 apps/platform/src/components/ui/confirm-delete.tsx create mode 100644 apps/platform/src/components/ui/edit-variable-dialog.tsx create mode 100644 apps/platform/src/components/ui/project-screen-loader.tsx diff --git a/apps/platform/.eslintrc.cjs b/apps/platform/.eslintrc.cjs index e67976a0..a2f9ef15 100644 --- a/apps/platform/.eslintrc.cjs +++ b/apps/platform/.eslintrc.cjs @@ -18,6 +18,7 @@ module.exports = { '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-unsafe-return': 'off' + '@typescript-eslint/no-unsafe-return': 'off', + 'no-nested-ternary': 'off' } } diff --git a/apps/platform/package.json b/apps/platform/package.json index 26b57e49..ea6cc8eb 100644 --- a/apps/platform/package.json +++ b/apps/platform/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-context-menu": "^2.1.5", diff --git a/apps/platform/public/svg/shared/index.ts b/apps/platform/public/svg/shared/index.ts index 64eb64d2..045cb020 100644 --- a/apps/platform/public/svg/shared/index.ts +++ b/apps/platform/public/svg/shared/index.ts @@ -8,8 +8,9 @@ import ThreeDotOptionSVG from './3dotOption.svg' import AddSVG from './add.svg' import LoadingSVG from './loading.svg' import MessageSVG from './message.svg' -import VectorSVG from './Vector.svg' +import VectorSVG from './vector.svg' import ErrorSVG from './Error.svg' +import TrashSVG from './trash.svg' export { DropdownSVG, @@ -23,5 +24,6 @@ export { LoadingSVG, MessageSVG, VectorSVG, - ErrorSVG + ErrorSVG, + TrashSVG } diff --git a/apps/platform/public/svg/shared/trash.svg b/apps/platform/public/svg/shared/trash.svg new file mode 100644 index 00000000..8c51e673 --- /dev/null +++ b/apps/platform/public/svg/shared/trash.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/platform/public/svg/shared/Vector.svg b/apps/platform/public/svg/shared/vector.svg similarity index 100% rename from apps/platform/public/svg/shared/Vector.svg rename to apps/platform/public/svg/shared/vector.svg diff --git a/apps/platform/src/app/(main)/page.tsx b/apps/platform/src/app/(main)/page.tsx index af66c101..6e818313 100644 --- a/apps/platform/src/app/(main)/page.tsx +++ b/apps/platform/src/app/(main)/page.tsx @@ -39,11 +39,13 @@ import { } from '@/components/ui/dialog' import ControllerInstance from '@/lib/controller-instance' import { Textarea } from '@/components/ui/textarea' +import ProjectScreenLoader from '@/components/ui/project-screen-loader' export default function Index(): JSX.Element { const [isSheetOpen, setIsSheetOpen] = useState(false) const [isProjectEmpty, setIsProjectEmpty] = useState(true) const [isDialogOpen, setIsDialogOpen] = useState(false) + const [loading, setLoading] = useState(false) // Projects to be displayed in the dashboard const [projects, setProjects] = useState([]) @@ -78,6 +80,8 @@ export default function Index(): JSX.Element { // under that workspace and display it in the dashboard. useEffect(() => { async function getAllProjects() { + setLoading(true) + if (currentWorkspace) { const { success, error, data } = await ControllerInstance.getInstance().projectController.getAllProjects( @@ -92,6 +96,8 @@ export default function Index(): JSX.Element { console.error(error) } } + + setLoading(false) } getAllProjects() @@ -326,7 +332,9 @@ export default function Index(): JSX.Element { - {!isProjectEmpty ? ( + {loading ? ( + + ) : !isProjectEmpty ? (
{projects.map((project: GetAllProjectsResponse['items'][number]) => { return ( diff --git a/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx b/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx index cf4ac3a1..b4aadba0 100644 --- a/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx +++ b/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx @@ -1,17 +1,21 @@ 'use client' import { useEffect, useState } from 'react' -import { Button } from '@/components/ui/button' -import { +import type { ClientResponse, GetAllVariablesOfProjectResponse, - Project, + Project } from '@keyshade/schema' import { FolderSVG } from '@public/svg/dashboard' import { MessageSVG } from '@public/svg/shared' import { ChevronDown } from 'lucide-react' +import { Button } from '@/components/ui/button' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from '@/components/ui/collapsible' import { Table, TableBody, @@ -21,19 +25,48 @@ import { TableRow } from '@/components/ui/table' import ControllerInstance from '@/lib/controller-instance' +import ConfirmDelete from '@/components/ui/confirm-delete' +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger +} from '@/components/ui/context-menu' +import EditVariableDialog from '@/components/ui/edit-variable-dialog' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from '@/components/ui/tooltip' interface VariablePageProps { currentProject: Project | undefined } +interface EditVariableDetails { + variableName: string + variableNote: string +} function VariablePage({ currentProject }: VariablePageProps): React.JSX.Element { - - const [allVariables, setAllVariables] = useState([]) + const [allVariables, setAllVariables] = useState< + GetAllVariablesOfProjectResponse['items'] + >([]) // Holds the currently open section ID const [openSections, setOpenSections] = useState>(new Set()) + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [selectedVariableSlug, setSelectedVariableSlug] = useState< + string | null + >(null) + const [editVariableDetails, setEditVariableDetails] = + useState({ + variableName: '', + variableNote: '' + }) //Environments table toggle logic const toggleSection = (id: string) => { @@ -48,16 +81,42 @@ function VariablePage({ }) } - useEffect(() => { + const toggleDeleteDialog = (variableSlug: string) => { + setIsDeleteDialogOpen(!isDeleteDialogOpen) + if (selectedVariableSlug === null) { + setSelectedVariableSlug(variableSlug) + } else { + setSelectedVariableSlug(null) + } + } - const getAllVariables = async () => { + const toggleEditDialog = ( + variableSlug: string, + variableName: string, + variableNote: string | null + ) => { + setIsEditDialogOpen(!isEditDialogOpen) + if (selectedVariableSlug === null) { + setSelectedVariableSlug(variableSlug) + setEditVariableDetails({ variableName, variableNote: variableNote ?? '' }) + } else { + setSelectedVariableSlug(null) + setEditVariableDetails({ variableName: '', variableNote: '' }) + } + } + useEffect(() => { + const getAllVariables = async () => { if (!currentProject) { return } - const { success, error, data }: ClientResponse = - await ControllerInstance.getInstance().variableController.getAllVariablesOfProject( + const { + success, + error, + data + }: ClientResponse = + await ControllerInstance.getInstance().variableController.getAllVariablesOfProject( { projectSlug: currentProject.slug }, {} ) @@ -74,8 +133,9 @@ function VariablePage({ }, [currentProject]) return ( -
- +
{/* Showing this when there are no variables present */} {allVariables.length === 0 ? (
@@ -96,72 +156,153 @@ function VariablePage({
) : ( // Showing this when variables are present -
+
{allVariables.map((variable) => ( - toggleSection(variable.variable.id)} - className="w-full" - > - -
- - {variable.variable.name} - - -
-
-
-
- {(() => { - const days = Math.ceil(Math.abs(new Date().getTime() - new Date(variable.variable.createdAt).getTime()) / (1000 * 60 * 60 * 24)); - return `${days} ${days === 1 ? 'day' : 'days'} ago by`; - })()} + + + toggleSection(variable.variable.id)} + open={openSections.has(variable.variable.id)} + > + +
+ + {variable.variable.name} + + {variable.variable.note ? ( + + + + + + +

{variable.variable.note}

+
+
+
+ ) : null}
-
-
- {variable.variable.lastUpdatedBy.name.split(' ')[0]} +
+
+
+ {(() => { + const days = Math.ceil( + Math.abs( + new Date().getTime() - + new Date( + variable.variable.createdAt + ).getTime() + ) / + (1000 * 60 * 60 * 24) + ) + return `${days} ${days === 1 ? 'day' : 'days'} ago by` + })()} +
+
+
+ {variable.variable.lastUpdatedBy.name.split(' ')[0]} +
+ + + + {variable.variable.lastUpdatedBy.name + .charAt(0) + .toUpperCase() + + variable.variable.lastUpdatedBy.name + .slice(1, 2) + .toLowerCase()} + + +
- - - - {variable.variable.lastUpdatedBy.name.charAt(0).toUpperCase() + variable.variable.lastUpdatedBy.name.slice(1, 2).toLowerCase()} - - +
-
- -
-
- - {variable.values ? ( - - - - Environment - Value - - - - {variable.values.map((env) => ( - - - {env.environment.name} - - - {env.value} - + + +
+ + + + Environment + + + Value + - ))} - -
- ) : ( - <> - )} -
-
+ + + {variable.values.map((env) => ( + + + {env.environment.name} + + + {env.value} + + + ))} + + + + +
+ + + Show Version History + + + toggleEditDialog( + variable.variable.slug, + variable.variable.name, + variable.variable.note + ) + } + > + Edit + + toggleDeleteDialog(variable.variable.slug)} + > + Delete + + +
))} + + {/* Delete variable alert dialog */} + {isDeleteDialogOpen ? ( + toggleDeleteDialog('')} + variableSlug={selectedVariableSlug} + /> + ) : null} + + {/* Edit variable dialog */} + {isEditDialogOpen ? ( + toggleEditDialog('', '', '')} + variableName={editVariableDetails.variableName} + variableNote={editVariableDetails.variableNote} + variableSlug={selectedVariableSlug} + /> + ) : null}
)}
diff --git a/apps/platform/src/app/(main)/project/[project]/layout.tsx b/apps/platform/src/app/(main)/project/[project]/layout.tsx index 36114df1..6924c99f 100644 --- a/apps/platform/src/app/(main)/project/[project]/layout.tsx +++ b/apps/platform/src/app/(main)/project/[project]/layout.tsx @@ -1,5 +1,5 @@ 'use client' -import type { MouseEvent, MouseEventHandler } from 'react' +import type { MouseEvent } from 'react' import { useEffect, useState } from 'react' import { useSearchParams } from 'next/navigation' import { AddSVG } from '@public/svg/shared' @@ -11,6 +11,7 @@ import type { Project } from '@keyshade/schema' import { toast } from 'sonner' +import VariablePage from './@variable/page' import { Button } from '@/components/ui/button' import { Dialog, @@ -21,7 +22,6 @@ import { DialogTrigger } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' import ControllerInstance from '@/lib/controller-instance' import { Select, @@ -30,8 +30,8 @@ import { SelectTrigger, SelectValue } from '@/components/ui/select' -import VariablePage from './@variable/page' import { Toaster } from '@/components/ui/sonner' +import AddSecretDialog from '@/components/ui/add-secret-dialog' interface DetailedProjectPageProps { params: { project: string } @@ -41,13 +41,8 @@ interface DetailedProjectPageProps { function DetailedProjectPage({ params, - secret, - variable + secret }: DetailedProjectPageProps): JSX.Element { - // eslint-disable-next-line @typescript-eslint/no-unused-vars -- will be used later - const [key, setKey] = useState('') - // eslint-disable-next-line @typescript-eslint/no-unused-vars -- will be used later - const [value, setValue] = useState('') const [currentProject, setCurrentProject] = useState() const [isOpen, setIsOpen] = useState(false) const [newVariableData, setNewVariableData] = useState({ @@ -56,7 +51,15 @@ function DetailedProjectPage({ environmentName: '', environmentValue: '' }) - const [availableEnvironments, setAvailableEnvironments] = useState([]) + const [newSecretData, setNewSecretData] = useState({ + secretName: '', + secretNote: '', + environmentName: '', + environmentValue: '' + }) + const [availableEnvironments, setAvailableEnvironments] = useState< + Environment[] + >([]) const searchParams = useSearchParams() const tab = searchParams.get('tab') ?? 'rollup-details' @@ -90,8 +93,7 @@ function DetailedProjectPage({ if (success) { toast.success('Variable added successfully', { - // eslint-disable-next-line react/no-unstable-nested-components -- we need to nest the description - description: () => ( + description: (

The variable has been added to the project

@@ -102,8 +104,7 @@ function DetailedProjectPage({ if (error) { if (error.statusCode === 409) { toast.error('Variable name already exists', { - // eslint-disable-next-line react/no-unstable-nested-components -- we need to nest the description - description: () => ( + description: (

Variable name is already there, kindly use different one.

@@ -164,61 +165,46 @@ function DetailedProjectPage({ getAllEnvironments() }, [currentProject]) + useEffect(() => { + const getAllEnvironments = async () => { + if (!currentProject) { + return + } + + const { + success, + error, + data + }: ClientResponse = + await ControllerInstance.getInstance().environmentController.getAllEnvironmentsOfProject( + { projectSlug: currentProject.slug }, + {} + ) + + if (success && data) { + setAvailableEnvironments(data.items) + } else { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) + } + } + + getAllEnvironments() + }, [currentProject]) + return (
-
+
{currentProject?.name}
{tab === 'secret' && ( - - - - - - - Add a new secret - - Add a new secret to the project. This secret will be encrypted - and stored securely. - - -
-
-
- - { - setKey(e.target.value) - }} - placeholder="Enter the name of the secret" - /> -
-
- - { - setValue(e.target.value) - }} - placeholder="Enter the value of the secret" - /> -
-
-
- -
-
-
-
+ )} {tab === 'variable' && ( @@ -230,7 +216,7 @@ function DetailedProjectPage({ Add Variable - + Add a new variable @@ -250,7 +236,7 @@ function DetailedProjectPage({ Variable Name setNewVariableData({ @@ -271,7 +257,7 @@ function DetailedProjectPage({ Extra Note setNewVariableData({ @@ -279,7 +265,7 @@ function DetailedProjectPage({ note: e.target.value }) } - placeholder="Enter the note of the secret" + placeholder="Enter the note of the variable" value={newVariableData.note} />
@@ -301,10 +287,10 @@ function DetailedProjectPage({ }) } > - + - + {availableEnvironments.map((env) => ( {env.name} @@ -322,7 +308,7 @@ function DetailedProjectPage({ Environment Value setNewVariableData({ @@ -351,14 +337,14 @@ function DetailedProjectPage({ )}
-
+
{tab === 'secret' && secret} {tab === 'variable' && } - {/* {tab === 'variable' && variable} */}
+
) } -export default DetailedProjectPage \ No newline at end of file +export default DetailedProjectPage diff --git a/apps/platform/src/components/common/online-status-handler.tsx b/apps/platform/src/components/common/online-status-handler.tsx index f5685750..ae8b3ee6 100644 --- a/apps/platform/src/components/common/online-status-handler.tsx +++ b/apps/platform/src/components/common/online-status-handler.tsx @@ -1,10 +1,10 @@ 'use client' -import { useOnlineStatus } from "@/hooks/use-online-status"; +import { useOnlineStatus } from '@/hooks/use-online-status' function OnlineStatusHandler() { - useOnlineStatus(); - return null; + useOnlineStatus() + return null } -export default OnlineStatusHandler; +export default OnlineStatusHandler diff --git a/apps/platform/src/components/ui/add-secret-dialog.tsx b/apps/platform/src/components/ui/add-secret-dialog.tsx new file mode 100644 index 00000000..d82af3e4 --- /dev/null +++ b/apps/platform/src/components/ui/add-secret-dialog.tsx @@ -0,0 +1,234 @@ +import React from 'react' +import { AddSVG } from '@public/svg/shared' +import type { CreateSecretRequest, Environment } from '@keyshade/schema' +import { toast } from 'sonner' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger +} from './dialog' +import { Button } from './button' +import { Input } from './input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from './select' +import ControllerInstance from '@/lib/controller-instance' + +interface NewSecretData { + secretName: string + secretNote: string + environmentName: string + environmentValue: string +} + +function AddSecretDialog({ + setIsOpen, + isOpen, + newSecretData, + setNewSecretData, + availableEnvironments, + currentProjectSlug +}: { + setIsOpen: React.Dispatch> + isOpen: boolean + newSecretData: NewSecretData + setNewSecretData: React.Dispatch> + availableEnvironments: Environment[] + currentProjectSlug: string +}) { + const addSecret = async () => { + if (currentProjectSlug === '') { + throw new Error("Current project doesn't exist") + } + + const request: CreateSecretRequest = { + name: newSecretData.secretName, + projectSlug: currentProjectSlug, + entries: newSecretData.environmentValue + ? [ + { + value: newSecretData.environmentValue, + environmentSlug: newSecretData.environmentName + } + ] + : undefined, + note: newSecretData.secretNote + } + + const { success, error, data } = + await ControllerInstance.getInstance().secretController.createSecret( + request, + {} + ) + + if (success && data) { + toast.success('Secret added successfully', { + description: ( +

You created a new secret

+ ) + }) + } + if (error) { + if (error.statusCode === 409) { + toast.error('Secret name already exists', { + description: ( +

+ Secret name already exists. Please use a different one. +

+ ) + }) + } else { + // eslint-disable-next-line no-console -- we need to log the error that are not in the if condition + console.error(error) + } + } + + setNewSecretData({ + secretName: '', + secretNote: '', + environmentName: '', + environmentValue: '' + }) + setIsOpen(false) + } + + return ( + + + + + + + + Add a new secret + + + Add a new secret to the project. This secret will be encrypted and + stored securely. + + + +
+
+
+ + + setNewSecretData({ + ...newSecretData, + secretName: e.target.value + }) + } + placeholder="Enter the key of the secret" + value={newSecretData.secretName} + /> +
+ +
+ + + setNewSecretData({ + ...newSecretData, + secretNote: e.target.value + }) + } + placeholder="Enter the note of the secret" + value={newSecretData.secretNote} + /> +
+ +
+
+ + +
+ +
+ + + setNewSecretData({ + ...newSecretData, + environmentValue: e.target.value + }) + } + placeholder="Environment Value" + value={newSecretData.environmentValue} + /> +
+
+ +
+ +
+
+
+
+
+ ) +} + +export default AddSecretDialog diff --git a/apps/platform/src/components/ui/alert-dialog.tsx b/apps/platform/src/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..cc62651f --- /dev/null +++ b/apps/platform/src/components/ui/alert-dialog.tsx @@ -0,0 +1,140 @@ +'use client' + +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/button' + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = 'AlertDialogHeader' + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = 'AlertDialogFooter' + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel +} diff --git a/apps/platform/src/components/ui/collapsible.tsx b/apps/platform/src/components/ui/collapsible.tsx index 28dd1567..0f1cf232 100644 --- a/apps/platform/src/components/ui/collapsible.tsx +++ b/apps/platform/src/components/ui/collapsible.tsx @@ -1,7 +1,7 @@ -"use client" +'use client' -import * as React from "react" -import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" +import * as React from 'react' +import * as CollapsiblePrimitive from '@radix-ui/react-collapsible' const Collapsible = CollapsiblePrimitive.Root @@ -9,4 +9,4 @@ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent -export { Collapsible, CollapsibleTrigger, CollapsibleContent } \ No newline at end of file +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/apps/platform/src/components/ui/confirm-delete.tsx b/apps/platform/src/components/ui/confirm-delete.tsx new file mode 100644 index 00000000..9346ec0a --- /dev/null +++ b/apps/platform/src/components/ui/confirm-delete.tsx @@ -0,0 +1,102 @@ +'use client' + +import React, { useCallback, useEffect } from 'react' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from '@/components/ui/alert-dialog' +import { TrashSVG } from '@public/svg/shared' +import ControllerInstance from '@/lib/controller-instance' +import { toast } from 'sonner' + +function ConfirmDelete({ + isOpen, + onClose, + variableSlug +}: { + isOpen: boolean + onClose: () => void + variableSlug: string | null +}) { + const deleteVariable = async () => { + if (variableSlug === null) { + return + } + + const { success, error } = + await ControllerInstance.getInstance().variableController.deleteVariable( + { variableSlug: variableSlug }, + {} + ) + + if (success) { + toast.success('Variable deleted successfully', { + // eslint-disable-next-line react/no-unstable-nested-components -- we need to nest the description + description: () => ( +

+ The variable has been deleted. +

+ ) + }) + } + if (error) { + console.error(error) + } + + onClose() + } + + //Cleaning the pointer events for the context menu after closing the alert dialog + const cleanup = useCallback(() => { + document.body.style.pointerEvents = '' + document.documentElement.style.pointerEvents = '' + }, []) + + useEffect(() => { + if (!open) { + cleanup() + } + return () => cleanup() + }, [open, cleanup]) + + return ( + + + +
+ + + Do you really want to delete this variable? + +
+ + This action cannot be undone. This will permanently delete your + variable and remove your variable data from our servers. + +
+ + onClose()} + > + Cancel + + + Yes, delete the variable + + +
+
+ ) +} + +export default ConfirmDelete diff --git a/apps/platform/src/components/ui/edit-variable-dialog.tsx b/apps/platform/src/components/ui/edit-variable-dialog.tsx new file mode 100644 index 00000000..9c8fc754 --- /dev/null +++ b/apps/platform/src/components/ui/edit-variable-dialog.tsx @@ -0,0 +1,128 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription +} from '@/components/ui/dialog' +import { UpdateVariableRequest } from '@keyshade/schema' +import ControllerInstance from '@/lib/controller-instance' +import { toast } from 'sonner' + +function EditVariableDialog({ + isOpen, + onClose, + variableSlug, + variableName, + variableNote +}: { + isOpen: boolean + onClose: () => void + variableSlug: string | null + variableName: string + variableNote: string +}) { + const [newVariableName, setNewVariableName] = useState(variableName) + const [extraNote, setExtraNote] = useState(variableNote) + + const updateVariable = async () => { + if (variableSlug === null) { + return + } + + const request: UpdateVariableRequest = { + variableSlug, + name: newVariableName === variableName ? undefined : newVariableName, + note: extraNote === '' ? undefined : extraNote, + entries: undefined + } + + const { success, error } = + await ControllerInstance.getInstance().variableController.updateVariable( + request, + {} + ) + + if (success) { + toast.success('Variable edited successfully', { + // eslint-disable-next-line react/no-unstable-nested-components -- we need to nest the description + description: () => ( +

+ You successfully edited the variable +

+ ) + }) + } + if (error) { + // eslint-disable-next-line no-console -- we need to log the error + console.error('Error while updating variable: ', error) + } + + onClose() + } + + return ( +
+ + + + + Edit this variable + + + Edit the variable name or the note + + +
+
+ + setNewVariableName(e.target.value)} + className="w-[20rem] bg-[#262626] text-base font-normal text-white" + /> +
+
+ +