Refactored the logic for frontend dashboard

This commit is contained in:
Vladyslav Matsiiako
2023-01-12 01:05:13 -08:00
parent 861639de27
commit 07c65ded40
15 changed files with 219 additions and 267 deletions

View File

@ -1,27 +1,11 @@
import React from "react";
import { Switch } from "@headlessui/react";
interface OverrideProps {
id: string;
keyName: string;
value: string;
pos: number;
comment: string;
}
interface ToggleProps {
enabled: boolean;
setEnabled: (value: boolean) => void;
addOverride: (value: OverrideProps) => void;
keyName: string;
value: string;
addOverride: (value: string | undefined, pos: number) => void;
pos: number;
id: string;
comment: string;
deleteOverride: (id: string) => void;
sharedToHide: string[];
setSharedToHide: (values: string[]) => void;
}
/**
@ -30,41 +14,23 @@ interface ToggleProps {
* @param {boolean} obj.enabled - whether the toggle is turned on or off
* @param {function} obj.setEnabled - change the state of the toggle
* @param {function} obj.addOverride - a function that adds an override to a certain secret
* @param {string} obj.keyName - key of a certain secret
* @param {string} obj.value - value of a certain secret
* @param {number} obj.pos - position of a certain secret
#TODO: make the secret id persistent?
* @param {string} obj.id - id of a certain secret (NOTE: THIS IS THE ID OF THE MAIN SECRET - NOT OF AN OVERRIDE)
* @param {function} obj.deleteOverride - a function that deleted an override for a certain secret
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
* @returns
*/
export default function Toggle ({
enabled,
setEnabled,
addOverride,
keyName,
value,
pos,
id,
comment,
deleteOverride,
sharedToHide,
setSharedToHide
pos
}: ToggleProps): JSX.Element {
return (
<Switch
checked={enabled}
onChange={() => {
if (enabled == false) {
addOverride({ id, keyName, value, pos, comment });
setSharedToHide([
...sharedToHide!,
id
])
addOverride('', pos);
} else {
deleteOverride(id);
addOverride(undefined, pos);
}
setEnabled(!enabled);
}}

View File

@ -3,7 +3,6 @@ import Image from "next/image";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import {
FontAwesomeIcon,
FontAwesomeIconProps,
} from "@fortawesome/react-fontawesome";
const classNames = require("classnames");
@ -101,7 +100,7 @@ export default function Button(props: ButtonProps): JSX.Element {
<div
className={`${
props.loading == true ? "opacity-100" : "opacity-0"
} absolute flex items-center px-2 duration-200`}
} absolute flex items-center px-3 bg-primary duration-200 w-full`}
>
<Image
src="/images/loading/loadingblack.gif"

View File

@ -9,7 +9,7 @@ const REGEX = /([$]{.*?})/g;
interface DashboardInputFieldProps {
position: number;
onChangeHandler: (value: string, position: number) => void;
value: string;
value: string | undefined;
type: 'varName' | 'value';
blurred?: boolean;
isDuplicate?: boolean;
@ -47,7 +47,7 @@ const DashboardInputField = ({
};
if (type === 'varName') {
const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != '';
const startsWithNumber = !isNaN(Number(value?.charAt(0))) && value != '';
const error = startsWithNumber || isDuplicate;
return (
@ -141,7 +141,7 @@ const DashboardInputField = ({
{blurred && (
<div className="absolute flex flex-row items-center z-20 peer pr-2 bg-bunker-800 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible h-9 w-full rounded-md text-gray-400/50 text-clip">
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
{value.split('').map(() => (
{value?.split('').map(() => (
<FontAwesomeIcon
key={guidGenerator()}
className="text-xxs mx-0.5"

View File

@ -2,21 +2,12 @@ import { Fragment } from 'react';
import { useTranslation } from "next-i18next";
import { faDownload } from '@fortawesome/free-solid-svg-icons';
import { Menu, Transition } from '@headlessui/react';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import Button from '../basic/buttons/Button';
import downloadDotEnv from '../utilities/secrets/downloadDotEnv';
import downloadYaml from '../utilities/secrets/downloadYaml';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This is the menu that is used to download secrets as .env ad .yml files (in future we may have more options)
* @param {object} obj

View File

@ -1,22 +1,15 @@
import React from 'react';
import { faEllipsis, faShuffle, faX } from '@fortawesome/free-solid-svg-icons';
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import Button from '../basic/buttons/Button';
import DashboardInputField from './DashboardInputField';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
}
interface KeyPairProps {
keyPair: SecretDataProps;
modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string, position: number) => void;
isBlurred: boolean;
isDuplicate: boolean;
toggleSidebar: (id: string) => void;
@ -30,6 +23,7 @@ interface KeyPairProps {
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
* @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.modifyValueOverride - modify the value of a certain environment variable if it is overriden
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
* @param {function} obj.toggleSidebar - open/close/switch sidebar
@ -41,6 +35,7 @@ const KeyPair = ({
keyPair,
modifyKey,
modifyValue,
modifyValueOverride,
isBlurred,
isDuplicate,
toggleSidebar,
@ -50,7 +45,7 @@ const KeyPair = ({
return (
<div className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && "pointer-events-none"} ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}>
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
{keyPair.type == "personal" && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
{keyPair.valueOverride && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
<div className='w-1 h-1 rounded-full bg-primary z-40'></div>
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
This secret is overriden
@ -70,12 +65,12 @@ const KeyPair = ({
<div className="w-full min-w-xl">
<div className={`flex min-w-xl items-center ${!isSnapshot && "pr-1.5"} rounded-lg mt-4 md:mt-0 max-h-10`}>
<DashboardInputField
onChangeHandler={modifyValue}
onChangeHandler={keyPair.valueOverride ? modifyValueOverride : modifyValue}
type="value"
position={keyPair.pos}
value={keyPair.value}
value={keyPair.valueOverride ? keyPair.valueOverride : keyPair.value}
blurred={isBlurred}
override={keyPair.type == "personal"}
override={Boolean(keyPair.valueOverride)}
/>
</div>
</div>

View File

@ -16,18 +16,15 @@ import GenerateSecretMenu from './GenerateSecretMenu';
interface SecretProps {
key: string;
value: string;
valueOverride: string | undefined;
pos: number;
type: string;
id: string;
comment: string;
}
interface OverrideProps {
id: string;
keyName: string;
value: string;
pos: number;
comment: string;
valueOverride: string;
}
export interface DeleteRowFunctionProps {
ids: string[];
@ -39,9 +36,8 @@ interface SideBarProps {
data: SecretProps[];
modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string | undefined, position: number) => void;
modifyComment: (value: string, position: number) => void;
addOverride: (value: OverrideProps) => void;
deleteOverride: (id: string) => void;
buttonReady: boolean;
savePush: () => void;
sharedToHide: string[];
@ -55,12 +51,9 @@ interface SideBarProps {
* @param {SecretProps[]} obj.data - data of a certain key valeu pair
* @param {function} obj.modifyKey - function that modifies the secret key
* @param {function} obj.modifyValue - function that modifies the secret value
* @param {function} obj.addOverride - override a certain secret
* @param {function} obj.deleteOverride - delete the personal override for a certain secret
* @param {function} obj.modifyValueOverride - function that modifies the secret value if it is an override
* @param {boolean} obj.buttonReady - is the button for saving chagnes active
* @param {function} obj.savePush - save changes andp ush secrets
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
* @param {function} obj.deleteRow - a function to delete a certain keyPair
* @returns the sidebar with 'secret's settings'
*/
@ -69,17 +62,14 @@ const SideBar = ({
data,
modifyKey,
modifyValue,
modifyValueOverride,
modifyComment,
addOverride,
deleteOverride,
buttonReady,
savePush,
sharedToHide,
setSharedToHide,
deleteRow
}: SideBarProps) => {
const [isLoading, setIsLoading] = useState(false);
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal"));
const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride != undefined);
const { t } = useTranslation();
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between'>
@ -111,19 +101,19 @@ const SideBar = ({
blurred={false}
/>
</div>
{data.filter(secret => secret.type == "shared")[0]?.value
{data[0]?.value
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.value")}</p>
<DashboardInputField
onChangeHandler={modifyValue}
type="value"
position={data.filter(secret => secret.type == "shared")[0]?.pos}
value={data.filter(secret => secret.type == "shared")[0]?.value}
position={data[0].pos}
value={data[0]?.value}
isDuplicate={false}
blurred={true}
/>
<div className='absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50'>
<GenerateSecretMenu modifyValue={modifyValue} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
<GenerateSecretMenu modifyValue={modifyValue} position={data[0]?.pos} />
</div>
</div>
: <div className='px-4 text-sm text-bunker-300 pt-4'>
@ -131,39 +121,32 @@ const SideBar = ({
{t("dashboard:sidebar.personal-explanation")}
</div>}
<div className='mt-4 px-4'>
{data.filter(secret => secret.type == "shared")[0]?.value &&
{data[0]?.value &&
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.override")}</p>
<Toggle
enabled={overrideEnabled}
setEnabled={setOverrideEnabled}
addOverride={addOverride}
keyName={data[0]?.key}
value={data[0]?.value}
addOverride={modifyValueOverride}
pos={data[0]?.pos}
id={data[0]?.id}
comment={data[0]?.comment}
deleteOverride={deleteOverride}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
/>
</div>}
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
<DashboardInputField
onChangeHandler={modifyValue}
onChangeHandler={modifyValueOverride}
type="value"
position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos}
value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.value : data[0]?.value}
position={data[0]?.pos}
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
isDuplicate={false}
blurred={true}
/>
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'>
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos} />
<GenerateSecretMenu modifyValue={modifyValueOverride} position={data[0]?.pos} />
</div>
</div>
</div>
<SecretVersionList secretId={data[0]?.id} />
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
<CommentField comment={data[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
</div>
)}
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
@ -176,7 +159,7 @@ const SideBar = ({
textDisabled="Saved"
/>
<DeleteActionButton
onSubmit={() => deleteRow({ ids: overrideEnabled ? data.map(secret => secret.id) : [data.filter(secret => secret.type == "shared")[0]?.id], secretName: data[0]?.key })}
onSubmit={() => deleteRow({ ids: data.map(secret => secret.id), secretName: data[0]?.key })}
/>
</div>
</div>

View File

@ -1,11 +1,4 @@
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
import { SecretDataProps } from "public/data/frequentInterfaces";
/**
* This function downloads the secrets as a .env file
@ -16,16 +9,16 @@ interface SecretDataProps {
const checkOverrides = async ({ data }: { data: SecretDataProps[]; }) => {
let secrets : SecretDataProps[] = data!.map((secret) => Object.create(secret));
const overridenSecrets = data!.filter(
(secret) => secret.type === 'personal'
(secret) => (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal'
);
if (overridenSecrets.length) {
overridenSecrets.forEach((secret) => {
const index = secrets!.findIndex(
(_secret) => _secret.key === secret.key && _secret.type === 'shared'
(_secret) => _secret.key === secret.key && (secret.valueOverride == undefined || secret?.value != secret?.valueOverride)
);
secrets![index].value = secret.value;
});
secrets = secrets!.filter((secret) => secret.type === 'shared');
secrets = secrets!.filter((secret) => (secret.valueOverride == undefined || secret?.value != secret?.valueOverride));
}
return secrets;
}

View File

@ -1,16 +1,9 @@
import { SecretDataProps } from "public/data/frequentInterfaces";
import { envMapping } from "../../../public/data/frequentConstants";
import checkOverrides from './checkOverrides';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This function downloads the secrets as a .env file
* @param {object} obj

View File

@ -1,19 +1,12 @@
// import YAML from 'yaml';
// import { YAMLSeq } from 'yaml/types';
import { SecretDataProps } from "public/data/frequentInterfaces";
// import { envMapping } from "../../../public/data/frequentConstants";
// import checkOverrides from './checkOverrides';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This function downloads the secrets as a .yml file
* @param {object} obj

View File

@ -1,3 +1,5 @@
import { SecretDataProps } from "public/data/frequentInterfaces";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
const crypto = require("crypto");
@ -9,15 +11,6 @@ const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
interface EncryptedSecretProps {
id: string;
createdAt: string;
@ -106,7 +99,7 @@ const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsT
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
type: secret.type,
type: (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal',
};
return result;

View File

@ -117,15 +117,19 @@ const getSecretsForProject = async ({
});
}
const result = tempDecryptedSecrets.map((secret, index) => {
const secretKeys = [...new Set(tempDecryptedSecrets.map(secret => secret.key))];
const result = secretKeys.map((key, index) => {
return {
id: secret['id'],
id: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.id,
idOverride: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'personal')[0]?.id,
pos: index,
key: secret['key'],
value: secret['value'],
type: secret['type'],
comment: secret['comment']
};
key: key,
value: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.value,
valueOverride: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'personal')[0]?.value,
comment: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.comment,
}
});
setData(result);

View File

@ -13,6 +13,15 @@ import { decryptAssymmetric, decryptSymmetric } from "~/components/utilities/cry
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
export interface SecretDataProps {
pos: number;
key: string;
value: string;
type: string;
id: string;
environment: string;
}
interface SideBarProps {
toggleSidebar: (value: boolean) => void;
setSnapshotData: (value: any) => void;
@ -43,8 +52,6 @@ interface EncrypetedSecretVersionListProps {
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
* @param {function} obj.setSnapshotData - state manager for snapshot data
* @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected
*
*
* @returns the sidebar with the options for point-in-time recovery (commits)
*/
const PITRecoverySidebar = ({
@ -111,7 +118,21 @@ const PITRecoverySidebar = ({
}
})
setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: decryptedSecretVersions })
const secretKeys = [...new Set(decryptedSecretVersions.map((secret: SecretDataProps) => secret.key))];
const result = secretKeys.map((key, index) => {
return {
id: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].id,
pos: index,
key: key,
environment: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].environment,
value: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0]?.value,
valueOverride: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'personal')[0]?.value,
}
});
setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: result, comment: '' })
}
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
@ -125,31 +146,35 @@ const PITRecoverySidebar = ({
></Image>
</div>
) : (
<div className='h-min overflow-y-auto'>
<div className='h-min'>
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
<p className="font-semibold text-lg text-bunker-200">{t("Point-in-time Recovery")}</p>
<div className='p-1' onClick={() => toggleSidebar(false)}>
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
</div>
</div>
<div className='flex flex-col px-2 py-2'>
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => <div key={snapshot._id} className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "bg-primary text-black" : "bg-mineshaft-700"} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}>
<div className="flex flex-row items-start">
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800" : "text-bunker-200"} text-sm mr-1.5`}>{timeSince(new Date(snapshot.createdAt))}</div>
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-900" : "text-bunker-300"} text-sm `}>{" - " + snapshot.secretVersions.length + " Secrets"}</div>
</div>
<div className='flex flex-col px-2 py-2 overflow-y-auto h-[92vh]'>
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) =>
<div
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800 pointer-events-none" : "text-bunker-200 hover:text-primary duration-200 cursor-pointer"} text-sm`}>
{id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"}
key={snapshot._id}
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "bg-primary text-black pointer-events-none" : "bg-mineshaft-700 hover:bg-mineshaft-500 duration-200 cursor-pointer"} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}
>
<div className="flex flex-row items-start">
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800" : "text-bunker-200"} text-sm mr-1.5`}>{timeSince(new Date(snapshot.createdAt))}</div>
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-900" : "text-bunker-300"} text-sm `}>{" - " + snapshot.secretVersions.length + " Secrets"}</div>
</div>
<div
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800 pointer-events-none" : "text-bunker-200 hover:text-primary duration-200 cursor-pointer"} text-sm`}>
{id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"}
</div>
</div>)}
<div className='flex justify-center w-full mb-14'>
<div className='items-center w-40'>
<Button text="View More" textDisabled="End of History" active={secretSnapshotsMetadata.length % 15 == 0 ? true : false} onButtonPressed={loadMoreSnapshots} size="md" color="mineshaft"/>
</div>
</div>)}
<div className='flex justify-center w-full mb-14'>
<div className='items-center w-40'>
<Button text="View More" textDisabled="End of History" active={secretSnapshotsMetadata.length % 15 == 0 ? true : false} onButtonPressed={loadMoreSnapshots} size="md" color="mineshaft"/>
</div>
</div>
</div>
</div>
)}
</div>

