import { Fragment, useCallback, useEffect, useState } from 'react'; import Head from 'next/head'; import Image from 'next/image'; import { useRouter } from 'next/router'; import { faArrowDownAZ, faArrowDownZA, faCheck, faCopy, faDownload, faEye, faEyeSlash, faFolderOpen, faMagnifyingGlass, faPlus, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Button from '~/components/basic/buttons/Button'; import ListBox from '~/components/basic/Listbox'; import BottonRightPopup from '~/components/basic/popups/BottomRightPopup'; import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; import DropZone from '~/components/dashboard/DropZone'; import KeyPair from '~/components/dashboard/KeyPair'; import SideBar from '~/components/dashboard/SideBar'; import NavHeader from '~/components/navigation/NavHeader'; import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject'; import pushKeys from '~/components/utilities/secrets/pushKeys'; import guidGenerator from '~/utilities/randomId'; import { envMapping } from '../../public/data/frequentConstants'; import getUser from '../api/user/getUser'; import checkUserAction from '../api/userActions/checkUserAction'; import registerUserAction from '../api/userActions/registerUserAction'; import getWorkspaces from '../api/workspace/getWorkspaces'; interface SecretDataProps { type: 'personal' | 'shared'; pos: number; key: string; value: string; id: string; comment: string; } /** * this function finds the teh duplicates in an array * @param arr - array of anything (e.g., with secret keys and types (personal/shared)) * @returns - a list with duplicates */ function findDuplicates(arr: any[]) { const map = new Map(); return arr.filter((item) => { if (map.has(item)) { map.set(item, false); return true; } else { map.set(item, true); return false; } }); } /** * This is the main component for the dashboard (aka the screen with all the encironemnt variable & secrets) * @returns */ export default function Dashboard() { const [data, setData] = useState(); const [fileState, setFileState] = useState([]); const [buttonReady, setButtonReady] = useState(false); const router = useRouter(); const [workspaceId, setWorkspaceId] = useState(''); const [blurred, setBlurred] = useState(true); const [isKeyAvailable, setIsKeyAvailable] = useState(true); const [env, setEnv] = useState( router.asPath.split('?').length == 1 ? 'Development' : Object.keys(envMapping).includes(router.asPath.split('?')[1]) ? router.asPath.split('?')[1] : 'Development' ); const [isNew, setIsNew] = useState(false); const [searchKeys, setSearchKeys] = useState(''); const [errorDragAndDrop, setErrorDragAndDrop] = useState(false); const [projectIdCopied, setProjectIdCopied] = useState(false); const [sortMethod, setSortMethod] = useState('alphabetical'); const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false); const [hasUserEverPushed, setHasUserEverPushed] = useState(false); const [sidebarSecretId, toggleSidebar] = useState("None"); const [sharedToHide, setSharedToHide] = useState([]); const { createNotification } = useNotificationContext(); // #TODO: fix save message for changing reroutes // const beforeRouteHandler = (url) => { // const warningText = // "Do you want to save your results bfore leaving this page?"; // if (!buttonReady) return; // if (router.asPath !== url && !confirm(warningText)) { // // router.events.emit('routeChangeError'); // // setData(data) // savePush(); // throw `Route change to "${url}" was aborted (this error can be safely ignored).`; // } else { // setButtonReady(false); // } // }; // prompt the user if they try and leave with unsaved changes useEffect(() => { const warningText = 'Do you want to save your results before leaving this page?'; const handleWindowClose = (e: any) => { if (!buttonReady) return; e.preventDefault(); return (e.returnValue = warningText); }; window.addEventListener('beforeunload', handleWindowClose); // router.events.on('routeChangeStart', beforeRouteHandler); return () => { window.removeEventListener('beforeunload', handleWindowClose); // router.events.off('routeChangeStart', beforeRouteHandler); }; }, [buttonReady]); /** * Reorder rows alphabetically or in the opprosite order */ const reorderRows = (dataToReorder: SecretDataProps[] | 1) => { setSortMethod((prevSort) => prevSort == 'alphabetical' ? '-alphabetical' : 'alphabetical' ); sortValuesHandler(dataToReorder, undefined); }; useEffect(() => { (async () => { try { const userWorkspaces = await getWorkspaces(); const listWorkspaces = userWorkspaces.map((workspace) => workspace._id); if ( !listWorkspaces.includes(router.asPath.split('/')[2].split('?')[0]) ) { router.push('/dashboard/' + listWorkspaces[0]); } if (env != router.asPath.split('?')[1]) { router.push(router.asPath.split('?')[0] + '?' + env); } setBlurred(true); setWorkspaceId(String(router.query.id)); const dataToSort = await getSecretsForProject({ env, setFileState, setIsKeyAvailable, setData, workspaceId: String(router.query.id) }); reorderRows(dataToSort); setSharedToHide( dataToSort?.filter(row => (dataToSort ?.map((item) => item.key) .filter( (item, index) => index !== dataToSort?.map((item) => item.key).indexOf(item) ).includes(row.key) && row.type == 'shared'))?.map((item) => item.id) ) const user = await getUser(); setIsNew( (Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3 ? true : false ); const userAction = await checkUserAction({ action: 'first_time_secrets_pushed' }); setHasUserEverPushed(userAction ? true : false); } catch (error) { console.log('Error', error); setData(undefined); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [env]); const addRow = () => { setIsNew(false); setData([ ...data!, { id: guidGenerator(), pos: data!.length, key: '', value: '', type: 'shared', comment: '', } ]); }; interface overrideProps { id: string; keyName: string; value: string; pos: number; comment: string; } /** * This function add an ovverrided version of a certain secret to the current user * @param {object} obj * @param {string} obj.id - if of this secret that is about to be overriden * @param {string} obj.keyName - key name of this secret * @param {string} obj.value - value of this secret * @param {string} obj.pos - position of this secret on the dashboard */ const addOverride = ({ id, keyName, value, pos, comment }: overrideProps) => { setIsNew(false); const tempdata: SecretDataProps[] | 1 = [ ...data!, { id: id, pos: pos, key: keyName, value: value, type: 'personal', comment: comment } ]; sortValuesHandler(tempdata, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical"); }; const deleteRow = (id: string) => { setButtonReady(true); setData(data!.filter((row: SecretDataProps) => row.id !== id)); }; /** * This function deleted the override of a certain secrer * @param {string} id - id of a secret to be deleted */ const deleteOverride = (id: string) => { setButtonReady(true); // find which shared secret corresponds to the overriden version const sharedVersionOfOverride = data!.filter(secret => secret.type == "shared" && secret.key == data!.filter(row => row.id == id)[0]?.key)[0]?.id; // change the sidebar to this shared secret; and unhide it toggleSidebar(sharedVersionOfOverride) setSharedToHide(sharedToHide!.filter(tempId => tempId != sharedVersionOfOverride)) // resort secrets const tempData = data!.filter((row: SecretDataProps) => !(row.id == id && row.type == 'personal')) sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical") }; const modifyValue = (value: string, pos: number) => { setData((oldData) => { oldData![pos].value = value; return [...oldData!]; }); setButtonReady(true); }; const modifyKey = (value: string, pos: number) => { setData((oldData) => { oldData![pos].key = value; return [...oldData!]; }); setButtonReady(true); }; const modifyVisibility = (value: "shared" | "personal", pos: number) => { setData((oldData) => { oldData![pos].type = value; return [...oldData!]; }); setButtonReady(true); }; const modifyComment = (value: string, pos: number) => { setData((oldData) => { oldData![pos].comment = value; return [...oldData!]; }); setButtonReady(true); }; // For speed purposes and better perforamance, we are using useCallback const listenChangeValue = useCallback((value: string, pos: number) => { modifyValue(value, pos); }, []); const listenChangeKey = useCallback((value: string, pos: number) => { modifyKey(value, pos); }, []); const listenChangeVisibility = useCallback((value: "shared" | "personal", pos: number) => { modifyVisibility(value, pos); }, []); const listenChangeComment = useCallback((value: string, pos: number) => { modifyComment(value, pos); }, []); /** * Save the changes of environment variables and push them to the database */ const savePush = async () => { // Format the new object with environment variables const obj = Object.assign( {}, ...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment] })) ); // Checking if any of the secret keys start with a number - if so, don't do anything const nameErrors = !Object.keys(obj) .map((key) => !isNaN(Number(key[0].charAt(0)))) .every((v) => v === false); const duplicatesExist = findDuplicates(data!.map((item: SecretDataProps) => item.key + item.type)).length > 0; if (nameErrors) { return createNotification({ text: 'Solve all name errors before saving secrets.', type: 'error' }); } if (duplicatesExist) { return createNotification({ text: 'Remove duplicated secret names before saving.', type: 'error' }); } // Once "Save changed is clicked", disable that button setButtonReady(false); pushKeys({ obj, workspaceId: String(router.query.id), env }); // If this user has never saved environment variables before, show them a prompt to read docs if (!hasUserEverPushed) { setCheckDocsPopUpVisible(true); await registerUserAction({ action: 'first_time_secrets_pushed' }); } }; const addData = (newData: SecretDataProps[]) => { setData(data!.concat(newData)); setButtonReady(true); }; const changeBlurred = () => { setBlurred(!blurred); }; const sortValuesHandler = (dataToSort: SecretDataProps[] | 1, specificSortMethod?: 'alphabetical' | '-alphabetical') => { const howToSort = specificSortMethod == undefined ? sortMethod : specificSortMethod; const sortedData = (dataToSort != 1 ? dataToSort : data)! .sort((a, b) => howToSort == 'alphabetical' ? a.key.localeCompare(b.key) : b.key.localeCompare(a.key) ) .map((item: SecretDataProps, index: number) => { return { ...item, pos: index }; }); setData(sortedData); }; // This function downloads the secrets as a .env file const download = () => { const file = data! .map((item: SecretDataProps) => [item.key, item.value].join('=')) .join('\n'); const blob = new Blob([file]); const fileDownloadUrl = URL.createObjectURL(blob); const alink = document.createElement('a'); alink.href = fileDownloadUrl; alink.download = envMapping[env] + '.env'; alink.click(); }; const deleteCertainRow = (id: string) => { deleteRow(id); }; /** * This function copies the project id to the clipboard */ function copyToClipboard() { const copyText = document.getElementById('myInput') as HTMLInputElement; if (copyText) { copyText.select(); copyText.setSelectionRange(0, 99999); // For mobile devices navigator.clipboard.writeText(copyText.value); setProjectIdCopied(true); setTimeout(() => setProjectIdCopied(false), 2000); } } return data ? (
Secrets
{sidebarSecretId != "None" && row.key == data.filter(row => row.id == sidebarSecretId)[0]?.key)} modifyKey={listenChangeKey} modifyValue={listenChangeValue} modifyComment={listenChangeComment} addOverride={addOverride} deleteOverride={deleteOverride} buttonReady={buttonReady} savePush={savePush} sharedToHide={sharedToHide} setSharedToHide={setSharedToHide} />}
{checkDocsPopUpVisible && ( )}

Secrets

{data?.length == 0 && ( )}

Project ID:

Click to Copy
{(data?.length !== 0 || buttonReady) && (
)}
{data?.length !== 0 && ( <>
setSearchKeys(e.target.value)} placeholder={'Search keys...'} />
)}
{data?.length !== 0 ? (
{data?.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => ( item.key + item.type))?.includes(keyPair.key + keyPair.type)} toggleSidebar={toggleSidebar} sidebarSecretId={sidebarSecretId} /> ))}
) : (
{isKeyAvailable && ( )} { // fileState.message == 'Access needed to pull the latest file' || (!isKeyAvailable && ( <>

To view this file, contact your administrator for permission.

They need to grant you access in the team tab.

))}
)}
) : (
loading animation
); } Dashboard.requireAuth = true;