Compare commits

...

1 Commits

Author SHA1 Message Date
017dedd4fa Started creating folders frontend 2023-05-09 17:08:39 -07:00
18 changed files with 704 additions and 45 deletions

View File

@ -29,7 +29,8 @@ export default function NavHeader({
isOrganizationRelated,
currentEnv,
userAvailableEnvs,
onEnvChange
onEnvChange,
secretsPath
}: {
pageName: string;
isProjectRelated?: boolean;
@ -37,6 +38,7 @@ export default function NavHeader({
currentEnv?: string;
userAvailableEnvs?: any[];
onEnvChange?: (slug: string) => void;
secretsPath?: string;
}): JSX.Element {
const { currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization();
@ -75,7 +77,7 @@ export default function NavHeader({
{currentEnv && (
<>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
<div className="rounded-md pl-3 hover:bg-bunker-100/10">
{(!secretsPath || secretsPath === "/") ? <div className="rounded-md pl-3 hover:bg-bunker-100/10">
<Tooltip content="Select environment">
<Select
value={userAvailableEnvs?.filter((uae) => uae.name === currentEnv)[0]?.slug}
@ -92,9 +94,23 @@ export default function NavHeader({
))}
</Select>
</Tooltip>
</div>
</div> : <Link
passHref
legacyBehavior
href={{ pathname: router.pathname, query: { id: router.query.id, env: router.query.env } }}
>
<a className="pl-1.5 text-sm font-semibold text-primary/80 hover:text-primary">{currentEnv}</a>
</Link>}
</>
)}
{secretsPath && secretsPath.split("/").map(folder =>
(folder) && (
<>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-xs text-gray-400" />
<div className="text-sm font-semibold text-bunker-300">{folder}</div>
</>
)
)}
</div>
);
}

View File

@ -7,9 +7,10 @@ type Props = {
isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void;
text: string;
subText?: string;
};
export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => (
export const UpgradePlanModal = ({ text, subText, isOpen, onOpenChange }: Props): JSX.Element => (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Unleash Infisical's Full Power"
@ -28,9 +29,10 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
]}
>
<p className="mb-4 text-bunker-300">{text}</p>
<p className="font-medium text-bunker-300">
{/* <p className="text-bunker-300">
Upgrade and get access to this, as well as to other powerful enhancements.
</p>
</p> */}
<p key={1} className="mt-6 text-xs text-bunker-300">{subText}</p>
</ModalContent>
</Modal>
);

View File

@ -0,0 +1,4 @@
export {
useFolderOp,
useGetProjectFolderById,
useGetProjectFolders} from './queries';

View File

@ -0,0 +1,114 @@
/* eslint-disable no-param-reassign */
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { secretSnapshotKeys } from '../secretSnapshots/queries';
import {
FolderDTO,
GetProjectFolderDTO,
GetProjectSecretsDTO,
} from './types';
export const folderKeys = {
// this is also used in secretSnapshot part
getProjectFolder: (workspaceId: string, env: string | string[]) => [
{ workspaceId, env },
'folders'
],
// getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions']
};
const fetchProjectFolders = async (workspaceId: string, env: string | string[], secretsPath: string) => {
if (typeof env === 'string') {
const { data } = await apiRequest.get<{ folders: any[] }>('/api/v2/secrets', {
params: {
workspaceId,
environment: env,
secretsPath: secretsPath || '/'
}
});
console.log('folders here', data.folders)
return data.folders;
}
if (typeof env === 'object') {
let allEnvData: any = [];
// eslint-disable-next-line no-restricted-syntax
for (const envPoint of env) {
// eslint-disable-next-line no-await-in-loop
const { data } = await apiRequest.get<{ folders: any[] }>('/api/v2/secrets', {
params: {
environment: envPoint,
workspaceId,
secretsPath: secretsPath || '/'
}
});
allEnvData = allEnvData.concat(data.folders);
}
console.log('folders here2', allEnvData)
return allEnvData;
// eslint-disable-next-line no-else-return
} else {
return null;
}
};
export const useGetProjectFolders = ({
workspaceId,
env,
secretsPath,
decryptFileKey,
isPaused
}: GetProjectSecretsDTO) =>
useQuery({
// wait for all values to be available
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: folderKeys.getProjectFolder(workspaceId, env),
queryFn: () => fetchProjectFolders(workspaceId, env, secretsPath),
select: (data) => {
console.log('folders', data)
return { folders: data };
}
});
const fetchProjectFolderById = async (folderId: string) => {
const { data } = await apiRequest.get<{ folder: any[] }>(`/api/v1/folder/${folderId}`);
return data.folder;
};
export const useGetProjectFolderById = ({
folderId,
workspaceId,
env,
isPaused
}: GetProjectFolderDTO) =>
useQuery({
// wait for all values to be available
enabled: Boolean(folderId) && !isPaused,
queryKey: folderKeys.getProjectFolder(workspaceId, env),
queryFn: () => fetchProjectFolderById(folderId),
select: (data) => {
console.log(666777, data)
return data;
}
});
export const useFolderOp = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, FolderDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.post('/api/v1/folder/', dto);
return data;
},
onSuccess: (_, dto) => {
queryClient.invalidateQueries(folderKeys.getProjectFolder(dto.workspaceId, dto.environment));
queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId));
queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId));
}
});
};