View File

@ -52,7 +52,7 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
});
}
const decryptedSecretVersions = encryptedSecretVersions.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps) => {
const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps) => {
return {
createdAt: encryptedSecretVersion.createdAt,
value: decryptSymmetric({
@ -87,28 +87,33 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
</div>
) : (
<div className='h-48 overflow-y-auto overflow-x-none'>
{secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.map((version: DecryptedSecretVersionListProps, index: number) =>
<div key={index} className='flex flex-row'>
<div className='pr-1 flex flex-col items-center'>
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
<div className='w-0 h-full border-l mt-1'></div>
</div>
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
<div className='pr-2 pt-1'>
{(new Date(version.createdAt)).toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
{secretVersions
? secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.map((version: DecryptedSecretVersionListProps, index: number) =>
<div key={index} className='flex flex-row'>
<div className='pr-1 flex flex-col items-center'>
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
<div className='w-0 h-full border-l mt-1'></div>
</div>
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
<div className='pr-2 pt-1'>
{(new Date(version.createdAt)).toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</div>
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
</div>
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
</div>
</div>
)}
)
: (
<div className='w-full h-full flex items-center justify-center text-bunker-400'>No version history yet.</div>
)
}
</div>
)}
</div>

View File

@ -45,11 +45,12 @@ import getWorkspaces from '../api/workspace/getWorkspaces';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
valueOverride: string | undefined;
id: string;
idOverride: string | undefined;
comment: string;
}
@ -68,10 +69,11 @@ interface SnapshotProps {
secretVersions: {
id: string;
pos: number;
type: "personal" | "shared";
environment: string;
key: string;
value: string;
valueOverride: string;
comment: string;
}[];
}
@ -99,7 +101,7 @@ function findDuplicates(arr: any[]) {
*/
export default function Dashboard() {
const [data, setData] = useState<SecretDataProps[] | null>();
const [initialData, setInitialData] = useState<SecretDataProps[]>([]);
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter();
const [workspaceId, setWorkspaceId] = useState('');
@ -119,6 +121,7 @@ export default function Dashboard() {
const [sharedToHide, setSharedToHide] = useState<string[]>([]);
const [snapshotData, setSnapshotData] = useState<SnapshotProps>();
const [numSnapshots, setNumSnapshots] = useState<number>();
const [saveLoading, setSaveLoading] = useState(false);
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
@ -213,16 +216,6 @@ export default function Dashboard() {
setInitialData(dataToSort);
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)
)
setIsLoading(false);
} catch (error) {
console.log('Error', error);
@ -238,39 +231,16 @@ export default function Dashboard() {
...data!,
{
id: guidGenerator(),
idOverride: guidGenerator(),
pos: data!.length,
key: '',
value: '',
type: 'shared',
valueOverride: undefined,
comment: '',
}
]);
};
/**
* 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 = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
setButtonReady(true);
toggleSidebar("None");
@ -289,15 +259,15 @@ export default function Dashboard() {
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;
// 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))
// toggleSidebar(sharedVersionOfOverride)
// setSharedToHide(sharedToHide!.filter(tempId => tempId != sharedVersionOfOverride))
// resort secrets
const tempData = data!.filter((row: SecretDataProps) => !(row.key == data!.filter(row => row.id == id)[0]?.key && row.type == 'personal'))
sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical")
// const tempData = data!.filter((row: SecretDataProps) => !(row.key == data!.filter(row => row.id == id)[0]?.key && row.type == 'personal'))
// sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical")
};
const modifyValue = (value: string, pos: number) => {
@ -308,6 +278,14 @@ export default function Dashboard() {
setButtonReady(true);
};
const modifyValueOverride = (value: string | undefined, pos: number) => {
setData((oldData) => {
oldData![pos].valueOverride = value;
return [...oldData!];
});
setButtonReady(true);
};
const modifyKey = (value: string, pos: number) => {
setData((oldData) => {
oldData![pos].key = value;
@ -329,6 +307,10 @@ export default function Dashboard() {
modifyValue(value, pos);
}, []);
const listenChangeValueOverride = useCallback((value: string | undefined, pos: number) => {
modifyValueOverride(value, pos);
}, []);
const listenChangeKey = useCallback((value: string, pos: number) => {
modifyKey(value, pos);
}, []);
@ -341,6 +323,7 @@ export default function Dashboard() {
* Save the changes of environment variables and push them to the database
*/
const savePush = async (dataToPush?: SecretDataProps[]) => {
setSaveLoading(true);
let newData: SecretDataProps[] | null | undefined;
// dataToPush is mostly used for rollbacks, otherwise we always take the current state data
if ((dataToPush ?? [])?.length > 0) {
@ -349,16 +332,11 @@ export default function Dashboard() {
newData = data;
}
const obj = Object.assign(
{},
...newData!.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))))
const nameErrors = !newData!
.map((secret) => !isNaN(Number(secret.key.charAt(0))))
.every((v) => v === false);
const duplicatesExist = findDuplicates(data!.map((item: SecretDataProps) => item.key + item.type)).length > 0;
const duplicatesExist = findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0;
if (nameErrors) {
return createNotification({
@ -378,34 +356,64 @@ export default function Dashboard() {
setButtonReady(false);
const secretsToBeDeleted
= initialData
= initialData!
.filter(initDataPoint => !newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id))
.map(secret => secret.id);
console.log('delete', secretsToBeDeleted.length)
const secretsToBeAdded
= newData!
.filter(newDataPoint => !initialData.map(initDataPoint => initDataPoint.id).includes(newDataPoint.id));
.filter(newDataPoint => !initialData!.map(initDataPoint => initDataPoint.id).includes(newDataPoint.id));
console.log('add', secretsToBeAdded.length)
const secretsToBeUpdated
= newData!.filter(newDataPoint => initialData
= newData!.filter(newDataPoint => initialData!
.filter(initDataPoint => newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id)
&& (newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].value != initDataPoint.value
|| newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].key != initDataPoint.key
|| newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].comment != initDataPoint.comment))
.map(secret => secret.id).includes(newDataPoint.id));
console.log('update', secretsToBeUpdated.length)
const newOverrides = newData!.filter(newDataPoint => newDataPoint.valueOverride != undefined)
const initOverrides = initialData!.filter(initDataPoint => initDataPoint.valueOverride != undefined)
const overridesToBeDeleted
= initOverrides
.filter(initDataPoint => !newOverrides!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id))
.map(secret => String(secret.idOverride));
console.log('override delete', overridesToBeDeleted.length)
const overridesToBeAdded
= newOverrides!
.filter(newDataPoint => !initOverrides.map(initDataPoint => initDataPoint.id).includes(newDataPoint.id))
.map(override => ({pos: override.pos, key: override.key, value: String(override.valueOverride), valueOverride: override.valueOverride, comment: '', id: String(override.idOverride), idOverride: String(override.idOverride)}));
console.log('override add', overridesToBeAdded.length)
const overridesToBeUpdated
= newOverrides!.filter(newDataPoint => initOverrides
.filter(initDataPoint => newOverrides!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id)
&& (newOverrides!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].valueOverride != initDataPoint.valueOverride
|| newOverrides!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].key != initDataPoint.key
|| newOverrides!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].comment != initDataPoint.comment))
.map(secret => secret.id).includes(newDataPoint.id))
.map(override => ({pos: override.pos, key: override.key, value: String(override.valueOverride), valueOverride: override.valueOverride, comment: '', id: String(override.idOverride), idOverride: String(override.idOverride)}));
console.log('override update', overridesToBeUpdated.length)
if (secretsToBeDeleted.length > 0) {
await deleteSecrets({ secretIds: secretsToBeDeleted });
if (secretsToBeDeleted.concat(overridesToBeDeleted).length > 0) {
await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) });
}
if (secretsToBeAdded.length > 0) {
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId, env: envMapping[env] })
if (secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded), workspaceId, env: envMapping[env] });
secrets && await addSecrets({ secrets, env: envMapping[env], workspaceId });
}
if (secretsToBeUpdated.length > 0) {
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated, workspaceId, env: envMapping[env] })
if (secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated), workspaceId, env: envMapping[env] });
secrets && await updateSecrets({ secrets });
}
setInitialData(newData);
// If this user has never saved environment variables before, show them a prompt to read docs
if (!hasUserEverPushed) {
setCheckDocsPopUpVisible(true);
@ -414,6 +422,7 @@ export default function Dashboard() {
// increasing the number of project commits
setNumSnapshots((numSnapshots ?? 0) + 1);
setSaveLoading(false);
};
const addData = (newData: SecretDataProps[]) => {
@ -462,9 +471,8 @@ export default function Dashboard() {
data={data.filter((row: SecretDataProps) => row.key == data.filter(row => row.id == sidebarSecretId)[0]?.key)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
addOverride={addOverride}
deleteOverride={deleteOverride}
buttonReady={buttonReady}
savePush={savePush}
sharedToHide={sharedToHide}
@ -533,6 +541,7 @@ export default function Dashboard() {
active={buttonReady}
iconDisabled={faCheck}
textDisabled={String(t("common:saved"))}
loading={saveLoading}
/>
</div>
)}
@ -545,21 +554,11 @@ export default function Dashboard() {
.filter(row => reverseEnvMapping[row.environment] == env)
.map((sv, position) => {
return {
id: sv.id, pos: position, type: sv.type, key: sv.key, value: sv.value, comment: ''
id: sv.id, idOverride: sv.id, pos: position, valueOverride: sv.valueOverride, key: sv.key, value: sv.value, comment: ''
}
});
setData(rolledBackSecrets);
setSharedToHide(
rolledBackSecrets?.filter(row => (rolledBackSecrets
?.map((item) => item.key)
.filter(
(item, index) =>
index !==
rolledBackSecrets?.map((item) => item.key).indexOf(item)
).includes(row.key) && row.type == 'shared'))?.map((item) => item.id)
)
// Perform the rollback globally
performSecretRollback({ workspaceId, version: snapshotData.version })
@ -663,16 +662,17 @@ export default function Dashboard() {
>
<div className="px-1 pt-2 bg-mineshaft-800 rounded-md p-2">
{!snapshotData && data?.filter(row => row.key?.toUpperCase().includes(searchKeys.toUpperCase()))
.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => (
.filter(row => !sharedToHide.includes(row.id)).map((keyPair) => (
<KeyPair
key={keyPair.id}
keyPair={keyPair}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
isBlurred={blurred}
isDuplicate={findDuplicates(
data?.map((item) => item.key + item.type)
)?.includes(keyPair.key + keyPair.type)}
data?.map((item) => item.key)
)?.includes(keyPair.key)}
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={false}
@ -681,22 +681,26 @@ export default function Dashboard() {
{snapshotData && snapshotData.secretVersions?.sort((a, b) => a.key.localeCompare(b.key))
.filter(row => reverseEnvMapping[row.environment] == snapshotEnv)
.filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
.filter(row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions
.filter(
row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions
?.map((item) => item.key)
.filter(
(item, index) =>
index !==
snapshotData.secretVersions?.map((item) => item.key).indexOf(item)
).includes(row.key) && row.type == 'shared'))?.map((item) => item.id).includes(row.id) && row.type == 'shared')).map((keyPair) => (
).includes(row.key)))?.map((item) => item.id).includes(row.id))
)
.map((keyPair) => (
<KeyPair
key={keyPair.id}
keyPair={keyPair}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
isBlurred={blurred}
isDuplicate={findDuplicates(
data?.map((item) => item.key + item.type)
)?.includes(keyPair.key + keyPair.type)}
data?.map((item) => item.key)
)?.includes(keyPair.key)}
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={true}

View File

@ -0,0 +1,8 @@
export interface SecretDataProps {
pos: number;
key: string;
value: string;
valueOverride: string | undefined;
id: string;
comment: string;
}