mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-19 10:36:23 +00:00
242 lines
10 KiB
TypeScript
242 lines
10 KiB
TypeScript
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 NavHeader from '@app/components/navigation/NavHeader';
|
|
import { Button, TableContainer, Tooltip } from '@app/components/v2';
|
|
import { useWorkspace } from '@app/context';
|
|
import {
|
|
useGetProjectSecretsByKey,
|
|
useGetUserWsEnvironments,
|
|
useGetUserWsKey
|
|
} from '@app/hooks/api';
|
|
import { WorkspaceEnv } from '@app/hooks/api/types';
|
|
|
|
import { EnvComparisonRow } from './components/EnvComparisonRow';
|
|
import { FormData, schema } from './DashboardPage.utils';
|
|
|
|
export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
|
|
const { t } = useTranslation();
|
|
const router = useRouter();
|
|
|
|
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
|
|
|
|
const { currentWorkspace, isLoading } = useWorkspace();
|
|
const workspaceId = currentWorkspace?._id as string;
|
|
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
|
|
|
useEffect(() => {
|
|
if (!isLoading && !workspaceId && router.isReady) {
|
|
router.push('/noprojects');
|
|
}
|
|
}, [isLoading, workspaceId, router.isReady]);
|
|
|
|
const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
|
|
workspaceId,
|
|
onSuccess: (data) => {
|
|
// get an env with one of the access available
|
|
const env = data.find(({ isReadDenied }) => !isReadDenied);
|
|
if (env) {
|
|
setSelectedEnv(env);
|
|
}
|
|
}
|
|
});
|
|
|
|
const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied);
|
|
|
|
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecretsByKey({
|
|
workspaceId,
|
|
env: userAvailableEnvs?.map((env) => env.slug) ?? [],
|
|
decryptFileKey: latestFileKey!,
|
|
isPaused: false
|
|
});
|
|
|
|
const method = useForm<FormData>({
|
|
// why any: well yup inferred ts expects other keys to defined as undefined
|
|
defaultValues: secrets as any,
|
|
values: secrets as any,
|
|
mode: 'onBlur',
|
|
resolver: yupResolver(schema)
|
|
});
|
|
|
|
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;
|
|
|
|
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]">
|
|
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// when secrets is not loading and secrets list is empty
|
|
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]">
|
|
<FormProvider {...method}>
|
|
<form autoComplete="off">
|
|
{/* breadcrumb row */}
|
|
<div className="relative right-5">
|
|
<NavHeader pageName={t('dashboard.title')} isProjectRelated />
|
|
</div>
|
|
<div className="mt-6">
|
|
<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="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="mx-1 text-primary/80 hover:text-primary"
|
|
href="https://infisical.com/docs/sdks/overview"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
Infisical SDKs
|
|
</a>
|
|
</p>
|
|
</div>
|
|
<div className="overflow-y-auto">
|
|
<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="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 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 ? '' : ''
|
|
} no-scrollbar::-webkit-scrollbar mt-3 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 rounded-md border border-mineshaft-600 no-scrollbar`}
|
|
>
|
|
{!isDashboardSecretEmpty && (
|
|
<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?.[key]}
|
|
isReadOnly={isReadOnly}
|
|
index={index}
|
|
isSecretValueHidden
|
|
userAvailableEnvs={userAvailableEnvs}
|
|
/>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</TableContainer>
|
|
)}
|
|
{isDashboardSecretEmpty && (
|
|
<div className="flex h-40 w-full flex-row rounded-md">
|
|
<div className="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 className="flex items-start justify-center h-full ml-10">
|
|
<Button
|
|
leftIcon={<FontAwesomeIcon icon={faPlus}/>}
|
|
onClick={() => prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false })}
|
|
variant="outline_bg"
|
|
colorSchema="primary"
|
|
isFullWidth
|
|
className="h-10"
|
|
>
|
|
Add Environment
|
|
</Button>
|
|
</div> */}
|
|
</div>
|
|
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center">
|
|
<div className="flex h-10 w-10 items-center justify-center border-none px-4">
|
|
<div className="w-10 text-center text-xs text-transparent">0</div>
|
|
</div>
|
|
<div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
|
<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}`)
|
|
variant="outline_bg"
|
|
colorSchema="primary"
|
|
isFullWidth
|
|
className="h-10"
|
|
>
|
|
Explore {env.name}
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</FormProvider>
|
|
</div>
|
|
);
|
|
};
|