import React, { 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, faCircleInfo, faCopy, faDownload, faEllipsis, faEye, faEyeSlash, faFolderOpen, faMagnifyingGlass, faPeopleGroup, faPerson, faPlus, faShuffle, faX } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Menu, Transition } from '@headlessui/react'; 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 DashboardInputField from '~/components/dashboard/DashboardInputField'; import DropZone from '~/components/dashboard/DropZone'; import NavHeader from '~/components/navigation/NavHeader'; import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject'; import pushKeys from '~/components/utilities/secrets/pushKeys'; import pushKeysIntegration from '~/components/utilities/secrets/pushKeysIntegration'; import guidGenerator from '~/utilities/randomId'; import { envMapping } from '../../public/data/frequentConstants'; import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations'; import getUser from '../api/user/getUser'; import checkUserAction from '../api/userActions/checkUserAction'; import registerUserAction from '../api/userActions/registerUserAction'; import getWorkspaces from '../api/workspace/getWorkspaces'; /** * This component represent a single row for an environemnt variable on the dashboard * @param {object} obj * @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private) * @param {function} obj.deleteRow - a function to delete a certain keyPair * @param {function} obj.modifyKey - modify the key of a certain environment variable * @param {function} obj.modifyValue - modify the value of a certain environment variable * @param {function} obj.modifyVisibility - switch between public/private visibility * @param {boolean} obj.isBlurred - if the blurring setting is turned on * @param {string[]} obj.duplicates - list of all the duplicates secret names on the dashboard * @returns */ const KeyPair = ({ keyPair, deleteRow, modifyKey, modifyValue, modifyVisibility, isBlurred, duplicates }) => { const [randomStringLength, setRandomStringLength] = useState(32); return (
modifyVisibility( keyPair.type == 'personal' ? 'shared' : 'personal', keyPair.pos ) } className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full" >
{keyPair.type == 'personal' ? 'Make Shared' : 'Make Personal'}
{ if (randomStringLength > 32) { setRandomStringLength(32); } else if (randomStringLength < 2) { setRandomStringLength(2); } else { modifyValue( [...Array(randomStringLength)] .map(() => Math.floor(Math.random() * 16).toString(16)) .join(''), keyPair.pos ); } }} className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full" >

Generate Random Hex

digits

{ if (randomStringLength > 1) { setRandomStringLength(randomStringLength - 1); } }} > -
setRandomStringLength(parseInt(e.target.value)) } value={randomStringLength} className="text-center z-20 peer text-sm bg-transparent w-full outline-none" spellCheck="false" />
{ if (randomStringLength < 32) { setRandomStringLength(randomStringLength + 1); } }} > +
); }; /** * 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 { 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) => { 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) => { setSortMethod((prevSort) => prevSort == 'alphabetical' ? '-alphabetical' : 'alphabetical' ); sortValuesHandler(dataToReorder); }; useEffect(() => { (async () => { try { let 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(router.query.id); const dataToSort = await getSecretsForProject({ env, setFileState, setIsKeyAvailable, setData, workspaceId: router.query.id }); reorderRows(dataToSort); const user = await getUser(); setIsNew( (Date.parse(new Date()) - Date.parse(user.createdAt)) / 60000 < 3 ? true : false ); let userAction = await checkUserAction({ action: 'first_time_secrets_pushed' }); setHasUserEverPushed(userAction ? true : false); } catch (error) { console.log('Error', error); setData([]); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [env]); const addRow = () => { setIsNew(false); setData([ ...data, { id: guidGenerator(), pos: data.length, key: '', value: '', type: 'shared' } ]); }; const deleteRow = (id) => { setButtonReady(true); setData(data.filter((row) => row.id !== id)); }; const modifyValue = (value, pos) => { setData((oldData) => { oldData[pos].value = value; return [...oldData]; }); setButtonReady(true); }; const modifyKey = (value, pos) => { setData((oldData) => { oldData[pos].key = value; return [...oldData]; }); setButtonReady(true); }; const modifyVisibility = (value, pos) => { setData((oldData) => { oldData[pos].type = value; return [...oldData]; }); setButtonReady(true); }; // For speed purposes and better perforamance, we are using useCallback const listenChangeValue = useCallback((value, pos) => { modifyValue(value, pos); }, []); const listenChangeKey = useCallback((value, pos) => { modifyKey(value, pos); }, []); const listenChangeVisibility = useCallback((value, pos) => { modifyVisibility(value, pos); }, []); /** * Save the changes of environment variables and push them to the database */ const savePush = async () => { // Format the new object with environment variables let obj = Object.assign( {}, ...data.map((row) => ({ [row.key]: [row.value, row.type] })) ); // 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(key.charAt(0))) .every((v) => v === false); const duplicatesExist = data ?.map((item) => item.key) .filter( (item, index) => index !== data?.map((item) => item.key).indexOf(item) ).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: 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) => { setData(data.concat(newData)); setButtonReady(true); }; const changeBlurred = () => { setBlurred(!blurred); }; const sortValuesHandler = (dataToSort) => { const sortedData = (dataToSort != 1 ? dataToSort : data) .sort((a, b) => sortMethod == 'alphabetical' ? a.key.localeCompare(b.key) : b.key.localeCompare(a.key) ) .map((item, index) => { return { ...item, pos: index }; }); setData(sortedData); }; // This function downloads the secrets as a .env file const download = () => { const file = data .map((item) => [item.key, item.value].join('=')) .join('\n'); const blob = new Blob([file]); const fileDownloadUrl = URL.createObjectURL(blob); let alink = document.createElement('a'); alink.href = fileDownloadUrl; alink.download = envMapping[env] + '.env'; alink.click(); }; const deleteCertainRow = (id) => { deleteRow(id); }; /** * This function copies the project id to the clipboard */ function copyToClipboard() { var copyText = document.getElementById('myInput'); copyText.select(); copyText.setSelectionRange(0, 99999); // For mobile devices navigator.clipboard.writeText(copyText.value); setProjectIdCopied(true); setTimeout(() => setProjectIdCopied(false), 2000); } return data ? (
Secrets
{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 ? (
{/* */}

Personal

Personal keys are only visible to you
{data .filter( (keyPair) => keyPair.key .toLowerCase() .includes(searchKeys.toLowerCase()) && keyPair.type == 'personal' ) ?.map((keyPair) => ( item.key) .filter( (item, index) => index !== data?.map((item) => item.key).indexOf(item) )} /> ))}
8 ? 'h-3/4' : 'h-min' }`} >
{/* */}

Shared

Shared keys are visible to your whole team
{data .filter( (keyPair) => keyPair.key .toLowerCase() .includes(searchKeys.toLowerCase()) && keyPair.type == 'shared' ) ?.map((keyPair) => ( item.key) .filter( (item, index) => index !== data?.map((item) => item.key).indexOf(item) )} /> ))}
) : (
{fileState.message != "There's nothing to pull" && fileState.message != undefined && ( )} {(fileState.message == "There's nothing to pull" || fileState.message == undefined) && isKeyAvailable && ( )} {fileState.message == 'Failed membership validation for workspace' && (

You are not authorized to view this project.

)} {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;