mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 13:28:27 +00:00
feat(folders): implemented ui for folders in dashboard
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user