Files
infisical/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx
T

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>
);
};