View File

@ -0,0 +1,120 @@
import type { UserWsKeyPair } from '../keys/types';
import type { WsTag } from '../tags/types';
export type EncryptedSecret = {
_id: string;
version: number;
workspace: string;
type: 'shared' | 'personal';
environment: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
__v: number;
createdAt: string;
updatedAt: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: WsTag[];
};
export type DecryptedSecret = {
_id: string;
key: string;
value: string;
comment: string;
tags: WsTag[];
createdAt: string;
updatedAt: string;
env: string;
valueOverride?: string;
idOverride?: string;
overrideAction?: string;
};
export type EncryptedSecretVersion = {
_id: string;
secret: string;
version: number;
workspace: string;
type: string;
environment: string;
isDeleted: boolean;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
tags: WsTag[];
__v: number;
createdAt: string;
updatedAt: string;
};
// dto
type SecretTagArg = { _id: string; name: string; slug: string };
export type UpdateSecretArg = {
_id: string;
type: 'shared' | 'personal';
secretName: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: SecretTagArg[];
};
export type CreateSecretArg = Omit<UpdateSecretArg, '_id'>;
export type DeleteSecretArg = { _id: string };
export type BatchSecretDTO = {
workspaceId: string;
environment: string;
requests: Array<
| { method: 'POST'; secret: CreateSecretArg }
| { method: 'PATCH'; secret: UpdateSecretArg }
| { method: 'DELETE'; secret: DeleteSecretArg }
>;
};
export type FolderDTO = {
workspaceId: string;
environment: string;
folderName: string;
parentId?: string;
}
export type GetProjectSecretsDTO = {
workspaceId: string;
env: string | string[];
secretsPath: string;
decryptFileKey: UserWsKeyPair;
isPaused?: boolean;
onSuccess?: (data: DecryptedSecret[]) => void;
};
export type GetProjectFolderDTO = {
folderId: string;
workspaceId: string;
env: string | string[];
isPaused?: boolean;
}
export type GetSecretVersionsDTO = {
secretId: string;
limit: number;
offset: number;
decryptFileKey: UserWsKeyPair;
};

View File

