diff --git a/apps/platform/public/svg/shared/Error.svg b/apps/platform/public/svg/shared/Error.svg new file mode 100644 index 000000000..6b5ae5ea9 --- /dev/null +++ b/apps/platform/public/svg/shared/Error.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/platform/public/svg/shared/Vector.svg b/apps/platform/public/svg/shared/Vector.svg new file mode 100644 index 000000000..c7aad118d --- /dev/null +++ b/apps/platform/public/svg/shared/Vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/platform/public/svg/shared/index.ts b/apps/platform/public/svg/shared/index.ts index 07133a8c4..0d09dde2e 100644 --- a/apps/platform/public/svg/shared/index.ts +++ b/apps/platform/public/svg/shared/index.ts @@ -7,6 +7,9 @@ import SettingsSVG from './settings.svg' 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 ErrorSVG from './Error.svg' export { DropdownSVG, @@ -17,5 +20,8 @@ export { SettingsSVG, ThreeDotOptionSVG, AddSVG, - LoadingSVG + LoadingSVG, + MessageSVG, + VectorSVG, + ErrorSVG } diff --git a/apps/platform/public/svg/shared/message.svg b/apps/platform/public/svg/shared/message.svg new file mode 100644 index 000000000..f77574980 --- /dev/null +++ b/apps/platform/public/svg/shared/message.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/platform/src/app/(main)/page.tsx b/apps/platform/src/app/(main)/page.tsx index 9a9cb2aca..af66c1016 100644 --- a/apps/platform/src/app/(main)/page.tsx +++ b/apps/platform/src/app/(main)/page.tsx @@ -38,6 +38,7 @@ import { DialogTrigger } from '@/components/ui/dialog' import ControllerInstance from '@/lib/controller-instance' +import { Textarea } from '@/components/ui/textarea' export default function Index(): JSX.Element { const [isSheetOpen, setIsSheetOpen] = useState(false) @@ -144,10 +145,12 @@ export default function Index(): JSX.Element { - + {isProjectEmpty ? null : ( + + )}
@@ -190,8 +193,8 @@ export default function Index(): JSX.Element { > Description - { setNewProjectData((prev) => ({ @@ -217,7 +220,10 @@ export default function Index(): JSX.Element { onChange={(e) => { setNewProjectData((prev) => ({ ...prev, - envName: e.target.value + environments: (prev.environments || []).map( + (env, index) => + index === 0 ? { ...env, name: e.target.value } : env + ) })) }} placeholder="Your project default environment name" @@ -232,13 +238,18 @@ export default function Index(): JSX.Element { > Env. Description - { setNewProjectData((prev) => ({ ...prev, - envDescription: e.target.value + environments: (prev.environments || []).map( + (env, index) => + index === 0 + ? { ...env, description: e.target.value } + : env + ) })) }} placeholder="Detailed description about your environment" @@ -265,7 +276,7 @@ export default function Index(): JSX.Element { })) }} > - + @@ -334,7 +345,9 @@ export default function Index(): JSX.Element {
Create a file and start setting up your environment and secret keys
- +
)} 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 a4362de47..cf4ac3a17 100644 --- a/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx +++ b/apps/platform/src/app/(main)/project/[project]/@variable/page.tsx @@ -1,7 +1,171 @@ -import React from 'react' +'use client' -function VariablePage(): React.JSX.Element { - return
VariablePage
+import { useEffect, useState } from 'react' +import { Button } from '@/components/ui/button' +import { + ClientResponse, + GetAllVariablesOfProjectResponse, + Project, +} from '@keyshade/schema' +import { FolderSVG } from '@public/svg/dashboard' +import { MessageSVG } from '@public/svg/shared' +import { ChevronDown } from 'lucide-react' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table' +import ControllerInstance from '@/lib/controller-instance' + +interface VariablePageProps { + currentProject: Project | undefined +} + + +function VariablePage({ + currentProject +}: VariablePageProps): React.JSX.Element { + + const [allVariables, setAllVariables] = useState([]) + // Holds the currently open section ID + const [openSections, setOpenSections] = useState>(new Set()) + + //Environments table toggle logic + const toggleSection = (id: string) => { + setOpenSections((prev) => { + const newSet = new Set(prev) + if (newSet.has(id)) { + newSet.delete(id) + } else { + newSet.add(id) + } + return newSet + }) + } + + useEffect(() => { + + const getAllVariables = async () => { + + if (!currentProject) { + return + } + + const { success, error, data }: ClientResponse = + await ControllerInstance.getInstance().variableController.getAllVariablesOfProject( + { projectSlug: currentProject.slug }, + {} + ) + + if (success && data) { + setAllVariables(data.items) + } else { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) + } + } + + getAllVariables() + }, [currentProject]) + + return ( +
+ + {/* Showing this when there are no variables present */} + {allVariables.length === 0 ? ( +
+ + +
+

+ Declare your first variable +

+

+ Declare and store a variable against different environments +

+
+ + +
+ ) : ( + // 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`; + })()} +
+
+
+ {variable.variable.lastUpdatedBy.name.split(' ')[0]} +
+ + + + {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} + + + ))} + +
+ ) : ( + <> + )} +
+
+ ))} +
+ )} +
+ ) } export default VariablePage diff --git a/apps/platform/src/app/(main)/project/[project]/layout.tsx b/apps/platform/src/app/(main)/project/[project]/layout.tsx index 9edfb0bb9..36114df19 100644 --- a/apps/platform/src/app/(main)/project/[project]/layout.tsx +++ b/apps/platform/src/app/(main)/project/[project]/layout.tsx @@ -1,8 +1,16 @@ 'use client' +import type { MouseEvent, MouseEventHandler } from 'react' import { useEffect, useState } from 'react' import { useSearchParams } from 'next/navigation' import { AddSVG } from '@public/svg/shared' -import type { Project } from '@keyshade/schema' +import type { + ClientResponse, + CreateVariableRequest, + Environment, + GetAllEnvironmentsOfProjectResponse, + Project +} from '@keyshade/schema' +import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Dialog, @@ -15,6 +23,15 @@ import { import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import ControllerInstance from '@/lib/controller-instance' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select' +import VariablePage from './@variable/page' +import { Toaster } from '@/components/ui/sonner' interface DetailedProjectPageProps { params: { project: string } @@ -31,12 +48,76 @@ function DetailedProjectPage({ 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({ + variableName: '', + note: '', + environmentName: '', + environmentValue: '' + }) + const [availableEnvironments, setAvailableEnvironments] = useState([]) const searchParams = useSearchParams() const tab = searchParams.get('tab') ?? 'rollup-details' + const addVariable = async (e: MouseEvent) => { + e.preventDefault() + + if (!currentProject) { + throw new Error("Current project doesn't exist") + } + + const request: CreateVariableRequest = { + name: newVariableData.variableName, + projectSlug: currentProject.slug, + entries: newVariableData.environmentValue + ? [ + { + value: newVariableData.environmentValue, + environmentSlug: newVariableData.environmentName + } + ] + : undefined, + note: newVariableData.note + } + + const { success, error } = + await ControllerInstance.getInstance().variableController.createVariable( + request, + {} + ) + + if (success) { + toast.success('Variable added successfully', { + // eslint-disable-next-line react/no-unstable-nested-components -- we need to nest the description + description: () => ( +

+ The variable has been added to the project +

+ ) + }) + } + + 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: () => ( +

+ Variable name is already there, kindly use 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) + } + } + + setIsOpen(false) + } + useEffect(() => { async function getProjectBySlug() { const { success, error, data } = @@ -56,67 +137,228 @@ function DetailedProjectPage({ getProjectBySlug() }, [params.project]) + 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}
- - - - - - - 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" - /> + {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" + /> +
-
- - { - setValue(e.target.value) - }} - placeholder="Enter the value of the secret" - /> +
+
-
- + +
+ )} + {tab === 'variable' && ( + + + + + + + + Add a new variable + + + Add a new variable to the project + + + +
+
+
+ + + setNewVariableData({ + ...newVariableData, + variableName: e.target.value + }) + } + placeholder="Enter the key of the variable" + value={newVariableData.variableName} + /> +
+ +
+ + + setNewVariableData({ + ...newVariableData, + note: e.target.value + }) + } + placeholder="Enter the note of the secret" + value={newVariableData.note} + /> +
+ +
+
+ + +
+ +
+ + + setNewVariableData({ + ...newVariableData, + environmentValue: e.target.value + }) + } + placeholder="Environment Value" + value={newVariableData.environmentValue} + /> +
+
+ +
+ +
+
-
- -
+ +
+ )} -
+ +
{tab === 'secret' && secret} - {tab === 'variable' && variable} + {tab === 'variable' && } + {/* {tab === 'variable' && variable} */}
+ ) } -export default DetailedProjectPage +export default DetailedProjectPage \ No newline at end of file diff --git a/apps/platform/src/components/dashboard/projectCard/index.tsx b/apps/platform/src/components/dashboard/projectCard/index.tsx index 4cd0ad523..1818c51f0 100644 --- a/apps/platform/src/components/dashboard/projectCard/index.tsx +++ b/apps/platform/src/components/dashboard/projectCard/index.tsx @@ -25,6 +25,7 @@ function ProjectCard({ }: ProjectCardProps): JSX.Element { const { id, + slug, name, description, environmentCount, @@ -69,7 +70,7 @@ function ProjectCard({
diff --git a/apps/platform/src/components/ui/collapsible.tsx b/apps/platform/src/components/ui/collapsible.tsx new file mode 100644 index 000000000..28dd15673 --- /dev/null +++ b/apps/platform/src/components/ui/collapsible.tsx @@ -0,0 +1,12 @@ +"use client" + +import * as React from "react" +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } \ No newline at end of file diff --git a/apps/platform/src/components/ui/textarea.tsx b/apps/platform/src/components/ui/textarea.tsx new file mode 100644 index 000000000..2a10e86f1 --- /dev/null +++ b/apps/platform/src/components/ui/textarea.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const Textarea = React.forwardRef< + HTMLTextAreaElement, + React.ComponentProps<'textarea'> +>(({ className, ...props }, ref) => { + return ( +