mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 22:30:15 +00:00
Add certificates modal and clean up workspace settings (#6993)
* Add new modal for managing certificates * update workspace settings modal * disable delete button while deleting an item * improve styles and add password viewer * better copy for add/manage certificates * fix weird key issue * tooltips * show filename * fix add cert form --------- Co-authored-by: jackkav <jackkav@gmail.com>
This commit is contained in:
parent
7ae1685c56
commit
cbd58dd0be
@ -85,7 +85,7 @@ const useDocumentActionPlugins = ({ workspace, apiSpec, project }: Props) => {
|
||||
};
|
||||
|
||||
export const WorkspaceCardDropdown: FC<Props> = props => {
|
||||
const { workspace, project, projects, workspaceMeta, clientCertificates, caCertificate } = props;
|
||||
const { workspace, project, projects } = props;
|
||||
const fetcher = useFetcher();
|
||||
const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
@ -209,10 +209,7 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
|
||||
{isSettingsModalOpen && (
|
||||
<WorkspaceSettingsModal
|
||||
workspace={workspace}
|
||||
workspaceMeta={workspaceMeta}
|
||||
clientCertificates={clientCertificates}
|
||||
caCertificate={caCertificate}
|
||||
onHide={() => setIsSettingsModalOpen(false)}
|
||||
onClose={() => setIsSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{isDeleteRemoteWorkspaceModalOpen && (
|
||||
|
@ -39,11 +39,8 @@ export const WorkspaceDropdown: FC = () => {
|
||||
invariant(organizationId, 'Expected organizationId');
|
||||
const {
|
||||
activeWorkspace,
|
||||
activeWorkspaceMeta,
|
||||
activeProject,
|
||||
activeApiSpec,
|
||||
clientCertificates,
|
||||
caCertificate,
|
||||
projects,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
const activeWorkspaceName = activeWorkspace.name;
|
||||
@ -257,10 +254,7 @@ export const WorkspaceDropdown: FC = () => {
|
||||
{isSettingsModalOpen && (
|
||||
<WorkspaceSettingsModal
|
||||
workspace={activeWorkspace}
|
||||
workspaceMeta={activeWorkspaceMeta}
|
||||
clientCertificates={clientCertificates}
|
||||
caCertificate={caCertificate}
|
||||
onHide={() => setIsSettingsModalOpen(false)}
|
||||
onClose={() => setIsSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{isDeleteRemoteWorkspaceModalOpen && (
|
||||
|
@ -0,0 +1,444 @@
|
||||
import React, { Fragment, useEffect, useId, useState } from 'react';
|
||||
import { Button, Dialog, FileTrigger, GridList, GridListItem, Heading, Input, Label, Modal, ModalOverlay, Tab, TabList, TabPanel, Tabs, ToggleButton } from 'react-aria-components';
|
||||
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { ClientCertificate } from '../../../models/client-certificate';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { Icon } from '../icon';
|
||||
import { PasswordViewer } from '../viewers/password-viewer';
|
||||
|
||||
const AddClientCertificateModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
|
||||
const createClientCertificateFetcher = useFetcher();
|
||||
const formId = useId();
|
||||
const [pfxPath, setPfxPath] = useState<string>('');
|
||||
const [certificatePath, setCertificatePath] = useState<string>('');
|
||||
const [keyPath, setKeyPath] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (createClientCertificateFetcher.data && createClientCertificateFetcher.data.certificate) {
|
||||
onClose();
|
||||
}
|
||||
}, [createClientCertificateFetcher.data, onClose]);
|
||||
|
||||
return (
|
||||
<ModalOverlay
|
||||
isOpen
|
||||
isDismissable
|
||||
onOpenChange={isOpen => {
|
||||
!isOpen && onClose();
|
||||
}}
|
||||
className="w-full h-[--visual-viewport-height] fixed z-20 top-0 left-0 flex items-center justify-center bg-black/30"
|
||||
>
|
||||
<Modal
|
||||
onOpenChange={isOpen => {
|
||||
!isOpen && onClose();
|
||||
}}
|
||||
className="flex flex-col w-full max-w-lg rounded-md border border-solid border-[--hl-sm] p-[--padding-lg] bg-[--color-bg] text-[--color-font]"
|
||||
>
|
||||
<Dialog
|
||||
className="outline-none flex-1 h-full flex flex-col overflow-y-hidden"
|
||||
>
|
||||
{({ close }) => (
|
||||
<div className='flex-1 flex flex-col gap-4 overflow-y-hidden h-full'>
|
||||
<div className='flex gap-2 items-center justify-between'>
|
||||
<Heading slot="title" className='text-2xl'>Add Client Certificate</Heading>
|
||||
<Button
|
||||
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
onPress={close}
|
||||
>
|
||||
<Icon icon="x" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='rounded flex-1 w-full basis-96 flex flex-col gap-2 select-none px-2 overflow-y-auto'>
|
||||
<form
|
||||
id={formId}
|
||||
className='flex flex-col gap-2'
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
const certificate = Object.fromEntries(formData.entries());
|
||||
|
||||
createClientCertificateFetcher.submit({
|
||||
...certificate,
|
||||
isPrivate: certificate.isPrivate === 'on',
|
||||
}, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/clientcert/new`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
name='parentId'
|
||||
type='text'
|
||||
value={workspaceId}
|
||||
readOnly
|
||||
className='hidden'
|
||||
/>
|
||||
<Label className='flex flex-col gap-1' aria-label='Host'>
|
||||
<span className='text-sm'>Host</span>
|
||||
<Input
|
||||
name='host'
|
||||
type='text'
|
||||
required
|
||||
placeholder='example.com'
|
||||
className='py-1 w-full pl-2 pr-7 rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors'
|
||||
/>
|
||||
</Label>
|
||||
<Tabs className="rounded-sm border border-solid border-[--hl-md]">
|
||||
<TabList className="flex items-center border-b border-solid border-[--hl-md]">
|
||||
<Tab className="hover:no-underline aria-selected:bg-[--hl-md] hover:bg-[--hl-sm] outline-none gap-2 flex items-center hover:bg-opacity-90 py-1 px-2 text-[--color-font] transition-colors" id="certificate">Certificate</Tab>
|
||||
<Tab className="hover:no-underline aria-selected:bg-[--hl-md] hover:bg-[--hl-sm] outline-none gap-2 flex items-center hover:bg-opacity-90 py-1 px-2 text-[--color-font] transition-colors" id="pfx">PFX or PKCS12</Tab>
|
||||
</TabList>
|
||||
<TabPanel className="p-2" id="pfx">
|
||||
<Label className='flex flex-col gap-1' aria-label='Host'>
|
||||
<span className='text-sm'>PFX or PKCS12 file</span>
|
||||
<FileTrigger
|
||||
allowsMultiple={false}
|
||||
onSelect={fileList => {
|
||||
if (!fileList) {
|
||||
return;
|
||||
}
|
||||
const files = Array.from(fileList);
|
||||
const file = files[0];
|
||||
|
||||
setPfxPath(file.path);
|
||||
}}
|
||||
>
|
||||
<Button className="flex flex-shrink-0 border-solid border border-[--hl-sm] py-1 gap-2 items-center justify-center px-2 h-full aria-pressed:bg-[--hl-sm] aria-selected:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-base">
|
||||
{!pfxPath && <Icon icon="plus" />}
|
||||
<span className='truncate' title={pfxPath}>{pfxPath ? pfxPath : 'Add PFX or PKCS12 file'}</span>
|
||||
</Button>
|
||||
</FileTrigger>
|
||||
<Input
|
||||
name='pfx'
|
||||
type='text'
|
||||
value={pfxPath}
|
||||
readOnly
|
||||
className='hidden'
|
||||
/>
|
||||
</Label>
|
||||
</TabPanel>
|
||||
<TabPanel className="flex flex-col overflow-hidden w-full gap-2 p-2" id="certificate">
|
||||
<Label className='flex-1 flex flex-col gap-1' aria-label='Certificate'>
|
||||
<span className='text-sm'>Certificate</span>
|
||||
<FileTrigger
|
||||
allowsMultiple={false}
|
||||
onSelect={fileList => {
|
||||
if (!fileList) {
|
||||
return;
|
||||
}
|
||||
const files = Array.from(fileList);
|
||||
const file = files[0];
|
||||
|
||||
setCertificatePath(file.path);
|
||||
}}
|
||||
>
|
||||
<Button className="flex flex-shrink-0 border-solid border border-[--hl-sm] py-1 gap-2 items-center justify-center px-2 h-full aria-pressed:bg-[--hl-sm] aria-selected:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-base">
|
||||
{!certificatePath && <Icon icon="plus" />}
|
||||
<span className='truncate' title={certificatePath}>{certificatePath ? certificatePath : 'Add certificate file'}</span>
|
||||
</Button>
|
||||
</FileTrigger>
|
||||
<Input
|
||||
name='cert'
|
||||
type='text'
|
||||
value={certificatePath}
|
||||
readOnly
|
||||
className='hidden'
|
||||
/>
|
||||
</Label>
|
||||
<Label className='flex-1 flex flex-col gap-1' aria-label='Key'>
|
||||
<span className='text-sm'>Key</span>
|
||||
<FileTrigger
|
||||
allowsMultiple={false}
|
||||
onSelect={fileList => {
|
||||
if (!fileList) {
|
||||
return;
|
||||
}
|
||||
const files = Array.from(fileList);
|
||||
const file = files[0];
|
||||
|
||||
setKeyPath(file.path);
|
||||
}}
|
||||
>
|
||||
<Button className="flex flex-shrink-0 border-solid border border-[--hl-sm] py-1 gap-2 items-center justify-center px-2 h-full aria-pressed:bg-[--hl-sm] aria-selected:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-base">
|
||||
{!keyPath && <Icon icon="plus" />}
|
||||
<span className='truncate' title={keyPath}>{keyPath ? keyPath : 'Add key file'}</span>
|
||||
</Button>
|
||||
</FileTrigger>
|
||||
<Input
|
||||
name='key'
|
||||
type='text'
|
||||
value={keyPath}
|
||||
readOnly
|
||||
className='hidden'
|
||||
/>
|
||||
</Label>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
<Label className='flex flex-col gap-1' aria-label='Passphrase'>
|
||||
<span className='text-sm'>Passphrase</span>
|
||||
<Input
|
||||
name='passphrase'
|
||||
type='password'
|
||||
className='py-1 w-full pl-2 pr-7 rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors'
|
||||
/>
|
||||
</Label>
|
||||
</form>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 justify-end'>
|
||||
<Button
|
||||
onPress={close}
|
||||
className="hover:no-underline hover:bg-opacity-90 border border-solid border-[--hl-md] hover:border-[--hl-sm] py-2 px-3 text-[--color-font] transition-colors rounded-sm"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" form={formId} className="hover:no-underline gap-2 flex items-center bg-opacity-100 bg-[rgba(var(--color-surprise-rgb),var(--tw-bg-opacity))] text-[--color-font-surprise] hover:bg-opacity-90 border border-solid border-[--hl-md] hover:border-[--hl-sm] py-2 px-3 transition-colors rounded-sm">
|
||||
<Icon icon="plus" />
|
||||
<span>Add certificate</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
);
|
||||
};
|
||||
|
||||
const ClientCertificateGridListItem = ({ certificate }: {
|
||||
certificate: ClientCertificate;
|
||||
}) => {
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
const updateClientCertificateFetcher = useFetcher();
|
||||
const deleteClientCertificateFetcher = useFetcher();
|
||||
|
||||
return (
|
||||
<GridListItem className="outline-none flex flex-col gap-2 pl-2 items-center justify-between p-1 ring-inset focus:ring-1 focus:ring-[--hl-md]">
|
||||
<div className='flex items-center gap-2 w-full'>
|
||||
{Boolean(certificate.pfx || certificate.cert) && <Icon icon="file-contract" className='w-4' title={certificate.pfx || certificate.cert || ''} />}
|
||||
{certificate.key && <Icon icon="key" title={certificate.key} />}
|
||||
<div className='flex-1 text-sm text-[--color-font] truncate'>{certificate.host}</div>
|
||||
<div className='flex items-center gap-2 h-6'>
|
||||
<ToggleButton
|
||||
onChange={isSelected => {
|
||||
updateClientCertificateFetcher.submit({ ...certificate, disabled: !isSelected }, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/clientcert/update`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
}}
|
||||
isSelected={!certificate.disabled}
|
||||
className="w-[12ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 h-full aria-pressed:bg-[--hl-sm] aria-selected:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<Fragment>
|
||||
<Icon icon={isSelected ? 'toggle-on' : 'toggle-off'} className={`${isSelected ? 'text-[--color-success]' : ''}`} />
|
||||
<span>{
|
||||
isSelected ? 'Enabled' : 'Disabled'
|
||||
}</span>
|
||||
</Fragment>
|
||||
)}
|
||||
</ToggleButton>
|
||||
<Button
|
||||
isDisabled={deleteClientCertificateFetcher.state !== 'idle'}
|
||||
onPress={() => {
|
||||
deleteClientCertificateFetcher.submit(JSON.stringify(certificate), {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/clientcert/delete`,
|
||||
method: 'delete',
|
||||
encType: 'application/json',
|
||||
});
|
||||
}}
|
||||
className="flex flex-shrink-0 items-center justify-center aspect-square h-full aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
>
|
||||
<Icon icon="trash" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{certificate.passphrase && (
|
||||
<div className='flex items-center gap-2 w-full truncate'>
|
||||
<span className='text-sm'>{'Password:'}</span>
|
||||
<div className='truncate text-sm'>
|
||||
<PasswordViewer text={certificate.passphrase} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</GridListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const CertificatesModal = ({ onClose }: {
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
|
||||
const routeData = useRouteLoaderData(
|
||||
':workspaceId'
|
||||
) as WorkspaceLoaderData;
|
||||
|
||||
const [isAddClientCertificateModalOpen, setIsAddClientCertificateModalOpen] = useState(false);
|
||||
|
||||
const createCertificateFetcher = useFetcher();
|
||||
const deleteCertificateFetcher = useFetcher();
|
||||
const updateCertificateFetcher = useFetcher();
|
||||
|
||||
const {
|
||||
caCertificate,
|
||||
clientCertificates,
|
||||
} = routeData;
|
||||
|
||||
if (!workspaceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalOverlay
|
||||
isOpen
|
||||
isDismissable
|
||||
onOpenChange={isOpen => {
|
||||
!isOpen && onClose();
|
||||
}}
|
||||
className="w-full h-[--visual-viewport-height] fixed z-10 top-0 left-0 flex items-center justify-center bg-black/30"
|
||||
>
|
||||
<Modal
|
||||
onOpenChange={isOpen => {
|
||||
!isOpen && onClose();
|
||||
}}
|
||||
className="flex flex-col w-full max-w-3xl h-[calc(100%-var(--padding-xl))] rounded-md border border-solid border-[--hl-sm] p-[--padding-lg] bg-[--color-bg] text-[--color-font]"
|
||||
>
|
||||
<Dialog
|
||||
className="outline-none flex-1 h-full flex flex-col overflow-hidden"
|
||||
>
|
||||
{({ close }) => (
|
||||
<div className='flex-1 flex flex-col gap-4 overflow-hidden h-full'>
|
||||
<div className='flex gap-2 items-center justify-between'>
|
||||
<Heading slot="title" className='text-2xl flex items-center gap-2'>Manage Certificates</Heading>
|
||||
<Button
|
||||
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
onPress={close}
|
||||
>
|
||||
<Icon icon="x" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='rounded flex-1 w-full overflow-hidden basis-96 flex flex-col gap-2 select-none overflow-y-auto'>
|
||||
<Heading className='text-xl'>CA Certificate</Heading>
|
||||
<p className="text-sm text-[--hl] italic max-w-[80ch]">
|
||||
<Icon icon='info-circle' className='pr-2' />
|
||||
One or more PEM format certificates in a single file to pass to curl. Overrides the root CA certificate.
|
||||
On MacOS please upload your local Keychain certificates here
|
||||
</p>
|
||||
{caCertificate ? (
|
||||
<div className='flex gap-2 pl-2 items-center justify-between rounded-sm border border-solid border-[--hl-sm] p-1'>
|
||||
<Icon icon="file-contract" className='w-4' />
|
||||
<div className='flex-1 text-sm text-[--color-font] truncate' title={caCertificate.path || ''}>{caCertificate?.path?.split('\\')?.pop()?.split('/')?.pop()}</div>
|
||||
<div className='flex items-center gap-2 h-6'>
|
||||
<ToggleButton
|
||||
onChange={isSelected => {
|
||||
updateCertificateFetcher.submit({ _id: caCertificate._id, disabled: !isSelected }, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/cacert/update`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
}}
|
||||
isSelected={!caCertificate.disabled}
|
||||
className="w-[12ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 h-full aria-pressed:bg-[--hl-sm] aria-selected:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<Fragment>
|
||||
<Icon icon={isSelected ? 'toggle-on' : 'toggle-off'} className={`${isSelected ? 'text-[--color-success]' : ''}`} />
|
||||
<span>{
|
||||
isSelected ? 'Enabled' : 'Disabled'
|
||||
}</span>
|
||||
</Fragment>
|
||||
)}
|
||||
</ToggleButton>
|
||||
<Button
|
||||
isDisabled={deleteCertificateFetcher.state !== 'idle'}
|
||||
onPress={() => {
|
||||
deleteCertificateFetcher.submit({}, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/cacert/delete`,
|
||||
method: 'delete',
|
||||
});
|
||||
}}
|
||||
className="flex flex-shrink-0 items-center justify-center aspect-square h-full aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
>
|
||||
<Icon icon="trash" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex gap-2 items-center justify-between'>
|
||||
<FileTrigger
|
||||
acceptedFileTypes={['.pem']}
|
||||
allowsMultiple={false}
|
||||
onSelect={fileList => {
|
||||
if (!fileList) {
|
||||
return;
|
||||
}
|
||||
const files = Array.from(fileList);
|
||||
const file = files[0];
|
||||
|
||||
createCertificateFetcher.submit({ parentId: workspaceId, path: file.path }, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/cacert/new`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button className="flex flex-1 flex-shrink-0 border-solid border border-[--hl-sm] py-1 gap-2 items-center justify-center px-2 h-full aria-pressed:bg-[--hl-sm] aria-selected:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-base">
|
||||
<Icon icon="plus" />
|
||||
<span>Add CA Certificate</span>
|
||||
</Button>
|
||||
</FileTrigger>
|
||||
</div>
|
||||
)}
|
||||
<span className='p-2' />
|
||||
<div className='flex items-center gap-2 justify-between'>
|
||||
<Heading className='text-xl'>Client Certificates</Heading>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setIsAddClientCertificateModalOpen(true);
|
||||
}}
|
||||
className="flex flex-shrink-0 gap-2 items-center justify-center px-2 h-full aria-pressed:bg-[--hl-sm] aria-selected:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-base"
|
||||
>
|
||||
<Icon icon="plus" />
|
||||
<span>Add client certificate</span>
|
||||
</Button>
|
||||
</div>
|
||||
{isAddClientCertificateModalOpen && (
|
||||
<AddClientCertificateModal
|
||||
onClose={() => {
|
||||
setIsAddClientCertificateModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<GridList
|
||||
className="border border-solid border-[--hl-md] rounded-sm divide-y divide-solid divide-[--hl-md] overflow-y-auto"
|
||||
items={clientCertificates.map(cert => ({
|
||||
cert,
|
||||
id: cert._id,
|
||||
key: cert._id,
|
||||
}))}
|
||||
>
|
||||
{item => <ClientCertificateGridListItem certificate={item.cert} />}
|
||||
</GridList>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 justify-end'>
|
||||
<Button
|
||||
onPress={close}
|
||||
className="hover:no-underline hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--color-font] transition-colors rounded-sm"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
);
|
||||
};
|
@ -1,104 +1,27 @@
|
||||
import React, { FC, ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { OverlayContainer } from 'react-aria';
|
||||
import React from 'react';
|
||||
import { Button, Dialog, Heading, Input, Label, Modal, ModalOverlay } from 'react-aria-components';
|
||||
import { useFetcher } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { database as db } from '../../../common/database';
|
||||
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
|
||||
import { CaCertificate } from '../../../models/ca-certificate';
|
||||
import type { ClientCertificate } from '../../../models/client-certificate';
|
||||
import * as models from '../../../models/index';
|
||||
import { isRequest } from '../../../models/request';
|
||||
import { isScratchpad, Workspace } from '../../../models/workspace';
|
||||
import { WorkspaceMeta } from '../../../models/workspace-meta';
|
||||
import { FileInputButton } from '../base/file-input-button';
|
||||
import { Modal, type ModalHandle, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { PromptButton } from '../base/prompt-button';
|
||||
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
|
||||
import { HelpTooltip } from '../help-tooltip';
|
||||
import { Icon } from '../icon';
|
||||
import { MarkdownEditor } from '../markdown-editor';
|
||||
import { PasswordViewer } from '../viewers/password-viewer';
|
||||
|
||||
const CertificateFields = styled.div({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
margin: 'var(--padding-sm) 0',
|
||||
});
|
||||
|
||||
const CertificateField: FC<{
|
||||
title: string;
|
||||
value: string | null;
|
||||
privateText?: boolean;
|
||||
optional?: boolean;
|
||||
}> = ({
|
||||
title,
|
||||
value,
|
||||
privateText,
|
||||
optional,
|
||||
}) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
if (optional && value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let display: ReactNode = value;
|
||||
if (privateText) {
|
||||
display = <PasswordViewer text={value} />;
|
||||
} else {
|
||||
const filename = value.split('/').pop();
|
||||
display = <span className="monospace selectable" title={value}>{filename}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="pad-right no-wrap">
|
||||
<strong>{title}:</strong>{' '}{display}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceSettingsModalState {
|
||||
showAddCertificateForm: boolean;
|
||||
host: string;
|
||||
crtPath: string;
|
||||
keyPath: string;
|
||||
pfxPath: string;
|
||||
isPrivate: boolean;
|
||||
passphrase: string;
|
||||
showDescription: boolean;
|
||||
defaultPreviewMode: boolean;
|
||||
}
|
||||
interface Props extends ModalProps {
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
workspace: Workspace;
|
||||
workspaceMeta: WorkspaceMeta;
|
||||
clientCertificates: ClientCertificate[];
|
||||
caCertificate: CaCertificate | null;
|
||||
}
|
||||
|
||||
export const WorkspaceSettingsModal = ({ workspace, clientCertificates, caCertificate, onHide }: Props) => {
|
||||
export const WorkspaceSettingsModal = ({ workspace, onClose }: Props) => {
|
||||
const hasDescription = !!workspace.description;
|
||||
const isScratchpadWorkspace = isScratchpad(workspace);
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [state, setState] = useState<WorkspaceSettingsModalState>({
|
||||
showAddCertificateForm: false,
|
||||
host: '',
|
||||
crtPath: '',
|
||||
keyPath: '',
|
||||
pfxPath: '',
|
||||
passphrase: '',
|
||||
isPrivate: false,
|
||||
showDescription: hasDescription,
|
||||
defaultPreviewMode: hasDescription,
|
||||
});
|
||||
|
||||
const activeWorkspaceName = workspace.name;
|
||||
useEffect(() => {
|
||||
modalRef.current?.show();
|
||||
}, []);
|
||||
|
||||
const { organizationId, projectId } = useParams<{ organizationId: string; projectId: string }>();
|
||||
const workspaceFetcher = useFetcher();
|
||||
@ -110,411 +33,88 @@ export const WorkspaceSettingsModal = ({ workspace, clientCertificates, caCertif
|
||||
});
|
||||
};
|
||||
|
||||
const _handleClearAllResponses = async () => {
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
const docs = await db.withDescendants(workspace, models.request.type);
|
||||
const requests = docs.filter(isRequest);
|
||||
for (const req of requests) {
|
||||
await models.response.removeForRequest(req._id);
|
||||
}
|
||||
modalRef.current?.hide();
|
||||
};
|
||||
|
||||
const _handleToggleCertificateForm = () => {
|
||||
setState({
|
||||
...state,
|
||||
showAddCertificateForm: !state.showAddCertificateForm,
|
||||
crtPath: '',
|
||||
keyPath: '',
|
||||
pfxPath: '',
|
||||
host: '',
|
||||
passphrase: '',
|
||||
isPrivate: false,
|
||||
});
|
||||
};
|
||||
const _handleCreateCertificate = async (event: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const { pfxPath, crtPath, keyPath, host, passphrase, isPrivate } = state;
|
||||
|
||||
newClientCert({
|
||||
host,
|
||||
isPrivate,
|
||||
parentId: workspace._id,
|
||||
passphrase: passphrase || null,
|
||||
disabled: false,
|
||||
cert: crtPath || null,
|
||||
key: keyPath || null,
|
||||
pfx: pfxPath || null,
|
||||
});
|
||||
_handleToggleCertificateForm();
|
||||
};
|
||||
|
||||
const renderCertificate = (certificate: ClientCertificate) => {
|
||||
return (
|
||||
<div className="row-spaced" key={certificate._id}>
|
||||
<CertificateFields>
|
||||
<CertificateField title="Host" value={certificate.host} />
|
||||
{certificate.pfx ? (
|
||||
<CertificateField title="PFX" value={certificate.pfx} />
|
||||
) : (
|
||||
<CertificateField title="CRT" value={certificate.cert} />
|
||||
)}
|
||||
<CertificateField title="Key" value={certificate.key} optional />
|
||||
<CertificateField title="Passphrase" value={certificate.passphrase} privateText optional />
|
||||
</CertificateFields>
|
||||
|
||||
<div className="no-wrap">
|
||||
<button
|
||||
className="btn btn--super-compact width-auto"
|
||||
title="Enable or disable certificate"
|
||||
onClick={() => toggleClientCert(certificate)}
|
||||
>
|
||||
{certificate.disabled ? (
|
||||
<i className="fa fa-square-o" />
|
||||
) : (
|
||||
<i className="fa fa-check-square-o" />
|
||||
)}
|
||||
</button>
|
||||
<PromptButton
|
||||
className="btn btn--super-compact width-auto"
|
||||
confirmMessage=""
|
||||
onClick={() => deleteClientCert(certificate)}
|
||||
>
|
||||
<i className="fa fa-trash-o" />
|
||||
</PromptButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const sharedCertificates = clientCertificates.filter(c => !c.isPrivate);
|
||||
const privateCertificates = clientCertificates.filter(c => c.isPrivate);
|
||||
const {
|
||||
pfxPath,
|
||||
crtPath,
|
||||
keyPath,
|
||||
isPrivate,
|
||||
showAddCertificateForm,
|
||||
showDescription,
|
||||
defaultPreviewMode,
|
||||
} = state;
|
||||
|
||||
const newCaCert = (path: string) => {
|
||||
workspaceFetcher.submit({ parentId: workspace._id, path }, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/cacert/new`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
const deleteCaCert = () => {
|
||||
workspaceFetcher.submit({}, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/cacert/delete`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
const toggleCaCert = (caCert: CaCertificate) => {
|
||||
workspaceFetcher.submit({ _id: caCert._id, disabled: !caCert.disabled }, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/cacert/update`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
const newClientCert = (certificate: Partial<ClientCertificate>) => {
|
||||
workspaceFetcher.submit(certificate, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/clientcert/new`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
const deleteClientCert = (certificate: ClientCertificate) => {
|
||||
workspaceFetcher.submit({ _id: certificate._id }, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/clientcert/delete`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
const toggleClientCert = (certificate: ClientCertificate) => {
|
||||
workspaceFetcher.submit({ _id: certificate._id, disabled: !certificate.disabled }, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/clientcert/update`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<Modal ref={modalRef} onHide={onHide}>
|
||||
{workspace ?
|
||||
<ModalHeader key={`header::${workspace._id}`}>
|
||||
{getWorkspaceLabel(workspace).singular} Settings{' '}
|
||||
<div data-testid="workspace-id" className="txt-sm selectable faint monospace">{workspace ? workspace._id : ''}</div>
|
||||
</ModalHeader> : null}
|
||||
{workspace ?
|
||||
<ModalBody key={`body::${workspace._id}`} noScroll>
|
||||
<Tabs
|
||||
items={[{
|
||||
title: 'Overview',
|
||||
children: <PanelContainer className="pad pad-top-sm">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
type="text"
|
||||
readOnly={isScratchpadWorkspace}
|
||||
placeholder="Awesome API"
|
||||
defaultValue={activeWorkspaceName}
|
||||
onChange={event => workspacePatcher(workspace._id, { name: event.target.value })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
{showDescription ? (
|
||||
<MarkdownEditor
|
||||
className="margin-top"
|
||||
defaultPreviewMode={defaultPreviewMode}
|
||||
placeholder="Write a description"
|
||||
defaultValue={workspace.description}
|
||||
onChange={(description: string) => {
|
||||
workspacePatcher(workspace._id, { description });
|
||||
if (state.defaultPreviewMode !== false) {
|
||||
setState(state => ({
|
||||
...state,
|
||||
defaultPreviewMode: false,
|
||||
}));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
setState({ ...state, showDescription: true });
|
||||
}}
|
||||
className="btn btn--outlined btn--super-duper-compact"
|
||||
>
|
||||
Add Description
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<h2>Actions</h2>
|
||||
<div className="form-control form-control--padded">
|
||||
<PromptButton
|
||||
onClick={_handleClearAllResponses}
|
||||
className="width-auto btn btn--clicky inline-block space-left"
|
||||
>
|
||||
<i className="fa fa-trash-o" /> Clear All Responses
|
||||
</PromptButton>
|
||||
</div>
|
||||
</PanelContainer>,
|
||||
},
|
||||
{
|
||||
title: 'Certificates',
|
||||
children: <PanelContainer className="pad">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
CA Certificate
|
||||
<HelpTooltip position="right" className="space-left">
|
||||
One or more PEM format certificates in a single file to pass to curl. Overrides the root CA certificate.
|
||||
</HelpTooltip>
|
||||
</label>
|
||||
<div className="row-spaced">
|
||||
<FileInputButton
|
||||
disabled={caCertificate !== null}
|
||||
className="btn btn--clicky"
|
||||
name="PEM file"
|
||||
onChange={newCaCert}
|
||||
path={caCertificate?.path || ''}
|
||||
showFileName
|
||||
showFileIcon
|
||||
/>
|
||||
<div className="no-wrap">
|
||||
<button
|
||||
disabled={caCertificate === null}
|
||||
className="btn btn--super-compact width-auto"
|
||||
title="Enable or disable certificate"
|
||||
onClick={() => caCertificate && toggleCaCert(caCertificate)}
|
||||
>
|
||||
{caCertificate?.disabled !== false ? (
|
||||
<i className="fa fa-square-o" />
|
||||
) : (
|
||||
<i className="fa fa-check-square-o" />
|
||||
)}
|
||||
</button>
|
||||
<PromptButton
|
||||
disabled={caCertificate === null}
|
||||
className="btn btn--super-compact width-auto"
|
||||
confirmMessage=""
|
||||
doneMessage=""
|
||||
onClick={deleteCaCert}
|
||||
>
|
||||
<i className="fa fa-trash-o" />
|
||||
</PromptButton>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-[--hl] italic">
|
||||
<Icon icon='info-circle' className='pr-2' />
|
||||
On MacOS please upload the local Keychain certificates here
|
||||
</p>
|
||||
</div>
|
||||
</PanelContainer>,
|
||||
},
|
||||
{
|
||||
title: 'Client Certificates',
|
||||
children: <PanelContainer className="pad">
|
||||
{!showAddCertificateForm ? (
|
||||
<div>
|
||||
{clientCertificates.length === 0 ? (
|
||||
<p className="notice surprise margin-top">
|
||||
You have not yet added any client certificates
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{!!sharedCertificates.length && (
|
||||
<div className="form-control form-control--outlined margin-top">
|
||||
<label>
|
||||
Shared Certificates
|
||||
<HelpTooltip position="right" className="space-left">
|
||||
Shared certificates will be synced.
|
||||
</HelpTooltip>
|
||||
</label>
|
||||
{sharedCertificates.map(renderCertificate)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!!privateCertificates.length && (
|
||||
<div className="form-control form-control--outlined margin-top">
|
||||
<label>
|
||||
Private Certificates
|
||||
<HelpTooltip position="right" className="space-left">
|
||||
Certificates will not be Git Synced.
|
||||
</HelpTooltip>
|
||||
</label>
|
||||
{privateCertificates.map(renderCertificate)}
|
||||
</div>
|
||||
)}
|
||||
<hr className="hr--spaced" />
|
||||
<div className="text-center">
|
||||
<button
|
||||
className="btn btn--clicky auto"
|
||||
onClick={_handleToggleCertificateForm}
|
||||
>
|
||||
New Client Certificate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={_handleCreateCertificate}>
|
||||
<div className="form-control form-control--outlined no-pad-top">
|
||||
<label>
|
||||
Host
|
||||
<HelpTooltip position="right" className="space-left">
|
||||
The host for which this client certificate is valid. Port number is optional
|
||||
and * can be used as a wildcard.
|
||||
</HelpTooltip>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="my-api.com"
|
||||
autoFocus
|
||||
onChange={event => setState({ ...state, host: event.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-control width-auto">
|
||||
<label>
|
||||
PFX <span className="faint">(or PKCS12)</span>
|
||||
<FileInputButton
|
||||
className="btn btn--clicky"
|
||||
onChange={pfxPath => setState({ ...state, pfxPath })}
|
||||
path={pfxPath}
|
||||
showFileName
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<br />
|
||||
<br />
|
||||
Or
|
||||
</div>
|
||||
<div className="row-fill">
|
||||
<div className="form-control">
|
||||
<label>
|
||||
CRT File
|
||||
<FileInputButton
|
||||
className="btn btn--clicky"
|
||||
name="Cert"
|
||||
onChange={crtPath => setState({ ...state, crtPath })}
|
||||
path={crtPath}
|
||||
showFileName
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control">
|
||||
<label>
|
||||
Key File
|
||||
<FileInputButton
|
||||
className="btn btn--clicky"
|
||||
name="Key"
|
||||
onChange={keyPath => setState({ ...state, keyPath })}
|
||||
path={keyPath}
|
||||
showFileName
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Passphrase
|
||||
<input
|
||||
type="password"
|
||||
placeholder="•••••••••••"
|
||||
onChange={event => setState({ ...state, passphrase: event.target.value })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--slim">
|
||||
<label>
|
||||
Private
|
||||
<HelpTooltip className="space-left">
|
||||
Certificates will not be Git Synced
|
||||
</HelpTooltip>
|
||||
<input
|
||||
type="checkbox"
|
||||
// @ts-expect-error -- TSCONVERSION boolean not valid
|
||||
value={isPrivate}
|
||||
onChange={event => setState({ ...state, isPrivate: event.target.checked })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<br />
|
||||
<div className="pad-top text-right">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--super-compact space-right"
|
||||
onClick={_handleToggleCertificateForm}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn--clicky space-right" type="submit">
|
||||
Create Certificate
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</PanelContainer>,
|
||||
}]}
|
||||
aria-label="Workspace settings tabs"
|
||||
>
|
||||
{props => (
|
||||
<TabItem key={props.title?.toString()} {...props} />
|
||||
)}
|
||||
</Tabs>
|
||||
</ModalBody> : null}
|
||||
<ModalOverlay
|
||||
isOpen
|
||||
isDismissable
|
||||
onOpenChange={isOpen => {
|
||||
!isOpen && onClose();
|
||||
}}
|
||||
className="w-full h-[--visual-viewport-height] fixed z-10 top-0 left-0 flex items-center justify-center bg-black/30"
|
||||
>
|
||||
<Modal
|
||||
onOpenChange={isOpen => {
|
||||
!isOpen && onClose();
|
||||
}}
|
||||
className="flex flex-col w-full max-w-3xl h-max max-h-[calc(100%-var(--padding-xl))] rounded-md border border-solid border-[--hl-sm] p-[--padding-lg] bg-[--color-bg] text-[--color-font]"
|
||||
>
|
||||
<Dialog
|
||||
className="outline-none flex-1 h-full flex flex-col overflow-hidden"
|
||||
>
|
||||
{({ close }) => (
|
||||
<div className='flex-1 flex flex-col gap-4 overflow-hidden h-full'>
|
||||
<div className='flex gap-2 items-center justify-between'>
|
||||
<Heading slot="title" className='text-2xl flex items-center gap-2'>{getWorkspaceLabel(workspace).singular} Settings{' '}</Heading>
|
||||
<Button
|
||||
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
onPress={close}
|
||||
>
|
||||
<Icon icon="x" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='rounded flex-1 w-full overflow-hidden basis-96 flex flex-col gap-2 select-none overflow-y-auto'>
|
||||
<Label className='flex flex-col gap-1 px-1' aria-label='Host'>
|
||||
<span>Name</span>
|
||||
<Input
|
||||
name='name'
|
||||
type='text'
|
||||
required
|
||||
readOnly={isScratchpadWorkspace}
|
||||
defaultValue={activeWorkspaceName}
|
||||
placeholder='Awesome API'
|
||||
className='p-2 w-full rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors'
|
||||
onChange={event => workspacePatcher(workspace._id, { name: event.target.value })}
|
||||
/>
|
||||
</Label>
|
||||
<Label className='flex flex-col px-1 gap-1' aria-label='Description'>
|
||||
<span>Description</span>
|
||||
<MarkdownEditor
|
||||
defaultPreviewMode={hasDescription}
|
||||
placeholder="Write a description"
|
||||
defaultValue={workspace.description}
|
||||
onChange={(description: string) => {
|
||||
workspacePatcher(workspace._id, { description });
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
<Heading>Actions</Heading>
|
||||
<PromptButton
|
||||
onClick={async () => {
|
||||
const docs = await db.withDescendants(workspace, models.request.type);
|
||||
const requests = docs.filter(isRequest);
|
||||
for (const req of requests) {
|
||||
await models.response.removeForRequest(req._id);
|
||||
}
|
||||
close();
|
||||
}}
|
||||
className="width-auto btn btn--clicky inline-block space-left"
|
||||
>
|
||||
<i className="fa fa-trash-o" /> Clear All Responses
|
||||
</PromptButton>
|
||||
<div className='flex items-center gap-2 justify-end'>
|
||||
<Button
|
||||
onPress={close}
|
||||
className="hover:no-underline hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--color-font] transition-colors rounded-sm"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</OverlayContainer>
|
||||
</ModalOverlay >
|
||||
);
|
||||
};
|
||||
WorkspaceSettingsModal.displayName = 'WorkspaceSettingsModal';
|
||||
|
@ -1123,8 +1123,10 @@ export const deleteCaCertificateAction: ActionFunction = async ({ params }) => {
|
||||
|
||||
export const createNewClientCertificateAction: ActionFunction = async ({ request }) => {
|
||||
const patch = await request.json();
|
||||
await models.clientCertificate.create(patch);
|
||||
return null;
|
||||
const certificate = await models.clientCertificate.create(patch);
|
||||
return {
|
||||
certificate,
|
||||
};
|
||||
};
|
||||
|
||||
export const updateClientCertificateAction: ActionFunction = async ({ request }) => {
|
||||
|
@ -70,6 +70,7 @@ import { GenerateCodeModal } from '../components/modals/generate-code-modal';
|
||||
import { PasteCurlModal } from '../components/modals/paste-curl-modal';
|
||||
import { PromptModal } from '../components/modals/prompt-modal';
|
||||
import { RequestSettingsModal } from '../components/modals/request-settings-modal';
|
||||
import { CertificatesModal } from '../components/modals/workspace-certificates-modal';
|
||||
import { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-environments-edit-modal';
|
||||
import { GrpcRequestPane } from '../components/panes/grpc-request-pane';
|
||||
import { GrpcResponsePane } from '../components/panes/grpc-response-pane';
|
||||
@ -160,6 +161,8 @@ export const Debug: FC = () => {
|
||||
activeProject,
|
||||
activeEnvironment,
|
||||
activeCookieJar,
|
||||
caCertificate,
|
||||
clientCertificates,
|
||||
grpcRequests,
|
||||
subEnvironments,
|
||||
baseEnvironment,
|
||||
@ -192,6 +195,7 @@ export const Debug: FC = () => {
|
||||
const [isRequestSettingsModalOpen, setIsRequestSettingsModalOpen] =
|
||||
useState(false);
|
||||
const [isEnvironmentModalOpen, setEnvironmentModalOpen] = useState(false);
|
||||
const [isCertificatesModalOpen, setCertificatesModalOpen] = useState(false);
|
||||
|
||||
const patchRequest = useRequestPatcher();
|
||||
const patchGroup = useRequestGroupPatcher();
|
||||
@ -781,6 +785,13 @@ export const Debug: FC = () => {
|
||||
<Icon icon="cookie-bite" className='w-5' />
|
||||
<span className='truncate'>{activeCookieJar.cookies.length === 0 ? 'Add' : 'Manage'} Cookies</span>
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => setCertificatesModalOpen(true)}
|
||||
className="px-4 py-1 max-w-full truncate flex-1 flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
>
|
||||
<Icon icon="file-contract" className='w-5' />
|
||||
<span className='truncate'>{clientCertificates.length === 0 || caCertificate ? 'Add' : 'Manage'} Certificates</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-1 overflow-hidden">
|
||||
@ -1132,6 +1143,9 @@ export const Debug: FC = () => {
|
||||
{isCookieModalOpen && (
|
||||
<CookiesModal onHide={() => setIsCookieModalOpen(false)} />
|
||||
)}
|
||||
{isCertificatesModalOpen && (
|
||||
<CertificatesModal onClose={() => setCertificatesModalOpen(false)} />
|
||||
)}
|
||||
{isPasteCurlModalOpen && (
|
||||
<PasteCurlModal
|
||||
onImport={req => {
|
||||
|
@ -61,6 +61,7 @@ import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dr
|
||||
import { Icon } from '../components/icon';
|
||||
import { InsomniaAI } from '../components/insomnia-ai-icon';
|
||||
import { CookiesModal } from '../components/modals/cookies-modal';
|
||||
import { CertificatesModal } from '../components/modals/workspace-certificates-modal';
|
||||
import { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-environments-edit-modal';
|
||||
import { SidebarLayout } from '../components/sidebar-layout';
|
||||
import { formatMethodName } from '../components/tags/method-tag';
|
||||
@ -205,6 +206,8 @@ const Design: FC = () => {
|
||||
activeProject,
|
||||
activeEnvironment,
|
||||
activeCookieJar,
|
||||
caCertificate,
|
||||
clientCertificates,
|
||||
subEnvironments,
|
||||
baseEnvironment,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
@ -217,6 +220,7 @@ const Design: FC = () => {
|
||||
|
||||
const [isCookieModalOpen, setIsCookieModalOpen] = useState(false);
|
||||
const [isEnvironmentModalOpen, setEnvironmentModalOpen] = useState(false);
|
||||
const [isCertificatesModalOpen, setCertificatesModalOpen] = useState(false);
|
||||
|
||||
const { apiSpec, lintMessages, rulesetPath, parsedSpec } = useLoaderData() as LoaderData;
|
||||
|
||||
@ -537,6 +541,13 @@ const Design: FC = () => {
|
||||
<Icon icon="cookie-bite" className='w-5' />
|
||||
<span className='truncate'>{activeCookieJar.cookies.length === 0 ? 'Add' : 'Manage'} Cookies</span>
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => setCertificatesModalOpen(true)}
|
||||
className="px-4 py-1 max-w-full truncate flex-1 flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
>
|
||||
<Icon icon="file-contract" className='w-5' />
|
||||
<span className='truncate'>{clientCertificates.length === 0 || caCertificate ? 'Add' : 'Manage'} Certificates</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-center gap-2 p-[--padding-sm]">
|
||||
<Heading className="text-[--hl] uppercase">Spec</Heading>
|
||||
@ -1020,6 +1031,9 @@ const Design: FC = () => {
|
||||
{isCookieModalOpen && (
|
||||
<CookiesModal onHide={() => setIsCookieModalOpen(false)} />
|
||||
)}
|
||||
{isCertificatesModalOpen && (
|
||||
<CertificatesModal onClose={() => setCertificatesModalOpen(false)} />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
renderPaneTwo={isSpecPaneOpen && <SwaggerUIDiv text={apiSpec.contents} />}
|
||||
|
@ -40,6 +40,7 @@ import { ErrorBoundary } from '../components/error-boundary';
|
||||
import { Icon } from '../components/icon';
|
||||
import { showPrompt } from '../components/modals';
|
||||
import { CookiesModal } from '../components/modals/cookies-modal';
|
||||
import { CertificatesModal } from '../components/modals/workspace-certificates-modal';
|
||||
import { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-environments-edit-modal';
|
||||
import { SidebarLayout } from '../components/sidebar-layout';
|
||||
import { TestRunStatus } from './test-results';
|
||||
@ -80,6 +81,8 @@ const TestRoute: FC = () => {
|
||||
activeProject,
|
||||
activeEnvironment,
|
||||
activeCookieJar,
|
||||
caCertificate,
|
||||
clientCertificates,
|
||||
subEnvironments,
|
||||
baseEnvironment,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
@ -92,6 +95,7 @@ const TestRoute: FC = () => {
|
||||
|
||||
const [isCookieModalOpen, setIsCookieModalOpen] = useState(false);
|
||||
const [isEnvironmentModalOpen, setEnvironmentModalOpen] = useState(false);
|
||||
const [isCertificatesModalOpen, setCertificatesModalOpen] = useState(false);
|
||||
|
||||
const createUnitTestSuiteFetcher = useFetcher();
|
||||
const deleteUnitTestSuiteFetcher = useFetcher();
|
||||
@ -325,6 +329,13 @@ const TestRoute: FC = () => {
|
||||
<Icon icon="cookie-bite" className='w-5' />
|
||||
<span className='truncate'>{activeCookieJar.cookies.length === 0 ? 'Add' : 'Manage'} Cookies</span>
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => setCertificatesModalOpen(true)}
|
||||
className="px-4 py-1 max-w-full truncate flex-1 flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
>
|
||||
<Icon icon="file-contract" className='w-5' />
|
||||
<span className='truncate'>{clientCertificates.length === 0 || caCertificate ? 'Add' : 'Manage'} Certificates</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="p-[--padding-sm]">
|
||||
<Button
|
||||
@ -443,6 +454,9 @@ const TestRoute: FC = () => {
|
||||
{isCookieModalOpen && (
|
||||
<CookiesModal onHide={() => setIsCookieModalOpen(false)} />
|
||||
)}
|
||||
{isCertificatesModalOpen && (
|
||||
<CertificatesModal onClose={() => setCertificatesModalOpen(false)} />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
}
|
||||
renderPaneOne={
|
||||
|
Loading…
Reference in New Issue
Block a user