@ -19,19 +19,20 @@ import {
export const secretKeys = {
// this is also used in secretSnapshot part
getProjectSecret: (workspaceId: string, env: string | string[]) => [
{ workspaceId, env },
getProjectSecret: (workspaceId: string, env: string | string[], secretsPath: string) => [
{ workspaceId, env, secretsPath },
'secrets'
],
getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions']
};
const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | string[]) => {
const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | string[], secretsPath: string) => {
if (typeof env === 'string') {
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
params: {
environment: env,
workspaceId
workspaceId,
secretsPath: secretsPath || '/'
}
});
return data.secrets;
@ -46,7 +47,8 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | s
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
params: {
environment: envPoint,
workspaceId
workspaceId,
secretsPath: secretsPath || '/'
}
});
allEnvData = allEnvData.concat(data.secrets);
@ -62,14 +64,15 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | s
export const useGetProjectSecrets = ({
workspaceId,
env,
secretsPath,
decryptFileKey,
isPaused
}: GetProjectSecretsDTO) =>
useQuery({
// wait for all values to be available
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env),
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretsPath),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, secretsPath),
select: (data) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const latestKey = decryptFileKey;
@ -145,14 +148,15 @@ export const useGetProjectSecrets = ({
export const useGetProjectSecretsByKey = ({
workspaceId,
env,
decryptFileKey,
decryptFileKey,
secretsPath,
isPaused
}: GetProjectSecretsDTO) =>
useQuery({
// wait for all values to be available
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env),
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretsPath),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, secretsPath),
select: (data) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const latestKey = decryptFileKey;
@ -283,7 +287,7 @@ export const useBatchSecretsOp = () => {
return data;
},
onSuccess: (_, dto) => {
queryClient.invalidateQueries(secretKeys.getProjectSecret(dto.workspaceId, dto.environment));
queryClient.invalidateQueries(secretKeys.getProjectSecret(dto.workspaceId, dto.environment, dto.secretsPath));
queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId));
queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId));
}

View File

