mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-19 00:30:56 +00:00
feat(ui): implemented the new dashboard page
This commit is contained in:
@@ -0,0 +1,673 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormProvider, useFieldArray, useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
faArrowLeft,
|
||||
faCheck,
|
||||
faClockRotateLeft,
|
||||
faCodeCommit,
|
||||
faDownload,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faMagnifyingGlass,
|
||||
faPlus
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||
import NavHeader from '@app/components/navigation/NavHeader';
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Select,
|
||||
SelectItem,
|
||||
TableContainer,
|
||||
Tag,
|
||||
Tooltip
|
||||
} from '@app/components/v2';
|
||||
import { leaveConfirmDefaultMessage } from '@app/const';
|
||||
import { useWorkspace } from '@app/context';
|
||||
import { useLeaveConfirm, usePersistentState, usePopUp, useToggle } from '@app/hooks';
|
||||
import {
|
||||
useBatchSecretsOp,
|
||||
useCreateWsTag,
|
||||
useGetProjectSecrets,
|
||||
useGetSecretVersion,
|
||||
useGetSnapshotSecrets,
|
||||
useGetUserAction,
|
||||
useGetUserWsEnvironments,
|
||||
useGetUserWsKey,
|
||||
useGetWorkspaceSecretSnapshots,
|
||||
useGetWsSnapshotCount,
|
||||
useGetWsTags,
|
||||
usePerformSecretRollback,
|
||||
useRegisterUserAction
|
||||
} from '@app/hooks/api';
|
||||
import { secretKeys } from '@app/hooks/api/secrets/queries';
|
||||
import { WorkspaceEnv } from '@app/hooks/api/types';
|
||||
|
||||
import { CompareSecret } from './components/CompareSecret';
|
||||
import { CreateTagModal } from './components/CreateTagModal';
|
||||
import { PitDrawer } from './components/PitDrawer';
|
||||
import { SecretDetailDrawer } from './components/SecretDetailDrawer';
|
||||
import { SecretDropzone } from './components/SecretDropzone';
|
||||
import { SecretInputRow } from './components/SecretInputRow';
|
||||
import { SecretTableHeader } from './components/SecretTableHeader';
|
||||
import {
|
||||
DEFAULT_SECRET_VALUE,
|
||||
downloadSecret,
|
||||
FormData,
|
||||
schema,
|
||||
transformSecretsToBatchSecretReq,
|
||||
TSecOverwriteOpt,
|
||||
TSecretDetailsOpen
|
||||
} from './DashboardPage.utils';
|
||||
|
||||
const USER_ACTION_PUSH = 'first_time_secrets_pushed';
|
||||
|
||||
/*
|
||||
* Some imp aspects to consider. Here there are multiple stats changing
|
||||
* Thus ideally we need to use a context. But instead we rely on react hook form
|
||||
* React hook form provides context and high performance proxy based rendering
|
||||
* It also handles error handling and transferring states between inputs
|
||||
*
|
||||
* Another thing is the purpose of overrideAction
|
||||
* Before we would remove the value for personal secret when user toggle and user couldn't get it back
|
||||
* They have to reload the browser or go back all over again
|
||||
* Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving
|
||||
* They will get it back
|
||||
*/
|
||||
export const DashboardPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
'secretDetails',
|
||||
'addTag',
|
||||
'secretSnapshots',
|
||||
'uploadedSecOpts',
|
||||
'compareSecrets'
|
||||
] as const);
|
||||
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle();
|
||||
const [isCommentColumnCollapsed, setIsCommentCollapsed] = usePersistentState(
|
||||
false,
|
||||
'infisical-comment-box'
|
||||
);
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
const [snapshotId, setSnaphotId] = useState<string | null>(null);
|
||||
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
|
||||
const [deletedSecretIds, setDeletedSecretIds] = useState<string[]>([]);
|
||||
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false });
|
||||
|
||||
const { currentWorkspace, isLoading } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?._id as string;
|
||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !workspaceId && router.isReady) {
|
||||
router.push('/noprojects');
|
||||
}
|
||||
}, [isLoading, workspaceId, router.isReady]);
|
||||
|
||||
// fetching data
|
||||
const { data: userAction } = useGetUserAction(USER_ACTION_PUSH);
|
||||
const hasUserPushed = Boolean(userAction);
|
||||
|
||||
const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
|
||||
workspaceId,
|
||||
onSuccess: (data) => {
|
||||
// get an env with one of the access available
|
||||
const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied);
|
||||
if (env) {
|
||||
setSelectedEnv(env);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { data: secretVersion } = useGetSecretVersion({
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
secretId: (popUp?.secretDetails?.data as TSecretDetailsOpen)?.id,
|
||||
decryptFileKey: latestFileKey!
|
||||
});
|
||||
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||
workspaceId,
|
||||
env: selectedEnv?.slug || '',
|
||||
decryptFileKey: latestFileKey!,
|
||||
isPaused: Boolean(snapshotId)
|
||||
});
|
||||
|
||||
const {
|
||||
data: secretSnaphots,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage
|
||||
} = useGetWorkspaceSecretSnapshots({
|
||||
workspaceId,
|
||||
limit: 10
|
||||
});
|
||||
|
||||
const {
|
||||
data: snapshotSecret,
|
||||
isLoading: isSnapshotSecretsLoading,
|
||||
isFetching: isSnapshotChanging
|
||||
} = useGetSnapshotSecrets({
|
||||
snapshotId: snapshotId || '',
|
||||
env: selectedEnv?.slug || '',
|
||||
decryptFileKey: latestFileKey!
|
||||
});
|
||||
|
||||
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } =
|
||||
useGetWsSnapshotCount(workspaceId);
|
||||
|
||||
const { data: wsTags } = useGetWsTags(workspaceId);
|
||||
// mutation calls
|
||||
const { mutateAsync: batchSecretOp } = useBatchSecretsOp();
|
||||
const { mutateAsync: performSecretRollback } = usePerformSecretRollback();
|
||||
const { mutateAsync: registerUserAction } = useRegisterUserAction();
|
||||
const { mutateAsync: createWsTag } = useCreateWsTag();
|
||||
|
||||
const method = useForm<FormData>({
|
||||
// why any: well yup inferred ts expects other keys to defined as undefined
|
||||
values: secrets as any,
|
||||
mode: 'onBlur',
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
getValues,
|
||||
setValue,
|
||||
formState: { isDirty, isSubmitting, dirtyFields },
|
||||
reset
|
||||
} = method;
|
||||
const formSecrets = useWatch({ control, name: 'secrets' });
|
||||
const { fields, prepend, append, remove, update } = useFieldArray({ control, name: 'secrets' });
|
||||
|
||||
const isRollbackMode = Boolean(snapshotId);
|
||||
const isReadOnly = selectedEnv?.isWriteDenied;
|
||||
const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied;
|
||||
const canDoRollback = !isReadOnly && !isAddOnly;
|
||||
const isSubmitDisabled =
|
||||
isReadOnly ||
|
||||
// on add only mode the formstate becomes dirty due to secrets missing some items
|
||||
// to avoid this we check dirtyFields in isAddOnly Mode
|
||||
(isAddOnly && Object.keys(dirtyFields).length === 0) ||
|
||||
(!isRollbackMode && !isAddOnly && !isDirty) ||
|
||||
isSubmitting;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSnapshotChanging && Boolean(snapshotId)) {
|
||||
reset({ secrets: snapshotSecret?.secrets, isSnapshotMode: true });
|
||||
}
|
||||
}, [isSnapshotChanging]);
|
||||
|
||||
useEffect(() => {
|
||||
setHasUnsavedChanges(!isSubmitDisabled);
|
||||
}, [isSubmitDisabled]);
|
||||
|
||||
const onSortSecrets = () => {
|
||||
const dir = sortDir === 'asc' ? 'desc' : 'asc';
|
||||
const sec = getValues('secrets') || [];
|
||||
const sortedSec = sec.sort((a, b) =>
|
||||
dir === 'asc' ? a?.key?.localeCompare(b?.key || '') : b?.key?.localeCompare(a?.key || '')
|
||||
);
|
||||
setValue('secrets', sortedSec);
|
||||
setSortDir(dir);
|
||||
};
|
||||
|
||||
const handleUploadedEnv = (uploadedSec: TSecOverwriteOpt['secrets']) => {
|
||||
const sec = getValues('secrets') || [];
|
||||
const conflictingSec = sec.filter(({ key }) => Boolean(uploadedSec?.[key]));
|
||||
const conflictingSecIds = conflictingSec.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => ({
|
||||
...prev,
|
||||
[curr.key]: true
|
||||
}),
|
||||
{}
|
||||
);
|
||||
// filter to get all conflicting ones
|
||||
const conflictingUploadedSec = { ...uploadedSec };
|
||||
// append non conflicting ones
|
||||
Object.keys(uploadedSec).forEach((key) => {
|
||||
if (!conflictingSecIds?.[key]) {
|
||||
append({
|
||||
...DEFAULT_SECRET_VALUE,
|
||||
key,
|
||||
value: uploadedSec[key].value,
|
||||
comment: uploadedSec[key].comments.join(',')
|
||||
});
|
||||
delete conflictingUploadedSec[key];
|
||||
}
|
||||
});
|
||||
if (conflictingSec.length > 0) {
|
||||
handlePopUpOpen('uploadedSecOpts', { secrets: conflictingUploadedSec });
|
||||
}
|
||||
};
|
||||
|
||||
const onOverwriteSecrets = () => {
|
||||
const sec = getValues('secrets') || [];
|
||||
const uploadedSec = (popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets;
|
||||
const data: Array<{ key: string; index: number }> = [];
|
||||
sec.forEach(({ key }, index) => {
|
||||
if (uploadedSec?.[key]) data.push({ key, index });
|
||||
});
|
||||
data.forEach(({ key, index }) => {
|
||||
const { value, comments } = uploadedSec[key];
|
||||
const comment = comments.join(', ');
|
||||
update(index, {
|
||||
...DEFAULT_SECRET_VALUE,
|
||||
key,
|
||||
value,
|
||||
comment,
|
||||
tags: sec[index].tags
|
||||
});
|
||||
});
|
||||
handlePopUpClose('uploadedSecOpts');
|
||||
};
|
||||
|
||||
const onSecretRollback = async () => {
|
||||
if (!snapshotSecret?.version) {
|
||||
createNotification({
|
||||
text: 'Failed to find secret version',
|
||||
type: 'success'
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await performSecretRollback({
|
||||
workspaceId,
|
||||
version: snapshotSecret.version
|
||||
});
|
||||
setValue('isSnapshotMode', false);
|
||||
setSnaphotId(null);
|
||||
queryClient.invalidateQueries(
|
||||
secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || '')
|
||||
);
|
||||
createNotification({
|
||||
text: 'Successfully rollback secrets',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
text: 'Failed to rollback secrets',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onAppendSecret = () => append(DEFAULT_SECRET_VALUE);
|
||||
|
||||
const onSaveSecret = async ({ secrets: userSec = [], isSnapshotMode }: FormData) => {
|
||||
if (isSnapshotMode) {
|
||||
await onSecretRollback();
|
||||
return;
|
||||
}
|
||||
// just closing this if save is triggered from drawer
|
||||
handlePopUpClose('secretDetails');
|
||||
// when add only mode remove rest of things not created
|
||||
const sec = isAddOnly ? userSec.filter(({ _id }) => !_id) : userSec;
|
||||
// encrypt and format the secrets to batch api format
|
||||
// requests = [ {method:"", secret:""} ]
|
||||
const batchedSecret = transformSecretsToBatchSecretReq(deletedSecretIds, latestFileKey, sec);
|
||||
// type check
|
||||
if (!selectedEnv?.slug) return;
|
||||
try {
|
||||
await batchSecretOp({
|
||||
requests: batchedSecret,
|
||||
workspaceId,
|
||||
environment: selectedEnv?.slug
|
||||
});
|
||||
createNotification({
|
||||
text: 'Successfully saved changes',
|
||||
type: 'success'
|
||||
});
|
||||
if (!hasUserPushed) {
|
||||
await registerUserAction(USER_ACTION_PUSH);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
text: 'Failed to save changes',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDrawerOpen = (dto: TSecretDetailsOpen) => {
|
||||
handlePopUpOpen('secretDetails', dto);
|
||||
};
|
||||
|
||||
const onEnvChange = (slug: string) => {
|
||||
if (hasUnsavedChanges) {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (!window.confirm(leaveConfirmDefaultMessage)) return;
|
||||
}
|
||||
const env = wsEnv?.find((el) => el.slug === slug);
|
||||
if (env) setSelectedEnv(env);
|
||||
};
|
||||
|
||||
// record all deleted ids
|
||||
// This will make final deletion easier
|
||||
const onSecretDelete = (index: number, id?: string, overrideId?: string) => {
|
||||
const ids: string[] = [];
|
||||
if (id) ids.push(id);
|
||||
if (overrideId) ids.push(overrideId);
|
||||
setDeletedSecretIds((state) => [...state, ...ids]);
|
||||
remove(index);
|
||||
// just the case if this is called from drawer
|
||||
handlePopUpClose('secretDetails');
|
||||
};
|
||||
|
||||
const onCreateWsTag = async (tagName: string) => {
|
||||
try {
|
||||
await createWsTag({
|
||||
workspaceID: workspaceId,
|
||||
tagName,
|
||||
tagSlug: tagName.replace(' ', '_')
|
||||
});
|
||||
handlePopUpClose('addTag');
|
||||
createNotification({
|
||||
text: 'Successfully created a tag',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: 'Failed to create a tag',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isSecretsLoading || isEnvListLoading) {
|
||||
return (
|
||||
<div className="container mx-auto flex h-1/2 w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// when secrets is not loading and secrets list is empty
|
||||
const isDashboardSecretEmpty = !isSecretsLoading && !formSecrets?.length;
|
||||
// when using snapshot mode and snapshot is loading and snapshot list is empty
|
||||
const isSnapshotSecretEmtpy =
|
||||
isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length;
|
||||
const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy;
|
||||
|
||||
const userAvailableEnvs = wsEnv?.filter(
|
||||
({ isReadDenied, isWriteDenied }) => !isReadDenied || !isWriteDenied
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto w-full px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
<FormProvider {...method}>
|
||||
<form autoComplete="off">
|
||||
{/* breadcrumb row */}
|
||||
<div className="relative right-5">
|
||||
<NavHeader pageName={t('dashboard:title')} isProjectRelated />
|
||||
</div>
|
||||
{/* Secrets, commit and save button section */}
|
||||
<div className="mt-6 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<h1 className="text-3xl font-semibold">
|
||||
{isRollbackMode ? 'Secret Snapshot' : 'Secrets'}
|
||||
</h1>
|
||||
{isRollbackMode && Boolean(snapshotSecret) && (
|
||||
<Tag colorSchema="green">
|
||||
{new Date(snapshotSecret?.createdAt || '').toLocaleString()}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{isRollbackMode && (
|
||||
<Button
|
||||
variant="star"
|
||||
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
|
||||
onClick={() => {
|
||||
setSnaphotId(null);
|
||||
reset({ ...secrets, isSnapshotMode: false });
|
||||
}}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="star"
|
||||
onClick={() => handlePopUpOpen('secretSnapshots')}
|
||||
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
|
||||
isLoading={isLoadingSnapshotCount}
|
||||
isDisabled={!canDoRollback}
|
||||
>
|
||||
{snapshotCount} Commits
|
||||
</Button>
|
||||
<Button
|
||||
isDisabled={isSubmitDisabled}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />}
|
||||
onClick={handleSubmit(onSaveSecret)}
|
||||
>
|
||||
{isRollbackMode ? 'Rollback' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Environment, search and other action row */}
|
||||
<div className="mt-4 flex items-center space-x-2">
|
||||
<div>
|
||||
<Tooltip content="Select environment">
|
||||
<Select
|
||||
value={selectedEnv?.slug}
|
||||
onValueChange={onEnvChange}
|
||||
position="popper"
|
||||
className="min-w-[180px] bg-mineshaft-600"
|
||||
>
|
||||
{userAvailableEnvs?.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<Input
|
||||
className="bg-mineshaft-600 placeholder-mineshaft-50"
|
||||
placeholder="Search keys..."
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<IconButton ariaLabel="download" variant="star">
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Button
|
||||
onClick={() => downloadSecret(getValues('secrets'), selectedEnv?.slug)}
|
||||
variant="star"
|
||||
className="bg-bunker-800"
|
||||
>
|
||||
Download as .env
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<Tooltip content={isSecretValueHidden ? 'Hide Secrets' : 'Reveal secrets'}>
|
||||
<IconButton
|
||||
ariaLabel="reveal"
|
||||
variant="star"
|
||||
onClick={() => setIsSecretValueHidden.toggle()}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSecretValueHidden ? faEyeSlash : faEye} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false })}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="star"
|
||||
>
|
||||
Add Secret
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
{!isSecretEmpty && (
|
||||
<TableContainer>
|
||||
<table className="secret-table">
|
||||
<SecretTableHeader
|
||||
isCommentColumnCollapsed={isCommentColumnCollapsed}
|
||||
sortDir={sortDir}
|
||||
onCommentColToggle={() => setIsCommentCollapsed(!isCommentColumnCollapsed)}
|
||||
onSort={onSortSecrets}
|
||||
/>
|
||||
<tbody>
|
||||
{fields.map(({ id, _id }, index) => (
|
||||
<SecretInputRow
|
||||
key={id}
|
||||
isReadOnly={isReadOnly}
|
||||
isRollbackMode={isRollbackMode}
|
||||
isAddOnly={isAddOnly}
|
||||
index={index}
|
||||
searchTerm={searchFilter}
|
||||
onSecretDelete={onSecretDelete}
|
||||
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
|
||||
isSecretValueHidden={isSecretValueHidden}
|
||||
isCommentColumnCollapsed={isCommentColumnCollapsed}
|
||||
wsTags={wsTags}
|
||||
onCreateTagOpen={() => handlePopUpOpen('addTag')}
|
||||
/>
|
||||
))}
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<Button
|
||||
variant="plain"
|
||||
className="my-1"
|
||||
onClick={onAppendSecret}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add Secret
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
)}
|
||||
<PitDrawer
|
||||
isDrawerOpen={popUp?.secretSnapshots?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle('secretSnapshots', isOpen)}
|
||||
fetchNextPage={fetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
snapshotId={snapshotId}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
secretSnaphots={secretSnaphots}
|
||||
onSelectSnapshot={setSnaphotId}
|
||||
/>
|
||||
<SecretDetailDrawer
|
||||
onSave={handleSubmit(onSaveSecret)}
|
||||
isReadOnly={isReadOnly || isRollbackMode}
|
||||
onSecretDelete={onSecretDelete}
|
||||
isDrawerOpen={popUp?.secretDetails?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle('secretDetails', isOpen)}
|
||||
secretVersion={secretVersion}
|
||||
index={(popUp?.secretDetails?.data as TSecretDetailsOpen)?.index}
|
||||
onEnvCompare={(key) => handlePopUpOpen('compareSecrets', key)}
|
||||
/>
|
||||
<SecretDropzone
|
||||
isSmaller={!isSecretEmpty}
|
||||
onParsedEnv={handleUploadedEnv}
|
||||
onAddNewSecret={onAppendSecret}
|
||||
/>
|
||||
</div>
|
||||
{/* secrets table and drawers, modals */}
|
||||
</form>
|
||||
{/* Create a new tag modal */}
|
||||
<Modal
|
||||
isOpen={popUp?.addTag?.isOpen}
|
||||
onOpenChange={(open) => {
|
||||
handlePopUpToggle('addTag', open);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Create tag"
|
||||
subTitle="Specify your tag name, and the slug will be created automatically."
|
||||
>
|
||||
<CreateTagModal onCreateTag={onCreateWsTag} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/* Uploaded env override or not confirmation modal */}
|
||||
<Modal
|
||||
isOpen={popUp?.uploadedSecOpts?.isOpen}
|
||||
onOpenChange={(open) => handlePopUpToggle('uploadedSecOpts', open)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Duplicate Secrets"
|
||||
footerContent={[
|
||||
<Button
|
||||
key="keep-old-btn"
|
||||
className="mr-4"
|
||||
onClick={() => handlePopUpClose('uploadedSecOpts')}
|
||||
>
|
||||
Keep old
|
||||
</Button>,
|
||||
<Button colorSchema="danger" key="overwrite-btn" onClick={onOverwriteSecrets}>
|
||||
Overwrite
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<div className="flex flex-col space-y-2 text-gray-300">
|
||||
<div>Your file contains following duplicate secrets</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {})
|
||||
?.map((key) => key)
|
||||
.join(', ')}
|
||||
</div>
|
||||
<div>Are you sure you want to overwrite these secrets?</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Modal
|
||||
isOpen={popUp?.compareSecrets?.isOpen}
|
||||
onOpenChange={(open) => handlePopUpToggle('compareSecrets', open)}
|
||||
>
|
||||
<ModalContent
|
||||
title={popUp?.compareSecrets?.data as string}
|
||||
subTitle="Secret value in available environments"
|
||||
overlayClassName="z-[90]"
|
||||
>
|
||||
<CompareSecret
|
||||
workspaceId={workspaceId}
|
||||
envs={userAvailableEnvs || []}
|
||||
secretKey={popUp?.compareSecrets?.data as string}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</FormProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user