mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #554 from akhilmhdh/feat/dashboard-v2
feat(ui): fixed lagging issues with new dashboard
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
@@ -21,6 +22,7 @@ import { Select, SelectItem, Tooltip } from '../v2';
|
||||
* @param {string} obj.onEnvChange - the action that happens when an env is changed
|
||||
* @returns
|
||||
*/
|
||||
// TODO(akhilmhdh): simply this header and nav system later
|
||||
export default function NavHeader({
|
||||
pageName,
|
||||
isProjectRelated,
|
||||
@@ -38,7 +40,7 @@ export default function NavHeader({
|
||||
}): JSX.Element {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="ml-6 flex flex-row items-center pt-8">
|
||||
@@ -59,31 +61,40 @@ export default function NavHeader({
|
||||
</>
|
||||
)}
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
{pageName === 'Secrets'
|
||||
? <a className="text-sm font-semibold text-primary/80 hover:text-primary" href={`${router.asPath.split("?")[0]}`}>{pageName}</a>
|
||||
: <div className="text-sm text-gray-400">{pageName}</div>}
|
||||
{currentEnv &&
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
|
||||
<div className='pl-3 rounded-md hover:bg-bunker-100/10'>
|
||||
<Tooltip content="Select environment">
|
||||
<Select
|
||||
value={userAvailableEnvs?.filter(uae => uae.name === currentEnv)[0]?.slug}
|
||||
onValueChange={(value) => {
|
||||
if (value && onEnvChange) onEnvChange(value);
|
||||
}}
|
||||
className="text-sm pl-0 font-medium text-primary/80 hover:text-primary bg-transparent"
|
||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl"
|
||||
>
|
||||
{userAvailableEnvs?.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>}
|
||||
{pageName === 'Secrets' ? (
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={{ pathname: '/dashboard/[id]', query: { id: router.query.id } }}
|
||||
>
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="text-sm text-gray-400">{pageName}</div>
|
||||
)}
|
||||
{currentEnv && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
|
||||
<div className="rounded-md pl-3 hover:bg-bunker-100/10">
|
||||
<Tooltip content="Select environment">
|
||||
<Select
|
||||
value={userAvailableEnvs?.filter((uae) => uae.name === currentEnv)[0]?.slug}
|
||||
onValueChange={(value) => {
|
||||
if (value && onEnvChange) onEnvChange(value);
|
||||
}}
|
||||
className="bg-transparent pl-0 text-sm font-medium text-primary/80 hover:text-primary"
|
||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl"
|
||||
>
|
||||
{userAvailableEnvs?.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
export { useBatchSecretsOp, useGetProjectSecrets, useGetSecretVersion } from './queries';
|
||||
export {
|
||||
useBatchSecretsOp,
|
||||
useGetProjectSecrets,
|
||||
useGetProjectSecretsByKey,
|
||||
useGetSecretVersion
|
||||
} from './queries';
|
||||
|
||||
@@ -19,7 +19,10 @@ import {
|
||||
|
||||
export const secretKeys = {
|
||||
// this is also used in secretSnapshot part
|
||||
getProjectSecret: (workspaceId: string, env: string | string[]) => [{ workspaceId, env }, 'secrets'],
|
||||
getProjectSecret: (workspaceId: string, env: string | string[]) => [
|
||||
{ workspaceId, env },
|
||||
'secrets'
|
||||
],
|
||||
getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions']
|
||||
};
|
||||
|
||||
@@ -32,11 +35,11 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | s
|
||||
}
|
||||
});
|
||||
return data.secrets;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (typeof env === 'object') {
|
||||
let allEnvData: any = [];
|
||||
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const envPoint of env) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
@@ -48,13 +51,12 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | s
|
||||
});
|
||||
allEnvData = allEnvData.concat(data.secrets);
|
||||
}
|
||||
|
||||
|
||||
return allEnvData;
|
||||
// eslint-disable-next-line no-else-return
|
||||
// eslint-disable-next-line no-else-return
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export const useGetProjectSecrets = ({
|
||||
@@ -117,7 +119,10 @@ export const useGetProjectSecrets = ({
|
||||
};
|
||||
|
||||
if (encSecret.type === 'personal') {
|
||||
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = { id: encSecret._id, value: secretValue };
|
||||
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
||||
id: encSecret._id,
|
||||
value: secretValue
|
||||
};
|
||||
} else {
|
||||
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
||||
sharedSecrets.push(decryptedSecret);
|
||||
@@ -126,17 +131,106 @@ export const useGetProjectSecrets = ({
|
||||
}
|
||||
});
|
||||
sharedSecrets.forEach((val) => {
|
||||
if (personalSecrets?.[val.key]) {
|
||||
val.idOverride = personalSecrets[val.key].id;
|
||||
val.valueOverride = personalSecrets[val.key].value;
|
||||
const dupKey = `${val.key}-${val.env}`;
|
||||
if (personalSecrets?.[dupKey]) {
|
||||
val.idOverride = personalSecrets[dupKey].id;
|
||||
val.valueOverride = personalSecrets[dupKey].value;
|
||||
val.overrideAction = 'modified';
|
||||
}
|
||||
});
|
||||
|
||||
return { secrets: sharedSecrets };
|
||||
}
|
||||
});
|
||||
|
||||
export const useGetProjectSecretsByKey = ({
|
||||
workspaceId,
|
||||
env,
|
||||
decryptFileKey,
|
||||
isPaused
|
||||
}: GetProjectSecretsDTO) =>
|
||||
useQuery({
|
||||
// wait for all values to be available
|
||||
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
||||
queryKey: secretKeys.getProjectSecret(workspaceId, env),
|
||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env),
|
||||
select: (data) => {
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
const latestKey = decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const sharedSecrets: Record<string, DecryptedSecret[]> = {};
|
||||
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||
// this used for add-only mode in dashboard
|
||||
// type won't be there thus only one key is shown
|
||||
const duplicateSecretKey: Record<string, boolean> = {};
|
||||
const uniqSecKeys: Record<string, boolean> = {};
|
||||
data.forEach((encSecret: EncryptedSecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
tag: encSecret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
iv: encSecret.secretValueIV,
|
||||
tag: encSecret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretComment = decryptSymmetric({
|
||||
ciphertext: encSecret.secretCommentCiphertext,
|
||||
iv: encSecret.secretCommentIV,
|
||||
tag: encSecret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
|
||||
const decryptedSecret = {
|
||||
_id: encSecret._id,
|
||||
env: encSecret.environment,
|
||||
key: secretKey,
|
||||
value: secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt
|
||||
};
|
||||
|
||||
if (encSecret.type === 'personal') {
|
||||
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
||||
id: encSecret._id,
|
||||
value: secretValue
|
||||
};
|
||||
} else {
|
||||
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
||||
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
|
||||
sharedSecrets[secretKey].push(decryptedSecret);
|
||||
}
|
||||
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
||||
}
|
||||
});
|
||||
Object.keys(sharedSecrets).forEach((secName) => {
|
||||
sharedSecrets[secName].forEach((val) => {
|
||||
const dupKey = `${val.key}-${val.env}`;
|
||||
if (personalSecrets?.[dupKey]) {
|
||||
val.idOverride = personalSecrets[dupKey].id;
|
||||
val.valueOverride = personalSecrets[dupKey].value;
|
||||
val.overrideAction = 'modified';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
|
||||
}
|
||||
});
|
||||
|
||||
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
||||
const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>(
|
||||
`/api/v1/secret/${secretId}/secret-versions`,
|
||||
|
||||
@@ -62,6 +62,7 @@ type SecretTagArg = { _id: string; name: string; slug: string };
|
||||
export type UpdateSecretArg = {
|
||||
_id: string;
|
||||
type: 'shared' | 'personal';
|
||||
secretName: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
|
||||
@@ -8,11 +8,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
faBookOpen,
|
||||
faMobile,
|
||||
faPlus,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { faBookOpen, faMobile, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import queryString from 'query-string';
|
||||
@@ -110,7 +106,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
) {
|
||||
router.push('/noprojects');
|
||||
} else if (router.asPath !== '/noprojects') {
|
||||
|
||||
// const pathSegments = router.asPath.split('/').filter(segment => segment.length > 0);
|
||||
|
||||
// let intendedWorkspaceId;
|
||||
@@ -123,8 +118,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
// .split('/')
|
||||
// [router.asPath.split('/').length - 1].split('?')[0];
|
||||
// }
|
||||
|
||||
const pathSegments = router.asPath.split('/').filter(segment => segment.length > 0);
|
||||
|
||||
const pathSegments = router.asPath.split('/').filter((segment) => segment.length > 0);
|
||||
|
||||
let intendedWorkspaceId;
|
||||
if (pathSegments.length >= 2 && pathSegments[0] === 'dashboard') {
|
||||
@@ -140,7 +135,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
// const lastPathSegment = router.asPath.split('/').pop().split('?');
|
||||
// [intendedWorkspaceId] = lastPathSegment;
|
||||
}
|
||||
|
||||
|
||||
if (!intendedWorkspaceId) return;
|
||||
|
||||
if (!['callback', 'create', 'authorize'].includes(intendedWorkspaceId)) {
|
||||
@@ -149,7 +144,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
|
||||
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
||||
if (
|
||||
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) && userWorkspaces[0]?._id !== undefined &&
|
||||
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) &&
|
||||
userWorkspaces[0]?._id !== undefined &&
|
||||
!userWorkspaces
|
||||
.map((workspace: { _id: string }) => workspace._id)
|
||||
.includes(intendedWorkspaceId)
|
||||
@@ -240,21 +236,21 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="hidden h-screen w-full flex-col overflow-x-hidden md:flex dark">
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||
<Navbar />
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<aside className="w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between">
|
||||
<div>
|
||||
{currentWorkspace ? (
|
||||
<div className="w-full p-4 mt-3 mb-4">
|
||||
<p className="text-xs font-semibold ml-1.5 mb-1 uppercase text-gray-400">
|
||||
<div className="mt-3 mb-4 w-full p-4">
|
||||
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">
|
||||
Project
|
||||
</p>
|
||||
<Select
|
||||
defaultValue={currentWorkspace?._id}
|
||||
value={currentWorkspace?._id}
|
||||
className="w-full py-2.5 bg-mineshaft-600 font-medium truncate"
|
||||
className="w-full truncate bg-mineshaft-600 py-2.5 font-medium"
|
||||
onValueChange={(value) => {
|
||||
router.push(`/dashboard/${value}`);
|
||||
}}
|
||||
@@ -273,7 +269,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
{/* <hr className="mt-1 mb-1 h-px border-0 bg-gray-700" /> */}
|
||||
<div className="w-full">
|
||||
<Button
|
||||
className="w-full py-2 text-bunker-200 bg-mineshaft-700"
|
||||
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
@@ -286,9 +282,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</Select>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full p-4 mt-3 mb-4">
|
||||
<div className="mt-3 mb-4 w-full p-4">
|
||||
<Button
|
||||
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
|
||||
className="w-full bg-mineshaft-500 py-2 text-bunker-200 hover:bg-primary/90 hover:text-black"
|
||||
color="mineshaft"
|
||||
size="sm"
|
||||
onClick={() => handlePopUpOpen('addNewWs')}
|
||||
@@ -331,13 +327,13 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/activity/${currentWorkspace?._id}`} passHref>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/activity/${currentWorkspace?._id}`}
|
||||
// icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
|
||||
icon="system-outline-168-view-headline"
|
||||
>
|
||||
Audit Logs
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/activity/${currentWorkspace?._id}`}
|
||||
// icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
|
||||
icon="system-outline-168-view-headline"
|
||||
>
|
||||
Audit Logs
|
||||
</MenuItem>
|
||||
</Link>
|
||||
<Link href={`/settings/project/${currentWorkspace?._id}`} passHref>
|
||||
<a>
|
||||
@@ -428,7 +424,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="pl-1 mt-4">
|
||||
<div className="mt-4 pl-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="addMembers"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,8 @@ export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1
|
||||
retry: 1,
|
||||
cacheTime: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,50 +1,26 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
|
||||
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||
import NavHeader from '@app/components/navigation/NavHeader';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalContent,
|
||||
TableContainer,
|
||||
Tooltip
|
||||
} from '@app/components/v2';
|
||||
import { Button, TableContainer, Tooltip } from '@app/components/v2';
|
||||
import { useWorkspace } from '@app/context';
|
||||
import { usePopUp } from '@app/hooks';
|
||||
import {
|
||||
useCreateWsTag,
|
||||
useGetProjectSecrets,
|
||||
useGetProjectSecretsByKey,
|
||||
useGetUserWsEnvironments,
|
||||
useGetUserWsKey,
|
||||
useGetUserWsKey
|
||||
} from '@app/hooks/api';
|
||||
import { WorkspaceEnv } from '@app/hooks/api/types';
|
||||
|
||||
import { CreateTagModal } from './components/CreateTagModal';
|
||||
import { EnvComparisonRow } from './components/EnvComparisonRow';
|
||||
import {
|
||||
FormData,
|
||||
schema
|
||||
} from './DashboardPage.utils';
|
||||
import { FormData, schema } from './DashboardPage.utils';
|
||||
|
||||
|
||||
export const DashboardEnvOverview = ({onEnvChange}: {onEnvChange: any;}) => {
|
||||
export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
const { popUp
|
||||
// , handlePopUpOpen
|
||||
, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
'secretDetails',
|
||||
'addTag',
|
||||
'secretSnapshots',
|
||||
'uploadedSecOpts',
|
||||
'compareSecrets'
|
||||
] as const);
|
||||
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
|
||||
|
||||
const { currentWorkspace, isLoading } = useWorkspace();
|
||||
@@ -68,20 +44,15 @@ export const DashboardEnvOverview = ({onEnvChange}: {onEnvChange: any;}) => {
|
||||
}
|
||||
});
|
||||
|
||||
const userAvailableEnvs = wsEnv?.filter(
|
||||
({ isReadDenied }) => !isReadDenied
|
||||
);
|
||||
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||
const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied);
|
||||
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecretsByKey({
|
||||
workspaceId,
|
||||
env: userAvailableEnvs?.map(env => env.slug) ?? [],
|
||||
env: userAvailableEnvs?.map((env) => env.slug) ?? [],
|
||||
decryptFileKey: latestFileKey!,
|
||||
isPaused: false
|
||||
});
|
||||
|
||||
// mutation calls
|
||||
const { mutateAsync: createWsTag } = useCreateWsTag();
|
||||
|
||||
const method = useForm<FormData>({
|
||||
// why any: well yup inferred ts expects other keys to defined as undefined
|
||||
defaultValues: secrets as any,
|
||||
@@ -90,39 +61,24 @@ export const DashboardEnvOverview = ({onEnvChange}: {onEnvChange: any;}) => {
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
// handleSubmit,
|
||||
// getValues,
|
||||
// setValue,
|
||||
// formState: { isSubmitting, dirtyFields },
|
||||
// reset
|
||||
} = method;
|
||||
const formSecrets = useWatch({ control, name: 'secrets' });
|
||||
const numSecretsMissingPerEnv = useMemo(() => {
|
||||
// first get all sec in the env then subtract with total to get missing ones
|
||||
const secPerEnvMissing: Record<string, number> = Object.fromEntries(
|
||||
(userAvailableEnvs || [])?.map(({ slug }) => [slug, 0])
|
||||
);
|
||||
Object.keys(secrets?.secrets || {}).forEach((key) =>
|
||||
secrets?.secrets?.[key].forEach((val) => {
|
||||
secPerEnvMissing[val.env] += 1;
|
||||
})
|
||||
);
|
||||
Object.keys(secPerEnvMissing).forEach((k) => {
|
||||
secPerEnvMissing[k] = (secrets?.uniqueSecCount || 0) - secPerEnvMissing[k];
|
||||
});
|
||||
return secPerEnvMissing;
|
||||
}, [secrets, userAvailableEnvs]);
|
||||
|
||||
const isReadOnly = selectedEnv?.isWriteDenied;
|
||||
|
||||
const onCreateWsTag = async (tagName: string) => {
|
||||
try {
|
||||
await createWsTag({
|
||||
workspaceID: workspaceId,
|
||||
tagName,
|
||||
tagSlug: tagName.replace(' ', '_')
|
||||
});
|
||||
handlePopUpClose('addTag');
|
||||
createNotification({
|
||||
text: 'Successfully created a tag',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: 'Failed to create a tag',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isSecretsLoading || isEnvListLoading) {
|
||||
return (
|
||||
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
@@ -132,9 +88,7 @@ export const DashboardEnvOverview = ({onEnvChange}: {onEnvChange: any;}) => {
|
||||
}
|
||||
|
||||
// when secrets is not loading and secrets list is empty
|
||||
const isDashboardSecretEmpty = !isSecretsLoading && !formSecrets?.length;
|
||||
|
||||
const numSecretsMissingPerEnv = userAvailableEnvs?.map(envir => ({[envir.slug]: [... new Set(secrets?.secrets.map((secret: any) => secret.key))].length - [... new Set(secrets?.secrets.filter(s => s.env === envir.slug).map((secret: any) => secret.key))].length})).reduce((acc, cur) => ({ ...acc, ...cur }), {})
|
||||
const isDashboardSecretEmpty = !isSecretsLoading && !Object.keys(secrets?.secrets || {})?.length;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
@@ -146,55 +100,77 @@ export const DashboardEnvOverview = ({onEnvChange}: {onEnvChange: any;}) => {
|
||||
</div>
|
||||
<div className="mt-6 ml-1">
|
||||
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
|
||||
<p className="text-md text-bunker-300">Inject your secrets using
|
||||
<a
|
||||
className="text-primary/80 hover:text-primary mx-1"
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
<p className="text-md text-bunker-300">
|
||||
Inject your secrets using
|
||||
<a
|
||||
className="mx-1 text-primary/80 hover:text-primary"
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Infisical CLI
|
||||
</a>
|
||||
or
|
||||
<a
|
||||
className="text-primary/80 hover:text-primary mx-1"
|
||||
href="https://infisical.com/docs/sdks/overview"
|
||||
</a>
|
||||
or
|
||||
<a
|
||||
className="mx-1 text-primary/80 hover:text-primary"
|
||||
href="https://infisical.com/docs/sdks/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Infisical SDKs
|
||||
</a> </p>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="overflow-y-auto">
|
||||
<div className="sticky top-0 absolute flex flex-row h-10 bg-mineshaft-800 border border-mineshaft-600 rounded-md mt-8 min-w-[60.3rem]">
|
||||
<div className="sticky top-0 w-10 px-4 flex items-center justify-center border-none">
|
||||
<div className='text-center w-10 text-xs text-transparent'>{0}</div>
|
||||
<div className="sticky top-0 mt-8 flex h-10 min-w-[60.3rem] flex-row rounded-md border border-mineshaft-600 bg-mineshaft-800">
|
||||
<div className="sticky top-0 flex w-10 items-center justify-center border-none px-4">
|
||||
<div className="w-10 text-center text-xs text-transparent">{0}</div>
|
||||
</div>
|
||||
<div className="sticky top-0 border-none">
|
||||
<div className="min-w-[200px] lg:min-w-[220px] xl:min-w-[250px] relative flex items-center justify-start h-full w-full">
|
||||
<div className="relative flex h-full w-full min-w-[200px] items-center justify-start lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<div className="text-sm font-medium ">Secret</div>
|
||||
</div>
|
||||
</div>
|
||||
{numSecretsMissingPerEnv && userAvailableEnvs?.map(env => {
|
||||
return <div key={`header-${env.slug}`} className="flex flex-row w-full bg-mineshaft-800 rounded-md items-center border-none min-w-[11rem]">
|
||||
<div className="text-sm font-medium w-full text-center text-bunker-200/[.99] flex flex-row justify-center">
|
||||
{env.name}
|
||||
{numSecretsMissingPerEnv[env.slug] > 0 && <div className="bg-red rounded-sm h-[1.1rem] w-[1.1rem] mt-0.5 text-bunker-100 ml-2.5 text-xs border border-red-400 flex items-center justify-center cursor-default">
|
||||
<Tooltip content={`${numSecretsMissingPerEnv[env.slug]} secrets missing compared to other environments`}><span className="text-bunker-100">{numSecretsMissingPerEnv[env.slug]}</span></Tooltip>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
})}
|
||||
{numSecretsMissingPerEnv &&
|
||||
userAvailableEnvs?.map((env) => {
|
||||
return (
|
||||
<div
|
||||
key={`header-${env.slug}`}
|
||||
className="flex w-full min-w-[11rem] flex-row items-center rounded-md border-none bg-mineshaft-800"
|
||||
>
|
||||
<div className="flex w-full flex-row justify-center text-center text-sm font-medium text-bunker-200/[.99]">
|
||||
{env.name}
|
||||
{numSecretsMissingPerEnv[env.slug] > 0 && (
|
||||
<div className="mt-0.5 ml-2.5 flex h-[1.1rem] w-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red text-xs text-bunker-100">
|
||||
<Tooltip
|
||||
content={`${
|
||||
numSecretsMissingPerEnv[env.slug]
|
||||
} secrets missing compared to other environments`}
|
||||
>
|
||||
<span className="text-bunker-100">
|
||||
{numSecretsMissingPerEnv[env.slug]}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={`${isDashboardSecretEmpty ? "" : ""} flex flex-row items-start justify-center mt-3 h-full max-h-[calc(100vh-370px)] min-w-[60.3rem] flex-grow w-full overflow-x-hidden no-scrollbar no-scrollbar::-webkit-scrollbar`}>
|
||||
<div
|
||||
className={`${
|
||||
isDashboardSecretEmpty ? '' : ''
|
||||
} no-scrollbar::-webkit-scrollbar mt-3 border rounded-md border-mineshaft-600 flex h-full max-h-[calc(100vh-370px)] w-full min-w-[60.3rem] flex-grow flex-row items-start justify-center overflow-x-hidden no-scrollbar`}
|
||||
>
|
||||
{!isDashboardSecretEmpty && (
|
||||
<TableContainer className='border-none'>
|
||||
<table className="relative secret-table w-full relative bg-bunker-800">
|
||||
<tbody className="overflow-y-auto max-h-screen">
|
||||
{[... new Set(secrets?.secrets.map((secret: any) => secret.key))].map((key, index) => (
|
||||
<TableContainer className="border-none">
|
||||
<table className="secret-table relative w-full bg-mineshaft-900">
|
||||
<tbody className="max-h-screen overflow-y-auto">
|
||||
{Object.keys(secrets?.secrets || {}).map((key, index) => (
|
||||
<EnvComparisonRow
|
||||
key={`row-${key}`}
|
||||
secrets={secrets?.secrets.filter(secret => secret.key === key)}
|
||||
secrets={secrets?.secrets?.[key]}
|
||||
isReadOnly={isReadOnly}
|
||||
index={index}
|
||||
isSecretValueHidden
|
||||
@@ -205,22 +181,23 @@ export const DashboardEnvOverview = ({onEnvChange}: {onEnvChange: any;}) => {
|
||||
</table>
|
||||
</TableContainer>
|
||||
)}
|
||||
{isDashboardSecretEmpty &&
|
||||
<div className='flex flex-row h-40 rounded-md mt-1 w-full'>
|
||||
<div className="sticky top-0 w-10 px-4 flex items-center justify-center border-none">
|
||||
<div className='text-center w-10 text-xs text-transparent'>{0}</div>
|
||||
{isDashboardSecretEmpty && (
|
||||
<div className="mt-1 flex h-40 w-full flex-row rounded-md">
|
||||
<div className="sticky top-0 flex w-10 items-center justify-center border-none px-4">
|
||||
<div className="w-10 text-center text-xs text-transparent">{0}</div>
|
||||
</div>
|
||||
<div className="sticky top-0 border-none">
|
||||
<div className="min-w-[200px] lg:min-w-[220px] xl:min-w-[250px] relative flex items-center justify-start h-full w-full">
|
||||
<div className="relative flex h-full w-full min-w-[200px] items-center justify-start lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<div className="text-sm font-medium text-transparent">Secret</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-full bg-mineshaft-800 text-bunker-300 rounded-md items-center justify-center border-none mx-2 min-w-[11rem]">
|
||||
<div className="mx-2 flex w-full min-w-[11rem] flex-col items-center justify-center rounded-md border-none bg-mineshaft-800 text-bunker-300">
|
||||
<span className="mb-1">No secrets are available in this project yet.</span>
|
||||
<span>You can go into any environment to add secrets there.</span>
|
||||
</div>
|
||||
</div>}
|
||||
{/* In future, we should add an option to add environments here
|
||||
</div>
|
||||
)}
|
||||
{/* In future, we should add an option to add environments here
|
||||
<div className="ml-10 h-full flex items-start justify-center">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus}/>}
|
||||
@@ -234,14 +211,22 @@ export const DashboardEnvOverview = ({onEnvChange}: {onEnvChange: any;}) => {
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="group flex flex-row items-center mt-4 min-w-[60.3rem]">
|
||||
<div className="w-10 h-10 px-4 flex items-center justify-center border-none"><div className='text-center w-10 text-xs text-transparent'>0</div></div>
|
||||
<div className="flex flex-row justify-between items-center min-w-[200px] lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<span className="text-transparent">0</span>
|
||||
<button type="button" className='mr-2 text-transparent'>1</button>
|
||||
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center">
|
||||
<div className="flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<div className="w-10 text-center text-xs text-transparent">0</div>
|
||||
</div>
|
||||
{userAvailableEnvs?.map(env => {
|
||||
return <div key={`button-${env.slug}`} className="flex flex-row w-full justify-center h-10 items-center border-none mb-1 mx-2 min-w-[11rem]">
|
||||
<div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<span className="text-transparent">0</span>
|
||||
<button type="button" className="mr-2 text-transparent">
|
||||
1
|
||||
</button>
|
||||
</div>
|
||||
{userAvailableEnvs?.map((env) => {
|
||||
return (
|
||||
<div
|
||||
key={`button-${env.slug}`}
|
||||
className="mx-2 mb-1 flex h-10 w-full min-w-[11rem] flex-row items-center justify-center border-none"
|
||||
>
|
||||
<Button
|
||||
onClick={() => onEnvChange(env.slug)}
|
||||
// router.push(`${router.asPath }?env=${env.slug}`)
|
||||
@@ -253,23 +238,11 @@ export const DashboardEnvOverview = ({onEnvChange}: {onEnvChange: any;}) => {
|
||||
Explore {env.name}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<Modal
|
||||
isOpen={popUp?.addTag?.isOpen}
|
||||
onOpenChange={(open) => {
|
||||
handlePopUpToggle('addTag', open);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Create tag"
|
||||
subTitle="Specify your tag name, and the slug will be created automatically."
|
||||
>
|
||||
<CreateTagModal onCreateTag={onCreateWsTag} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</FormProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormProvider, useFieldArray, useForm, useWatch } from 'react-hook-form';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { FormProvider, useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
@@ -90,6 +90,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const secretContainer = useRef<HTMLDivElement | null>(null);
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
'secretDetails',
|
||||
'addTag',
|
||||
@@ -102,7 +103,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
const [snapshotId, setSnaphotId] = useState<string | null>(null);
|
||||
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
|
||||
const [deletedSecretIds, setDeletedSecretIds] = useState<string[]>([]);
|
||||
const deletedSecretIds = useRef<string[]>([]);
|
||||
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false });
|
||||
|
||||
const { currentWorkspace, isLoading } = useWorkspace();
|
||||
@@ -124,8 +125,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
onSuccess: (data) => {
|
||||
// get an env with one of the access available
|
||||
const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied);
|
||||
if (env && data?.map(wsenv => wsenv.slug).includes(envFromTop)) {
|
||||
setSelectedEnv(data?.filter(dp => dp.slug === envFromTop)[0]);
|
||||
if (env && data?.map((wsenv) => wsenv.slug).includes(envFromTop)) {
|
||||
setSelectedEnv(data?.filter((dp) => dp.slug === envFromTop)[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -187,23 +188,15 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
handleSubmit,
|
||||
getValues,
|
||||
setValue,
|
||||
formState: { isSubmitting, dirtyFields },
|
||||
formState: { isSubmitting, isDirty },
|
||||
reset
|
||||
} = method;
|
||||
const formSecrets = useWatch({ control, name: 'secrets' });
|
||||
const { fields, prepend, append, remove, update } = useFieldArray({ control, name: 'secrets' });
|
||||
|
||||
const { fields, prepend, append, remove } = useFieldArray({ control, name: 'secrets' });
|
||||
const isRollbackMode = Boolean(snapshotId);
|
||||
const isReadOnly = selectedEnv?.isWriteDenied;
|
||||
const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied;
|
||||
const canDoRollback = !isReadOnly && !isAddOnly;
|
||||
const isSubmitDisabled =
|
||||
isReadOnly ||
|
||||
// on add only mode the formstate becomes dirty due to secrets missing some items
|
||||
// to avoid this we check dirtyFields in isAddOnly Mode
|
||||
(isAddOnly && Object.keys(dirtyFields).length === 0) ||
|
||||
(!isRollbackMode && !isAddOnly && Object.keys(dirtyFields).length === 0) ||
|
||||
isSubmitting;
|
||||
const isSubmitDisabled = isReadOnly || (!isRollbackMode && !isDirty) || isAddOnly || isSubmitting;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSnapshotChanging && Boolean(snapshotId)) {
|
||||
@@ -240,15 +233,16 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
// append non conflicting ones
|
||||
Object.keys(uploadedSec).forEach((key) => {
|
||||
if (!conflictingSecIds?.[key]) {
|
||||
append({
|
||||
delete conflictingUploadedSec[key];
|
||||
sec.push({
|
||||
...DEFAULT_SECRET_VALUE,
|
||||
key,
|
||||
value: uploadedSec[key].value,
|
||||
comment: uploadedSec[key].comments.join(',')
|
||||
});
|
||||
delete conflictingUploadedSec[key];
|
||||
}
|
||||
});
|
||||
setValue('secrets', sec, { shouldDirty: true });
|
||||
if (conflictingSec.length > 0) {
|
||||
handlePopUpOpen('uploadedSecOpts', { secrets: conflictingUploadedSec });
|
||||
}
|
||||
@@ -264,14 +258,15 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
data.forEach(({ key, index }) => {
|
||||
const { value, comments } = uploadedSec[key];
|
||||
const comment = comments.join(', ');
|
||||
update(index, {
|
||||
sec[index] = {
|
||||
...DEFAULT_SECRET_VALUE,
|
||||
key,
|
||||
value,
|
||||
comment,
|
||||
tags: sec[index].tags
|
||||
});
|
||||
};
|
||||
});
|
||||
setValue('secrets', sec, { shouldDirty: true });
|
||||
handlePopUpClose('uploadedSecOpts');
|
||||
};
|
||||
|
||||
@@ -319,7 +314,12 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
const sec = isAddOnly ? userSec.filter(({ _id }) => !_id) : userSec;
|
||||
// encrypt and format the secrets to batch api format
|
||||
// requests = [ {method:"", secret:""} ]
|
||||
const batchedSecret = transformSecretsToBatchSecretReq(deletedSecretIds, latestFileKey, sec);
|
||||
const batchedSecret = transformSecretsToBatchSecretReq(
|
||||
deletedSecretIds.current,
|
||||
latestFileKey,
|
||||
sec,
|
||||
secrets?.secrets
|
||||
);
|
||||
// type check
|
||||
if (!selectedEnv?.slug) return;
|
||||
try {
|
||||
@@ -332,6 +332,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
text: 'Successfully saved changes',
|
||||
type: 'success'
|
||||
});
|
||||
deletedSecretIds.current = [];
|
||||
if (!hasUserPushed) {
|
||||
await registerUserAction(USER_ACTION_PUSH);
|
||||
}
|
||||
@@ -355,16 +356,17 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
}
|
||||
const env = wsEnv?.find((el) => el.slug === slug);
|
||||
if (env) setSelectedEnv(env);
|
||||
router.push(`${router.asPath.split("?")[0]}?env=${slug}`)
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, env: slug }
|
||||
});
|
||||
};
|
||||
|
||||
// record all deleted ids
|
||||
// This will make final deletion easier
|
||||
const onSecretDelete = (index: number, id?: string, overrideId?: string) => {
|
||||
const ids: string[] = [];
|
||||
if (id) ids.push(id);
|
||||
if (overrideId) ids.push(overrideId);
|
||||
setDeletedSecretIds((state) => [...state, ...ids]);
|
||||
if (id) deletedSecretIds.current.push(id);
|
||||
if (overrideId) deletedSecretIds.current.push(overrideId);
|
||||
remove(index);
|
||||
// just the case if this is called from drawer
|
||||
handlePopUpClose('secretDetails');
|
||||
@@ -400,7 +402,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
}
|
||||
|
||||
// when secrets is not loading and secrets list is empty
|
||||
const isDashboardSecretEmpty = !isSecretsLoading && !formSecrets?.length;
|
||||
const isDashboardSecretEmpty = !isSecretsLoading && false;
|
||||
// when using snapshot mode and snapshot is loading and snapshot list is empty
|
||||
const isSnapshotSecretEmtpy =
|
||||
isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length;
|
||||
@@ -411,15 +413,17 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
<FormProvider {...method}>
|
||||
<form autoComplete="off">
|
||||
{/* breadcrumb row */}
|
||||
<div className="relative right-5">
|
||||
<NavHeader
|
||||
pageName={t('dashboard:title')}
|
||||
currentEnv={userAvailableEnvs?.filter(envir => envir.slug === envFromTop)[0].name || ''}
|
||||
isProjectRelated
|
||||
<NavHeader
|
||||
pageName={t('dashboard:title')}
|
||||
currentEnv={
|
||||
userAvailableEnvs?.filter((envir) => envir.slug === envFromTop)[0].name || ''
|
||||
}
|
||||
isProjectRelated
|
||||
userAvailableEnvs={userAvailableEnvs}
|
||||
onEnvChange={onEnvChange}
|
||||
/>
|
||||
@@ -445,7 +449,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
setSnaphotId(null);
|
||||
reset({ ...secrets, isSnapshotMode: false });
|
||||
}}
|
||||
className='h-10'
|
||||
className="h-10"
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
@@ -456,7 +460,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
|
||||
isLoading={isLoadingSnapshotCount}
|
||||
isDisabled={!canDoRollback}
|
||||
className='h-10'
|
||||
className="h-10"
|
||||
>
|
||||
{snapshotCount} Commits
|
||||
</Button>
|
||||
@@ -465,7 +469,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />}
|
||||
onClick={handleSubmit(onSaveSecret)}
|
||||
className='h-10'
|
||||
className="h-10"
|
||||
>
|
||||
{isRollbackMode ? 'Rollback' : 'Save Changes'}
|
||||
</Button>
|
||||
@@ -475,7 +479,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
<div className="mt-4 flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Input
|
||||
className="bg-mineshaft-600 h-[2.3rem] placeholder-mineshaft-50"
|
||||
className="h-[2.3rem] bg-mineshaft-600 placeholder-mineshaft-50"
|
||||
placeholder="Search keys..."
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
@@ -490,12 +494,15 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto bg-mineshaft-800 border border-mineshaft-600 p-1" hideCloseBtn>
|
||||
<PopoverContent
|
||||
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-1"
|
||||
hideCloseBtn
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Button
|
||||
onClick={() => downloadSecret(getValues('secrets'), selectedEnv?.slug)}
|
||||
variant="star"
|
||||
className="bg-bunker-700 h-8"
|
||||
className="h-8 bg-bunker-700"
|
||||
>
|
||||
Download as .env
|
||||
</Button>
|
||||
@@ -514,26 +521,38 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{!isReadOnly && !isRollbackMode && <Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false })}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="star"
|
||||
className="h-10"
|
||||
>
|
||||
Add Secret
|
||||
</Button>}
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (secretContainer.current) {
|
||||
secretContainer.current.scroll({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
|
||||
}}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="star"
|
||||
className="h-10"
|
||||
>
|
||||
Add Secret
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${isSecretEmpty ? "flex flex-col items-center justify-center" : ""} mt-4 h-[calc(100vh-270px)] overflow-y-scroll overflow-x-hidden no-scrollbar no-scrollbar::-webkit-scrollbar`}>
|
||||
<div
|
||||
className={`${
|
||||
isSecretEmpty ? 'flex flex-col items-center justify-center' : ''
|
||||
} no-scrollbar::-webkit-scrollbar mt-4 h-[calc(100vh-270px)] overflow-x-hidden overflow-y-scroll no-scrollbar`}
|
||||
ref={secretContainer}
|
||||
>
|
||||
{!isSecretEmpty && (
|
||||
<TableContainer>
|
||||
<table className="secret-table relative">
|
||||
<SecretTableHeader
|
||||
sortDir={sortDir}
|
||||
onSort={onSortSecrets}
|
||||
/>
|
||||
<tbody className="overflow-y-auto max-h-screen">
|
||||
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
||||
<tbody className="max-h-screen overflow-y-auto">
|
||||
{fields.map(({ id, _id }, index) => (
|
||||
<SecretInputRow
|
||||
key={id}
|
||||
@@ -554,11 +573,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
<td colSpan={3} className="hover:bg-mineshaft-700">
|
||||
<button
|
||||
type="button"
|
||||
className="w-[calc(100vw-400px)] h-8 ml-12 font-normal text-bunker-300 flex justify-start items-center"
|
||||
className="ml-12 flex h-8 items-center justify-start font-normal text-bunker-300"
|
||||
onClick={onAppendSecret}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
<span className="w-20 ml-2">Add Secret</span>
|
||||
<span className="ml-2 w-20">Add Secret</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import crypto from 'crypto';
|
||||
|
||||
import * as yup from 'yup';
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
decryptAssymmetric,
|
||||
encryptSymmetric
|
||||
} from '@app/components/utilities/cryptography/crypto';
|
||||
import { BatchSecretDTO } from '@app/hooks/api/secrets/types';
|
||||
import { BatchSecretDTO, DecryptedSecret } from '@app/hooks/api/secrets/types';
|
||||
|
||||
export enum SecretActionType {
|
||||
Created = 'created',
|
||||
@@ -147,10 +148,18 @@ const encryptASecret = (randomBytes: string, key: string, value?: string, commen
|
||||
};
|
||||
};
|
||||
|
||||
const deepCompareSecrets = (lhs: DecryptedSecret, rhs: any) =>
|
||||
lhs.key === rhs.key &&
|
||||
lhs.value === rhs.value &&
|
||||
lhs.comment === rhs.comment &&
|
||||
lhs?.valueOverride === rhs?.valueOverride &&
|
||||
JSON.stringify(lhs.tags) === JSON.stringify(rhs.tags);
|
||||
|
||||
export const transformSecretsToBatchSecretReq = (
|
||||
deletedSecretIds: string[],
|
||||
latestFileKey: any,
|
||||
secrets: FormData['secrets']
|
||||
secrets: FormData['secrets'],
|
||||
intialValues: DecryptedSecret[] = []
|
||||
) => {
|
||||
// deleted secrets
|
||||
const secretsToBeDeleted: BatchSecretDTO['requests'] = deletedSecretIds.map((id) => ({
|
||||
@@ -171,61 +180,80 @@ export const transformSecretsToBatchSecretReq = (
|
||||
})
|
||||
: crypto.randomBytes(16).toString('hex');
|
||||
|
||||
secrets?.forEach(
|
||||
({ _id, idOverride, value, valueOverride, overrideAction, tags = [], comment, key }) => {
|
||||
if (!idOverride && overrideAction === SecretActionType.Created) {
|
||||
secretsToBeCreated.push({
|
||||
method: 'POST',
|
||||
secret: {
|
||||
type: 'personal',
|
||||
tags,
|
||||
...encryptASecret(randomBytes, key, valueOverride, comment)
|
||||
}
|
||||
});
|
||||
}
|
||||
// to be created ones as they don't have server generated id
|
||||
if (!_id) {
|
||||
secretsToBeCreated.push({
|
||||
method: 'POST',
|
||||
secret: {
|
||||
type: 'shared',
|
||||
tags,
|
||||
...encryptASecret(randomBytes, key, value, comment)
|
||||
}
|
||||
});
|
||||
return; // exit as updated and delete case won't happen when created
|
||||
}
|
||||
// has an id means this is updated one
|
||||
if (_id) {
|
||||
secrets?.forEach((secret) => {
|
||||
const {
|
||||
_id,
|
||||
idOverride,
|
||||
value,
|
||||
valueOverride,
|
||||
overrideAction,
|
||||
tags = [],
|
||||
comment,
|
||||
key
|
||||
} = secret;
|
||||
if (!idOverride && overrideAction === SecretActionType.Created) {
|
||||
secretsToBeCreated.push({
|
||||
method: 'POST',
|
||||
secret: {
|
||||
type: 'personal',
|
||||
tags,
|
||||
secretName: key,
|
||||
...encryptASecret(randomBytes, key, valueOverride, comment)
|
||||
}
|
||||
});
|
||||
}
|
||||
// to be created ones as they don't have server generated id
|
||||
if (!_id) {
|
||||
secretsToBeCreated.push({
|
||||
method: 'POST',
|
||||
secret: {
|
||||
type: 'shared',
|
||||
tags,
|
||||
secretName: key,
|
||||
...encryptASecret(randomBytes, key, value, comment)
|
||||
}
|
||||
});
|
||||
return; // exit as updated and delete case won't happen when created
|
||||
}
|
||||
// has an id means this is updated one
|
||||
if (_id) {
|
||||
// check value has changed or not
|
||||
const initialSecretValue = intialValues?.find(({ _id: secId }) => secId === _id)!;
|
||||
if (!deepCompareSecrets(initialSecretValue, secret)) {
|
||||
secretsToBeUpdated.push({
|
||||
method: 'PATCH',
|
||||
secret: {
|
||||
_id,
|
||||
type: 'shared',
|
||||
tags,
|
||||
secretName: key,
|
||||
...encryptASecret(randomBytes, key, value, comment)
|
||||
}
|
||||
});
|
||||
}
|
||||
if (idOverride) {
|
||||
// if action is deleted meaning override has been removed but id is kept to collect at this point
|
||||
if (overrideAction === SecretActionType.Deleted) {
|
||||
secretsToBeDeleted.push({ method: 'DELETE', secret: { _id: idOverride } });
|
||||
} else {
|
||||
// if not deleted action then as id is there its an updated
|
||||
}
|
||||
if (idOverride) {
|
||||
// if action is deleted meaning override has been removed but id is kept to collect at this point
|
||||
if (overrideAction === SecretActionType.Deleted) {
|
||||
secretsToBeDeleted.push({ method: 'DELETE', secret: { _id: idOverride } });
|
||||
} else {
|
||||
// if not deleted action then as id is there its an updated
|
||||
const initialSecretValue = intialValues?.find(({ _id: secId }) => secId === _id)!;
|
||||
if (!deepCompareSecrets(initialSecretValue, secret)) {
|
||||
secretsToBeUpdated.push({
|
||||
method: 'PATCH',
|
||||
secret: {
|
||||
_id: idOverride,
|
||||
type: 'personal',
|
||||
tags,
|
||||
secretName: key,
|
||||
...encryptASecret(randomBytes, key, valueOverride, comment)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return secretsToBeCreated.concat(secretsToBeUpdated, secretsToBeDeleted);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { SyntheticEvent, useRef, useState } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import guidGenerator from '@app/components/utilities/randomId';
|
||||
|
||||
import { FormData } from '../../DashboardPage.utils';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
type Props = {
|
||||
index: number;
|
||||
@@ -19,84 +15,97 @@ type Props = {
|
||||
|
||||
const REGEX = /([$]{.*?})/g;
|
||||
|
||||
const DashboardInput = ({ isOverridden, isSecretValueHidden, isReadOnly, secret, index }: { isOverridden: boolean, isSecretValueHidden: boolean, isReadOnly?: boolean, secret: any, index: number } ): JSX.Element => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||
if (ref.current === null) return;
|
||||
|
||||
ref.current.scrollTop = e.currentTarget.scrollTop;
|
||||
ref.current.scrollLeft = e.currentTarget.scrollLeft;
|
||||
};
|
||||
|
||||
return <td key={`row-${secret?.key || ''}--`} className={`flex cursor-default flex-row w-full justify-center h-10 items-center ${!(secret?.value || secret?.value === '') ? "bg-red-400/10" : "bg-mineshaft-900/30"}`}>
|
||||
<div className="group relative whitespace-pre flex flex-col justify-center w-full cursor-default">
|
||||
<input
|
||||
// {...register(`secrets.${index}.valueOverride`)}
|
||||
defaultValue={(isOverridden ? secret.valueOverride : secret?.value || '')}
|
||||
onScroll={syncScroll}
|
||||
readOnly={isReadOnly}
|
||||
className={`${
|
||||
isSecretValueHidden
|
||||
? 'text-transparent focus:text-transparent active:text-transparent'
|
||||
: ''
|
||||
} z-10 peer cursor-default font-mono ph-no-capture bg-transparent caret-transparent text-transparent text-sm px-2 py-2 w-full outline-none duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`${
|
||||
isSecretValueHidden && !isOverridden && secret?.value
|
||||
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400 duration-200'
|
||||
: ''
|
||||
} ${!secret?.value && "text-bunker-400 justify-center"}
|
||||
absolute cursor-default flex flex-row whitespace-pre font-mono z-0 ${isSecretValueHidden && secret?.value ? 'invisible' : 'visible'} peer-focus:visible mt-0.5 ph-no-capture overflow-x-scroll bg-transparent h-10 text-sm px-2 py-2 w-full min-w-16 outline-none duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
>
|
||||
{(secret?.value || secret?.value === '') && (isOverridden ? secret.valueOverride : secret?.value)?.split('').length === 0 && <span className='text-bunker-400/80 font-sans w-full'>EMPTY</span>}
|
||||
{(secret?.value || secret?.value === '') && (isOverridden ? secret.valueOverride : secret?.value)?.split(REGEX).map((word: string) => {
|
||||
if (word.match(REGEX) !== null) {
|
||||
return (
|
||||
<span className="ph-no-capture text-yellow" key={word}>
|
||||
{word.slice(0, 2)}
|
||||
<span className="ph-no-capture text-yellow-200/80">
|
||||
{word.slice(2, word.length - 1)}
|
||||
</span>
|
||||
{word.slice(word.length - 1, word.length) === '}' ? (
|
||||
<span className="ph-no-capture text-yellow">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="ph-no-capture text-yellow-400">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span key={`${word}_${index + 1}`} className="ph-no-capture">
|
||||
{word}
|
||||
const DashboardInput = ({
|
||||
isOverridden,
|
||||
isSecretValueHidden,
|
||||
secret,
|
||||
isReadOnly = true
|
||||
}: {
|
||||
isOverridden: boolean;
|
||||
isSecretValueHidden: boolean;
|
||||
isReadOnly?: boolean;
|
||||
secret?: any;
|
||||
}): JSX.Element => {
|
||||
const syntaxHighlight = useCallback((val: string) => {
|
||||
if (val === undefined)
|
||||
return (
|
||||
<span className="cursor-default font-sans text-xs italic text-red-500/80">missing</span>
|
||||
);
|
||||
if (val?.length === 0)
|
||||
return <span className="w-full font-sans text-bunker-400/80">EMPTY</span>;
|
||||
return val?.split(REGEX).map((word, index) =>
|
||||
word.match(REGEX) !== null ? (
|
||||
<span className="ph-no-capture text-yellow" key={`${val}-${index + 1}`}>
|
||||
{word.slice(0, 2)}
|
||||
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
|
||||
{word.slice(word.length - 1, word.length) === '}' ? (
|
||||
<span className="ph-no-capture text-yellow">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{!(secret?.value || secret?.value === '') && <span className='text-red-500/80 cursor-default font-sans text-xs italic'>missing</span>}
|
||||
</div>
|
||||
{(isSecretValueHidden && secret?.value) && (
|
||||
<div className='absolute flex flex-row justify-between items-center z-0 peer pr-2 peer-active:hidden peer-focus:hidden group-hover:bg-white/[0.00] duration-100 h-10 w-full text-bunker-400 text-clip'>
|
||||
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||
{(isOverridden ? secret.valueOverride : secret?.value || '')?.split('').map(() => (
|
||||
<FontAwesomeIcon
|
||||
key={guidGenerator()}
|
||||
className="text-xxs mr-0.5"
|
||||
icon={faCircle}
|
||||
/>
|
||||
))}
|
||||
{(isOverridden ? secret.valueOverride : secret?.value || '')?.split('').length === 0 && <span className='text-bunker-400/80 text-sm'>EMPTY</span>}
|
||||
</div>
|
||||
) : (
|
||||
<span className="ph-no-capture text-yellow-400">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span key={word} className="ph-no-capture">
|
||||
{word}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<td
|
||||
key={`row-${secret?.key || ''}--`}
|
||||
className={`flex h-10 w-full cursor-default flex-row items-center justify-center ${
|
||||
!(secret?.value || secret?.value === '') ? 'bg-red-800/10' : 'bg-mineshaft-900/30'
|
||||
}`}
|
||||
>
|
||||
<div className="group relative flex w-full cursor-default flex-col justify-center whitespace-pre">
|
||||
<input
|
||||
value={isOverridden ? secret.valueOverride : secret?.value || ''}
|
||||
readOnly={isReadOnly}
|
||||
className={twMerge(
|
||||
'ph-no-capture no-scrollbar::-webkit-scrollbar duration-50 peer z-10 w-full cursor-default bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-transparent outline-none no-scrollbar',
|
||||
isSecretValueHidden && 'text-transparent focus:text-transparent active:text-transparent'
|
||||
)}
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
className={twMerge(
|
||||
'ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 absolute z-0 mt-0.5 flex h-10 w-full cursor-default flex-row overflow-x-scroll whitespace-pre bg-transparent px-2 py-2 font-mono text-sm outline-none no-scrollbar peer-focus:visible',
|
||||
isSecretValueHidden && secret?.value ? 'invisible' : 'visible',
|
||||
isSecretValueHidden &&
|
||||
secret?.value &&
|
||||
'duration-50 text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400',
|
||||
!secret?.value && 'justify-center text-bunker-400'
|
||||
)}
|
||||
>
|
||||
{syntaxHighlight(secret?.value)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
{isSecretValueHidden && secret?.value && (
|
||||
<div className="duration-50 peer absolute z-0 flex h-10 w-full flex-row items-center justify-between text-clip pr-2 text-bunker-400 group-hover:bg-white/[0.00] peer-focus:hidden peer-active:hidden">
|
||||
<div className="no-scrollbar::-webkit-scrollbar flex flex-row items-center overflow-x-scroll px-2 no-scrollbar">
|
||||
{(isOverridden ? secret.valueOverride : secret?.value || '')
|
||||
?.split('')
|
||||
.map((_a: string, index: number) => (
|
||||
<FontAwesomeIcon
|
||||
key={`${secret?.value}_${index + 1}`}
|
||||
className="mr-0.5 text-xxs"
|
||||
icon={faCircle}
|
||||
/>
|
||||
))}
|
||||
{(isOverridden ? secret.valueOverride : secret?.value || '')?.split('').length ===
|
||||
0 && <span className="text-sm text-bunker-400/80">EMPTY</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
export const EnvComparisonRow = ({
|
||||
index,
|
||||
@@ -105,27 +114,39 @@ export const EnvComparisonRow = ({
|
||||
isReadOnly,
|
||||
userAvailableEnvs
|
||||
}: Props): JSX.Element => {
|
||||
const {
|
||||
// register, setValue,
|
||||
control } = useFormContext<FormData>();
|
||||
|
||||
// to get details on a secret
|
||||
const secret = useWatch({ name: `secrets.${index}`, control });
|
||||
|
||||
const [areValuesHiddenThisRow, setAreValuesHiddenThisRow] = useState(true);
|
||||
|
||||
const getSecretByEnv = useCallback(
|
||||
(secEnv: string, secs?: any[]) => secs?.find(({ env }) => env === secEnv),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<tr className="group min-w-full flex flex-row items-center hover:bg-bunker-700">
|
||||
<td className="w-10 h-10 px-4 flex items-center justify-center border-none"><div className='text-center w-10 text-xs text-bunker-400'>{index + 1}</div></td>
|
||||
<td className="flex flex-row justify-between items-center h-full min-w-[200px] lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<div className="flex truncate flex-row items-center h-8 cursor-default">{secrets![0].key || ''}</div>
|
||||
<button type="button" className='mr-1 ml-2 text-bunker-400 hover:text-bunker-300 invisible group-hover:visible' onClick={() => setAreValuesHiddenThisRow(!areValuesHiddenThisRow)}>
|
||||
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800">
|
||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
|
||||
</td>
|
||||
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<div className="flex h-8 cursor-default flex-row items-center truncate">
|
||||
{secrets![0].key || ''}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="invisible mr-1 ml-2 text-bunker-400 hover:text-bunker-300 group-hover:visible"
|
||||
onClick={() => setAreValuesHiddenThisRow(!areValuesHiddenThisRow)}
|
||||
>
|
||||
<FontAwesomeIcon icon={areValuesHiddenThisRow ? faEye : faEyeSlash} />
|
||||
</button>
|
||||
</td>
|
||||
{userAvailableEnvs?.map(env => {
|
||||
return <DashboardInput key={`row-${secret?.key || ''}-${env.slug}`} isOverridden={false} isSecretValueHidden={areValuesHiddenThisRow && isSecretValueHidden} isReadOnly={isReadOnly} secret={secrets?.filter(sec => sec.env === env.slug)[0]} index={index} />
|
||||
})}
|
||||
{userAvailableEnvs?.map(({ slug }) => (
|
||||
<DashboardInput
|
||||
isReadOnly={isReadOnly}
|
||||
key={`row-${secrets![0].key || ''}-${slug}`}
|
||||
isOverridden={false}
|
||||
secret={getSecretByEnv(slug, secrets)}
|
||||
isSecretValueHidden={areValuesHiddenThisRow && isSecretValueHidden}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { faCircle, faCircleDot, faShuffle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -44,14 +44,14 @@ export const SecretDetailDrawer = ({
|
||||
const [canRevealSecVal, setCanRevealSecVal] = useToggle();
|
||||
const [canRevealSecOverride, setCanRevealSecOverride] = useToggle();
|
||||
|
||||
const { register, setValue, watch } = useFormContext<FormData>();
|
||||
const secret = watch(`secrets.${index}`);
|
||||
const { register, setValue, control, getValues } = useFormContext<FormData>();
|
||||
|
||||
const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction` });
|
||||
const isOverridden =
|
||||
secret?.overrideAction === SecretActionType.Created ||
|
||||
secret?.overrideAction === SecretActionType.Modified;
|
||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||
|
||||
const onSecretOverride = () => {
|
||||
const secret = getValues(`secrets.${index}`);
|
||||
if (isOverridden) {
|
||||
// when user created a new override but then removes
|
||||
if (SecretActionType.Created) {
|
||||
@@ -67,21 +67,17 @@ export const SecretDetailDrawer = ({
|
||||
}
|
||||
};
|
||||
|
||||
if (!secret) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer onOpenChange={onOpenChange} isOpen={isDrawerOpen}>
|
||||
<DrawerContent
|
||||
className="border-l border-mineshaft-500 bg-bunker dark"
|
||||
className="dark border-l border-mineshaft-500 bg-bunker"
|
||||
title="Secret"
|
||||
footerContent={
|
||||
<div className="flex flex-col space-y-2 pt-4 shadow-md">
|
||||
<div>
|
||||
<Button
|
||||
variant="star"
|
||||
onClick={() => onEnvCompare(secret?.key)}
|
||||
onClick={() => onEnvCompare(getValues(`secrets.${index}.key`))}
|
||||
isFullWidth
|
||||
isDisabled={isReadOnly}
|
||||
>
|
||||
@@ -95,7 +91,10 @@ export const SecretDetailDrawer = ({
|
||||
<Button
|
||||
colorSchema="danger"
|
||||
isDisabled={isReadOnly}
|
||||
onClick={() => onSecretDelete(index, secret._id, secret.idOverride)}
|
||||
onClick={() => {
|
||||
const secret = getValues(`secrets.${index}`);
|
||||
onSecretDelete(index, secret._id, secret.idOverride);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
@@ -173,9 +172,9 @@ export const SecretDetailDrawer = ({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</FormControl>
|
||||
<div className="mb-4 text-sm text-bunker-300 dark">
|
||||
<div className="dark mb-4 text-sm text-bunker-300">
|
||||
<div className="mb-2">Version History</div>
|
||||
<div className="flex h-48 flex-col space-y-2 border border-mineshaft-600 overflow-y-auto overflow-x-hidden rounded-md bg-bunker-800 p-2 dark:[color-scheme:dark]">
|
||||
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
|
||||
{secretVersion?.map(({ createdAt, value, id }, i) => (
|
||||
<div key={id} className="flex flex-col space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -202,7 +201,12 @@ export const SecretDetailDrawer = ({
|
||||
</div>
|
||||
</div>
|
||||
<FormControl label="Comments & Notes">
|
||||
<TextArea className="border border-mineshaft-600 text-sm" isDisabled={isReadOnly} {...register(`secrets.${index}.comment`)} rows={5} />
|
||||
<TextArea
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
isDisabled={isReadOnly}
|
||||
{...register(`secrets.${index}.comment`)}
|
||||
rows={5}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { FormData } from '../../DashboardPage.utils';
|
||||
|
||||
type Props = {
|
||||
isReadOnly?: boolean;
|
||||
isSecretValueHidden?: boolean;
|
||||
isOverridden?: boolean;
|
||||
index: number;
|
||||
};
|
||||
|
||||
const REGEX = /([$]{.*?})/g;
|
||||
|
||||
export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridden }: Props) => {
|
||||
const { register, control } = useFormContext<FormData>();
|
||||
|
||||
const secretValue = useWatch({ control, name: `secrets.${index}.value` });
|
||||
const secretValueOverride = useWatch({ control, name: `secrets.${index}.valueOverride` });
|
||||
const value = isOverridden ? secretValueOverride : secretValue;
|
||||
|
||||
const syntaxHighlight = useCallback((val: string) => {
|
||||
if (val?.length === 0) return <span className="font-sans text-bunker-400/80">EMPTY</span>;
|
||||
return val?.split(REGEX).map((word) =>
|
||||
word.match(REGEX) !== null ? (
|
||||
<span className="ph-no-capture text-yellow" key={`${val}-${index + 1}`}>
|
||||
{word.slice(0, 2)}
|
||||
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
|
||||
{word.slice(word.length - 1, word.length) === '}' ? (
|
||||
<span className="ph-no-capture text-yellow">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="ph-no-capture text-yellow-400">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span key={`${word}_${index + 1}`} className="ph-no-capture">
|
||||
{word}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="group relative flex w-full flex-col justify-center whitespace-pre px-1.5">
|
||||
{isOverridden ? (
|
||||
<input
|
||||
{...register(`secrets.${index}.valueOverride`)}
|
||||
readOnly={isReadOnly}
|
||||
className={twMerge(
|
||||
'ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 peer z-10 w-full bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar',
|
||||
!isSecretValueHidden &&
|
||||
'text-transparent focus:text-transparent active:text-transparent'
|
||||
)}
|
||||
spellCheck="false"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
{...register(`secrets.${index}.value`)}
|
||||
readOnly={isReadOnly}
|
||||
className={twMerge(
|
||||
'ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 peer z-10 w-full bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar',
|
||||
!isSecretValueHidden &&
|
||||
'text-transparent focus:text-transparent active:text-transparent'
|
||||
)}
|
||||
spellCheck="false"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
'ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 absolute z-0 mt-0.5 flex h-10 w-full flex-row overflow-x-scroll whitespace-pre bg-transparent px-2 py-2 font-mono text-sm outline-none no-scrollbar peer-focus:visible',
|
||||
isSecretValueHidden ? 'invisible' : 'visible',
|
||||
isOverridden
|
||||
? 'text-primary-300'
|
||||
: 'duration-50 text-gray-400 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400'
|
||||
)}
|
||||
>
|
||||
{syntaxHighlight(value || '')}
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
'duration-50 peer absolute z-0 flex h-10 w-full flex-row items-center justify-between text-clip pr-2 text-bunker-400 group-hover:bg-white/[0.00] peer-focus:hidden peer-active:hidden',
|
||||
!isSecretValueHidden ? 'invisible' : 'visible'
|
||||
)}
|
||||
>
|
||||
<div className="no-scrollbar::-webkit-scrollbar flex flex-row items-center overflow-x-scroll px-2 no-scrollbar">
|
||||
{value?.split('').map((val, i) => (
|
||||
<FontAwesomeIcon
|
||||
key={`${value}_${val}_${i + 1}`}
|
||||
className="mr-0.5 text-xxs"
|
||||
icon={faCircle}
|
||||
/>
|
||||
))}
|
||||
{value?.split('').length === 0 && (
|
||||
<span className="text-sm text-bunker-400/80">EMPTY</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { SyntheticEvent, useRef } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||
import {
|
||||
faCircle,
|
||||
faCodeBranch,
|
||||
faComment,
|
||||
faEllipsis,
|
||||
@@ -15,7 +14,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { cx } from 'cva';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import guidGenerator from '@app/components/utilities/randomId';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -32,10 +30,10 @@ import {
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from '@app/components/v2';
|
||||
import { useToggle } from '@app/hooks';
|
||||
import { WsTag } from '@app/hooks/api/types';
|
||||
|
||||
import { FormData, SecretActionType } from '../../DashboardPage.utils';
|
||||
import { MaskedInput } from './MaskedInput';
|
||||
|
||||
type Props = {
|
||||
index: number;
|
||||
@@ -64,361 +62,327 @@ const tagColors = [
|
||||
{ bg: 'bg-[#C40B13]/40', text: 'text-[#FFDEDE]/70' },
|
||||
{ bg: 'bg-[#332FD0]/40', text: 'text-[#DFF6FF]/70' }
|
||||
];
|
||||
const REGEX = /([$]{.*?})/g;
|
||||
|
||||
export const SecretInputRow = ({
|
||||
index,
|
||||
isSecretValueHidden,
|
||||
onRowExpand,
|
||||
isReadOnly,
|
||||
isRollbackMode,
|
||||
isAddOnly,
|
||||
wsTags,
|
||||
onCreateTagOpen,
|
||||
onSecretDelete,
|
||||
searchTerm
|
||||
}: Props): JSX.Element => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||
if (ref.current === null) return;
|
||||
export const SecretInputRow = memo(
|
||||
({
|
||||
index,
|
||||
isSecretValueHidden,
|
||||
onRowExpand,
|
||||
isReadOnly,
|
||||
isRollbackMode,
|
||||
isAddOnly,
|
||||
wsTags,
|
||||
onCreateTagOpen,
|
||||
onSecretDelete,
|
||||
searchTerm
|
||||
}: Props): JSX.Element => {
|
||||
const isKeySubDisabled = useRef<boolean>(false);
|
||||
const { register, setValue, control } = useFormContext<FormData>();
|
||||
// comment management in a row
|
||||
const {
|
||||
fields: secretTags,
|
||||
remove,
|
||||
append
|
||||
} = useFieldArray({ control, name: `secrets.${index}.tags` });
|
||||
|
||||
ref.current.scrollTop = e.currentTarget.scrollTop;
|
||||
ref.current.scrollLeft = e.currentTarget.scrollLeft;
|
||||
};
|
||||
const { register, setValue, control } = useFormContext<FormData>();
|
||||
const [canRevealSecret] = useToggle();
|
||||
// comment management in a row
|
||||
const {
|
||||
fields: secretTags,
|
||||
remove,
|
||||
append
|
||||
} = useFieldArray({ control, name: `secrets.${index}.tags` });
|
||||
// to get details on a secret
|
||||
const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction` });
|
||||
const idOverride = useWatch({ control, name: `secrets.${index}.idOverride` });
|
||||
const secComment = useWatch({ control, name: `secrets.${index}.comment` });
|
||||
const hasComment = Boolean(secComment);
|
||||
const secKey = useWatch({
|
||||
control,
|
||||
name: `secrets.${index}.key`,
|
||||
disabled: isKeySubDisabled.current
|
||||
});
|
||||
const secId = useWatch({ control, name: `secrets.${index}._id` });
|
||||
|
||||
// to get details on a secret
|
||||
const secret = useWatch({ name: `secrets.${index}`, control });
|
||||
const hasComment = Boolean(secret.comment);
|
||||
const tags = secret.tags || [];
|
||||
const selectedTagIds = tags.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => ({ ...prev, [curr.slug]: true }),
|
||||
{}
|
||||
);
|
||||
const tags = useWatch({ control, name: `secrets.${index}.tags`, defaultValue: [] }) || [];
|
||||
const selectedTagIds = tags.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => ({ ...prev, [curr.slug]: true }),
|
||||
{}
|
||||
);
|
||||
|
||||
// when secret is override by personal values
|
||||
const isOverridden =
|
||||
secret.overrideAction === SecretActionType.Created ||
|
||||
secret.overrideAction === SecretActionType.Modified;
|
||||
// when secret is override by personal values
|
||||
const isOverridden =
|
||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||
|
||||
const onSecretOverride = () => {
|
||||
if (isOverridden) {
|
||||
// when user created a new override but then removes
|
||||
if (secret?.overrideAction === SecretActionType.Created)
|
||||
const onSecretOverride = () => {
|
||||
if (isOverridden) {
|
||||
// when user created a new override but then removes
|
||||
if (overrideAction === SecretActionType.Created)
|
||||
setValue(`secrets.${index}.valueOverride`, '');
|
||||
setValue(`secrets.${index}.overrideAction`, SecretActionType.Deleted, {
|
||||
shouldDirty: true
|
||||
});
|
||||
} else {
|
||||
setValue(`secrets.${index}.valueOverride`, '');
|
||||
setValue(`secrets.${index}.overrideAction`, SecretActionType.Deleted, { shouldDirty: true });
|
||||
} else {
|
||||
setValue(`secrets.${index}.valueOverride`, '');
|
||||
setValue(
|
||||
`secrets.${index}.overrideAction`,
|
||||
secret?.idOverride ? SecretActionType.Modified : SecretActionType.Created,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
setValue(
|
||||
`secrets.${index}.overrideAction`,
|
||||
idOverride ? SecretActionType.Modified : SecretActionType.Created,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectTag = (selectedTag: WsTag) => {
|
||||
const shouldAppend = !selectedTagIds[selectedTag.slug];
|
||||
if (shouldAppend) {
|
||||
append(selectedTag);
|
||||
} else {
|
||||
const pos = tags.findIndex(({ slug }) => selectedTag.slug === slug);
|
||||
remove(pos);
|
||||
}
|
||||
};
|
||||
|
||||
const isCreatedSecret = !secId;
|
||||
const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly;
|
||||
|
||||
// Why this instead of filter in parent
|
||||
// Because rhf field.map has default values so basically
|
||||
// keys are not updated there and index needs to kept so that we can monitor
|
||||
// values individually here
|
||||
if (
|
||||
!(
|
||||
secKey?.toUpperCase().includes(searchTerm?.toUpperCase()) ||
|
||||
tags
|
||||
?.map((tag) => tag.name)
|
||||
.join(' ')
|
||||
?.toUpperCase()
|
||||
.includes(searchTerm?.toUpperCase()) ||
|
||||
secComment?.toUpperCase().includes(searchTerm?.toUpperCase())
|
||||
)
|
||||
) {
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectTag = (selectedTag: WsTag) => {
|
||||
const shouldAppend = !selectedTagIds[selectedTag.slug];
|
||||
if (shouldAppend) {
|
||||
append(selectedTag);
|
||||
} else {
|
||||
const pos = tags.findIndex(({ slug }) => selectedTag.slug === slug);
|
||||
remove(pos);
|
||||
}
|
||||
};
|
||||
|
||||
const isCreatedSecret = !secret?._id;
|
||||
const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly;
|
||||
|
||||
// Why this instead of filter in parent
|
||||
// Because rhf field.map has default values so basically
|
||||
// keys are not updated there and index needs to kept so that we can monitor
|
||||
// values individually here
|
||||
if (
|
||||
!(
|
||||
secret.key?.toUpperCase().includes(searchTerm?.toUpperCase()) ||
|
||||
tags
|
||||
?.map((tag) => tag.name)
|
||||
.join(' ')
|
||||
?.toUpperCase()
|
||||
.includes(searchTerm?.toUpperCase()) ||
|
||||
secret.comment?.toUpperCase().includes(searchTerm?.toUpperCase())
|
||||
)
|
||||
) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="group min-w-full flex flex-row items-center" key={index}>
|
||||
<td className="w-10 h-10 px-4 flex items-center justify-center"><div className='text-center w-10 text-xs text-bunker-400'>{index + 1}</div></td>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name={`secrets.${index}.key`}
|
||||
render={({ fieldState: { error }, field }) => (
|
||||
<HoverCard openDelay={0} open={error?.message ? undefined : false}>
|
||||
<HoverCardTrigger asChild>
|
||||
<td className={cx(error?.message ? 'rounded ring ring-red/50' : null)}>
|
||||
<div className="min-w-[220px] lg:min-w-[240px] xl:min-w-[280px] relative flex items-center justify-end w-full">
|
||||
<Input
|
||||
autoComplete="off"
|
||||
variant="plain"
|
||||
isDisabled={isReadOnly || shouldBeBlockedInAddOnly || isRollbackMode}
|
||||
className="w-full focus:text-bunker-100 focus:ring-transparent"
|
||||
{...field}
|
||||
/>
|
||||
<div className="w-max flex flex-row items-center justify-end">
|
||||
<Tooltip content="Comment">
|
||||
<div className={`${hasComment ? "w-5" : "w-0"} overflow-hidden group-hover:w-5 mt-0.5`}>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<IconButton
|
||||
return (
|
||||
<tr className="group flex flex-row items-center" key={index}>
|
||||
<td className="flex h-10 w-10 items-center justify-center px-4">
|
||||
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
|
||||
</td>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name={`secrets.${index}.key`}
|
||||
render={({ fieldState: { error }, field }) => (
|
||||
<HoverCard openDelay={0} open={error?.message ? undefined : false}>
|
||||
<HoverCardTrigger asChild>
|
||||
<td className={cx(error?.message ? 'rounded ring ring-red/50' : null)}>
|
||||
<div className="relative flex w-full min-w-[220px] items-center justify-end lg:min-w-[240px] xl:min-w-[280px]">
|
||||
<Input
|
||||
autoComplete="off"
|
||||
onFocus={() => {
|
||||
isKeySubDisabled.current = true;
|
||||
}}
|
||||
variant="plain"
|
||||
isDisabled={isReadOnly || shouldBeBlockedInAddOnly || isRollbackMode}
|
||||
className="w-full focus:text-bunker-100 focus:ring-transparent"
|
||||
{...field}
|
||||
onBlur={() => {
|
||||
isKeySubDisabled.current = false;
|
||||
field.onBlur();
|
||||
}}
|
||||
/>
|
||||
<div className="flex w-max flex-row items-center justify-end">
|
||||
<Tooltip content="Comment">
|
||||
<div
|
||||
className={`${
|
||||
hasComment ? 'w-5' : 'w-0'
|
||||
} mt-0.5 overflow-hidden group-hover:w-5`}
|
||||
>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<IconButton
|
||||
className={twMerge(
|
||||
'w-0 overflow-hidden p-0 group-hover:w-5',
|
||||
hasComment && 'w-5 text-primary'
|
||||
)}
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="add-tag"
|
||||
>
|
||||
<FontAwesomeIcon icon={faComment} />
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl">
|
||||
<FormControl label="Comment" className="mb-0">
|
||||
<TextArea
|
||||
isDisabled={
|
||||
isReadOnly || isRollbackMode || shouldBeBlockedInAddOnly
|
||||
}
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
{...register(`secrets.${index}.comment`)}
|
||||
rows={8}
|
||||
cols={30}
|
||||
/>
|
||||
</FormControl>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{!isAddOnly && (
|
||||
<div>
|
||||
<Tooltip content="Override with a personal value">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={twMerge(
|
||||
'w-0 overflow-hidden p-0 group-hover:w-5',
|
||||
hasComment && 'w-5 text-primary'
|
||||
'mt-0.5 w-0 overflow-hidden p-0 group-hover:ml-1 group-hover:w-6',
|
||||
isOverridden && 'ml-1 w-6 text-primary'
|
||||
)}
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="add-tag"
|
||||
onClick={onSecretOverride}
|
||||
size="md"
|
||||
isDisabled={isRollbackMode || isReadOnly}
|
||||
ariaLabel="info"
|
||||
>
|
||||
<FontAwesomeIcon icon={faComment} />
|
||||
<div className="flex items-center space-x-1">
|
||||
<FontAwesomeIcon icon={faCodeBranch} className="text-base" />
|
||||
</div>
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl p-2">
|
||||
<FormControl label="Comment" className="mb-0">
|
||||
<TextArea
|
||||
isDisabled={isReadOnly || isRollbackMode || shouldBeBlockedInAddOnly}
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
{...register(`secrets.${index}.comment`)}
|
||||
rows={8}
|
||||
cols={30}
|
||||
/>
|
||||
</FormControl>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{!isAddOnly && (
|
||||
<div>
|
||||
<Tooltip content="Override with a personal value">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={twMerge(
|
||||
'w-0 overflow-hidden p-0 group-hover:w-6 group-hover:ml-1 mt-0.5',
|
||||
isOverridden && 'w-6 text-primary ml-1'
|
||||
)}
|
||||
onClick={onSecretOverride}
|
||||
size="md"
|
||||
isDisabled={isRollbackMode || isReadOnly}
|
||||
ariaLabel="info"
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
<FontAwesomeIcon icon={faCodeBranch} className="text-base" />
|
||||
</div>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-auto py-2 pt-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="text-red" />
|
||||
</div>
|
||||
<div className="text-sm">{error?.message}</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)}
|
||||
/>
|
||||
<td className="flex flex-row w-full justify-center h-8 items-center">
|
||||
<div className="group relative whitespace-pre flex flex-col justify-center w-full px-1.5">
|
||||
{isOverridden
|
||||
? <input
|
||||
{...register(`secrets.${index}.valueOverride`)}
|
||||
onScroll={syncScroll}
|
||||
readOnly={isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)}
|
||||
className={`${
|
||||
(!canRevealSecret && isSecretValueHidden)
|
||||
? 'text-transparent focus:text-transparent active:text-transparent'
|
||||
: ''
|
||||
} z-10 peer font-mono ph-no-capture bg-transparent caret-white text-transparent text-sm px-2 py-2 w-full min-w-16 outline-none duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
spellCheck="false"
|
||||
/>
|
||||
: <input
|
||||
{...register(`secrets.${index}.value`)}
|
||||
onScroll={syncScroll}
|
||||
readOnly={isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)}
|
||||
className={`${
|
||||
(!canRevealSecret && isSecretValueHidden)
|
||||
? 'text-transparent focus:text-transparent active:text-transparent'
|
||||
: ''
|
||||
} z-10 peer font-mono ph-no-capture bg-transparent caret-white text-transparent text-sm px-2 py-2 w-full min-w-16 outline-none duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
spellCheck="false"
|
||||
/>}
|
||||
<div
|
||||
ref={ref}
|
||||
className={`${
|
||||
(!canRevealSecret && isSecretValueHidden) && !isOverridden
|
||||
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400 duration-200'
|
||||
: ''
|
||||
} ${isOverridden ? 'text-primary-300' : 'text-gray-400'}
|
||||
absolute flex flex-row whitespace-pre font-mono z-0 ${(!canRevealSecret && isSecretValueHidden) ? 'invisible' : 'visible'} peer-focus:visible mt-0.5 ph-no-capture overflow-x-scroll bg-transparent h-10 text-sm px-2 py-2 w-full min-w-16 outline-none duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
>
|
||||
{(isOverridden ? secret.valueOverride : secret.value)?.split('').length === 0 && <span className='text-bunker-400/80 font-sans'>EMPTY</span>}
|
||||
{(isOverridden ? secret.valueOverride : secret.value)?.split(REGEX).map((word) => {
|
||||
if (word.match(REGEX) !== null) {
|
||||
return (
|
||||
<span className="ph-no-capture text-yellow" key={guidGenerator()}>
|
||||
{word.slice(0, 2)}
|
||||
<span className="ph-no-capture text-yellow-200/80">
|
||||
{word.slice(2, word.length - 1)}
|
||||
</span>
|
||||
{word.slice(word.length - 1, word.length) === '}' ? (
|
||||
<span className="ph-no-capture text-yellow">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="ph-no-capture text-yellow-400">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span key={`${word}_${index + 1}`} className="ph-no-capture">
|
||||
{word}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{(!canRevealSecret && isSecretValueHidden) && (
|
||||
<div className='absolute flex flex-row justify-between items-center z-0 peer pr-2 peer-active:hidden peer-focus:hidden group-hover:bg-white/[0.00] duration-100 h-10 w-full text-bunker-400 text-clip'>
|
||||
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||
{(isOverridden ? secret.valueOverride : secret.value)?.split('').map(() => (
|
||||
<FontAwesomeIcon
|
||||
key={guidGenerator()}
|
||||
className="text-xxs mr-0.5"
|
||||
icon={faCircle}
|
||||
/>
|
||||
))}
|
||||
{(isOverridden ? secret.valueOverride : secret.value)?.split('').length === 0 && <span className='text-bunker-400/80 text-sm'>EMPTY</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex items-center min-w-sm h-10">
|
||||
<div className="flex items-center pl-2">
|
||||
{secretTags.map(({ id, slug }, i) => (
|
||||
<Tag
|
||||
className={cx(
|
||||
tagColors[i % tagColors.length].bg,
|
||||
tagColors[i % tagColors.length].text
|
||||
)}
|
||||
isDisabled={isReadOnly || isAddOnly || isRollbackMode}
|
||||
onClose={() => remove(i)}
|
||||
key={id}
|
||||
>
|
||||
{slug}
|
||||
</Tag>
|
||||
))}
|
||||
{!(isReadOnly || isAddOnly || isRollbackMode) && (
|
||||
<div className="w-0 overflow-hidden group-hover:w-8">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
</td>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-auto py-2 pt-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>
|
||||
<Tooltip content="Add tags">
|
||||
<IconButton variant="star" size="xs" ariaLabel="add-tag" className="py-[0.42rem]">
|
||||
<FontAwesomeIcon icon={faTags} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="text-red" />
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="left"
|
||||
className="max-h-96 w-auto min-w-[200px] overflow-y-auto overflow-x-hidden p-2 text-bunker-200 bg-mineshaft-800 border border-mineshaft-600"
|
||||
hideCloseBtn
|
||||
>
|
||||
<div className="mb-2 text-sm font-medium text-center text-bunker-200 px-2">Add tags to {secret.key || "this secret"}</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
{wsTags?.map((wsTag) => (
|
||||
<Button
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
'justify-start bg-mineshaft-600 text-bunker-100 hover:bg-mineshaft-500',
|
||||
selectedTagIds?.[wsTag.slug] && 'text-primary'
|
||||
)}
|
||||
onClick={() => onSelectTag(wsTag)}
|
||||
leftIcon={
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary mr-0"
|
||||
id="autoCapitalization"
|
||||
isChecked={selectedTagIds?.[wsTag.slug]}
|
||||
onCheckedChange={() => {}}
|
||||
>
|
||||
{}
|
||||
</Checkbox>
|
||||
}
|
||||
key={wsTag._id}
|
||||
>
|
||||
{wsTag.slug}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="star"
|
||||
color="primary"
|
||||
size="sm"
|
||||
className="mt-4 justify-start bg-mineshaft-600 h-7 px-1"
|
||||
onClick={onCreateTagOpen}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add new tag
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="text-sm">{error?.message}</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-0 group-hover:w-14 invisible group-hover:visible duration-0 items-center justify-end space-x-2 overflow-hidden transition-all">
|
||||
{!isAddOnly && (
|
||||
/>
|
||||
<td className="flex h-8 w-full flex-grow flex-row items-center justify-center">
|
||||
<MaskedInput
|
||||
isReadOnly={
|
||||
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
||||
}
|
||||
isOverridden={isOverridden}
|
||||
isSecretValueHidden={isSecretValueHidden}
|
||||
index={index}
|
||||
/>
|
||||
</td>
|
||||
<td className="min-w-sm flex h-10 items-center">
|
||||
<div className="flex items-center pl-2">
|
||||
{secretTags.map(({ id, slug }, i) => (
|
||||
<Tag
|
||||
className={cx(
|
||||
tagColors[i % tagColors.length].bg,
|
||||
tagColors[i % tagColors.length].text
|
||||
)}
|
||||
isDisabled={isReadOnly || isAddOnly || isRollbackMode}
|
||||
onClose={() => remove(i)}
|
||||
key={id}
|
||||
>
|
||||
{slug}
|
||||
</Tag>
|
||||
))}
|
||||
{!(isReadOnly || isAddOnly || isRollbackMode) && (
|
||||
<div className="w-0 overflow-hidden group-hover:w-8">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div>
|
||||
<Tooltip content="Add tags">
|
||||
<IconButton
|
||||
variant="star"
|
||||
size="xs"
|
||||
ariaLabel="add-tag"
|
||||
className="py-[0.42rem]"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTags} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="left"
|
||||
className="max-h-96 w-auto min-w-[200px] overflow-y-auto overflow-x-hidden border border-mineshaft-600 bg-mineshaft-800 p-2 text-bunker-200"
|
||||
hideCloseBtn
|
||||
>
|
||||
<div className="mb-2 px-2 text-center text-sm font-medium text-bunker-200">
|
||||
Add tags to {secKey || 'this secret'}
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
{wsTags?.map((wsTag) => (
|
||||
<Button
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
'justify-start bg-mineshaft-600 text-bunker-100 hover:bg-mineshaft-500',
|
||||
selectedTagIds?.[wsTag.slug] && 'text-primary'
|
||||
)}
|
||||
onClick={() => onSelectTag(wsTag)}
|
||||
leftIcon={
|
||||
<Checkbox
|
||||
className="mr-0 data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isChecked={selectedTagIds?.[wsTag.slug]}
|
||||
onCheckedChange={() => {}}
|
||||
>
|
||||
{}
|
||||
</Checkbox>
|
||||
}
|
||||
key={wsTag._id}
|
||||
>
|
||||
{wsTag.slug}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="star"
|
||||
color="primary"
|
||||
size="sm"
|
||||
className="mt-4 h-7 justify-start bg-mineshaft-600 px-1"
|
||||
onClick={onCreateTagOpen}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add new tag
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="duration-0 invisible flex w-0 items-center justify-end space-x-2 overflow-hidden transition-all group-hover:visible group-hover:w-14">
|
||||
{!isAddOnly && (
|
||||
<div>
|
||||
<Tooltip content="Settings">
|
||||
<IconButton
|
||||
size="lg"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
onClick={onRowExpand}
|
||||
ariaLabel="expand"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsis} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Tooltip content="Settings">
|
||||
<IconButton size="lg" colorSchema="primary" variant="plain" onClick={onRowExpand} ariaLabel="expand">
|
||||
<FontAwesomeIcon icon={faEllipsis} />
|
||||
<Tooltip content="Delete">
|
||||
<IconButton
|
||||
size="md"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
onClick={() => onSecretDelete(index, secId, idOverride)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Tooltip content="Delete">
|
||||
<IconButton
|
||||
size="md"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
onClick={() => onSecretDelete(index, secret._id, secret?.idOverride)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SecretInputRow.displayName = 'SecretInputRow';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { faArrowDown, faArrowUp } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -9,36 +8,29 @@ type Props = {
|
||||
onSort: () => void;
|
||||
};
|
||||
|
||||
export const SecretTableHeader = ({
|
||||
sortDir,
|
||||
onSort
|
||||
}: Props): JSX.Element => (
|
||||
export const SecretTableHeader = ({ sortDir, onSort }: Props): JSX.Element => (
|
||||
<thead>
|
||||
<tr className="absolute flex flex-row sticky top-0">
|
||||
<td className="w-10 px-4 flex items-center justify-center">
|
||||
<div className='text-center w-10 text-xs text-transparent'>{0}</div>
|
||||
<tr className="sticky top-0 flex flex-row">
|
||||
<td className="flex w-10 items-center justify-center px-4">
|
||||
<div className="w-10 text-center text-xs text-transparent">{0}</div>
|
||||
</td>
|
||||
<Controller
|
||||
defaultValue=""
|
||||
name="na"
|
||||
render={() => (
|
||||
<td className='flex items-center'>
|
||||
<div className="min-w-[220px] lg:min-w-[240px] xl:min-w-[280px] relative flex items-center justify-start pl-2.5 w-full">
|
||||
<div className="inline-flex items-end text-md font-medium">
|
||||
Key
|
||||
<IconButton variant="plain" className="ml-2" ariaLabel="sort" onClick={onSort}>
|
||||
<FontAwesomeIcon icon={sortDir === 'asc' ? faArrowDown : faArrowUp} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="w-max flex flex-row items-center justify-end">
|
||||
<div className="w-5 overflow-hidden group-hover:w-5 mt-1"/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
/>
|
||||
<th className="flex flex-row w-full"><div className="text-sm font-medium">Value</div></th>
|
||||
<td className="flex items-center">
|
||||
<div className="relative flex w-full min-w-[220px] items-center justify-start pl-2.5 lg:min-w-[240px] xl:min-w-[280px]">
|
||||
<div className="text-md inline-flex items-end font-medium">
|
||||
Key
|
||||
<IconButton variant="plain" className="ml-2" ariaLabel="sort" onClick={onSort}>
|
||||
<FontAwesomeIcon icon={sortDir === 'asc' ? faArrowDown : faArrowUp} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="flex w-max flex-row items-center justify-end">
|
||||
<div className="mt-1 w-5 overflow-hidden group-hover:w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<th className="flex w-full flex-row">
|
||||
<div className="text-sm font-medium">Value</div>
|
||||
</th>
|
||||
</tr>
|
||||
<tr className='h-0 w-full border border-mineshaft-600'/>
|
||||
<tr className="h-0 w-full border border-mineshaft-600" />
|
||||
</thead>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user