Merge pull request #312 from Infisical/secret-tagging

Added tags to secrets in the dashboard
This commit is contained in:
mv-turtle
2023-02-07 16:57:01 -08:00
committed by GitHub
32 changed files with 709 additions and 109 deletions
@@ -103,6 +103,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[] tags: string[]
} }
@@ -115,6 +118,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags tags
}: secretsToCreateType) => { }: secretsToCreateType) => {
return ({ return ({
@@ -129,6 +135,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags tags
}); });
}) })
@@ -160,6 +169,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash, secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags tags
}) => ({ }) => ({
_id: new Types.ObjectId(), _id: new Types.ObjectId(),
@@ -178,6 +190,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash, secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags tags
})) }))
}); });
+2 -2
View File
@@ -52,9 +52,9 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => {
UnauthorizedRequestError({ message: 'Failed to validate membership' }); UnauthorizedRequestError({ message: 'Failed to validate membership' });
} }
await Tag.findByIdAndDelete(tagId) const result = await Tag.findByIdAndDelete(tagId);
res.sendStatus(200) res.json(result);
} }
export const getWorkspaceTags = async (req: Request, res: Response) => { export const getWorkspaceTags = async (req: Request, res: Response) => {
@@ -15,7 +15,13 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
secretSnapshot = await SecretSnapshot secretSnapshot = await SecretSnapshot
.findById(secretSnapshotId) .findById(secretSnapshotId)
.populate('secretVersions'); .populate({
path: 'secretVersions',
populate: {
path: 'tags',
model: 'Tag',
}
});
if (!secretSnapshot) throw new Error('Failed to find secret snapshot'); if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
@@ -1,3 +1,12 @@
export interface Tag {
_id: string;
name: string;
slug: string;
user: string;
workspace: string;
createdAt: string;
}
export interface SecretDataProps { export interface SecretDataProps {
pos: number; pos: number;
key: string; key: string;
@@ -5,4 +14,5 @@ export interface SecretDataProps {
valueOverride: string | undefined; valueOverride: string | undefined;
id: string; id: string;
comment: string; comment: string;
tags: Tag[];
} }
@@ -36,7 +36,7 @@ const AddWorkspaceDialog = ({
return ( return (
<div> <div>
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={closeModal}> <Dialog as="div" className="relative z-[100]" onClose={closeModal}>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
@@ -49,7 +49,7 @@ const AddWorkspaceDialog = ({
<div className="fixed inset-0 bg-black bg-opacity-70" /> <div className="fixed inset-0 bg-black bg-opacity-70" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 overflow-y-auto z-50"> <div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center"> <div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
@@ -60,7 +60,7 @@ const AddWorkspaceDialog = ({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400" className="text-lg font-medium leading-6 text-gray-400"
@@ -69,7 +69,7 @@ const AddWorkspaceDialog = ({
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
This project will contain your environmental variables. This project will contain your secrets and configs.
</p> </p>
</div> </div>
<div className="max-h-28 mt-4"> <div className="max-h-28 mt-4">
@@ -83,7 +83,7 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
</div> </div>
<div className="w-48"> <div className="w-48">
<Button <Button
text="Add New Env" text="Add New Environment"
onButtonPressed={() => { onButtonPressed={() => {
if (plan !== plans.starter || host !== 'https://app.infisical.com') { if (plan !== plans.starter || host !== 'https://app.infisical.com') {
handlePopUpOpen('createUpdateEnv'); handlePopUpOpen('createUpdateEnv');
@@ -0,0 +1,61 @@
import { Fragment } from 'react';
import { useRouter } from 'next/router';
import { faCheckSquare, faPlus, faSquare } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react';
import { Tag } from 'public/data/frequentInterfaces';
/**
* 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
* @param {SecretDataProps[]} obj.data -
*
*/
const AddTagsMenu = ({ allTags, currentTags, modifyTags, position }: { allTags: Tag[]; currentTags: Tag[]; modifyTags: (value: Tag[], position: number) => void; position: number; }) => {
const router = useRouter();
return (
<Menu as="div" className="ml-2 relative inline-block text-left">
<Menu.Button
as="div"
className="flex justify-center items-center font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
>
<div className='bg-mineshaft/30 cursor-pointer rounded-sm text-sm text-mineshaft-200/50 hover:bg-mineshaft/70 duration-200 flex items-center'>
<FontAwesomeIcon icon={faPlus} className="p-[0.28rem]"/>
{currentTags.length > 2 && <span className='pr-2'>{currentTags.length - 2}</span>}
</div>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute z-[90] text-sm drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-mineshaft-600 border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-1 space-y-1">
{allTags?.map((tag) => { return (
<Menu.Item key={tag._id}>
<button
type="button"
className={`${currentTags.map(currentTag => currentTag.name).includes(tag.name) ? "opacity-30 cursor-default" : "hover:bg-mineshaft-700"} w-full text-left bg-mineshaft-800 px-2 py-0.5 text-bunker-200 rounded-sm flex items-center`}
onClick={() => {if (!currentTags.map(currentTag => currentTag.name).includes(tag.name)) {modifyTags(currentTags.concat([tag]), position)}}}
>
{currentTags.map(currentTag => currentTag.name).includes(tag.name) ? <FontAwesomeIcon icon={faCheckSquare} className="text-xs mr-2 text-primary"/> : <FontAwesomeIcon icon={faSquare} className="text-xs mr-2"/>} {tag.name}
</button>
</Menu.Item>
)})}
<button
type="button"
className='w-full text-left bg-mineshaft-800 hover:bg-primary hover:text-black duration-200 px-2 py-0.5 text-bunker-200 rounded-sm'
onClick={() => router.push(`/settings/project/${String(router.query.id)}`)}
>
<FontAwesomeIcon icon={faPlus} className="mr-2 text-xs" />Add more tags
</button>
</Menu.Items>
</Transition>
</Menu>
);
};
export default AddTagsMenu;
@@ -3,7 +3,6 @@ import { faCircle, faExclamationCircle, faEye, faLayerGroup } from '@fortawesome
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import guidGenerator from '../utilities/randomId'; import guidGenerator from '../utilities/randomId';
import { Button } from '../v2';
import { HoverObject } from '../v2/HoverCard'; import { HoverObject } from '../v2/HoverCard';
const REGEX = /([$]{.*?})/g; const REGEX = /([$]{.*?})/g;
@@ -99,8 +98,8 @@ const DashboardInputField = ({
)} )}
{!error && <div className={`absolute right-0 top-0 text-red z-50 ${ {!error && <div className={`absolute right-0 top-0 text-red z-50 ${
overrideEnabled ? 'visible group-hover:bg-mineshaft-700' : 'invisible group-hover:visible bg-mineshaft-700' overrideEnabled ? 'visible group-hover:bg-mineshaft-700' : 'invisible group-hover:visible bg-mineshaft-700'
} cursor-pointer duration-0`}> } cursor-pointer duration-0 h-10 flex items-center px-2`}>
<Button variant="plain" onClick={() => { <button type="button" onClick={() => {
if (modifyValueOverride) { if (modifyValueOverride) {
if (overrideEnabled === false) { if (overrideEnabled === false) {
modifyValueOverride('', position); modifyValueOverride('', position);
@@ -114,7 +113,7 @@ const DashboardInputField = ({
icon={faLayerGroup} icon={faLayerGroup}
color={overrideEnabled ? 'primary' : 'bunker-400'} color={overrideEnabled ? 'primary' : 'bunker-400'}
/> />
</Button> </button>
</div>} </div>}
</div> </div>
); );
@@ -19,7 +19,7 @@ export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
<div className={`${ <div className={`${
!isPlain !isPlain
? 'bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2' ? 'bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2'
: 'cursor-pointer w-[2.35rem] h-[2.35rem] flex items-center justfy-center'}`}> : 'cursor-pointer w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'}`}>
{isPlain {isPlain
? <div ? <div
onKeyDown={() => null} onKeyDown={() => null}
@@ -64,7 +64,8 @@ const DropZone = ({
key, key,
value: keyPairs[key as keyof typeof keyPairs].value, value: keyPairs[key as keyof typeof keyPairs].value,
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'), comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
type: 'shared' type: 'shared',
tags: []
})); }));
break; break;
} }
@@ -86,7 +87,8 @@ const DropZone = ({
key, key,
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '', value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '',
comment, comment,
type: 'shared' type: 'shared',
tags: []
}; };
}); });
break; break;
+70 -21
View File
@@ -1,7 +1,8 @@
import { faEllipsis } from '@fortawesome/free-solid-svg-icons'; import { faEllipsis, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SecretDataProps } from 'public/data/frequentInterfaces'; import { SecretDataProps, Tag } from 'public/data/frequentInterfaces';
import AddTagsMenu from './AddTagsMenu';
import DashboardInputField from './DashboardInputField'; import DashboardInputField from './DashboardInputField';
import { DeleteActionButton } from './DeleteActionButton'; import { DeleteActionButton } from './DeleteActionButton';
@@ -11,12 +12,15 @@ interface KeyPairProps {
modifyValue: (value: string, position: number) => void; modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string | undefined, position: number) => void; modifyValueOverride: (value: string | undefined, position: number) => void;
modifyComment: (value: string, position: number) => void; modifyComment: (value: string, position: number) => void;
modifyTags: (value: Tag[], position: number) => void;
isBlurred: boolean; isBlurred: boolean;
isDuplicate: boolean; isDuplicate: boolean;
toggleSidebar: (id: string) => void; toggleSidebar: (id: string) => void;
sidebarSecretId: string; sidebarSecretId: string;
isSnapshot: boolean; isSnapshot: boolean;
deleteRow?: (props: DeleteRowFunctionProps) => void; deleteRow?: (props: DeleteRowFunctionProps) => void;
tags: Tag[];
togglePITSidebar?: (value: boolean) => void;
} }
export interface DeleteRowFunctionProps { export interface DeleteRowFunctionProps {
@@ -24,6 +28,23 @@ export interface DeleteRowFunctionProps {
secretName: string; secretName: string;
} }
const colors = [
'bg-[#f1c40f]/40',
'bg-[#cb1c8d]/40',
'bg-[#badc58]/40',
'bg-[#ff5400]/40',
'bg-[#00bbf9]/40'
]
const colorsText = [
'text-[#fcf0c3]/70',
'text-[#f2c6e3]/70',
'text-[#eef6d5]/70',
'text-[#ffddcc]/70',
'text-[#f0fffd]/70'
]
/** /**
* This component represent a single row for an environemnt variable on the dashboard * This component represent a single row for an environemnt variable on the dashboard
* @param {object} obj * @param {object} obj
@@ -32,12 +53,15 @@ export interface DeleteRowFunctionProps {
* @param {function} obj.modifyValue - modify the value 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 {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden
* @param {function} obj.modifyComment - modify the comment of a certain environment variable * @param {function} obj.modifyComment - modify the comment of a certain environment variable
* @param {function} obj.modifyTags - modify the tags of a certain environment variable
* @param {boolean} obj.isBlurred - if the blurring setting is turned on * @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 {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
* @param {function} obj.toggleSidebar - open/close/switch sidebar * @param {function} obj.toggleSidebar - open/close/switch sidebar
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed * @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar * @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
* @param {function} obj.deleteRow - a function to delete a certain keyPair * @param {function} obj.deleteRow - a function to delete a certain keyPair
* @param {function} obj.togglePITSidebar - open or close the Point-in-time recovery sidebar
* @param {Tag[]} obj.tags - tags for a certain secret
* @returns * @returns
*/ */
const KeyPair = ({ const KeyPair = ({
@@ -46,21 +70,31 @@ const KeyPair = ({
modifyValue, modifyValue,
modifyValueOverride, modifyValueOverride,
modifyComment, modifyComment,
modifyTags,
isBlurred, isBlurred,
isDuplicate, isDuplicate,
toggleSidebar, toggleSidebar,
sidebarSecretId, sidebarSecretId,
isSnapshot, isSnapshot,
deleteRow deleteRow,
}: KeyPairProps) => ( togglePITSidebar,
tags
}: KeyPairProps) => {
const tagData = (tags.map((tag, index) => {return {
...tag,
color: colors[index%colors.length],
colorText: colorsText[index%colorsText.length]
}}));
return (
<div <div
className={`group flex flex-col items-center border-b border-mineshaft-500 hover:bg-white/[0.03] duration-100 ${isSnapshot && 'pointer-events-none'} ${ className={`group flex flex-col items-center border-b border-mineshaft-500 hover:bg-white/[0.03] duration-100 ${isSnapshot && 'pointer-events-none'} ${
keyPair.id === sidebarSecretId && 'bg-mineshaft-700 duration-200' keyPair.id === sidebarSecretId && 'bg-mineshaft-700 duration-200'
}`} }`}
> >
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center"> <div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
<div className='text-bunker-400 text-xs flex items-center justify-center w-14 h-10 cursor-default'>{keyPair.pos + 1}</div> <div className="w-2/12 border-r border-mineshaft-600 flex flex-row items-center">
<div className="w-80 border-r border-mineshaft-600"> <div className='text-bunker-400 text-xs flex items-center justify-center w-14 h-10 cursor-default'>{keyPair.pos + 1}</div>
<div className="flex items-center max-h-16"> <div className="flex items-center max-h-16">
<DashboardInputField <DashboardInputField
onChangeHandler={modifyKey} onChangeHandler={modifyKey}
@@ -74,7 +108,7 @@ const KeyPair = ({
/> />
</div> </div>
</div> </div>
<div className="w-full border-r border-mineshaft-600"> <div className="w-5/12 border-r border-mineshaft-600">
<div <div
className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10' className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10'
> >
@@ -89,7 +123,7 @@ const KeyPair = ({
/> />
</div> </div>
</div> </div>
<div className="w-96 border-r border-mineshaft-600"> <div className="w-2/12 border-r border-mineshaft-600">
<div className="flex items-center max-h-16"> <div className="flex items-center max-h-16">
<DashboardInputField <DashboardInputField
onChangeHandler={modifyComment} onChangeHandler={modifyComment}
@@ -101,27 +135,42 @@ const KeyPair = ({
/> />
</div> </div>
</div> </div>
{!isSnapshot && ( <div className="w-2/12 h-10 flex items-center overflow-visible overflow-r-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
<div <div className="flex items-center max-h-16">
onKeyDown={() => null} {keyPair.tags.map((tag, index) => (
role="button" index < 2 && <div key={keyPair.pos} className={`ml-2 px-1.5 ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.color} rounded-sm text-sm ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.colorText} flex items-center`}>
tabIndex={0} <span className='mb-0.5 cursor-default'>{tag.name}</span>
onClick={() => toggleSidebar(keyPair.id)} <FontAwesomeIcon icon={faXmark} className="ml-1 cursor-pointer p-1" onClick={() => modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.pos)}/>
className="cursor-pointer w-[2.35rem] h-[2.35rem] px-6 rounded-md invisible group-hover:visible flex flex-row justify-center items-center" </div>
> ))}
<FontAwesomeIcon className="text-bunker-300 hover:text-primary text-lg" icon={faEllipsis} />
<AddTagsMenu allTags={tags} currentTags={keyPair.tags} modifyTags={modifyTags} position={keyPair.pos} />
</div> </div>
)} </div>
{!isSnapshot && ( <div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => {
if (togglePITSidebar) {
togglePITSidebar(false);
}
toggleSidebar(keyPair.id)
}}
className={`cursor-pointer w-[1.5rem] h-[2.35rem] ml-auto group-hover:bg-mineshaft-700 z-50 rounded-md invisible group-hover:visible flex flex-row justify-center items-center ${isSnapshot ?? 'invisible'}`}
>
<FontAwesomeIcon className="text-bunker-300 hover:text-primary text-lg" icon={faEllipsis} />
</div>
<div className={`group-hover:bg-mineshaft-700 z-50 ${isSnapshot ?? 'invisible'}`}>
<DeleteActionButton <DeleteActionButton
onSubmit={() => { if (deleteRow) { onSubmit={() => { if (deleteRow) {
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key }) deleteRow({ ids: [keyPair.id], secretName: keyPair?.key })
}}} }}}
isPlain isPlain
/> />
)} </div>
</div> </div>
</div> </div>
); )};
export default KeyPair; export default KeyPair;
@@ -80,7 +80,7 @@ const SideBar = ({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-96 right-0 z-[70] shadow-xl flex flex-col justify-between"> <div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-[28rem] sticky top-0 right-0 z-[70] shadow-xl flex flex-col justify-between">
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<Image <Image
@@ -138,7 +138,8 @@ const attemptLogin = async (
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net', value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
valueOverride: undefined, valueOverride: undefined,
comment: 'This is an example of secret referencing.', comment: 'This is an example of secret referencing.',
id: '' id: '',
tags: []
}, },
{ {
pos: 1, pos: 1,
@@ -147,7 +148,8 @@ const attemptLogin = async (
valueOverride: undefined, valueOverride: undefined,
comment: comment:
'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need', 'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need',
id: '' id: '',
tags: []
}, },
{ {
pos: 2, pos: 2,
@@ -156,7 +158,8 @@ const attemptLogin = async (
valueOverride: undefined, valueOverride: undefined,
comment: comment:
'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need', 'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need',
id: '' id: '',
tags: []
}, },
{ {
pos: 3, pos: 3,
@@ -164,7 +167,8 @@ const attemptLogin = async (
value: 'user1234', value: 'user1234',
valueOverride: 'user1234', valueOverride: 'user1234',
comment: '', comment: '',
id: '' id: '',
tags: []
}, },
{ {
pos: 4, pos: 4,
@@ -172,7 +176,8 @@ const attemptLogin = async (
value: 'example_password', value: 'example_password',
valueOverride: 'example_password', valueOverride: 'example_password',
comment: '', comment: '',
id: '' id: '',
tags: []
}, },
{ {
pos: 5, pos: 5,
@@ -180,7 +185,8 @@ const attemptLogin = async (
value: 'example_twillio_token', value: 'example_twillio_token',
valueOverride: undefined, valueOverride: undefined,
comment: '', comment: '',
id: '' id: '',
tags: []
}, },
{ {
pos: 6, pos: 6,
@@ -188,7 +194,8 @@ const attemptLogin = async (
value: 'http://localhost:3000', value: 'http://localhost:3000',
valueOverride: undefined, valueOverride: undefined,
comment: '', comment: '',
id: '' id: '',
tags: []
} }
]; ];
const secrets = await encryptSecrets({ const secrets = await encryptSecrets({
@@ -1,6 +1,6 @@
import crypto from 'crypto'; import crypto from 'crypto';
import { SecretDataProps } from 'public/data/frequentInterfaces'; import { SecretDataProps, Tag } from 'public/data/frequentInterfaces';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey'; import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
@@ -20,6 +20,7 @@ interface EncryptedSecretProps {
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
type: 'personal' | 'shared'; type: 'personal' | 'shared';
tags: Tag[];
} }
/** /**
@@ -105,7 +106,8 @@ const encryptSecrets = async ({
type: type:
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
? 'shared' ? 'shared'
: 'personal' : 'personal',
tags: secret.tags
}; };
return result; return result;
@@ -1,3 +1,5 @@
import { Tag } from 'public/data/frequentInterfaces';
import getSecrets from '@app/pages/api/files/GetSecrets'; import getSecrets from '@app/pages/api/files/GetSecrets';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey'; import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
@@ -17,6 +19,7 @@ interface EncryptedSecretProps {
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
type: 'personal' | 'shared'; type: 'personal' | 'shared';
tags: Tag[];
} }
interface SecretProps { interface SecretProps {
@@ -25,6 +28,7 @@ interface SecretProps {
type: 'personal' | 'shared'; type: 'personal' | 'shared';
comment: string; comment: string;
id: string; id: string;
tags: Tag[];
} }
interface FunctionProps { interface FunctionProps {
@@ -107,7 +111,8 @@ const getSecretsForProject = async ({
key: plainTextKey, key: plainTextKey,
value: plainTextValue, value: plainTextValue,
type: secret.type, type: secret.type,
comment: plainTextComment comment: plainTextComment,
tags: secret.tags
}); });
}); });
} }
@@ -130,7 +135,10 @@ const getSecretsForProject = async ({
)[0]?.value, )[0]?.value,
comment: tempDecryptedSecrets.filter( comment: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared' (secret) => secret.key === key && secret.type === 'shared'
)[0]?.comment )[0]?.comment,
tags: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.tags
})); }));
if (typeof setData === 'function') { if (typeof setData === 'function') {
@@ -52,7 +52,7 @@ export const DeleteActionModal = ({
> >
<ModalContent <ModalContent
title={title} title={title}
subTitle="This action is irreversible!!!" subTitle="This action is irreversible!"
footerContent={ footerContent={
<div className="flex items-center"> <div className="flex items-center">
<Button <Button
+1 -1
View File
@@ -41,7 +41,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
className={twMerge( className={twMerge(
'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md', 'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
dropdownContainerClassName dropdownContainerClassName
)} )}
> >
@@ -5,6 +5,7 @@ import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { faXmark } from '@fortawesome/free-solid-svg-icons'; import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tag } from 'public/data/frequentInterfaces';
import Button from '@app/components/basic/buttons/Button'; import Button from '@app/components/basic/buttons/Button';
import { import {
@@ -48,6 +49,7 @@ interface EncrypetedSecretVersionListProps {
secretKeyTag: string; secretKeyTag: string;
environment: string; environment: string;
type: 'personal' | 'shared'; type: 'personal' | 'shared';
tags: Tag[];
} }
/** /**
@@ -106,6 +108,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
pos, pos,
type: encryptedSecretVersion.type, type: encryptedSecretVersion.type,
environment: encryptedSecretVersion.environment, environment: encryptedSecretVersion.environment,
tags: encryptedSecretVersion.tags,
key: decryptSymmetric({ key: decryptSymmetric({
ciphertext: encryptedSecretVersion.secretKeyCiphertext, ciphertext: encryptedSecretVersion.secretKeyCiphertext,
iv: encryptedSecretVersion.secretKeyIV, iv: encryptedSecretVersion.secretKeyIV,
@@ -134,6 +137,9 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
environment: decryptedSecretVersions.filter( environment: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared' (secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0].environment, )[0].environment,
tags: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0].tags,
value: decryptedSecretVersions.filter( value: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared' (secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0]?.value, )[0]?.value,
@@ -155,7 +161,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
<div <div
className={`absolute border-l border-mineshaft-500 ${ className={`absolute border-l border-mineshaft-500 ${
isLoading ? 'bg-bunker-800' : 'bg-bunker' isLoading ? 'bg-bunker-800' : 'bg-bunker'
} fixed h-full w-96 right-0 z-[70] shadow-xl flex flex-col justify-between`} } fixed h-full w-[28rem] right-0 z-[70] shadow-xl flex flex-col justify-between sticky top-0`}
> >
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-full mb-8"> <div className="flex items-center justify-center h-full mb-8">
+1
View File
@@ -2,4 +2,5 @@ export * from './auth';
export * from './keys'; export * from './keys';
export * from './serviceTokens'; export * from './serviceTokens';
export * from './subscriptions'; export * from './subscriptions';
export * from './tags';
export * from './workspace'; export * from './workspace';
+3
View File
@@ -0,0 +1,3 @@
export { useGetWsTags } from './queries';
export { useCreateWsTag } from './queries';
export { useDeleteWsTag } from './queries';
+62
View File
@@ -0,0 +1,62 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import {
CreateTagDTO,
CreateTagRes,
DeleteTagDTO,
DeleteWsTagRes,
UserWsTags
} from './types';
const workspaceTags = {
getWsTags: (workspaceID: string) => ['workspace-tags', { workspaceID }] as const
};
const fetchWsTag = async (workspaceID: string) => {
const { data } = await apiRequest.get<{ workspaceTags: UserWsTags }>(
`/api/v2/workspace/${workspaceID}/tags`
);
return data.workspaceTags;
};
export const useGetWsTags = (workspaceID: string) =>
useQuery({
queryKey: workspaceTags.getWsTags(workspaceID),
queryFn: () => fetchWsTag(workspaceID),
enabled: Boolean(workspaceID)
});
export const useCreateWsTag = () => {
const queryClient = useQueryClient();
return useMutation<CreateTagRes, {}, CreateTagDTO>({
mutationFn: async ({ workspaceID, tagName, tagSlug }) => {
const { data } = await apiRequest.post(`/api/v2/workspace/${workspaceID}/tags`, {
name: tagName,
slug: tagSlug
})
return data;
},
onSuccess: (tagData) => {
queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace));
}
});
};
export const useDeleteWsTag = () => {
const queryClient = useQueryClient();
return useMutation<DeleteWsTagRes, {}, DeleteTagDTO>({
mutationFn: async ({ tagID }) => {
const { data } = await apiRequest.delete(`/api/v2/workspace/tags/${tagID}`);
return data
},
onSuccess: (tagData) => {
queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace));
}
});
};
+39
View File
@@ -0,0 +1,39 @@
export type UserWsTags = WsTag[];
export type WsTag = {
_id: string;
name: string;
slug: string;
workspace: string;
createdAt: string;
updatedAt: string;
__v: number;
}
export type WorkspaceTag = { _id: string; name: string; slug: string };
export type CreateTagDTO = {
workspaceID: string;
tagSlug: string;
tagName: string;
};
export type CreateTagRes = {
name: string;
slug: string;
workspace: string;
createdAt: string;
user: string;
_id: string;
};
export type DeleteTagDTO = { tagID: string; };
export type DeleteWsTagRes = {
name: string;
slug: string;
workspace: string;
createdAt: string;
user: string;
_id: string;
};
+2 -1
View File
@@ -9,5 +9,6 @@ export type {
RenameWorkspaceDTO, RenameWorkspaceDTO,
UpdateEnvironmentDTO, UpdateEnvironmentDTO,
Workspace, Workspace,
WorkspaceEnv WorkspaceEnv,
WorkspaceTag
} from './workspace/types'; } from './workspace/types';
+1 -2
View File
@@ -5,5 +5,4 @@ export {
useGetUserWorkspaces, useGetUserWorkspaces,
useGetWorkspaceById, useGetWorkspaceById,
useRenameWorkspace, useRenameWorkspace,
useUpdateWsEnvironment useUpdateWsEnvironment} from './queries';
} from './queries';
@@ -7,6 +7,7 @@ export type Workspace = {
}; };
export type WorkspaceEnv = { name: string; slug: string }; export type WorkspaceEnv = { name: string; slug: string };
export type WorkspaceTag = { _id: string; name: string; slug: string };
// mutation dto // mutation dto
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string }; export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
@@ -0,0 +1,22 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
/**
* This route lets us get the tags for a certain project
* @param {string} workspaceId
* @returns
*/
const getWorkspaceTags = ({ workspaceId }: { workspaceId: string }) =>
SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/tags`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(async (res) => {
if (res?.status === 200) {
return (await res.json()).workspaceTags;
}
console.log('Failed to get the tags available in a certain project');
return undefined;
});
export default getWorkspaceTags;
+118 -53
View File
@@ -14,9 +14,11 @@ import {
faEyeSlash, faEyeSlash,
faFolderOpen, faFolderOpen,
faMagnifyingGlass, faMagnifyingGlass,
faPlus faPlus,
faXmark
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tag } from 'public/data/frequentInterfaces';
import Button from '@app/components/basic/buttons/Button'; import Button from '@app/components/basic/buttons/Button';
import ListBox from '@app/components/basic/Listbox'; import ListBox from '@app/components/basic/Listbox';
@@ -44,6 +46,7 @@ import checkUserAction from '../api/userActions/checkUserAction';
import registerUserAction from '../api/userActions/registerUserAction'; import registerUserAction from '../api/userActions/registerUserAction';
import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments'; import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments';
import getWorkspaces from '../api/workspace/getWorkspaces'; import getWorkspaces from '../api/workspace/getWorkspaces';
import getWorkspaceTags from '../api/workspace/getWorkspaceTags';
type WorkspaceEnv = { type WorkspaceEnv = {
name: string; name: string;
@@ -59,6 +62,7 @@ interface SecretDataProps {
id: string; id: string;
idOverride: string | undefined; idOverride: string | undefined;
comment: string; comment: string;
tags: Tag[];
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -82,6 +86,7 @@ interface SnapshotProps {
value: string; value: string;
valueOverride: string; valueOverride: string;
comment: string; comment: string;
tags: Tag[];
}[]; }[];
} }
@@ -126,6 +131,7 @@ export default function Dashboard() {
const [snapshotData, setSnapshotData] = useState<SnapshotProps>(); const [snapshotData, setSnapshotData] = useState<SnapshotProps>();
const [numSnapshots, setNumSnapshots] = useState<number>(); const [numSnapshots, setNumSnapshots] = useState<number>();
const [saveLoading, setSaveLoading] = useState(false); const [saveLoading, setSaveLoading] = useState(false);
const [projectTags, setProjectTags] = useState<Tag[]>([]);
const { t } = useTranslation(); const { t } = useTranslation();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
@@ -233,6 +239,8 @@ export default function Dashboard() {
action: 'first_time_secrets_pushed' action: 'first_time_secrets_pushed'
}); });
setHasUserEverPushed(!!userAction); setHasUserEverPushed(!!userAction);
setProjectTags(await getWorkspaceTags({ workspaceId }));
} catch (error) { } catch (error) {
console.log('Error', error); console.log('Error', error);
setData(undefined); setData(undefined);
@@ -275,7 +283,8 @@ export default function Dashboard() {
key: '', key: '',
value: '', value: '',
valueOverride: undefined, valueOverride: undefined,
comment: '' comment: '',
tags: []
}, },
...data! ...data!
]); ]);
@@ -295,7 +304,8 @@ export default function Dashboard() {
key: '', key: '',
value: '', value: '',
valueOverride: undefined, valueOverride: undefined,
comment: '' comment: '',
tags: []
}, },
]); ]);
}; };
@@ -333,6 +343,11 @@ export default function Dashboard() {
setButtonReady(true); setButtonReady(true);
}; };
const modifyTags = (tags: Tag[], pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
setButtonReady(true);
};
// For speed purposes and better perforamance, we are using useCallback // For speed purposes and better perforamance, we are using useCallback
const listenChangeValue = useCallback((value: string, pos: number) => { const listenChangeValue = useCallback((value: string, pos: number) => {
modifyValue(value, pos); modifyValue(value, pos);
@@ -350,6 +365,10 @@ export default function Dashboard() {
modifyComment(value, pos); modifyComment(value, pos);
}, []); }, []);
const listenChangeTags = useCallback((value: Tag[], pos: number) => {
modifyTags(value, pos);
}, []);
/** /**
* Save the changes of environment variables and push them to the database * Save the changes of environment variables and push them to the database
*/ */
@@ -422,7 +441,9 @@ export default function Dashboard() {
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !== newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key || initDataPoint.key ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !== newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !==
initDataPoint.comment) initDataPoint.comment) ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].tags !==
initDataPoint.tags
) )
.map((secret) => secret.id) .map((secret) => secret.id)
.includes(newDataPoint.id) .includes(newDataPoint.id)
@@ -456,7 +477,8 @@ export default function Dashboard() {
valueOverride: override.valueOverride, valueOverride: override.valueOverride,
comment: '', comment: '',
id: String(override.idOverride), id: String(override.idOverride),
idOverride: String(override.idOverride) idOverride: String(override.idOverride),
tags: override.tags
})); }));
console.log('override add', overridesToBeAdded.length); console.log('override add', overridesToBeAdded.length);
@@ -471,7 +493,9 @@ export default function Dashboard() {
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !== newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key || initDataPoint.key ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0] newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.comment !== initDataPoint.comment) .comment !== initDataPoint.comment ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.tags !== initDataPoint.tags)
) )
.map((secret) => secret.id) .map((secret) => secret.id)
.includes(newDataPoint.id) .includes(newDataPoint.id)
@@ -483,7 +507,8 @@ export default function Dashboard() {
valueOverride: override.valueOverride, valueOverride: override.valueOverride,
comment: '', comment: '',
id: String(override.idOverride), id: String(override.idOverride),
idOverride: String(override.idOverride) idOverride: String(override.idOverride),
tags: override.tags
})); }));
console.log('override update', overridesToBeUpdated.length); console.log('override update', overridesToBeUpdated.length);
@@ -544,34 +569,6 @@ export default function Dashboard() {
<meta name="og:description" content={String(t('dashboard:og-description'))} /> <meta name="og:description" content={String(t('dashboard:og-description'))} />
</Head> </Head>
<div className="flex flex-row h-full dark:[color-scheme:dark]"> <div className="flex flex-row h-full dark:[color-scheme:dark]">
{sidebarSecretId !== 'None' && (
<SideBar
toggleSidebar={toggleSidebar}
data={data.filter(
(row: SecretDataProps) =>
row.key === data.filter((r) => r.id === sidebarSecretId)[0]?.key
)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv!}
workspaceId={workspaceId}
savePush={savePush}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
deleteRow={deleteCertainRow}
/>
)}
{PITSidebarOpen && (
<PITRecoverySidebar
toggleSidebar={togglePITSidebar}
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : '')}
setSnapshotData={setSnapshotData}
/>
)}
<div className="w-full max-h-96 pb-2 dark:[color-scheme:dark]"> <div className="w-full max-h-96 pb-2 dark:[color-scheme:dark]">
<NavHeader pageName={t('dashboard:title')} isProjectRelated /> <NavHeader pageName={t('dashboard:title')} isProjectRelated />
{checkDocsPopUpVisible && ( {checkDocsPopUpVisible && (
@@ -626,7 +623,10 @@ export default function Dashboard() {
<div className="flex justify-start max-w-sm mt-1 mr-2"> <div className="flex justify-start max-w-sm mt-1 mr-2">
<Button <Button
text={String(`${numSnapshots} ${t('Commits')}`)} text={String(`${numSnapshots} ${t('Commits')}`)}
onButtonPressed={() => togglePITSidebar(true)} onButtonPressed={() => {
toggleSidebar('None');
togglePITSidebar(true)
}}
color="mineshaft" color="mineshaft"
size="md" size="md"
icon={faClockRotateLeft} icon={faClockRotateLeft}
@@ -661,7 +661,8 @@ export default function Dashboard() {
valueOverride: sv.valueOverride, valueOverride: sv.valueOverride,
key: sv.key, key: sv.key,
value: sv.value, value: sv.value,
comment: '' comment: '',
tags: sv.tags
})); }));
setData(rolledBackSecrets); setData(rolledBackSecrets);
@@ -779,27 +780,58 @@ export default function Dashboard() {
className="mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar border border-mineshaft-600 rounded-md" className="mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar border border-mineshaft-600 rounded-md"
> >
<div ref={secretsTop} /> <div ref={secretsTop} />
<div className='bg-mineshaft-800 text-sm rounded-t-md h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]'> <div
<div className='w-14'/> className='group flex flex-col items-center bg-mineshaft-800 border-b-2 border-mineshaft-500 duration-100 sticky top-0 z-[60]'
<div className='text-bunker-300 relative font-semibold h-10 flex items-center w-80 pl-2 border-r border-mineshaft-600'> >
<span>Key</span> <div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
{!snapshotData && <IconButton <div className="w-2/12 border-r border-mineshaft-600 flex flex-row items-center">
ariaLabel="copy icon" <div className='text-transparent text-xs flex items-center justify-center w-14 h-10 cursor-default'>0</div>
variant="plain" <span className='px-2 text-bunker-300 font-semibold'>Key</span>
className="group relative ml-2" {!snapshotData && <IconButton
onClick={() => reorderRows(1)} ariaLabel="copy icon"
> variant="plain"
{sortMethod === 'alphabetical' ? <FontAwesomeIcon icon={faArrowUp} /> : <FontAwesomeIcon icon={faArrowDown} />} className="group relative ml-2"
</IconButton>} onClick={() => reorderRows(1)}
>
{sortMethod === 'alphabetical' ? <FontAwesomeIcon icon={faArrowUp} /> : <FontAwesomeIcon icon={faArrowDown} />}
</IconButton>}
</div>
<div className="w-5/12 border-r border-mineshaft-600">
<div
className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10'
>
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12'>Value</div>
</div>
</div>
<div className="w-2/12 border-r border-mineshaft-600">
<div className="flex items-center max-h-16">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div>
</div>
</div>
<div className="w-2/12">
<div className="flex items-center max-h-16">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Tags</div>
</div>
</div>
<div
className="w-[1.5rem] h-[2.35rem] ml-auto rounded-md flex flex-row justify-center items-center"
/>
<div className='w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'>
<div
onKeyDown={() => null}
role="none"
onClick={() => {}}
className="invisible group-hover:visible"
>
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5 invisible" icon={faXmark} />
</div>
</div>
</div> </div>
<div className='text-bunker-300 pl-2 font-semibold h-10 flex items-center w-full border-r border-mineshaft-600'>Value</div>
<div className='text-bunker-300 pl-2 font-semibold h-10 flex items-center w-96'>Comment</div>
{!snapshotData && <div className='w-[9.3rem]'/>}
</div> </div>
<div className="bg-mineshaft-800 rounded-b-md border-bunker-600"> <div className="bg-mineshaft-800 rounded-b-md border-bunker-600">
{!snapshotData && {!snapshotData &&
data data
?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase())) ?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase()) || row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase()))
.filter((row) => !sharedToHide.includes(row.id)) .filter((row) => !sharedToHide.includes(row.id))
.map((keyPair) => ( .map((keyPair) => (
<KeyPair <KeyPair
@@ -809,14 +841,17 @@ export default function Dashboard() {
modifyValueOverride={listenChangeValueOverride} modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey} modifyKey={listenChangeKey}
modifyComment={listenChangeComment} modifyComment={listenChangeComment}
modifyTags={listenChangeTags}
isBlurred={blurred} isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes( isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes(
keyPair.key keyPair.key
)} )}
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
togglePITSidebar={togglePITSidebar}
sidebarSecretId={sidebarSecretId} sidebarSecretId={sidebarSecretId}
isSnapshot={false} isSnapshot={false}
deleteRow={deleteCertainRow} deleteRow={deleteCertainRow}
tags={projectTags}
/> />
))} ))}
{snapshotData && {snapshotData &&
@@ -848,6 +883,7 @@ export default function Dashboard() {
modifyValueOverride={listenChangeValueOverride} modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey} modifyKey={listenChangeKey}
modifyComment={listenChangeComment} modifyComment={listenChangeComment}
modifyTags={listenChangeTags}
isBlurred={blurred} isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes( isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes(
keyPair.key keyPair.key
@@ -855,6 +891,7 @@ export default function Dashboard() {
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId} sidebarSecretId={sidebarSecretId}
isSnapshot isSnapshot
tags={projectTags}
/> />
))} ))}
<div className='bg-mineshaft-800 text-sm rounded-t-md hover:bg-mineshaft-700 h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]'> <div className='bg-mineshaft-800 text-sm rounded-t-md hover:bg-mineshaft-700 h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]'>
@@ -908,6 +945,34 @@ export default function Dashboard() {
)} )}
</div> </div>
</div> </div>
{sidebarSecretId !== 'None' && (
<SideBar
toggleSidebar={toggleSidebar}
data={data.filter(
(row: SecretDataProps) =>
row.key === data.filter((r) => r.id === sidebarSecretId)[0]?.key
)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv!}
workspaceId={workspaceId}
savePush={savePush}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
deleteRow={deleteCertainRow}
/>
)}
{PITSidebarOpen && (
<PITRecoverySidebar
toggleSidebar={togglePITSidebar}
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : '')}
setSnapshotData={setSnapshotData}
/>
)}
</div> </div>
</div> </div>
) : ( ) : (
@@ -18,19 +18,24 @@ import { useToggle } from '@app/hooks';
import { import {
useCreateServiceToken, useCreateServiceToken,
useCreateWsEnvironment, useCreateWsEnvironment,
useCreateWsTag,
useDeleteServiceToken, useDeleteServiceToken,
useDeleteWorkspace, useDeleteWorkspace,
useDeleteWsEnvironment, useDeleteWsEnvironment,
useDeleteWsTag,
useGetUserWsKey, useGetUserWsKey,
useGetUserWsServiceTokens, useGetUserWsServiceTokens,
useGetWsTags,
useRenameWorkspace, useRenameWorkspace,
useUpdateWsEnvironment useUpdateWsEnvironment
} from '@app/hooks/api'; } from '@app/hooks/api';
import { SecretTagsSection } from './components/SecretTagsSection';
import { import {
CopyProjectIDSection, CopyProjectIDSection,
CreateServiceToken, CreateServiceToken,
CreateUpdateEnvFormData, CreateUpdateEnvFormData,
CreateWsTag,
EnvironmentSection, EnvironmentSection,
ProjectNameChangeSection, ProjectNameChangeSection,
ServiceTokenSection ServiceTokenSection
@@ -61,6 +66,11 @@ export const ProjectSettingsPage = () => {
const createServiceToken = useCreateServiceToken(); const createServiceToken = useCreateServiceToken();
const deleteServiceToken = useDeleteServiceToken(); const deleteServiceToken = useDeleteServiceToken();
// tag
const { data: wsTags } = useGetWsTags(workspaceID);
const createWsTag = useCreateWsTag();
const deleteWsTag = useDeleteWsTag();
// get user subscription // get user subscription
const { subscriptionPlan } = useSubscription(); const { subscriptionPlan } = useSubscription();
const host = window.location.origin; const host = window.location.origin;
@@ -207,6 +217,44 @@ export const ProjectSettingsPage = () => {
return ''; return '';
}; };
const onCreateWsTag = async ({ name }: CreateWsTag) => {
try {
const res = await createWsTag.mutateAsync({
workspaceID,
tagName: name,
tagSlug: name.replace(" ", "_")
});
createNotification({
text: 'Successfully created a tag',
type: 'success'
});
return res.name;
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to create a tag',
type: 'error'
});
}
return '';
};
const onDeleteTag = async (tagID: string) => {
try {
await deleteWsTag.mutateAsync({ tagID });
createNotification({
text: 'Successfully deleted tag',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to delete the tag',
type: 'error'
});
}
};
const onDeleteServiceToken = async (tokenID: string) => { const onDeleteServiceToken = async (tokenID: string) => {
try { try {
await deleteServiceToken.mutateAsync(tokenID); await deleteServiceToken.mutateAsync(tokenID);
@@ -224,7 +272,7 @@ export const ProjectSettingsPage = () => {
}; };
return ( return (
<div className="container mx-auto flex flex-col px-8 text-mineshaft-50"> <div className="container mx-auto flex flex-col px-8 text-mineshaft-50 dark dark:[color-scheme:dark]">
{/* TODO(akhilmhdh): Remove this right when layout is refactored */} {/* TODO(akhilmhdh): Remove this right when layout is refactored */}
<div className="relative right-5"> <div className="relative right-5">
<NavHeader pageName={t('settings-project:title')} isProjectRelated /> <NavHeader pageName={t('settings-project:title')} isProjectRelated />
@@ -256,6 +304,12 @@ export const ProjectSettingsPage = () => {
workspaceName={currentWorkspace?.name || ''} workspaceName={currentWorkspace?.name || ''}
onCreateToken={onCreateServiceToken} onCreateToken={onCreateServiceToken}
/> />
<SecretTagsSection
tags={wsTags || []}
onDeleteTag={onDeleteTag}
workspaceName={currentWorkspace?.name || ''}
onCreateTag={onCreateWsTag}
/>
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md border-l border-red bg-white/5 px-6 pl-6 pb-4 pt-4"> <div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md border-l border-red bg-white/5 px-6 pl-6 pb-4 pt-4">
<p className="text-xl font-bold text-red">{t('settings-project:danger-zone')}</p> <p className="text-xl font-bold text-red">{t('settings-project:danger-zone')}</p>
<p className="text-md mt-2 text-gray-400">{t('settings-project:danger-zone-note')}</p> <p className="text-md mt-2 text-gray-400">{t('settings-project:danger-zone-note')}</p>
@@ -102,7 +102,7 @@ export const EnvironmentSection = ({
colorSchema="primary" colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
> >
Add New Env Add New Environment
</Button> </Button>
</div> </div>
</div> </div>
@@ -0,0 +1,186 @@
import { Controller, useForm } from 'react-hook-form';
import { faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import {
Button,
DeleteActionModal,
FormControl,
IconButton,
Input,
Modal,
ModalClose,
ModalContent,
ModalTrigger,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr,
} from '@app/components/v2';
import { usePopUp } from '@app/hooks';
import { WorkspaceTag } from '@app/hooks/api/types';
const createTagSchema = yup.object({
name: yup.string().required().label('Tag Name'),
});
export type CreateWsTag = yup.InferType<typeof createTagSchema>;
type Props = {
tags: WorkspaceTag[];
workspaceName: string;
onDeleteTag: (tagID: string) => Promise<void>;
onCreateTag: (data: CreateWsTag) => Promise<string>;
};
type DeleteModalData = { name: string; id: string };
export const SecretTagsSection = ({
tags = [],
onDeleteTag,
workspaceName,
onCreateTag
}: Props): JSX.Element => {
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
'CreateSecretTag',
'deleteTagConfirmation'
] as const);
const {
control,
reset,
handleSubmit,
formState: { isSubmitting }
} = useForm<CreateWsTag>({
resolver: yupResolver(createTagSchema)
});
const onFormSubmit = async (data: CreateWsTag) => {
console.log(19191, data);
await onCreateTag(data);
handlePopUpClose('CreateSecretTag');
};
const onDeleteApproved = async () => {
await onDeleteTag((popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id);
handlePopUpClose('deleteTagConfirmation');
};
return (
<div className="mt-4 mb-4 flex w-full flex-col items-start rounded-md bg-white/5 p-6">
<div className="flex w-full flex-row justify-between">
<div className="flex w-full flex-col">
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
<p className="text-sm text-gray-400">Every secret can be assigned to one or more tags. Here you can add and remove tags for the current project.</p>
</div>
<div>
<Modal
isOpen={popUp?.CreateSecretTag?.isOpen}
onOpenChange={(open) => {
handlePopUpToggle('CreateSecretTag', open);
reset();
}}
>
<ModalTrigger asChild>
<Button color="mineshaft" leftIcon={<FontAwesomeIcon icon={faPlus} />}>
Add New Tag
</Button>
</ModalTrigger>
<ModalContent
title={`Add a tag for ${ workspaceName}`}
subTitle='Specify your tag name, and the slug will be created automatically.'
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label='Tag Name'
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Type your tag name" />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
type="submit"
isDisabled={isSubmitting}
isLoading={isSubmitting}
>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
</ModalContent>
</Modal>
</div>
</div>
<TableContainer className='mt-4'>
<Table>
<THead>
<Tr>
<Th>Tag</Th>
<Th>Slug</Th>
<Th aria-label="button" />
</Tr>
</THead>
<TBody>
{tags?.length > 0 ? (
tags.map(({ _id, name, slug }) => (
<Tr key={name}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td className="flex items-center justify-end">
<IconButton
onClick={() =>
handlePopUpOpen('deleteTagConfirmation', {
name,
id: _id
})
}
colorSchema="danger"
ariaLabel="update"
>
<FontAwesomeIcon icon={faTrashCan} />
</IconButton>
</Td>
</Tr>
))
) : (
<Tr>
<Td colSpan={4} className="py-6 text-center text-bunker-400">
No tags found for this project
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
<DeleteActionModal
isOpen={popUp.deleteTagConfirmation.isOpen}
title={`Delete ${
(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name || ' '
} api key?`}
onChange={(isOpen) => handlePopUpToggle('deleteTagConfirmation', isOpen)}
deleteKey={(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose('deleteTagConfirmation')}
onDeleteApproved={onDeleteApproved}
/>
</div>
);
};
@@ -0,0 +1 @@
export {SecretTagsSection} from './SecretTagsSection'
@@ -2,5 +2,6 @@ export { CopyProjectIDSection } from './CopyProjectIDSection';
export { EnvironmentSection } from './EnvironmentSection'; export { EnvironmentSection } from './EnvironmentSection';
export type { CreateUpdateEnvFormData } from './EnvironmentSection/EnvironmentSection'; export type { CreateUpdateEnvFormData } from './EnvironmentSection/EnvironmentSection';
export { ProjectNameChangeSection } from './ProjectNameChangeSection'; export { ProjectNameChangeSection } from './ProjectNameChangeSection';
export type { CreateWsTag } from './SecretTagsSection/SecretTagsSection';
export { ServiceTokenSection } from './ServiceTokenSection'; export { ServiceTokenSection } from './ServiceTokenSection';
export type { CreateServiceToken } from './ServiceTokenSection/ServiceTokenSection'; export type { CreateServiceToken } from './ServiceTokenSection/ServiceTokenSection';