feat(folders): implemented ui for folders in dashboard

This commit is contained in:
akhilmhdh
2023-06-04 13:18:05 +05:30
parent deaa85cbe7
commit 579516bd38
19 changed files with 708 additions and 128 deletions
@@ -1,12 +1,23 @@
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; import { faAngleRight, faHome } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useOrganization, useWorkspace } from '@app/context'; import { useOrganization, useWorkspace } from '@app/context';
import { Select, SelectItem, Tooltip } from '../v2'; import { Select, SelectItem, Tooltip } from '../v2';
type Props = {
pageName: string;
isProjectRelated?: boolean;
isOrganizationRelated?: boolean;
currentEnv?: string;
userAvailableEnvs?: any[];
onEnvChange?: (slug: string) => void;
folders?: Array<{ id: string; name: string }>;
isFolderMode?: boolean;
};
// TODO: make links clickable and clean up // TODO: make links clickable and clean up
/** /**
@@ -29,15 +40,10 @@ export default function NavHeader({
isOrganizationRelated, isOrganizationRelated,
currentEnv, currentEnv,
userAvailableEnvs, userAvailableEnvs,
onEnvChange onEnvChange,
}: { folders,
pageName: string; isFolderMode
isProjectRelated?: boolean; }: Props): JSX.Element {
isOrganizationRelated?: boolean;
currentEnv?: string;
userAvailableEnvs?: any[];
onEnvChange?: (slug: string) => void;
}): JSX.Element {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const router = useRouter(); const router = useRouter();
@@ -95,6 +101,26 @@ export default function NavHeader({
</div> </div>
</> </>
)} )}
{isFolderMode &&
folders?.map(({ id, name }, index) => {
const query = { ...router.query };
if (name !== 'root') query.folderId = id;
else delete query.folderId;
return (
<div className="flex items-center space-x-3" key={`breadcrumb-folder-${id}`}>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
{index + 1 === folders?.length ? (
<span className="text-sm font-semibold text-bunker-300">{name}</span>
) : (
<Link passHref legacyBehavior href={{ pathname: '/dashboard/[id]', query }}>
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
{name === 'root' ? <FontAwesomeIcon icon={faHome} /> : name}
</a>
</Link>
)}
</div>
);
})}
</div> </div>
); );
} }
@@ -1,4 +1,5 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { SizeProp } from '@fortawesome/fontawesome-svg-core';
import { faCubesStacked, IconDefinition } from '@fortawesome/free-solid-svg-icons'; import { faCubesStacked, IconDefinition } 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';
@@ -8,13 +9,25 @@ type Props = {
className?: string; className?: string;
children?: ReactNode; children?: ReactNode;
icon?: IconDefinition; icon?: IconDefinition;
iconSize?: SizeProp;
}; };
export const EmptyState = ({ title, className, children, icon = faCubesStacked }: Props) => ( export const EmptyState = ({
<div className={twMerge('flex w-full bg-bunker-700 flex-col items-center px-2 pt-6 text-bunker-300', className)}> title,
<FontAwesomeIcon icon={icon} size="2x" className='mr-4' /> className,
<div className='flex flex-row items-center py-4'> children,
<div className="text-bunker-300 text-sm">{title}</div> icon = faCubesStacked,
iconSize = '2x'
}: Props) => (
<div
className={twMerge(
'flex w-full flex-col items-center bg-bunker-700 px-2 pt-6 text-bunker-300',
className
)}
>
<FontAwesomeIcon icon={icon} size={iconSize} className="mr-4" />
<div className="flex flex-row items-center py-4">
<div className="text-sm text-bunker-300">{title}</div>
<div>{children}</div> <div>{children}</div>
</div> </div>
</div> </div>
@@ -35,7 +35,7 @@ export const Tooltip = ({
sideOffset={5} sideOffset={5}
{...props} {...props}
className={twMerge( className={twMerge(
`z-20 select-none max-w-[15rem] rounded-md bg-mineshaft-800 border border-mineshaft-600 py-2 px-4 font-light text-sm text-bunker-200 shadow-md `z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
+1
View File
@@ -2,6 +2,7 @@ export * from './auth';
export * from './incidentContacts'; export * from './incidentContacts';
export * from './keys'; export * from './keys';
export * from './organization'; export * from './organization';
export * from './secretFolders';
export * from './secrets'; export * from './secrets';
export * from './secretSnapshots'; export * from './secretSnapshots';
export * from './serviceAccounts'; export * from './serviceAccounts';
@@ -0,0 +1 @@
export { useCreateFolder, useDeleteFolder, useGetProjectFolders, useUpdateFolder } from './queries';
@@ -0,0 +1,129 @@
import { useCallback } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { secretSnapshotKeys } from '../secretSnapshots/queries';
import {
CreateFolderDTO,
DeleteFolderDTO,
GetProjectFoldersDTO,
TSecretFolder,
UpdateFolderDTO
} from './types';
const queryKeys = {
getSecretFolders: (workspaceId: string, environment: string, parentFolderId?: string) =>
['secret-folders', { workspaceId, environment, parentFolderId }] as const
};
export const useGetProjectFolders = ({
workspaceId,
parentFolderId,
environment,
isPaused,
sortDir
}: GetProjectFoldersDTO) =>
useQuery({
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId),
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
queryFn: async () => {
const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>(
'/api/v1/folders',
{
params: {
workspaceId,
environment,
parentFolderId
}
}
);
return data;
},
select: useCallback(
({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
dir,
folders: folders.sort((a, b) =>
sortDir === 'asc'
? a?.name?.localeCompare(b?.name || '')
: b?.name?.localeCompare(a?.name || '')
)
}),
[sortDir]
)
});
export const useCreateFolder = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, CreateFolderDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.post('/api/v1/folders', dto);
return data;
},
onSuccess: (_, { workspaceId, environment, parentFolderId }) => {
queryClient.invalidateQueries(
queryKeys.getSecretFolders(workspaceId, environment, parentFolderId)
);
queryClient.invalidateQueries(
secretSnapshotKeys.count(workspaceId, environment, parentFolderId)
);
queryClient.invalidateQueries(
secretSnapshotKeys.list(workspaceId, environment, parentFolderId)
);
}
});
};
export const useUpdateFolder = (parentFolderId: string) => {
const queryClient = useQueryClient();
return useMutation<{}, {}, UpdateFolderDTO>({
mutationFn: async ({ folderId, name, environment, workspaceId }) => {
const { data } = await apiRequest.patch(`/api/v1/folders/${folderId}`, {
name,
environment,
workspaceId
});
return data;
},
onSuccess: (_, { workspaceId, environment }) => {
queryClient.invalidateQueries(
queryKeys.getSecretFolders(workspaceId, environment, parentFolderId)
);
queryClient.invalidateQueries(
secretSnapshotKeys.count(workspaceId, environment, parentFolderId)
);
queryClient.invalidateQueries(
secretSnapshotKeys.list(workspaceId, environment, parentFolderId)
);
}
});
};
export const useDeleteFolder = (parentFolderId: string) => {
const queryClient = useQueryClient();
return useMutation<{}, {}, DeleteFolderDTO>({
mutationFn: async ({ folderId, environment, workspaceId }) => {
const { data } = await apiRequest.delete(`/api/v1/folders/${folderId}`, {
data: {
environment,
workspaceId
}
});
return data;
},
onSuccess: (_, { workspaceId, environment }) => {
queryClient.invalidateQueries(
queryKeys.getSecretFolders(workspaceId, environment, parentFolderId)
);
queryClient.invalidateQueries(
secretSnapshotKeys.count(workspaceId, environment, parentFolderId)
);
queryClient.invalidateQueries(
secretSnapshotKeys.list(workspaceId, environment, parentFolderId)
);
}
});
};
@@ -0,0 +1,32 @@
export type TSecretFolder = {
id: string;
name: string;
};
export type GetProjectFoldersDTO = {
workspaceId: string;
environment: string;
parentFolderId?: string;
isPaused?: boolean;
sortDir?: 'asc' | 'desc';
};
export type CreateFolderDTO = {
workspaceId: string;
environment: string;
folderName: string;
parentFolderId?: string;
};
export type UpdateFolderDTO = {
workspaceId: string;
environment: string;
name: string;
folderId: string;
};
export type DeleteFolderDTO = {
workspaceId: string;
environment: string;
folderId: string;
};
@@ -17,18 +17,31 @@ import {
} from './types'; } from './types';
export const secretSnapshotKeys = { export const secretSnapshotKeys = {
list: (workspaceId: string) => [{ workspaceId }, 'secret-snapshot'] as const, list: (workspaceId: string, env: string, folderId?: string) =>
[{ workspaceId, env, folderId }, 'secret-snapshot'] as const,
snapshotSecrets: (snapshotId: string) => [{ snapshotId }, 'secret-snapshot'] as const, snapshotSecrets: (snapshotId: string) => [{ snapshotId }, 'secret-snapshot'] as const,
count: (workspaceId: string) => [{ workspaceId }, 'count', 'secret-snapshot'] count: (workspaceId: string, env: string, folderId?: string) => [
{ workspaceId, env, folderId },
'count',
'secret-snapshot'
]
}; };
const fetchWorkspaceSecretSnaphots = async (workspaceId: string, limit = 10, offset = 0) => { const fetchWorkspaceSecretSnaphots = async (
workspaceId: string,
environment: string,
folderId?: string,
limit = 10,
offset = 0
) => {
const res = await apiRequest.get<{ secretSnapshots: TWorkspaceSecretSnapshot[] }>( const res = await apiRequest.get<{ secretSnapshots: TWorkspaceSecretSnapshot[] }>(
`/api/v1/workspace/${workspaceId}/secret-snapshots`, `/api/v1/workspace/${workspaceId}/secret-snapshots`,
{ {
params: { params: {
limit, limit,
offset offset,
environment,
folderId
} }
} }
); );
@@ -38,9 +51,16 @@ const fetchWorkspaceSecretSnaphots = async (workspaceId: string, limit = 10, off
export const useGetWorkspaceSecretSnapshots = (dto: GetWorkspaceSecretSnapshotsDTO) => export const useGetWorkspaceSecretSnapshots = (dto: GetWorkspaceSecretSnapshotsDTO) =>
useInfiniteQuery({ useInfiniteQuery({
enabled: Boolean(dto.workspaceId), enabled: Boolean(dto.workspaceId && dto.environment),
queryKey: secretSnapshotKeys.list(dto.workspaceId), queryKey: secretSnapshotKeys.list(dto.workspaceId, dto.environment, dto?.folder),
queryFn: ({ pageParam }) => fetchWorkspaceSecretSnaphots(dto.workspaceId, dto.limit, pageParam), queryFn: ({ pageParam }) =>
fetchWorkspaceSecretSnaphots(
dto.workspaceId,
dto.environment,
dto?.folder,
dto.limit,
pageParam
),
getNextPageParam: (lastPage, pages) => getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * dto.limit : undefined lastPage.length !== 0 ? pages.length * dto.limit : undefined
}); });
@@ -115,40 +135,53 @@ export const useGetSnapshotSecrets = ({ decryptFileKey, env, snapshotId }: TSnap
} }
}); });
return { version: data.version, secrets: sharedSecrets, createdAt: data.createdAt }; return {
version: data.version,
secrets: sharedSecrets,
createdAt: data.createdAt,
folders: data.folderVersion
};
} }
}); });
const fetchWorkspaceSecretSnaphotCount = async (workspaceId: string) => { const fetchWorkspaceSecretSnaphotCount = async (
workspaceId: string,
environment: string,
folderId?: string
) => {
const res = await apiRequest.get<{ count: number }>( const res = await apiRequest.get<{ count: number }>(
`/api/v1/workspace/${workspaceId}/secret-snapshots/count` `/api/v1/workspace/${workspaceId}/secret-snapshots/count`,
{
params: {
environment,
folderId
}
}
); );
return res.data.count; return res.data.count;
}; };
export const useGetWsSnapshotCount = (workspaceId: string) => export const useGetWsSnapshotCount = (workspaceId: string, env: string, folderId?: string) =>
useQuery({ useQuery({
enabled: Boolean(workspaceId), enabled: Boolean(workspaceId && env),
queryKey: secretSnapshotKeys.count(workspaceId), queryKey: secretSnapshotKeys.count(workspaceId, env, folderId),
queryFn: () => fetchWorkspaceSecretSnaphotCount(workspaceId) queryFn: () => fetchWorkspaceSecretSnaphotCount(workspaceId, env, folderId)
}); });
export const usePerformSecretRollback = () => { export const usePerformSecretRollback = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, TSecretRollbackDTO>({ return useMutation<{}, {}, TSecretRollbackDTO>({
mutationFn: async (dto) => { mutationFn: async ({ workspaceId, ...dto }) => {
const { data } = await apiRequest.post( const { data } = await apiRequest.post(
`/api/v1/workspace/${dto.workspaceId}/secret-snapshots/rollback`, `/api/v1/workspace/${workspaceId}/secret-snapshots/rollback`,
{ dto
version: dto.version
}
); );
return data; return data;
}, },
onSuccess: (_, dto) => { onSuccess: (_, { workspaceId, environment, folderId }) => {
queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId)); queryClient.invalidateQueries(secretSnapshotKeys.list(workspaceId, environment, folderId));
queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId)); queryClient.invalidateQueries(secretSnapshotKeys.count(workspaceId, environment, folderId));
} }
}); });
}; };
@@ -13,6 +13,7 @@ export type TWorkspaceSecretSnapshot = {
export type TSnapshotSecret = Omit<TWorkspaceSecretSnapshot, 'secretVersions'> & { export type TSnapshotSecret = Omit<TWorkspaceSecretSnapshot, 'secretVersions'> & {
secretVersions: EncryptedSecretVersion[]; secretVersions: EncryptedSecretVersion[];
folderVersion: Array<{ name: string; id: string }>;
}; };
export type TSnapshotSecretProps = { export type TSnapshotSecretProps = {
@@ -24,9 +25,13 @@ export type TSnapshotSecretProps = {
export type GetWorkspaceSecretSnapshotsDTO = { export type GetWorkspaceSecretSnapshotsDTO = {
workspaceId: string; workspaceId: string;
limit: number; limit: number;
environment: string;
folder?: string;
}; };
export type TSecretRollbackDTO = { export type TSecretRollbackDTO = {
workspaceId: string; workspaceId: string;
version: number; version: number;
environment: string;
folderId?: string;
}; };
+24 -11
View File
@@ -19,19 +19,24 @@ 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[], folderId?: string) => [
{ workspaceId, env }, { workspaceId, env, folderId },
'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[],
folderId?: 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,
folderId: folderId || undefined
} }
}); });
return data.secrets; return data.secrets;
@@ -46,7 +51,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,
folderId
} }
}); });
allEnvData = allEnvData.concat(data.secrets); allEnvData = allEnvData.concat(data.secrets);
@@ -63,13 +69,14 @@ export const useGetProjectSecrets = ({
workspaceId, workspaceId,
env, env,
decryptFileKey, decryptFileKey,
isPaused isPaused,
folderId
}: 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, folderId),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env), queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
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,9 +290,15 @@ export const useBatchSecretsOp = () => {
return data; return data;
}, },
onSuccess: (_, dto) => { onSuccess: (_, dto) => {
queryClient.invalidateQueries(secretKeys.getProjectSecret(dto.workspaceId, dto.environment)); queryClient.invalidateQueries(
queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId)); secretKeys.getProjectSecret(dto.workspaceId, dto.environment, dto.folderId)
queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId)); );
queryClient.invalidateQueries(
secretSnapshotKeys.list(dto.workspaceId, dto.environment, dto?.folderId)
);
queryClient.invalidateQueries(
secretSnapshotKeys.count(dto.workspaceId, dto.environment, dto?.folderId)
);
} }
}); });
}; };
+3
View File
@@ -61,6 +61,7 @@ type SecretTagArg = { _id: string; name: string; slug: string };
export type UpdateSecretArg = { export type UpdateSecretArg = {
_id: string; _id: string;
folderId?: string;
type: 'shared' | 'personal'; type: 'shared' | 'personal';
secretName: string; secretName: string;
secretKeyCiphertext: string; secretKeyCiphertext: string;
@@ -81,6 +82,7 @@ export type DeleteSecretArg = { _id: string };
export type BatchSecretDTO = { export type BatchSecretDTO = {
workspaceId: string; workspaceId: string;
folderId: string;
environment: string; environment: string;
requests: Array< requests: Array<
| { method: 'POST'; secret: CreateSecretArg } | { method: 'POST'; secret: CreateSecretArg }
@@ -93,6 +95,7 @@ export type GetProjectSecretsDTO = {
workspaceId: string; workspaceId: string;
env: string | string[]; env: string | string[];
decryptFileKey: UserWsKeyPair; decryptFileKey: UserWsKeyPair;
folderId?: string;
isPaused?: boolean; isPaused?: boolean;
onSuccess?: (data: DecryptedSecret[]) => void; onSuccess?: (data: DecryptedSecret[]) => void;
}; };
@@ -10,6 +10,7 @@ import {
faDownload, faDownload,
faEye, faEye,
faEyeSlash, faEyeSlash,
faFolderPlus,
faMagnifyingGlass, faMagnifyingGlass,
faPlus faPlus
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
@@ -21,6 +22,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,
DeleteActionModal,
IconButton, IconButton,
Input, Input,
Modal, Modal,
@@ -37,7 +39,10 @@ import { useWorkspace } from '@app/context';
import { useLeaveConfirm, usePopUp, useToggle } from '@app/hooks'; import { useLeaveConfirm, usePopUp, useToggle } from '@app/hooks';
import { import {
useBatchSecretsOp, useBatchSecretsOp,
useCreateFolder,
useCreateWsTag, useCreateWsTag,
useDeleteFolder,
useGetProjectFolders,
useGetProjectSecrets, useGetProjectSecrets,
useGetSecretVersion, useGetSecretVersion,
useGetSnapshotSecrets, useGetSnapshotSecrets,
@@ -48,13 +53,20 @@ import {
useGetWsSnapshotCount, useGetWsSnapshotCount,
useGetWsTags, useGetWsTags,
usePerformSecretRollback, usePerformSecretRollback,
useRegisterUserAction useRegisterUserAction,
useUpdateFolder
} from '@app/hooks/api'; } from '@app/hooks/api';
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';
import { CompareSecret } from './components/CompareSecret'; import { CompareSecret } from './components/CompareSecret';
import { CreateTagModal } from './components/CreateTagModal'; import { CreateTagModal } from './components/CreateTagModal';
import {
FolderForm,
FolderSection,
TDeleteFolderForm,
TEditFolderForm
} from './components/FolderSection';
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';
@@ -96,7 +108,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
'addTag', 'addTag',
'secretSnapshots', 'secretSnapshots',
'uploadedSecOpts', 'uploadedSecOpts',
'compareSecrets' 'compareSecrets',
'folderForm',
'deleteFolder'
] as const); ] as const);
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true); const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
const [searchFilter, setSearchFilter] = useState(''); const [searchFilter, setSearchFilter] = useState('');
@@ -106,6 +120,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const deletedSecretIds = useRef<string[]>([]); const deletedSecretIds = useRef<string[]>([]);
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false }); const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false });
const folderId = router.query.folderId as string;
const isRollbackMode = Boolean(snapshotId);
const { currentWorkspace, isLoading } = useWorkspace(); const { currentWorkspace, isLoading } = useWorkspace();
const workspaceId = currentWorkspace?._id as string; const workspaceId = currentWorkspace?._id as string;
const { data: latestFileKey } = useGetUserWsKey(workspaceId); const { data: latestFileKey } = useGetUserWsKey(workspaceId);
@@ -142,7 +159,16 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
workspaceId, workspaceId,
env: selectedEnv?.slug || '', env: selectedEnv?.slug || '',
decryptFileKey: latestFileKey!, decryptFileKey: latestFileKey!,
isPaused: Boolean(snapshotId) isPaused: Boolean(snapshotId),
folderId
});
const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({
workspaceId: workspaceId || '',
environment: selectedEnv?.slug || '',
parentFolderId: folderId,
isPaused: isRollbackMode,
sortDir
}); });
const { const {
@@ -152,6 +178,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
isFetchingNextPage isFetchingNextPage
} = useGetWorkspaceSecretSnapshots({ } = useGetWorkspaceSecretSnapshots({
workspaceId, workspaceId,
environment: selectedEnv?.slug || '',
folder: folderId,
limit: 10 limit: 10
}); });
@@ -165,8 +193,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
decryptFileKey: latestFileKey! decryptFileKey: latestFileKey!
}); });
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount(
useGetWsSnapshotCount(workspaceId); workspaceId,
selectedEnv?.slug || '',
folderId
);
const { data: wsTags } = useGetWsTags(workspaceId); const { data: wsTags } = useGetWsTags(workspaceId);
// mutation calls // mutation calls
@@ -174,6 +205,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
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: createFolder } = useCreateFolder();
const { mutateAsync: updateFolder } = useUpdateFolder(folderId);
const { mutateAsync: deleteFolder } = useDeleteFolder(folderId);
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
@@ -192,7 +226,6 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
reset reset
} = method; } = method;
const { fields, prepend, append, remove } = useFieldArray({ control, name: 'secrets' }); const { fields, prepend, append, remove } = useFieldArray({ control, name: 'secrets' });
const isRollbackMode = Boolean(snapshotId);
const isReadOnly = selectedEnv?.isWriteDenied; const isReadOnly = selectedEnv?.isWriteDenied;
const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied; const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied;
const canDoRollback = !isReadOnly && !isAddOnly; const canDoRollback = !isReadOnly && !isAddOnly;
@@ -281,7 +314,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
try { try {
await performSecretRollback({ await performSecretRollback({
workspaceId, workspaceId,
version: snapshotSecret.version version: snapshotSecret.version,
environment: selectedEnv?.slug || '',
folderId
}); });
setValue('isSnapshotMode', false); setValue('isSnapshotMode', false);
setSnaphotId(null); setSnaphotId(null);
@@ -326,6 +361,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
await batchSecretOp({ await batchSecretOp({
requests: batchedSecret, requests: batchedSecret,
workspaceId, workspaceId,
folderId,
environment: selectedEnv?.slug environment: selectedEnv?.slug
}); });
createNotification({ createNotification({
@@ -393,7 +429,99 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
} }
}; };
if (isSecretsLoading || isEnvListLoading) { const handleFolderOpen = (id: string) => {
setSearchFilter('');
router.push({
pathname: router.pathname,
query: {
...router.query,
folderId: id
}
});
};
const isEditFolder = Boolean(popUp?.folderForm?.data);
const handleFolderCreate = async (name: string) => {
try {
await createFolder({
workspaceId,
environment: selectedEnv?.slug || '',
folderName: name,
parentFolderId: folderId
});
createNotification({
type: 'success',
text: 'Successfully created folder'
});
handlePopUpClose('folderForm');
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to create folder',
type: 'error'
});
}
};
const handleFolderUpdate = async (name: string) => {
const { id } = popUp?.folderForm?.data as TDeleteFolderForm;
try {
await updateFolder({
folderId: id,
workspaceId,
environment: selectedEnv?.slug || '',
name
});
createNotification({
type: 'success',
text: 'Successfully updated folder'
});
handlePopUpClose('folderForm');
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to update folder',
type: 'error'
});
}
};
const handleFolderDelete = async () => {
const { id } = popUp?.deleteFolder?.data as TDeleteFolderForm;
try {
deleteFolder({
workspaceId,
environment: selectedEnv?.slug || '',
folderId: id
});
createNotification({
type: 'success',
text: 'Successfully removed folder'
});
handlePopUpClose('deleteFolder');
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to remove folder',
type: 'error'
});
}
};
// when secrets is not loading and secrets list is empty
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
// folder list checks
const isFolderListLoading = isRollbackMode ? isSnapshotSecretsLoading : isFoldersLoading;
const folderList = isRollbackMode ? snapshotSecret?.folders : folderData?.folders;
// 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;
if (isSecretsLoading || isEnvListLoading || isFolderListLoading) {
return ( 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]"> <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" /> <img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
@@ -401,64 +529,42 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
); );
} }
// 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
const isSnapshotSecretEmtpy =
isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length;
const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy;
const userAvailableEnvs = wsEnv?.filter( const userAvailableEnvs = wsEnv?.filter(
({ isReadDenied, isWriteDenied }) => !isReadDenied || !isWriteDenied ({ isReadDenied, isWriteDenied }) => !isReadDenied || !isWriteDenied
); );
return ( return (
<div className="mr-auto container px-6 text-mineshaft-50 dark:[color-scheme:dark] h-full"> <div className="container mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<FormProvider {...method}> <FormProvider {...method}>
<form autoComplete="off"> <form autoComplete="off" className="h-full">
{/* breadcrumb row */} {/* breadcrumb row */}
<div className="relative right-6 mb-6 -top-2"> <div className="relative right-6 -top-2 mb-2">
<NavHeader <NavHeader
pageName={t('dashboard.title')} pageName={t('dashboard.title')}
currentEnv={ currentEnv={
userAvailableEnvs?.filter((envir) => envir.slug === envFromTop)[0].name || '' userAvailableEnvs?.filter((envir) => envir.slug === envFromTop)[0].name || ''
} }
isFolderMode
folders={folderData?.dir}
isProjectRelated isProjectRelated
userAvailableEnvs={userAvailableEnvs} userAvailableEnvs={userAvailableEnvs}
onEnvChange={onEnvChange} onEnvChange={onEnvChange}
/> />
</div> </div>
{/* This is only for rollbacks */} <div className="mb-4">
{isRollbackMode && <h6 className="text-2xl">{isRollbackMode ? 'Secret Snapshot' : 'Secrets'}</h6>
<div className="flex items-center justify-between"> {isRollbackMode && Boolean(snapshotSecret) && (
<div className="flex items-center space-x-4"> <Tag colorSchema="green">
<h1 className="text-3xl font-semibold">Secret Snapshot</h1> {new Date(snapshotSecret?.createdAt || '').toLocaleString()}
{isRollbackMode && Boolean(snapshotSecret) && ( </Tag>
<Tag colorSchema="green"> )}
{new Date(snapshotSecret?.createdAt || '').toLocaleString()} </div>
</Tag>
)}
</div>
<div className="flex items-center space-x-2">
<Button
variant="star"
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
onClick={() => {
setSnaphotId(null);
reset({ ...secrets, isSnapshotMode: false });
}}
className="h-10"
>
Go back
</Button>
</div>
</div>}
{/* Environment, search and other action row */} {/* Environment, search and other action row */}
<div className="mt-2 flex items-center space-x-2 justify-between"> <div className="flex items-center justify-between space-x-2">
<div className="flex-grow max-w-sm"> <div className="flex max-w-lg flex-grow space-x-2">
<Input <Input
className="h-[2.3rem] bg-mineshaft-800 focus:bg-mineshaft-700/80 duration-200 placeholder-mineshaft-50" className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search keys..." placeholder="Search by folder name, key name, comment..."
value={searchFilter} value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)} onChange={(e) => setSearchFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
@@ -500,8 +606,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</div> </div>
<div className='block xl:hidden'> <div className="block xl:hidden">
<Tooltip content='Point-in-time Recovery'> <Tooltip content="Point-in-time Recovery">
<IconButton <IconButton
ariaLabel="recovery" ariaLabel="recovery"
variant="outline_bg" variant="outline_bg"
@@ -511,7 +617,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</div> </div>
<div className='hidden xl:block'> <div className="hidden xl:block">
<Button <Button
variant="outline_bg" variant="outline_bg"
onClick={() => handlePopUpOpen('secretSnapshots')} onClick={() => handlePopUpOpen('secretSnapshots')}
@@ -525,8 +631,20 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</div> </div>
{!isReadOnly && !isRollbackMode && ( {!isReadOnly && !isRollbackMode && (
<> <>
<div className='block lg:hidden'> <div className="block lg:hidden">
<Tooltip content='Point-in-time Recovery'> <Tooltip content="Add Folder">
<IconButton
ariaLabel="recovery"
variant="outline_bg"
onClick={() => handlePopUpOpen('folderForm')}
>
<FontAwesomeIcon icon={faFolderPlus} />
</IconButton>
</Tooltip>
</div>
<div className="block lg:hidden">
<Tooltip content="Point-in-time Recovery">
<IconButton <IconButton
ariaLabel="recovery" ariaLabel="recovery"
variant="outline_bg" variant="outline_bg"
@@ -544,7 +662,19 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</div> </div>
<div className='hidden lg:block'> <div className="hidden lg:block">
<Button
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
onClick={() => handlePopUpOpen('folderForm')}
isDisabled={isReadOnly || isRollbackMode}
variant="outline_bg"
className="h-10"
>
Add Folder
</Button>
</div>
<div className="hidden lg:block">
<Button <Button
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
@@ -565,6 +695,19 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</div> </div>
</> </>
)} )}
{isRollbackMode && (
<Button
variant="star"
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
onClick={() => {
setSnaphotId(null);
reset({ ...secrets, isSnapshotMode: false });
}}
className="h-10"
>
Go back
</Button>
)}
<Button <Button
isDisabled={isSubmitDisabled} isDisabled={isSubmitDisabled}
isLoading={isSubmitting} isLoading={isSubmitting}
@@ -581,14 +724,21 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
<div <div
className={`${ className={`${
isSecretEmpty ? 'flex flex-col items-center justify-center' : '' isSecretEmpty ? 'flex flex-col items-center justify-center' : ''
} no-scrollbar::-webkit-scrollbar mt-3 h-[calc(100vh-220px)] overflow-x-hidden overflow-y-scroll no-scrollbar`} } no-scrollbar::-webkit-scrollbar mt-3 h-3/4 overflow-x-hidden overflow-y-scroll no-scrollbar`}
ref={secretContainer} ref={secretContainer}
> >
{!isSecretEmpty && ( {!isSecretEmpty && (
<TableContainer className="max-h-[calc(100%-40px)] no-scrollbar no-scrollbar::-webkit-scrollbar"> <TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
<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">
<FolderSection
onFolderOpen={handleFolderOpen}
onFolderUpdate={(id, name) => handlePopUpOpen('folderForm', { id, name })}
onFolderDelete={(id, name) => handlePopUpOpen('deleteFolder', { id, name })}
folders={folderList}
search={searchFilter}
/>
{fields.map(({ id, _id }, index) => ( {fields.map(({ id, _id }, index) => (
<SecretInputRow <SecretInputRow
key={id} key={id}
@@ -609,7 +759,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
<td colSpan={3} className="hover:bg-mineshaft-700"> <td colSpan={3} className="hover:bg-mineshaft-700">
<button <button
type="button" type="button"
className="pl-12 cursor-default w-full flex h-8 items-center justify-start font-normal text-bunker-300" className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
onClick={onAppendSecret} onClick={onAppendSecret}
> >
<FontAwesomeIcon icon={faPlus} /> <FontAwesomeIcon icon={faPlus} />
@@ -695,6 +845,26 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</div> </div>
</ModalContent> </ModalContent>
</Modal> </Modal>
<Modal
isOpen={popUp?.folderForm?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('folderForm', isOpen)}
>
<ModalContent title={isEditFolder ? 'Edit Folder' : 'Create Folder'}>
<FolderForm
isEdit={isEditFolder}
onUpdateFolder={handleFolderUpdate}
onCreateFolder={handleFolderCreate}
defaultFolderName={(popUp?.folderForm?.data as TEditFolderForm)?.name}
/>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.deleteFolder.isOpen}
deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name}
title="Do you want to delete this folder?"
onChange={(isOpen) => handlePopUpToggle('deleteFolder', isOpen)}
onDeleteApproved={handleFolderDelete}
/>
<Modal <Modal
isOpen={popUp?.compareSecrets?.isOpen} isOpen={popUp?.compareSecrets?.isOpen}
onOpenChange={(open) => handlePopUpToggle('compareSecrets', open)} onOpenChange={(open) => handlePopUpToggle('compareSecrets', open)}
@@ -0,0 +1,75 @@
import { Controller, useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { Button, FormControl, Input, ModalClose } from '@app/components/v2';
type Props = {
onCreateFolder: (folderName: string) => Promise<void>;
onUpdateFolder: (folderName: string) => Promise<void>;
isEdit?: boolean;
defaultFolderName?: string;
};
const formSchema = yup.object({
name: yup
.string()
.required()
.trim()
.matches(/^[a-zA-Z0-9-_]+$/, 'Folder name cannot contain spaces. Only underscore and dashes')
.label('Tag Name')
});
type TFormData = yup.InferType<typeof formSchema>;
export const FolderForm = ({
isEdit,
onCreateFolder,
defaultFolderName,
onUpdateFolder
}: Props): JSX.Element => {
const {
control,
reset,
formState: { isSubmitting },
handleSubmit
} = useForm<TFormData>({
resolver: yupResolver(formSchema),
defaultValues: {
name: defaultFolderName
}
});
const onSubmit = async ({ name }: TFormData) => {
if (isEdit) {
await onUpdateFolder(name);
} else {
await onCreateFolder(name);
}
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl label="Folder Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="Type your folder name" />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
{isEdit ? 'Save' : 'Create'}
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
@@ -0,0 +1,77 @@
import { faEdit, faFolder, faTrash } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconButton, Tooltip } from '@app/components/v2';
type Props = {
folders?: Array<{ id: string; name: string }>;
search?: string;
onFolderUpdate: (folderId: string, name: string) => void;
onFolderDelete: (folderId: string, name: string) => void;
onFolderOpen: (folderId: string) => void;
};
export const FolderSection = ({
onFolderUpdate: handleFolderUpdate,
onFolderDelete: handleFolderDelete,
onFolderOpen: handleFolderOpen,
search = '',
folders = []
}: Props) => {
return (
<>
{folders
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
.map(({ id, name }) => (
<tr key={id} className="group flex flex-row items-center">
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
</td>
<td
colSpan={2}
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis uppercase lg:min-w-[240px] xl:min-w-[280px]"
style={{ paddingTop: '0', paddingBottom: '0' }}
>
<div
className="flex-grow cursor-pointer p-2 pl-3"
onKeyDown={() => null}
tabIndex={0}
role="button"
onClick={() => handleFolderOpen(id)}
>
{name}
</div>
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Settings">
<IconButton
size="lg"
colorSchema="primary"
variant="plain"
onClick={() => handleFolderUpdate(id, name)}
ariaLabel="expand"
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
</Tooltip>
</div>
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete">
<IconButton
size="md"
variant="plain"
colorSchema="danger"
ariaLabel="delete"
onClick={() => handleFolderDelete(id, name)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</div>
</div>
</td>
</tr>
))}
</>
);
};
@@ -0,0 +1,3 @@
export { FolderForm } from './FolderForm';
export { FolderSection } from './FolderSection';
export * from './types';
@@ -0,0 +1,2 @@
export type TEditFolderForm = { id: string; name: string };
export type TDeleteFolderForm = { id: string; name: string };
@@ -24,9 +24,9 @@ export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridd
const syntaxHighlight = useCallback((val: string) => { const syntaxHighlight = useCallback((val: string) => {
if (val?.length === 0) return <span className="font-sans text-bunker-400/80">EMPTY</span>; if (val?.length === 0) return <span className="font-sans text-bunker-400/80">EMPTY</span>;
return val?.split(REGEX).map((word) => return val?.split(REGEX).map((word, i) =>
word.match(REGEX) !== null ? ( word.match(REGEX) !== null ? (
<span className="ph-no-capture text-yellow" key={`${val}-${index + 1}`}> <span className="ph-no-capture text-yellow" key={`${val}-${i + 1}`}>
{word.slice(0, 2)} {word.slice(0, 2)}
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span> <span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
{word.slice(word.length - 1, word.length) === '}' ? ( {word.slice(word.length - 1, word.length) === '}' ? (
@@ -40,7 +40,7 @@ export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridd
)} )}
</span> </span>
) : ( ) : (
<span key={`${word}_${index + 1}`} className="ph-no-capture"> <span key={`${word}_${i + 1}`} className="ph-no-capture">
{word} {word}
</span> </span>
) )
@@ -6,7 +6,6 @@ import {
faComment, faComment,
faEllipsis, faEllipsis,
faInfoCircle, faInfoCircle,
faKey,
faPlus, faPlus,
faTags, faTags,
faXmark faXmark
@@ -159,9 +158,8 @@ export const SecretInputRow = memo(
return ( return (
<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 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 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>
</td> </td>
<Controller <Controller
control={control} control={control}
@@ -200,7 +198,7 @@ export const SecretInputRow = memo(
</HoverCard> </HoverCard>
)} )}
/> />
<td className="flex h-10 border-none w-full flex-grow flex-row items-center justify-center border-r border-red"> <td className="flex h-10 w-full flex-grow flex-row items-center justify-center border-r border-none border-red">
<MaskedInput <MaskedInput
isReadOnly={ isReadOnly={
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly) isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
@@ -226,10 +224,10 @@ export const SecretInputRow = memo(
</Tag> </Tag>
))} ))}
{!(isReadOnly || isAddOnly || isRollbackMode) && ( {!(isReadOnly || isAddOnly || isRollbackMode) && (
<div className="overflow-hidden duration-0 ml-1"> <div className="duration-0 ml-1 overflow-hidden">
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<div className="w-0 data-[state=open]:w-6 group-hover:w-6"> <div className="w-0 group-hover:w-6 data-[state=open]:w-6">
<Tooltip content="Add tags"> <Tooltip content="Add tags">
<IconButton <IconButton
variant="plain" variant="plain"
@@ -291,7 +289,7 @@ export const SecretInputRow = memo(
</div> </div>
)} )}
</div> </div>
<div className="flex flex-row items-center h-full pr-2"> <div className="flex h-full flex-row items-center pr-2">
{!isAddOnly && ( {!isAddOnly && (
<div> <div>
<Tooltip content="Override with a personal value"> <Tooltip content="Override with a personal value">
@@ -314,16 +312,14 @@ export const SecretInputRow = memo(
</div> </div>
)} )}
<Tooltip content="Comment"> <Tooltip content="Comment">
<div <div className={`mt-0.5 overflow-hidden `}>
className={`mt-0.5 overflow-hidden `}
>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<IconButton <IconButton
className={twMerge( className={twMerge(
'overflow-hidden p-0 w-7', 'w-7 overflow-hidden p-0',
'data-[state=open]:w-7 group-hover:w-7 w-0', 'w-0 group-hover:w-7 data-[state=open]:w-7',
hasComment ? 'text-primary w-7' : 'group-hover:w-7' hasComment ? 'w-7 text-primary' : 'group-hover:w-7'
)} )}
variant="plain" variant="plain"
size="md" size="md"
@@ -332,12 +328,13 @@ export const SecretInputRow = memo(
<FontAwesomeIcon icon={faComment} /> <FontAwesomeIcon icon={faComment} />
</IconButton> </IconButton>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl" sticky="always"> <PopoverContent
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl"
sticky="always"
>
<FormControl label="Comment" className="mb-0"> <FormControl label="Comment" className="mb-0">
<TextArea <TextArea
isDisabled={ isDisabled={isReadOnly || isRollbackMode || shouldBeBlockedInAddOnly}
isReadOnly || isRollbackMode || shouldBeBlockedInAddOnly
}
className="border border-mineshaft-600 text-sm" className="border border-mineshaft-600 text-sm"
{...register(`secrets.${index}.comment`)} {...register(`secrets.${index}.comment`)}
rows={8} rows={8}
@@ -349,7 +346,7 @@ export const SecretInputRow = memo(
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
<div className="duration-0 w-0 flex items-center justify-end space-x-2.5 overflow-hidden transition-all w-16 border-l border-mineshaft-600 h-10"> <div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
{!isAddOnly && ( {!isAddOnly && (
<div className="opacity-0 group-hover:opacity-100"> <div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Settings"> <Tooltip content="Settings">
@@ -11,7 +11,7 @@ type Props = {
export const SecretTableHeader = ({ sortDir, onSort }: Props): JSX.Element => ( export const SecretTableHeader = ({ sortDir, onSort }: Props): JSX.Element => (
<thead className="sticky top-0 z-50 bg-mineshaft-800"> <thead className="sticky top-0 z-50 bg-mineshaft-800">
<tr className="top-0 flex flex-row"> <tr className="top-0 flex flex-row">
<td className="flex w-10 items-center justify-center px-4 border-none"> <td className="flex w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-transparent">{0}</div> <div className="w-10 text-center text-xs text-transparent">{0}</div>
</td> </td>
<td className="flex items-center"> <td className="flex items-center">