mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-17 01:12:59 +00:00
373 lines
13 KiB
TypeScript
373 lines
13 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Controller, useForm } from 'react-hook-form';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { faCheck, faCopy, faKey, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import { yupResolver } from '@hookform/resolvers/yup';
|
|
import * as yup from 'yup';
|
|
|
|
import {
|
|
Button,
|
|
Checkbox,
|
|
DeleteActionModal,
|
|
EmptyState,
|
|
FormControl,
|
|
IconButton,
|
|
Input,
|
|
Modal,
|
|
ModalClose,
|
|
ModalContent,
|
|
ModalTrigger,
|
|
Select,
|
|
SelectItem,
|
|
Table,
|
|
TableContainer,
|
|
TableSkeleton,
|
|
TBody,
|
|
Td,
|
|
Th,
|
|
THead,
|
|
Tr
|
|
} from '@app/components/v2';
|
|
import { usePopUp, useToggle } from '@app/hooks';
|
|
import { ServiceToken, WorkspaceEnv } from '@app/hooks/api/types';
|
|
|
|
const apiTokenExpiry = [
|
|
{ label: '1 Day', value: 86400 },
|
|
{ label: '7 Days', value: 604800 },
|
|
{ label: '1 Month', value: 2592000 },
|
|
{ label: '6 months', value: 15552000 },
|
|
{ label: '12 months', value: 31104000 },
|
|
{ label: 'Never', value: null }
|
|
];
|
|
|
|
const createServiceTokenSchema = yup.object({
|
|
name: yup.string().required().label('Service Token Name'),
|
|
environment: yup.string().required().label('Environment'),
|
|
expiresIn: yup.string().optional().label('Service Token Expiration'),
|
|
permissions: yup
|
|
.object()
|
|
.shape({
|
|
read: yup.boolean().required(),
|
|
write: yup.boolean().required()
|
|
})
|
|
.defined()
|
|
.required()
|
|
});
|
|
|
|
export type CreateServiceToken = yup.InferType<typeof createServiceTokenSchema>;
|
|
|
|
type Props = {
|
|
tokens: ServiceToken[];
|
|
isLoading?: boolean;
|
|
workspaceName: string;
|
|
environments: WorkspaceEnv[];
|
|
onDeleteToken: (serviceTokenID: string) => Promise<void>;
|
|
onCreateToken: (data: CreateServiceToken) => Promise<string>;
|
|
};
|
|
|
|
type DeleteModalData = { name: string; id: string };
|
|
|
|
export const ServiceTokenSection = ({
|
|
tokens = [],
|
|
isLoading,
|
|
onDeleteToken,
|
|
workspaceName,
|
|
environments = [],
|
|
onCreateToken
|
|
}: Props): JSX.Element => {
|
|
const [newToken, setToken] = useState('');
|
|
const { t } = useTranslation();
|
|
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
|
|
|
|
useEffect(() => {
|
|
let timer: NodeJS.Timeout;
|
|
if (isTokenCopied) {
|
|
timer = setTimeout(() => setIsTokenCopied.off(), 2000);
|
|
}
|
|
return () => clearTimeout(timer);
|
|
}, [isTokenCopied]);
|
|
|
|
const copyTokenToClipboard = () => {
|
|
navigator.clipboard.writeText(newToken);
|
|
setIsTokenCopied.on();
|
|
};
|
|
|
|
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
|
'createAPIToken',
|
|
'deleteAPITokenConfirmation'
|
|
] as const);
|
|
|
|
const {
|
|
control,
|
|
reset,
|
|
handleSubmit,
|
|
formState: { isSubmitting }
|
|
} = useForm<CreateServiceToken>({
|
|
resolver: yupResolver(createServiceTokenSchema)
|
|
});
|
|
|
|
const hasServiceToken = Boolean(newToken);
|
|
|
|
const onFormSubmit = async (data: CreateServiceToken) => {
|
|
const token = await onCreateToken(data);
|
|
setToken(token);
|
|
};
|
|
|
|
const onDeleteApproved = async () => {
|
|
await onDeleteToken((popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id);
|
|
handlePopUpClose('deleteAPITokenConfirmation');
|
|
};
|
|
|
|
return (
|
|
<div className="mt-4 mb-4 flex w-full flex-col items-start rounded-md bg-white/5 p-6">
|
|
<div className="flex w-full flex-row justify-between">
|
|
<div className="flex w-full flex-col">
|
|
<p className="mb-3 text-xl font-semibold">{t('section.token.service-tokens')}</p>
|
|
<p className="text-sm text-gray-400">{t('section.token.service-tokens-description')}</p>
|
|
<p className="mb-4 text-sm text-gray-400">
|
|
Please, make sure you are on the
|
|
<a
|
|
className="ml-1 text-primary underline underline-offset-2"
|
|
href="https://infisical.com/docs/cli/overview"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
latest version of CLI
|
|
</a>
|
|
.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Modal
|
|
isOpen={popUp?.createAPIToken?.isOpen}
|
|
onOpenChange={(open) => {
|
|
handlePopUpToggle('createAPIToken', open);
|
|
reset();
|
|
setToken('');
|
|
}}
|
|
>
|
|
<ModalTrigger asChild>
|
|
<Button color="mineshaft" leftIcon={<FontAwesomeIcon icon={faPlus} />}>
|
|
{t('section.token.add-new')}
|
|
</Button>
|
|
</ModalTrigger>
|
|
<ModalContent
|
|
title={
|
|
t('section.token.add-dialog.title', {
|
|
target: workspaceName
|
|
}) as string
|
|
}
|
|
subTitle={t('section.token.add-dialog.description') as string}
|
|
>
|
|
{!hasServiceToken ? (
|
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
|
<Controller
|
|
control={control}
|
|
name="name"
|
|
defaultValue=""
|
|
render={({ field, fieldState: { error } }) => (
|
|
<FormControl
|
|
label={t('section.token.add-dialog.name')}
|
|
isError={Boolean(error)}
|
|
errorText={error?.message}
|
|
>
|
|
<Input {...field} placeholder="Type your token name" />
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
name="environment"
|
|
defaultValue={environments?.[0]?.slug}
|
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="Environment"
|
|
errorText={error?.message}
|
|
isError={Boolean(error)}
|
|
>
|
|
<Select
|
|
defaultValue={field.value}
|
|
{...field}
|
|
onValueChange={(e) => onChange(e)}
|
|
className="w-full"
|
|
>
|
|
{environments.map(({ name, slug }) => (
|
|
<SelectItem value={slug} key={slug}>
|
|
{name}
|
|
</SelectItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
name="expiresIn"
|
|
defaultValue={String(apiTokenExpiry?.[0]?.value)}
|
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="Expiration"
|
|
errorText={error?.message}
|
|
isError={Boolean(error)}
|
|
>
|
|
<Select
|
|
defaultValue={field.value}
|
|
{...field}
|
|
onValueChange={(e) => onChange(e)}
|
|
className="w-full"
|
|
>
|
|
{apiTokenExpiry.map(({ label, value }) => (
|
|
<SelectItem value={String(value || '')} key={label}>
|
|
{label}
|
|
</SelectItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
name="permissions"
|
|
defaultValue={{
|
|
read: true,
|
|
write: false
|
|
}}
|
|
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
|
const options = [
|
|
{
|
|
label: 'Read (default)',
|
|
value: 'read'
|
|
},
|
|
{
|
|
label: 'Write (optional)',
|
|
value: 'write'
|
|
}
|
|
];
|
|
|
|
return (
|
|
<FormControl
|
|
label="Permissions"
|
|
errorText={error?.message}
|
|
isError={Boolean(error)}
|
|
>
|
|
<>
|
|
{options.map(({ label, value: optionValue }) => {
|
|
// TODO: refactor
|
|
return (
|
|
<Checkbox
|
|
id={value[optionValue]}
|
|
key={optionValue}
|
|
className="data-[state=checked]:bg-primary"
|
|
isChecked={value[optionValue]}
|
|
isDisabled={optionValue === 'read'}
|
|
onCheckedChange={(state) => {
|
|
onChange({
|
|
...value,
|
|
[optionValue]: state
|
|
});
|
|
}}
|
|
>
|
|
{label}
|
|
</Checkbox>
|
|
);
|
|
})}
|
|
</>
|
|
</FormControl>
|
|
);
|
|
}}
|
|
/>
|
|
<div className="mt-8 flex items-center">
|
|
<Button
|
|
className="mr-4"
|
|
type="submit"
|
|
isDisabled={isSubmitting}
|
|
isLoading={isSubmitting}
|
|
>
|
|
Create
|
|
</Button>
|
|
<ModalClose asChild>
|
|
<Button variant="plain" colorSchema="secondary">
|
|
Cancel
|
|
</Button>
|
|
</ModalClose>
|
|
</div>
|
|
</form>
|
|
) : (
|
|
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
|
<p className="mr-4 break-all">{newToken}</p>
|
|
<IconButton
|
|
ariaLabel="copy icon"
|
|
colorSchema="secondary"
|
|
className="group relative"
|
|
onClick={copyTokenToClipboard}
|
|
>
|
|
<FontAwesomeIcon icon={isTokenCopied ? faCheck : faCopy} />
|
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
{t('common.click-to-copy')}
|
|
</span>
|
|
</IconButton>
|
|
</div>
|
|
)}
|
|
</ModalContent>
|
|
</Modal>
|
|
</div>
|
|
</div>
|
|
<DeleteActionModal
|
|
isOpen={popUp.deleteAPITokenConfirmation.isOpen}
|
|
title={`Delete ${
|
|
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || ' '
|
|
} service token?`}
|
|
onChange={(isOpen) => handlePopUpToggle('deleteAPITokenConfirmation', isOpen)}
|
|
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
|
|
onClose={() => handlePopUpClose('deleteAPITokenConfirmation')}
|
|
onDeleteApproved={onDeleteApproved}
|
|
/>
|
|
<TableContainer>
|
|
<Table>
|
|
<THead>
|
|
<Tr>
|
|
<Th>Token Name</Th>
|
|
<Th>Environment</Th>
|
|
<Th>Valid Until</Th>
|
|
<Th aria-label="button" />
|
|
</Tr>
|
|
</THead>
|
|
<TBody>
|
|
{isLoading && <TableSkeleton columns={4} key="project-service-tokens" />}
|
|
{!isLoading &&
|
|
tokens.map((row) => (
|
|
<Tr key={row._id}>
|
|
<Td>{row.name}</Td>
|
|
<Td>{row.environment}</Td>
|
|
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
|
|
<Td className="flex items-center justify-end">
|
|
<IconButton
|
|
onClick={() =>
|
|
handlePopUpOpen('deleteAPITokenConfirmation', {
|
|
name: row.name,
|
|
id: row._id
|
|
})
|
|
}
|
|
colorSchema="danger"
|
|
ariaLabel="delete"
|
|
>
|
|
<FontAwesomeIcon icon={faTrashCan} />
|
|
</IconButton>
|
|
</Td>
|
|
</Tr>
|
|
))}
|
|
{!isLoading && tokens?.length === 0 && (
|
|
<Tr>
|
|
<Td colSpan={4} className="py-6 text-center text-bunker-400">
|
|
<EmptyState title="No service tokens found" icon={faKey} />
|
|
</Td>
|
|
</Tr>
|
|
)}
|
|
</TBody>
|
|
</Table>
|
|
</TableContainer>
|
|
</div>
|
|
);
|
|
};
|