import { IconAlertCircle, IconExternalLink, IconX } from '@tabler/icons-react'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import Select from 'react-select'; import { toast, ToastOptions } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { useLoader } from '~/contexts/LoaderContext'; import { alertOptions, Optional, Repository } from '~/types'; import classes from './RepoManage.module.css'; type RepoManageProps = { mode: 'add' | 'edit'; repoList: Optional>; closeHandler: () => void; }; type DataForm = { alias: string; storageSize: string; sshkey: string; comment: string; alert: { value: Optional; label: string }; lanCommand: boolean; appendOnlyMode: boolean; }; export default function RepoManage(props: RepoManageProps) { const router = useRouter(); const targetRepo = props.mode === 'edit' && router.query.slug ? props.repoList?.find((repo) => repo.id.toString() === router.query.slug) : undefined; const { register, handleSubmit, control, formState: { errors, isSubmitting, isValid }, } = useForm({ mode: 'onChange' }); const toastOptions: ToastOptions = { position: 'top-right', autoClose: 5000, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, draggable: true, progress: undefined, }; const [deleteDialog, setDeleteDialog] = useState(false); const [isLoading, setIsLoading] = useState(false); const { start, stop } = useLoader(); //router.query.slug is undefined for few milliseconds on first render for a direct URL access (https://github.com/vercel/next.js/discussions/11484). //If I call repoManage with edit mode (props), i'm firstly waiting that router.query.slug being available before rendering. if (props.mode === 'edit') { if (!router.query.slug) { start(); return; } else if (!targetRepo) { stop(); router.push('/404'); } } //Delete a repo const deleteHandler = async (repositoryName?: string) => { start(); if (!repositoryName) { stop(); toast.error('Repository name not found', toastOptions); router.replace('/'); return; } //API Call for delete await fetch('/api/v1/repositories/' + repositoryName, { method: 'DELETE', headers: { 'Content-type': 'application/json', }, }) .then(async (response) => { if (response.ok) { toast.success( '🗑 The repository ' + repositoryName + ' has been successfully deleted', toastOptions ); router.replace('/'); } else { if (response.status == 403) { toast.warning( '🔒 The server is currently protected against repository deletion.', toastOptions ); setIsLoading(false); router.replace('/'); } else { const errorMessage = await response.json(); toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions); router.replace('/'); console.log('Fail to delete'); } } }) .catch((error) => { toast.error('An error has occurred', toastOptions); router.replace('/'); console.log(error); }) .finally(() => { stop(); }); }; const isSSHKeyUnique = async (sshPublicKey: string): Promise => { try { // Extract the first two columns of the SSH key in the form const publicKeyPrefix = sshPublicKey.split(' ').slice(0, 2).join(' '); const response = await fetch('/api/v1/repositories', { method: 'GET' }); const data: { repoList: Repository[] } = await response.json(); const conflictingRepo = data.repoList.find((repo: { sshPublicKey: string; id: number }) => { const repoPublicKeyPrefix = repo.sshPublicKey.split(' ').slice(0, 2).join(' '); return ( repoPublicKeyPrefix === publicKeyPrefix && (!targetRepo || repo.id !== targetRepo.id) ); }); if (conflictingRepo) { toast.error( `The SSH key is already used in repository ${conflictingRepo.repositoryName}. Please use another key or delete the key from the other repository.`, toastOptions ); return false; } return true; } catch (error) { console.log(error); toast.error('An error has occurred', toastOptions); return false; } }; //Form submit Handler for ADD or EDIT a repo const formSubmitHandler = async (dataForm: DataForm) => { setIsLoading(true); start(); // Clean SSH key by removing leading/trailing whitespace and line breaks const cleanedSSHKey = dataForm.sshkey.trim(); //Verify that the SSH key is unique if (!(await isSSHKeyUnique(cleanedSSHKey))) { stop(); setIsLoading(false); return; } //ADD a repo if (props.mode == 'add') { const newRepo = { alias: dataForm.alias, storageSize: parseInt(dataForm.storageSize), sshPublicKey: cleanedSSHKey, comment: dataForm.comment, alert: dataForm.alert.value, lanCommand: dataForm.lanCommand, appendOnlyMode: dataForm.appendOnlyMode, }; //POST API to send new repo await fetch('/api/v1/repositories', { method: 'POST', headers: { 'Content-type': 'application/json', }, body: JSON.stringify(newRepo), }) .then(async (response) => { if (response.ok) { toast.success('New repository added ! 🥳', toastOptions); router.replace('/'); } else { const errorMessage = await response.json(); toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions); router.replace('/'); console.log(`Fail to ${props.mode}`); } }) .catch((error) => { toast.error('An error has occurred', toastOptions); router.replace('/'); console.log(error); }) .finally(() => { stop(); setIsLoading(false); }); //EDIT a repo } else if (props.mode == 'edit') { const dataEdited = { alias: dataForm.alias, storageSize: parseInt(dataForm.storageSize), sshPublicKey: cleanedSSHKey, comment: dataForm.comment, alert: dataForm.alert.value, lanCommand: dataForm.lanCommand, appendOnlyMode: dataForm.appendOnlyMode, }; await fetch('/api/v1/repositories/' + targetRepo?.repositoryName, { method: 'PATCH', headers: { 'Content-type': 'application/json', }, body: JSON.stringify(dataEdited), }) .then(async (response) => { if (response.ok) { toast.success( 'The repository ' + targetRepo?.repositoryName + ' has been successfully edited !', toastOptions ); router.replace('/'); } else { const errorMessage = await response.json(); toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions); router.replace('/'); console.log(`Fail to ${props.mode}`); } }) .catch((error) => { toast.error('An error has occurred', toastOptions); router.replace('/'); console.log(error); }) .finally(() => { stop(); setIsLoading(false); }); } }; return ( <>
{deleteDialog ? (

Delete the repository{' '} {targetRepo?.repositoryName} {' '} ?

You are about to permanently delete the repository{' '} {targetRepo?.repositoryName} and all the backups it contains.
The data will not be recoverable and it will not be possible to go back.
<>
) : (
{props.mode == 'edit' && (

Edit the repository{' '} {targetRepo?.repositoryName}

)} {props.mode == 'add' &&

Add a repository

}
{/* ALIAS */} {errors.alias && {errors.alias.message}} {/* SSH KEY */}