diff --git a/frontend/components/basic/dialog/AddEnvironmentDialog.tsx b/frontend/components/basic/dialog/AddEnvironmentDialog.tsx new file mode 100644 index 000000000..83b578d81 --- /dev/null +++ b/frontend/components/basic/dialog/AddEnvironmentDialog.tsx @@ -0,0 +1,127 @@ +import { FormEventHandler, Fragment, useEffect, useState } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; + +import Button from '../buttons/Button'; +import InputField from '../InputField'; + +type FormFields = { name: string; slug: string }; + +type Props = { + isOpen?: boolean; + isEditMode?: boolean; + // on edit mode load up initial values + initialValues?: FormFields; + onClose: () => void; + onSubmit: (envName: string, envSlug: string) => void; +}; + +/** + * The dialog modal for when the user wants to create a new workspace + * @param {*} param0 + * @returns + */ +export const AddEnvironmentDialog = ({ isOpen, onClose, onSubmit, initialValues, isEditMode }: Props) => { + const [formInput, setFormInput] = useState({ + name: '', + slug: '', + }); + + // This use effect can be removed when the unmount is happening from outside the component + // When unmount happens outside state gets unmounted also + useEffect(() => { + setFormInput(initialValues || { name: '', slug: '' }); + }, [isOpen]); + + // REFACTOR: Move to react-hook-form with yup for better form management + const onInputChange = (fieldName: string, fieldValue: string) => { + setFormInput((state) => ({ ...state, [fieldName]: fieldValue })); + }; + + const onFormSubmit: FormEventHandler = (e) => { + e.preventDefault(); + console.log(formInput); + }; + + return ( +
+ + + +
+ + +
+
+ + + + {isEditMode + ? 'Update environment' + : 'Create a new environment'} + +
+
+ onInputChange('name', val)} + type='varName' + value={formInput.name} + placeholder='' + isRequired + // error={error.length > 0} + // errorText={error} + /> +
+
+ onInputChange('slug', val)} + type='varName' + value={formInput.slug} + placeholder='' + isRequired + // error={error.length > 0} + // errorText={error} + /> +
+

+ Slugs are shorthands used in cli to access environment +

+
+
+
+
+
+
+
+
+
+
+ ); +}; diff --git a/frontend/components/basic/dialog/DeleteActionModal.tsx b/frontend/components/basic/dialog/DeleteActionModal.tsx new file mode 100644 index 000000000..cafd832ae --- /dev/null +++ b/frontend/components/basic/dialog/DeleteActionModal.tsx @@ -0,0 +1,100 @@ +import { Fragment, useState } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; + +import InputField from '../InputField'; + +// REFACTOR: Move all these modals into one reusable one +type Props = { + isOpen?: boolean; + onClose: ()=>void; + title: string; + onSubmit:()=>void; + deleteKey?:string; +} + +const DeleteActionModal = ({ + isOpen, + onClose, + title, + onSubmit, + deleteKey +}:Props) => { + const [deleteInputField, setDeleteInputField] = useState("") + + return ( +
+ + + +
+ +
+
+ + + + {title} + +
+

+ This action is irrevertible. +

