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, isOrganizationRelated,
currentEnv, currentEnv,
userAvailableEnvs, userAvailableEnvs,
onEnvChange onEnvChange,
secretsPath
}: { }: {
pageName: string; pageName: string;
isProjectRelated?: boolean; isProjectRelated?: boolean;
@ -37,6 +38,7 @@ export default function NavHeader({
currentEnv?: string; currentEnv?: string;
userAvailableEnvs?: any[]; userAvailableEnvs?: any[];
onEnvChange?: (slug: string) => void; onEnvChange?: (slug: string) => void;
secretsPath?: string;
}): JSX.Element { }): JSX.Element {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
@ -75,7 +77,7 @@ export default function NavHeader({
{currentEnv && ( {currentEnv && (
<> <>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" /> <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"> <Tooltip content="Select environment">
<Select <Select
value={userAvailableEnvs?.filter((uae) => uae.name === currentEnv)[0]?.slug} value={userAvailableEnvs?.filter((uae) => uae.name === currentEnv)[0]?.slug}
@ -92,9 +94,23 @@ export default function NavHeader({
))} ))}
</Select> </Select>
</Tooltip> </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> </div>
); );
} }

View File

@ -7,9 +7,10 @@ type Props = {
isOpen?: boolean; isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void; onOpenChange?: (isOpen: boolean) => void;
text: string; 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}> <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent <ModalContent
title="Unleash Infisical's Full Power" 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="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. 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> </ModalContent>
</Modal> </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 = { export const secretKeys = {
// this is also used in secretSnapshot part // this is also used in secretSnapshot part
getProjectSecret: (workspaceId: string, env: string | string[]) => [ getProjectSecret: (workspaceId: string, env: string | string[], secretsPath: string) => [
{ workspaceId, env }, { workspaceId, env, secretsPath },
'secrets' 'secrets'
], ],
getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions'] 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') { if (typeof env === 'string') {
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
params: { params: {
environment: env, environment: env,
workspaceId workspaceId,
secretsPath: secretsPath || '/'
} }
}); });
return data.secrets; 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', { const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
params: { params: {
environment: envPoint, environment: envPoint,
workspaceId workspaceId,
secretsPath: secretsPath || '/'
} }
}); });
allEnvData = allEnvData.concat(data.secrets); allEnvData = allEnvData.concat(data.secrets);
@ -62,14 +64,15 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | s
export const useGetProjectSecrets = ({ export const useGetProjectSecrets = ({
workspaceId, workspaceId,
env, env,
secretsPath,
decryptFileKey, decryptFileKey,
isPaused isPaused
}: GetProjectSecretsDTO) => }: GetProjectSecretsDTO) =>
useQuery({ useQuery({
// wait for all values to be available // wait for all values to be available
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env), queryKey: secretKeys.getProjectSecret(workspaceId, env, secretsPath),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env), queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, secretsPath),
select: (data) => { select: (data) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const latestKey = decryptFileKey; const latestKey = decryptFileKey;
@ -145,14 +148,15 @@ export const useGetProjectSecrets = ({
export const useGetProjectSecretsByKey = ({ export const useGetProjectSecretsByKey = ({
workspaceId, workspaceId,
env, env,
decryptFileKey, decryptFileKey,
secretsPath,
isPaused isPaused
}: GetProjectSecretsDTO) => }: GetProjectSecretsDTO) =>
useQuery({ useQuery({
// wait for all values to be available // wait for all values to be available
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env), queryKey: secretKeys.getProjectSecret(workspaceId, env, secretsPath),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env), queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, secretsPath),
select: (data) => { select: (data) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const latestKey = decryptFileKey; const latestKey = decryptFileKey;
@ -283,7 +287,7 @@ export const useBatchSecretsOp = () => {
return data; return data;
}, },
onSuccess: (_, dto) => { 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.list(dto.workspaceId));
queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId)); queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId));
} }

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
import { useEffect, useRef, useState } from 'react'; 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 { useTranslation } from 'react-i18next';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { import {
@ -10,6 +12,7 @@ import {
faDownload, faDownload,
faEye, faEye,
faEyeSlash, faEyeSlash,
faFolder,
faMagnifyingGlass, faMagnifyingGlass,
faPlus faPlus
} from '@fortawesome/free-solid-svg-icons'; } 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 NavHeader from '@app/components/navigation/NavHeader';
import { import {
Button, Button,
// FormControl,
IconButton, IconButton,
Input, Input,
Modal, Modal,
@ -50,6 +54,7 @@ import {
usePerformSecretRollback, usePerformSecretRollback,
useRegisterUserAction useRegisterUserAction
} from '@app/hooks/api'; } from '@app/hooks/api';
import { useFolderOp, useGetProjectFolderById, useGetProjectFolders } from '@app/hooks/api/secretFolders/queries';
import { secretKeys } from '@app/hooks/api/secrets/queries'; import { secretKeys } from '@app/hooks/api/secrets/queries';
import { WorkspaceEnv } from '@app/hooks/api/types'; import { WorkspaceEnv } from '@app/hooks/api/types';
@ -58,6 +63,7 @@ import { CreateTagModal } from './components/CreateTagModal';
import { PitDrawer } from './components/PitDrawer'; import { PitDrawer } from './components/PitDrawer';
import { SecretDetailDrawer } from './components/SecretDetailDrawer'; import { SecretDetailDrawer } from './components/SecretDetailDrawer';
import { SecretDropzone } from './components/SecretDropzone'; import { SecretDropzone } from './components/SecretDropzone';
import { SecretFolderRow } from './components/SecretFolderRow';
import { SecretInputRow } from './components/SecretInputRow'; import { SecretInputRow } from './components/SecretInputRow';
import { SecretTableHeader } from './components/SecretTableHeader'; import { SecretTableHeader } from './components/SecretTableHeader';
import { import {
@ -96,7 +102,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
'addTag', 'addTag',
'secretSnapshots', 'secretSnapshots',
'uploadedSecOpts', 'uploadedSecOpts',
'compareSecrets' 'compareSecrets',
'createUpdateFolder',
'deleteFolder'
] as const); ] as const);
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true); const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
const [searchFilter, setSearchFilter] = useState(''); const [searchFilter, setSearchFilter] = useState('');
@ -138,13 +146,41 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
decryptFileKey: latestFileKey! 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({ const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId, workspaceId,
env: selectedEnv?.slug || '', env: selectedEnv?.slug || '',
secretsPath: folderInfo?.path || "/",
decryptFileKey: latestFileKey!, decryptFileKey: latestFileKey!,
isPaused: Boolean(snapshotId) 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 { const {
data: secretSnaphots, data: secretSnaphots,
fetchNextPage, fetchNextPage,
@ -168,12 +204,14 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = const { data: snapshotCount, isLoading: isLoadingSnapshotCount } =
useGetWsSnapshotCount(workspaceId); useGetWsSnapshotCount(workspaceId);
console.log(45678, workspaceId)
const { data: wsTags } = useGetWsTags(workspaceId); const { data: wsTags } = useGetWsTags(workspaceId);
// mutation calls // mutation calls
const { mutateAsync: batchSecretOp } = useBatchSecretsOp(); const { mutateAsync: batchSecretOp } = useBatchSecretsOp();
const { mutateAsync: performSecretRollback } = usePerformSecretRollback(); const { mutateAsync: performSecretRollback } = usePerformSecretRollback();
const { mutateAsync: registerUserAction } = useRegisterUserAction(); const { mutateAsync: registerUserAction } = useRegisterUserAction();
const { mutateAsync: createWsTag } = useCreateWsTag(); const { mutateAsync: createWsTag } = useCreateWsTag();
const { mutateAsync: addFolder } = useFolderOp();
const method = useForm<FormData>({ const method = useForm<FormData>({
// why any: well yup inferred ts expects other keys to defined as undefined // 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) 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 { const {
control, control,
handleSubmit, handleSubmit,
@ -191,7 +237,24 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
formState: { isSubmitting, isDirty }, formState: { isSubmitting, isDirty },
reset reset
} = method; } = 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, 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 isRollbackMode = Boolean(snapshotId);
const isReadOnly = selectedEnv?.isWriteDenied; const isReadOnly = selectedEnv?.isWriteDenied;
const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied; const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied;
@ -286,7 +349,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
setValue('isSnapshotMode', false); setValue('isSnapshotMode', false);
setSnaphotId(null); setSnaphotId(null);
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || '') secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || '', folderInfo?.path)
); );
createNotification({ createNotification({
text: 'Successfully rollback secrets', text: 'Successfully rollback secrets',
@ -314,17 +377,32 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const sec = isAddOnly ? userSec.filter(({ _id }) => !_id) : userSec; const sec = isAddOnly ? userSec.filter(({ _id }) => !_id) : userSec;
// encrypt and format the secrets to batch api format // encrypt and format the secrets to batch api format
// requests = [ {method:"", secret:""} ] // 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( const batchedSecret = transformSecretsToBatchSecretReq(
deletedSecretIds.current, deletedSecretIds.current,
latestFileKey, latestFileKey,
sec, sec,
secrets?.secrets String(router.query.folder),
secrets?.secrets,
); );
// type check // type check
if (!selectedEnv?.slug) return; if (!selectedEnv?.slug) return;
try { try {
console.log(5, {
requests: batchedSecret,
secretsPath: folderInfo?.path || "/",
workspaceId,
environment: selectedEnv?.slug
})
await batchSecretOp({ await batchSecretOp({
requests: batchedSecret, requests: batchedSecret,
secretsPath: folderInfo?.path || "/",
workspaceId, workspaceId,
environment: selectedEnv?.slug 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 // when secrets is not loading and secrets list is empty
const isDashboardSecretEmpty = !isSecretsLoading && false; const isDashboardSecretEmpty = !isSecretsLoading && false;
// when using snapshot mode and snapshot is loading and snapshot list is empty // when using snapshot mode and snapshot is loading and snapshot list is empty
@ -426,6 +538,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
isProjectRelated isProjectRelated
userAvailableEnvs={userAvailableEnvs} userAvailableEnvs={userAvailableEnvs}
onEnvChange={onEnvChange} onEnvChange={onEnvChange}
secretsPath={folderInfo?.path || "/"}
/> />
</div> </div>
{/* This is only for rollbacks */} {/* This is only for rollbacks */}
@ -526,11 +639,19 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
{!isReadOnly && !isRollbackMode && ( {!isReadOnly && !isRollbackMode && (
<> <>
<div className='block lg:hidden'> <div className='block lg:hidden'>
<Tooltip content='Point-in-time Recovery'> <Tooltip content='Add Secret'>
<IconButton <IconButton
ariaLabel="recovery" ariaLabel="recovery"
variant="outline_bg" 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} /> <FontAwesomeIcon icon={faPlus} />
</IconButton> </IconButton>
@ -555,6 +676,20 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
Add Secret Add Secret
</Button> </Button>
</div> </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 <Button
@ -579,6 +714,21 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
<table className="secret-table relative"> <table className="secret-table relative">
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} /> <SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
<tbody className="max-h-96 overflow-y-auto"> <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) => ( {fields.map(({ id, _id }, index) => (
<SecretInputRow <SecretInputRow
key={id} key={id}
@ -612,6 +762,46 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</table> </table>
</TableContainer> </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 <PitDrawer
isDrawerOpen={popUp?.secretSnapshots?.isOpen} isDrawerOpen={popUp?.secretSnapshots?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('secretSnapshots', isOpen)} onOpenChange={(isOpen) => handlePopUpToggle('secretSnapshots', isOpen)}

View File

@ -66,9 +66,15 @@ const secretSchema = yup.object({
valueOverride: yup.string().trim().notRequired() valueOverride: yup.string().trim().notRequired()
}); });
const folderSchema = yup.object({
_id: yup.string(),
name: yup.string().trim(),
});
export const schema = yup.object({ export const schema = yup.object({
isSnapshotMode: yup.bool().notRequired(), isSnapshotMode: yup.bool().notRequired(),
secrets: yup.array(secretSchema) secrets: yup.array(secretSchema),
folders: yup.array(folderSchema)
}); });
export type FormData = yup.InferType<typeof schema>; export type FormData = yup.InferType<typeof schema>;
@ -159,7 +165,8 @@ export const transformSecretsToBatchSecretReq = (
deletedSecretIds: string[], deletedSecretIds: string[],
latestFileKey: any, latestFileKey: any,
secrets: FormData['secrets'], secrets: FormData['secrets'],
intialValues: DecryptedSecret[] = [] folderId: string,
intialValues: DecryptedSecret[] = [],
) => { ) => {
// deleted secrets // deleted secrets
const secretsToBeDeleted: BatchSecretDTO['requests'] = deletedSecretIds.map((id) => ({ const secretsToBeDeleted: BatchSecretDTO['requests'] = deletedSecretIds.map((id) => ({
@ -198,6 +205,7 @@ export const transformSecretsToBatchSecretReq = (
type: 'personal', type: 'personal',
tags, tags,
secretName: key, secretName: key,
folderId,
...encryptASecret(randomBytes, key, valueOverride, comment) ...encryptASecret(randomBytes, key, valueOverride, comment)
} }
}); });
@ -210,6 +218,7 @@ export const transformSecretsToBatchSecretReq = (
type: 'shared', type: 'shared',
tags, tags,
secretName: key, secretName: key,
folderId,
...encryptASecret(randomBytes, key, value, comment) ...encryptASecret(randomBytes, key, value, comment)
} }
}); });
@ -227,6 +236,7 @@ export const transformSecretsToBatchSecretReq = (
type: 'shared', type: 'shared',
tags, tags,
secretName: key, secretName: key,
folderId,
...encryptASecret(randomBytes, key, value, comment) ...encryptASecret(randomBytes, key, value, comment)
} }
}); });
@ -247,6 +257,7 @@ export const transformSecretsToBatchSecretReq = (
type: 'personal', type: 'personal',
tags, tags,
secretName: key, secretName: key,
folderId,
...encryptASecret(randomBytes, key, valueOverride, comment) ...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 */ /* eslint-disable react/jsx-no-useless-fragment */
import { useCallback, useState } from 'react'; 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
type Props = { type Props = {
index: number;
secrets: any[] | undefined; secrets: any[] | undefined;
// permission and external state's that decided to hide or show // permission and external state's that decided to hide or show
isReadOnly?: boolean; isReadOnly?: boolean;
@ -108,7 +107,6 @@ const DashboardInput = ({
}; };
export const EnvComparisonRow = ({ export const EnvComparisonRow = ({
index,
secrets, secrets,
isSecretValueHidden, isSecretValueHidden,
isReadOnly, isReadOnly,
@ -123,8 +121,10 @@ export const EnvComparisonRow = ({
return ( return (
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800"> <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"> <td className="flex h-10 w-14 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div> <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>
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]"> <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"> <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'; 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}> <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"> <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">{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> </td>
<Controller <Controller
control={control} control={control}

View File

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