@ -73,6 +73,7 @@ export type UpdateSecretArg = {
secretCommentIV: string;
secretCommentTag: string;
tags: SecretTagArg[];
folderId: string;
};
export type CreateSecretArg = Omit<UpdateSecretArg, '_id'>;
@ -82,6 +83,7 @@ export type DeleteSecretArg = { _id: string };
export type BatchSecretDTO = {
workspaceId: string;
environment: string;
secretsPath: string;
requests: Array<
| { method: 'POST'; secret: CreateSecretArg }
| { method: 'PATCH'; secret: UpdateSecretArg }
@ -92,6 +94,7 @@ export type BatchSecretDTO = {
export type GetProjectSecretsDTO = {
workspaceId: string;
env: string | string[];
secretsPath: string;
decryptFileKey: UserWsKeyPair;
isPaused?: boolean;
onSuccess?: (data: DecryptedSecret[]) => void;

View File

@ -297,34 +297,28 @@ export const AppLayout = ({ children }: LayoutProps) => {
<div className={`${currentWorkspace ? 'block' : 'hidden'}`}>
<Menu>
<Link href={`/dashboard/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={router.asPath.includes(`/dashboard/${currentWorkspace?._id}`)}
icon="system-outline-90-lock-closed"
>
{t('nav:menu.secrets')}
</MenuItem>
</a>
</Link>
<Link href={`/users/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/users/${currentWorkspace?._id}`}
icon="system-outline-96-groups"
>
{t('nav:menu.members')}
</MenuItem>
</a>
</Link>
<Link href={`/integrations/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/integrations/${currentWorkspace?._id}`}
icon="system-outline-82-extension"
>
{t('nav:menu.integrations')}
</MenuItem>
</a>
</Link>
<Link href={`/activity/${currentWorkspace?._id}`} passHref>
<MenuItem
@ -336,7 +330,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem>
</Link>
<Link href={`/settings/project/${currentWorkspace?._id}`} passHref>
<a>
<MenuItem
isSelected={
router.asPath === `/settings/project/${currentWorkspace?._id}`
@ -345,7 +338,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
>
{t('nav:menu.project-settings')}
</MenuItem>
</a>
</Link>
</Menu>
</div>

View File

@ -14,7 +14,7 @@ import {
} from '@app/hooks/api';
import { WorkspaceEnv } from '@app/hooks/api/types';
import { EnvComparisonRow } from './components/EnvComparisonRow';
import { EnvComparisonFolder, EnvComparisonRow } from './components/EnvComparisonRow';
import { FormData, schema } from './DashboardPage.utils';
export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
@ -49,6 +49,7 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecretsByKey({
workspaceId,
env: userAvailableEnvs?.map((env) => env.slug) ?? [],
secretsPath: "/",
decryptFileKey: latestFileKey!,
isPaused: false
});
@ -167,12 +168,14 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
<TableContainer className="border-none">
<table className="secret-table relative w-full bg-mineshaft-900">
<tbody className="max-h-screen overflow-y-auto">
<EnvComparisonFolder
folderName="FOLDER_A"
/>
{Object.keys(secrets?.secrets || {}).map((key, index) => (
<EnvComparisonRow
key={`row-${key}`}
key={`row-${key}-${String(index)}`}
secrets={secrets?.secrets?.[key]}
isReadOnly={isReadOnly}
index={index}
isSecretValueHidden
userAvailableEnvs={userAvailableEnvs}
/>
@ -204,7 +207,7 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
</div> */}
</div>
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center">
<div className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="flex h-10 w-14 items-center justify-center border-none px-1">
<div className="w-10 text-center text-xs text-transparent">0</div>
</div>
<div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
@ -217,7 +220,7 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
return (
<div
key={`button-${env.slug}`}
className="mx-2 mb-1 flex h-10 w-full min-w-[11rem] flex-row items-center justify-center border-none"
className="mx-2 mb-1 flex h-10 w-full min-w-[10rem] flex-row items-center justify-center border-none"
>
<Button
onClick={() => onEnvChange(env.slug)}

View File

@ -1,5 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { FormProvider, useFieldArray, useForm } from 'react-hook-form';
import {
// Controller,
FormProvider, useFieldArray, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/router';
import {
@ -10,6 +12,7 @@ import {
faDownload,
faEye,
faEyeSlash,
faFolder,
faMagnifyingGlass,
faPlus
} from '@fortawesome/free-solid-svg-icons';
@ -21,6 +24,7 @@ import { useNotificationContext } from '@app/components/context/Notifications/No
import NavHeader from '@app/components/navigation/NavHeader';
import {
Button,
// FormControl,
IconButton,
Input,
Modal,
@ -50,6 +54,7 @@ import {
usePerformSecretRollback,
useRegisterUserAction
} from '@app/hooks/api';
import { useFolderOp, useGetProjectFolderById, useGetProjectFolders } from '@app/hooks/api/secretFolders/queries';
import { secretKeys } from '@app/hooks/api/secrets/queries';
import { WorkspaceEnv } from '@app/hooks/api/types';
@ -58,6 +63,7 @@ import { CreateTagModal } from './components/CreateTagModal';
import { PitDrawer } from './components/PitDrawer';
import { SecretDetailDrawer } from './components/SecretDetailDrawer';
import { SecretDropzone } from './components/SecretDropzone';
import { SecretFolderRow } from './components/SecretFolderRow';
import { SecretInputRow } from './components/SecretInputRow';
import { SecretTableHeader } from './components/SecretTableHeader';
import {
@ -96,7 +102,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
'addTag',
'secretSnapshots',
'uploadedSecOpts',
'compareSecrets'
'compareSecrets',
'createUpdateFolder',
'deleteFolder'
] as const);
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
const [searchFilter, setSearchFilter] = useState('');
@ -138,13 +146,41 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
decryptFileKey: latestFileKey!
});
let folderInfo: any;
let isFolderInfoLoading;
if (router.query.folder) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { data: folderInfoTemp, isLoading: isFolderInfoLoadingTemp } = useGetProjectFolderById({
workspaceId,
env: selectedEnv?.slug || '',
folderId: String(router.query.folder),
isPaused: Boolean(snapshotId)
});
folderInfo = folderInfoTemp;
isFolderInfoLoading = isFolderInfoLoadingTemp;
console.log(98765, folderInfo, isFolderInfoLoading)
}
console.log('getting secrets in', folderInfo?.path || "/")
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId,
env: selectedEnv?.slug || '',
secretsPath: folderInfo?.path || "/",
decryptFileKey: latestFileKey!,
isPaused: Boolean(snapshotId)
});
const { data: folders, isLoading: isFoldersLoading } = useGetProjectFolders({
workspaceId,
env: selectedEnv?.slug || '',
secretsPath: folderInfo?.path || "/",
decryptFileKey: latestFileKey!,
isPaused: Boolean(snapshotId)
});
console.log(444, folders, isFoldersLoading)
const {
data: secretSnaphots,
fetchNextPage,
@ -168,12 +204,14 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } =
useGetWsSnapshotCount(workspaceId);
console.log(45678, 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 { mutateAsync: addFolder } = useFolderOp();
const method = useForm<FormData>({
// why any: well yup inferred ts expects other keys to defined as undefined
@ -183,6 +221,14 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
resolver: yupResolver(schema)
});
const methodFolders = useForm<FormData>({
// why any: well yup inferred ts expects other keys to defined as undefined
defaultValues: folders as any,
values: folders as any,
mode: 'onBlur',
resolver: yupResolver(schema)
});
const {
control,
handleSubmit,
@ -191,7 +237,24 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
formState: { isSubmitting, isDirty },
reset
} = method;
const {
control: controlFolders,
// handleSubmit: handleSubmitFolders,
// getValues: getValuesFolders,
// setValue: setValueFolders,
// formState: { isFoldersSubmitting, isFoldersDirty },
// reset: resetFolders
} = methodFolders;
const { fields, prepend, append, remove } = useFieldArray({ control, name: 'secrets' });
const {
fields: fieldsFolders,
prepend: prependFolders,
// append: appendFolders,
// remove: removeFolders,
update: updateFolders
} = useFieldArray({ control: controlFolders, name: 'folders' });
console.log(777, fields, fieldsFolders)
const isRollbackMode = Boolean(snapshotId);
const isReadOnly = selectedEnv?.isWriteDenied;
const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied;
@ -286,7 +349,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
setValue('isSnapshotMode', false);
setSnaphotId(null);
queryClient.invalidateQueries(
secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || '')
secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || '', folderInfo?.path)
);
createNotification({
text: 'Successfully rollback secrets',
@ -314,17 +377,32 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const sec = isAddOnly ? userSec.filter(({ _id }) => !_id) : userSec;
// encrypt and format the secrets to batch api format
// requests = [ {method:"", secret:""} ]
fieldsFolders.filter(folder => !folder._id).map(async (folder) => {
await addFolder({
workspaceId,
environment: String(selectedEnv?.slug),
folderName: String(folder.name)
})
})
const batchedSecret = transformSecretsToBatchSecretReq(
deletedSecretIds.current,
latestFileKey,
sec,
secrets?.secrets
String(router.query.folder),
secrets?.secrets,
);
// type check
if (!selectedEnv?.slug) return;
try {
console.log(5, {
requests: batchedSecret,
secretsPath: folderInfo?.path || "/",
workspaceId,
environment: selectedEnv?.slug
})
await batchSecretOp({
requests: batchedSecret,
secretsPath: folderInfo?.path || "/",
workspaceId,
environment: selectedEnv?.slug
});
@ -401,6 +479,40 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
);
}
const isFolderUpdate = Boolean(popUp?.createUpdateFolder?.data);
// const oldFolderName = (popUp?.createUpdateFolder?.data as { slug: string })?.slug;
const onFolderModalSubmit = async (data: any) => {
console.log('checkcheckcheckcheck', isFolderUpdate)
if (isFolderUpdate) {
console.log('updating', data.id)
// removeFolders(data.id)
updateFolders(fieldsFolders.map(folder => folder.id).indexOf(data.id), {
// id: data.id,
_id: data._id,
name: data.name,
});
} else {
// await onCreate(data);
if (secretContainer.current) {
secretContainer.current.scroll({
top: 0,
behavior: 'smooth'
});
}
console.log(555666)
prependFolders({
_id: undefined,
name: data.name,
}, { shouldFocus: false });
}
handlePopUpClose('createUpdateFolder');
};
// const onFolderDeleteSubmit = async (envSlug: string) => {
// await onDelete(envSlug);
// handlePopUpClose('deleteFolder');
// };
// when secrets is not loading and secrets list is empty
const isDashboardSecretEmpty = !isSecretsLoading && false;
// when using snapshot mode and snapshot is loading and snapshot list is empty
@ -426,6 +538,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
isProjectRelated
userAvailableEnvs={userAvailableEnvs}
onEnvChange={onEnvChange}
secretsPath={folderInfo?.path || "/"}
/>
</div>
{/* This is only for rollbacks */}
@ -526,11 +639,19 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
{!isReadOnly && !isRollbackMode && (
<>
<div className='block lg:hidden'>
<Tooltip content='Point-in-time Recovery'>
<Tooltip content='Add Secret'>
<IconButton
ariaLabel="recovery"
variant="outline_bg"
onClick={() => setIsSecretValueHidden.toggle()}
onClick={() => {
if (secretContainer.current) {
secretContainer.current.scroll({
top: 0,
behavior: 'smooth'
});
}
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
}}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
@ -555,6 +676,20 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
Add Secret
</Button>
</div>
<div className=''>
<Tooltip content='Add Folder'>
<IconButton
ariaLabel="recovery"
variant="outline_bg"
onClick={() => {
handlePopUpOpen('createUpdateFolder', undefined)
}}
>
<FontAwesomeIcon icon={faPlus} />
<FontAwesomeIcon icon={faFolder} className="pl-2"/>
</IconButton>
</Tooltip>
</div>
</>
)}
<Button
@ -579,6 +714,21 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
<table className="secret-table relative">
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
<tbody className="max-h-96 overflow-y-auto">
{/* ["Folder 1", "Folder 2", "Folder 3"] */}
{fieldsFolders.map(({ _id, name }, index) => (
<SecretFolderRow
key={_id}
index={index}
_id={String(_id)}
handlePopUpOpen={handlePopUpOpen}
// index={index}
searchTerm={searchFilter}
name={String(name)}
reset={reset}
onFolderDelete={() => {}}
// onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
/>
))}
{fields.map(({ id, _id }, index) => (
<SecretInputRow
key={id}
@ -612,6 +762,46 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</table>
</TableContainer>
)}
<Modal
isOpen={popUp?.createUpdateFolder?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle('createUpdateFolder', isOpen);
reset();
}}
>
<ModalContent title={isFolderUpdate ? 'Update folder name' : 'Create a new folder'}>
<form onSubmit={handleSubmit(onFolderModalSubmit)}>
{/* <Controller
control={control}
// defaultValue=""
name="folders"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Folder Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/> */}
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isFolderUpdate ? 'Update' : 'Create'}
</Button>
<Button colorSchema="secondary" variant="outline" className="border-none">
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
<PitDrawer
isDrawerOpen={popUp?.secretSnapshots?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('secretSnapshots', isOpen)}

View File

@ -66,9 +66,15 @@ const secretSchema = yup.object({
valueOverride: yup.string().trim().notRequired()
});
const folderSchema = yup.object({
_id: yup.string(),
name: yup.string().trim(),
});
export const schema = yup.object({
isSnapshotMode: yup.bool().notRequired(),
secrets: yup.array(secretSchema)
secrets: yup.array(secretSchema),
folders: yup.array(folderSchema)
});
export type FormData = yup.InferType<typeof schema>;
@ -159,7 +165,8 @@ export const transformSecretsToBatchSecretReq = (
deletedSecretIds: string[],
latestFileKey: any,
secrets: FormData['secrets'],
intialValues: DecryptedSecret[] = []
folderId: string,
intialValues: DecryptedSecret[] = [],
) => {
// deleted secrets
const secretsToBeDeleted: BatchSecretDTO['requests'] = deletedSecretIds.map((id) => ({
@ -198,6 +205,7 @@ export const transformSecretsToBatchSecretReq = (
type: 'personal',
tags,
secretName: key,
folderId,
...encryptASecret(randomBytes, key, valueOverride, comment)
}
});
@ -210,6 +218,7 @@ export const transformSecretsToBatchSecretReq = (
type: 'shared',
tags,
secretName: key,
folderId,
...encryptASecret(randomBytes, key, value, comment)
}
});
@ -227,6 +236,7 @@ export const transformSecretsToBatchSecretReq = (
type: 'shared',
tags,
secretName: key,
folderId,
...encryptASecret(randomBytes, key, value, comment)
}
});
@ -247,6 +257,7 @@ export const transformSecretsToBatchSecretReq = (
type: 'personal',
tags,
secretName: key,
folderId,
...encryptASecret(randomBytes, key, valueOverride, comment)
}
});

View File

@ -0,0 +1,43 @@
/* eslint-disable react/jsx-no-useless-fragment */
import { faAngleRight, faFolder } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
type Props = {
folderName: string;
};
export const EnvComparisonFolder = ({
folderName,
}: Props): JSX.Element => {
// const [isOpen, setIsOpen] = useState(true);
// const getSecretByEnv = useCallback(
// (secEnv: string, secs?: any[]) => secs?.find(({ env }) => env === secEnv),
// []
// );
return (
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800">
<td className="flex h-10 w-14 items-center justify-center border-none">
<div className="text-center text-xs flex itesm-center flex-row pr-4">
<FontAwesomeIcon icon={faAngleRight} className="w-3.5 h-3.5 text-bunker-400 pl-6 pt-[0.05rem]" />
<FontAwesomeIcon icon={faFolder} className="w-4 h-4 text-yellow-400/50 pl-1" />
</div>
</td>
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
<div className="flex h-8 cursor-default flex-row items-center truncate">
{folderName}
</div>
</td>
{/* {userAvailableEnvs?.map(({ slug }) => (
<DashboardInput
isReadOnly={isReadOnly}
key={`row-${folderName || ''}-${slug}`}
isOverridden={false}
secret={getSecretByEnv(slug, folderName)}
isSecretValueHidden={areValuesHiddenThisRow && isSecretValueHidden}
/>
))} */}
</tr>
);
};

View File

@ -1,11 +1,10 @@
/* eslint-disable react/jsx-no-useless-fragment */
import { useCallback, useState } from 'react';
import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
import { faCircle, faEye, faEyeSlash, faKey } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { twMerge } from 'tailwind-merge';
type Props = {
index: number;
secrets: any[] | undefined;
// permission and external state's that decided to hide or show
isReadOnly?: boolean;
@ -108,7 +107,6 @@ const DashboardInput = ({
};
export const EnvComparisonRow = ({
index,
secrets,
isSecretValueHidden,
isReadOnly,
@ -123,8 +121,10 @@ export const EnvComparisonRow = ({
return (
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800">
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
<td className="flex h-10 w-14 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs">
<FontAwesomeIcon icon={faKey} className="w-3 h-3 text-blue-400/50 pl-7 pt-0.5" />
</div>
</td>
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
<div className="flex h-8 cursor-default flex-row items-center truncate">

View File

@ -1 +1,2 @@
export { EnvComparisonFolder } from './EnvComparisonFolder';
export { EnvComparisonRow } from './EnvComparisonRow';

View File

@ -0,0 +1,156 @@
/* eslint-disable react/jsx-no-useless-fragment */
import { memo, useRef } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useRouter } from 'next/router';
import {
faFolder,
faPencil,
faXmark
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
IconButton,
Tooltip
} from '@app/components/v2';
import { FormData } from '../../DashboardPage.utils';
type Props = {
index: number;
_id: string;
// permission and external state's that decided to hide or show
isReadOnly?: boolean;
isAddOnly?: boolean;
isRollbackMode?: boolean;
searchTerm: string;
// to record the ids of deleted ones
onFolderDelete: (index: number, id?: string, overrideId?: string) => void;
// sidebar control props
handlePopUpOpen: (popUpName: "createUpdateFolder", data: any) => void;
name: string;
reset: any;
};
export const SecretFolderRow = memo(
({
index,
_id,
handlePopUpOpen,
isReadOnly,
isRollbackMode,
isAddOnly,
onFolderDelete,
searchTerm,
name,
reset
}: Props): JSX.Element => {
const isKeySubDisabled = useRef<boolean>(false);
const {
// register, setValue,
control } = useFormContext<FormData>();
console.log(123, _id, name)
const router = useRouter();
// to get details on a secret
// const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction` });
const idOverride = useWatch({ control, name: `secrets.${index}.idOverride` });
const secComment = useWatch({ control, name: `secrets.${index}.comment` });
const secKey = useWatch({
control,
name: `secrets.${index}.key`,
disabled: isKeySubDisabled.current
});
const secId = useWatch({ control, name: `secrets.${index}._id` });
const tags = useWatch({ control, name: `secrets.${index}.tags`, defaultValue: [] }) || [];
// const selectedTagIds = tags.reduce<Record<string, boolean>>(
// (prev, curr) => ({ ...prev, [curr.slug]: true }),
// {}
// );
// const isCreatedSecret = !secId;
// const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly;
// Why this instead of filter in parent
// Because rhf field.map has default values so basically
// keys are not updated there and index needs to kept so that we can monitor
// values individually here
if (
!(
secKey?.toUpperCase().includes(searchTerm?.toUpperCase()) ||
tags
?.map((tag) => tag.name)
.join(' ')
?.toUpperCase()
.includes(searchTerm?.toUpperCase()) ||
secComment?.toUpperCase().includes(searchTerm?.toUpperCase())
)
) {
return <></>;
}
return (
<tr
className="group flex flex-row items-center cursor-default hover:bg-mineshaft-700"
key={index}
>
<td className="flex h-10 w-10 items-center justify-center px-4 border-none">
{/* <div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div> */}
<div className="w-10 text-center text-xs"><FontAwesomeIcon icon={faFolder} className="w-4 h-4 text-yellow-400/50 pl-2.5 pt-0.5" /></div>
</td>
<button
type="button"
className="w-full border-none ml-2.5 text-left cursor-default"
onClick={async () => {
await router.push({
pathname: router.pathname,
query: { ...router.query, folder: _id }
})
router.reload();
}}
>{name}</button>
<td className="min-w-sm flex h-10 items-center">
<div className="duration-0 ml-auto w-0 flex items-center justify-end space-x-2.5 overflow-hidden transition-all w-16 border-l border-mineshaft-600 h-10">
{!isAddOnly && (
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Settings" className="z-50">
<IconButton
size="md"
colorSchema="primary"
variant="plain"
onClick={() => {
console.log(888, {id: _id, name})
handlePopUpOpen('createUpdateFolder', {id: _id, name});
reset({id: _id, name});
}}
ariaLabel="expand"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
</div>
)}
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete" className="z-50">
<IconButton
size="md"
variant="plain"
colorSchema="danger"
ariaLabel="delete"
isDisabled={isReadOnly || isRollbackMode}
onClick={() => onFolderDelete(index, secId, idOverride)}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Tooltip>
</div>
</div>
</td>
</tr>
);
}
);
SecretFolderRow.displayName = 'SecretFolderRow';

View File

@ -0,0 +1 @@
export { SecretFolderRow } from './SecretFolderRow';

View File

@ -161,7 +161,7 @@ export const SecretInputRow = memo(
<tr className="group flex flex-row items-center" key={index}>
<td className="flex h-10 w-10 items-center justify-center px-4 border-none">
{/* <div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div> */}
<div className="w-10 text-center text-xs text-bunker-400"><FontAwesomeIcon icon={faKey} className="w-4 h-4 text-bunker-400/60 pl-2.5 pt-0.5" /></div>
<div className="w-10 text-center text-xs text-bunker-400"><FontAwesomeIcon icon={faKey} className="w-4 h-4 text-blue-400/50 pl-2.5 pt-0.5" /></div>
</td>
<Controller
control={control}

View File

@ -216,7 +216,6 @@ export const EnvironmentSection = ({
>
{isEnvUpdate ? 'Update' : 'Create'}
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>