mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 17:26:36 +00:00
Merge pull request #312 from Infisical/secret-tagging
Added tags to secrets in the dashboard
This commit is contained in:
@@ -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
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { useGetWsTags } from './queries';
|
||||||
|
export { useCreateWsTag } from './queries';
|
||||||
|
export { useDeleteWsTag } from './queries';
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -9,5 +9,6 @@ export type {
|
|||||||
RenameWorkspaceDTO,
|
RenameWorkspaceDTO,
|
||||||
UpdateEnvironmentDTO,
|
UpdateEnvironmentDTO,
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceEnv
|
WorkspaceEnv,
|
||||||
|
WorkspaceTag
|
||||||
} from './workspace/types';
|
} from './workspace/types';
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
|
|||||||
+186
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user