+
+
+ setDeleteInputField(val)} + value={deleteInputField} + type='text' + /> +
+
+ + +
+
+
+
+
+
+
+
+ ); +}; + +export default DeleteActionModal; diff --git a/frontend/components/basic/table/EnvironmentsTable.tsx b/frontend/components/basic/table/EnvironmentsTable.tsx new file mode 100644 index 000000000..501b7f9bc --- /dev/null +++ b/frontend/components/basic/table/EnvironmentsTable.tsx @@ -0,0 +1,118 @@ +import { faPencil,faPlus,faX } from '@fortawesome/free-solid-svg-icons'; + +import { usePopUp } from '../../../hooks/usePopUp'; +import Button from '../buttons/Button'; +import {AddEnvironmentDialog} from '../dialog/AddEnvironmentDialog'; +import DeleteActionModal from '../dialog/DeleteActionModal'; + +const EnvironmentTable = ({ data = [] }) => { + const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ + 'createUpdateEnv', + 'deleteEnv', + ] as const); + + return ( + <> +
+
+

Project Environments

+

+ Choose which environments will show up in your dashboard like + development, staging, production +

+

+ Note: the text in slugs shows how these environmant should be + accessed in CLI. +

+
+
+
+
+
+
+ + + + + + + + + + {data?.length > 0 ? ( + data.map(({ name, slug }) => { + return ( + + + + + + ); + }) + ) : ( + + + + )} + +
NameSlug
+ {name} + + {slug} + +
+
+
+
+
+ No environmants found +
+ handlePopUpClose('deleteEnv')} + onSubmit={() => handlePopUpClose('deleteEnv')} + /> + handlePopUpClose('createUpdateEnv')} + onSubmit={() => null} + /> +
+ + ); +}; + +export default EnvironmentTable; diff --git a/frontend/hooks/index.ts b/frontend/hooks/index.ts new file mode 100644 index 000000000..dcea2eb7c --- /dev/null +++ b/frontend/hooks/index.ts @@ -0,0 +1 @@ +export { usePopUp } from './usePopUp'; diff --git a/frontend/hooks/usePopUp.tsx b/frontend/hooks/usePopUp.tsx new file mode 100644 index 000000000..eb0835b49 --- /dev/null +++ b/frontend/hooks/usePopUp.tsx @@ -0,0 +1,69 @@ +import { useCallback, useState } from 'react'; + +interface usePopUpProps { + name: Readonly; + isOpen: boolean; +} + +/** + * to provide better intellisense + * checks which type of inputProps were given and converts them into key-names + * SIDENOTE: On inputting give it as const and not string with (as const) + */ +type usePopUpState | usePopUpProps[]> = { + [P in T extends usePopUpProps[] ? T[number]['name'] : T[number]]: { + isOpen: boolean; + data?: unknown; + }; +}; + +interface usePopUpReturn | usePopUpProps[]> { + popUp: usePopUpState; + handlePopUpOpen: (popUpName: keyof usePopUpState, data?: unknown) => void; + handlePopUpClose: (popUpName: keyof usePopUpState) => void; + handlePopUpToggle: (popUpName: keyof usePopUpState) => void; +} + +/** + * This hook is used to manage multiple popUps/modal/dialog in a page + * Provides api to open,close,toggle and also store temporary data for the popUp + * @param popUpNames: the names of popUp containers eg: ["popUp1","second"] or [{name:"popUp2",isOpen:bool}] + */ +export const usePopUp = | usePopUpProps[]>( + popUpNames: T +): usePopUpReturn => { + const [popUp, setPopUp] = useState>( + Object.fromEntries( + popUpNames.map((popUpName) => + typeof popUpName === 'string' + ? [popUpName, { isOpen: false }] + : [popUpName.name, { isOpen: popUpName.isOpen }] + ) // convert into an array of [[popUpName,state]] then into Object + ) as usePopUpState // to override generic string return type of the function + ); + + const handlePopUpOpen = useCallback( + (popUpName: keyof usePopUpState, data?: unknown) => { + setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: true, data } })); + }, + [] + ); + + const handlePopUpClose = useCallback((popUpName: keyof usePopUpState) => { + setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: false } })); + }, []); + + const handlePopUpToggle = useCallback((popUpName: keyof usePopUpState) => { + setPopUp((popUp) => ({ + ...popUp, + [popUpName]: { isOpen: !popUp[popUpName].isOpen }, + })); + }, []); + + return { + popUp, + handlePopUpOpen, + handlePopUpClose, + handlePopUpToggle, + }; +}; diff --git a/frontend/pages/settings/project/[id].tsx b/frontend/pages/settings/project/[id].tsx new file mode 100644 index 000000000..4a64c3e65 --- /dev/null +++ b/frontend/pages/settings/project/[id].tsx @@ -0,0 +1,245 @@ +import { useEffect, useRef, useState } from "react"; +import Head from "next/head"; +import { useRouter } from "next/router"; +import { useTranslation } from "next-i18next"; +import { faCheck, faPlus } from "@fortawesome/free-solid-svg-icons"; + +import Button from "~/components/basic/buttons/Button"; +import AddServiceTokenDialog from "~/components/basic/dialog/AddServiceTokenDialog"; +import InputField from "~/components/basic/InputField"; +import EnvironmentTable from '~/components/basic/table/EnvironmentsTable'; +import ServiceTokenTable from "~/components/basic/table/ServiceTokenTable"; +import NavHeader from "~/components/navigation/NavHeader"; +import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps"; + +import getServiceTokens from "../../api/serviceToken/getServiceTokens"; +import deleteWorkspace from "../../api/workspace/deleteWorkspace"; +import getWorkspaces from "../../api/workspace/getWorkspaces"; +import renameWorkspace from "../../api/workspace/renameWorkspace"; + +export default function SettingsBasic() { + const [buttonReady, setButtonReady] = useState(false); + const router = useRouter(); + const [workspaceName, setWorkspaceName] = useState(""); + const [serviceTokens, setServiceTokens] = useState([]); + const [environments,setEnvironments] = useState([]); + const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState(""); + const [isAddOpen, setIsAddOpen] = useState(false); + const [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] = useState(false); + + const { t } = useTranslation(); + + useEffect(async () => { + const userWorkspaces = await getWorkspaces(); + userWorkspaces.forEach((userWorkspace) => { + if (userWorkspace._id == router.query.id) { + setWorkspaceName(userWorkspace.name); + setEnvironments(userWorkspace.environments); + } + }); + const tempServiceTokens = await getServiceTokens({ + workspaceId: router.query.id, + }); + setServiceTokens(tempServiceTokens); + }, []); + + const modifyWorkspaceName = (newName) => { + setButtonReady(true); + setWorkspaceName(newName); + }; + + const submitChanges = (newWorkspaceName) => { + renameWorkspace(router.query.id, newWorkspaceName); + setButtonReady(false); + }; + + const closeAddServiceTokenModal = () => { + setIsAddServiceTokenDialogOpen(false); + }; + + /** + * This function deleted a workspace. + * It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete + * It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete. + * It then deletes the workspace and forwards the user to another aviable workspace. + */ + const executeDeletingWorkspace = async () => { + const userWorkspaces = await getWorkspaces(); + + if (userWorkspaces.length > 1) { + if ( + userWorkspaces.filter( + (workspace) => workspace._id == router.query.id + )[0].name == workspaceToBeDeletedName + ) { + await deleteWorkspace(router.query.id); + const userWorkspaces = await getWorkspaces(); + router.push("/dashboard/" + userWorkspaces[0]._id); + } + } + }; + + return ( +
+ + + {t('common:head-title', { title: t('settings-project:title') })} + + + + +
+
+ +
+
+

+ {t('settings-project:title')} +

+

+ {t('settings-project:description')} +

+
+
+
+
+
+
+

+ {t('common:display-name')} +

+
+ +
+
+
+
+
+
+
+

+ {t('common:project-id')} +

+

+ {t('settings-project:project-id-description')} +

+

+ {t('settings-project:project-id-description2')} + {/* eslint-disable-next-line react/jsx-no-target-blank */} + + {t('settings-project:docs')} + +

+
+ +
+
+
+
+
+

+ {t('section-token:service-tokens')} +

+

+ {t('section-token:service-tokens-description')} +

+
+
+
+
+ +
+
+ +
+
+
+
+

+ {t('settings-project:danger-zone')} +

+

+ {t('settings-project:danger-zone-note')} +

+
+ +
+ +

+ {t('settings-project:delete-project-note')} +

+
+
+
+
+
+ ); +} + +SettingsBasic.requireAuth = true; + +export const getServerSideProps = getTranslatedServerSideProps([ + "settings", + "settings-project", + "section-token", +]); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index e936a69a2..f51378ec3 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -3,6 +3,7 @@ "baseUrl": ".", "paths": { "~/components/*": ["components/*"], + "~/hooks/*": ["hooks/*"], "~/utilities/*": ["components/utilities/*"], "~/*": ["const"], "~/pages/*": ["pages/*"]