Skip to content

Commit

Permalink
Add intermediate confirmation step in ApiKeySettingsModal (#2514)
Browse files Browse the repository at this point in the history
* Add intermediate confirmation step in ApiKeySettingsModal

* settings e2e tests fixed for api key settings

* Change confirmation modal label
  • Loading branch information
CDimonaco authored Apr 15, 2024
1 parent b3a7b64 commit 29b4a41
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 86 deletions.
215 changes: 130 additions & 85 deletions assets/js/common/ApiKeySettingsModal/ApiKeySettingsModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Select from '@common/Select';
import Switch from '@common/Switch';
import CopyButton from '@common/CopyButton';
import ApiKeyBox from '@common/ApiKeyBox';
import Banner from '../Banners/Banner';

const normalizeExpiration = (expiration) =>
setMinutes(setHours(expiration, 0), 0);
Expand Down Expand Up @@ -52,15 +53,13 @@ function ApiKeySettingsModal({

const [quantityError, setQuantityError] = useState(false);
const [apiKeyNeverExpires, setApiKeyNeverExpires] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);

const generateApiKeyExpiration = () => {
if (apiKeyNeverExpires) {
onGenerate({ apiKeyExpiration: null });
setKeyGenerated(true);
return;
}
if (timeQuantity === 0 || !timeQuantity) {
setQuantityError(true);
setShowConfirmation(false);
return;
}
const timeQuantitySettings = availableTimeOptions.find(
Expand All @@ -70,6 +69,15 @@ function ApiKeySettingsModal({

onGenerate({ apiKeyExpiration: normalizeExpiration(apiKeyExpiration) });
setKeyGenerated(true);
setShowConfirmation(false);
};

const validateApiKeySettingsRequest = () => {
if (!apiKeyNeverExpires && (timeQuantity === 0 || !timeQuantity)) {
setQuantityError(true);
return;
}
setShowConfirmation(true);
};

useEffect(() => {
Expand All @@ -78,103 +86,140 @@ function ApiKeySettingsModal({
setTimeQuantityType(timeOptions[0]);
setTimeQuantity(0);
setKeyGenerated(false);
setShowConfirmation(false);
}, [open]);

return (
<Modal
title="API Key Settings"
title={showConfirmation ? 'Generate API Key' : 'API Key Settings'}
className="!w-3/4 !max-w-3xl"
open={open}
onClose={onClose}
>
<div className="flex flex-col my-2">
<span className="my-1 mb-4 text-gray-500">
{' '}
By generating a new key, you will need to replace the API key on all
hosts.{' '}
</span>
{showConfirmation ? (
<>
<Banner type="warning">
<span className="text-sm">
Generating a new API Key forces an update of the agent
configuration on all the registered hosts. <br />
This action cannot be undone.
</span>
</Banner>
<span className="my-1 mb-4 text-gray-500">
Are you sure you want to generate a new API key?
</span>
</>
) : (
<>
<span className="my-1 mb-4 text-gray-500">
{' '}
By generating a new key, you will need to replace the API key on
all hosts.{' '}
</span>

<div className="flex space-x-1">
<div className="w-1/5">
<Label>Never Expires</Label>
</div>

<Switch
selected={apiKeyNeverExpires}
onChange={() => {
setApiKeyNeverExpires((enabled) => !enabled);
setQuantityError(false);
}}
/>
</div>
<div className="flex items-center my-1 space-x-2">
<div className="w-1/3">
<Label>Key Expiration</Label>
</div>
<div className="flex space-x-1">
<div className="w-1/5">
<Label>Never Expires</Label>
</div>

<div className="w-2/4 pt-1">
<InputNumber
value={timeQuantity}
className="!h-8"
type="number"
min="0"
disabled={apiKeyNeverExpires}
error={quantityError}
onChange={(value) => {
setTimeQuantity(parseInt(value, 10));
setQuantityError(false);
}}
/>
</div>
<div className="w-2/4 pt-4">
<Select
optionsName=""
options={timeOptions}
disabled={apiKeyNeverExpires}
value={timeQuantityType}
onChange={(value) => setTimeQuantityType(value)}
/>
</div>
<div className="w-1/6 h-4/5">
<Button
className="generate-api-key"
onClick={() => generateApiKeyExpiration()}
disabled={quantityError || loading}
>
Generate
</Button>
</div>
</div>
{quantityError && (
<span className="my-1 mb-4 text-red-500">
{' '}
Key expiration value needs to be greater than 0{' '}
</span>
)}
{generatedApiKey && keyGenerated && !loading && (
<div className="flex flex-col my-1 mb-4">
<div className="flex space-x-2">
<ApiKeyBox apiKey={generatedApiKey} />
<CopyButton content={generatedApiKey} />
<Switch
selected={apiKeyNeverExpires}
onChange={() => {
setApiKeyNeverExpires((enabled) => !enabled);
setQuantityError(false);
}}
/>
</div>
<div className="flex space-x-2">
<EOS_INFO_OUTLINED size="20" className="mt-2" />
<div className="flex items-center my-1 space-x-2">
<div className="w-1/3">
<Label>Key Expiration</Label>
</div>

<div className="mt-2 text-gray-600 text-sm">
{generatedApiKeyExpiration
? `Key will expire ${format(
parseISO(generatedApiKeyExpiration),
'd LLL yyyy'
)}`
: 'Key will never expire'}
<div className="w-2/4 pt-1">
<InputNumber
value={timeQuantity}
className="!h-8"
type="number"
min="0"
disabled={apiKeyNeverExpires}
error={quantityError}
onChange={(value) => {
setTimeQuantity(parseInt(value, 10));
setQuantityError(false);
}}
/>
</div>
<div className="w-2/4 pt-4">
<Select
optionsName=""
options={timeOptions}
disabled={apiKeyNeverExpires}
value={timeQuantityType}
onChange={(value) => setTimeQuantityType(value)}
/>
</div>
<div className="w-1/6 h-4/5">
<Button
className="generate-api-key"
onClick={() => validateApiKeySettingsRequest()}
disabled={quantityError || loading}
>
Generate
</Button>
</div>
</div>
</div>
{quantityError && (
<span className="my-1 mb-4 text-red-500">
{' '}
Key expiration value needs to be greater than 0{' '}
</span>
)}
{generatedApiKey && keyGenerated && !loading && (
<div className="flex flex-col my-1 mb-4">
<div className="flex space-x-2">
<ApiKeyBox apiKey={generatedApiKey} />
<CopyButton content={generatedApiKey} />
</div>
<div className="flex space-x-2">
<EOS_INFO_OUTLINED size="20" className="mt-2" />

<div className="mt-2 text-gray-600 text-sm">
{generatedApiKeyExpiration
? `Key will expire ${format(
parseISO(generatedApiKeyExpiration),
'd LLL yyyy'
)}`
: 'Key will never expire'}
</div>
</div>
</div>
)}
</>
)}
<div className="w-1/6 h-4/5">
<Button type="primary-white" onClick={onClose}>
Close
</Button>
<div className="w-1/6 h-4/5 flex">
{showConfirmation ? (
<>
<Button
className="w-1/6 mr-2 generate-api-confirmation"
onClick={() => generateApiKeyExpiration()}
disabled={loading}
>
Generate
</Button>
<Button
type="primary-white"
onClick={() => setShowConfirmation(false)}
className="w-1/6"
>
Cancel
</Button>
</>
) : (
<Button type="primary-white" onClick={onClose}>
Close
</Button>
)}
</div>
</div>
</Modal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe('ApiKeySettingsModal', () => {
).not.toBeInTheDocument();
});

it('should return on onGenerate the correct expiration date when months are selected with a valid quantity', async () => {
it('should return on onGenerate the correct expiration date when months are selected with a valid quantity after confirmation', async () => {
const user = userEvent.setup();
const onGenerate = jest.fn();

Expand All @@ -96,6 +96,8 @@ describe('ApiKeySettingsModal', () => {

await user.click(screen.getByRole('button', { name: 'Generate' }));

await user.click(screen.getByRole('button', { name: 'Generate' }));

const [{ apiKeyExpiration: generatedApiKeyExpiration }] =
onGenerate.mock.lastCall;

Expand Down Expand Up @@ -130,6 +132,8 @@ describe('ApiKeySettingsModal', () => {

await user.click(screen.getByRole('button', { name: 'Generate' }));

await user.click(screen.getByRole('button', { name: 'Generate' }));

const [{ apiKeyExpiration: generatedApiKeyExpiration }] =
onGenerate.mock.lastCall;

Expand Down Expand Up @@ -164,6 +168,8 @@ describe('ApiKeySettingsModal', () => {

await user.click(screen.getByRole('button', { name: 'Generate' }));

await user.click(screen.getByRole('button', { name: 'Generate' }));

const [{ apiKeyExpiration: generatedApiKeyExpiration }] =
onGenerate.mock.lastCall;

Expand Down Expand Up @@ -212,6 +218,7 @@ describe('ApiKeySettingsModal', () => {

await user.click(screen.getByRole('switch'));

await user.click(screen.getByRole('button', { name: 'Generate' }));
await user.click(screen.getByRole('button', { name: 'Generate' }));

expect(onGenerate).toBeCalledWith({ apiKeyExpiration: null });
Expand All @@ -236,11 +243,87 @@ describe('ApiKeySettingsModal', () => {
await user.click(screen.getByRole('switch'));
await user.click(screen.getByRole('button', { name: 'Generate' }));

await user.click(screen.getByRole('button', { name: 'Generate' }));

await user.click(
screen.getByRole('button', { name: 'copy to clipboard' })
);

expect(screen.getByText(apiKey)).toBeVisible();
});
});

describe('Intermediate confirmation step', () => {
it('should display the confirmation step when the user clicks on form button Generate', async () => {
const user = userEvent.setup();
const apiKey = faker.string.alpha({ length: { min: 100, max: 100 } });
const nowISO = new Date().toISOString();

await act(async () => {
render(
<ApiKeySettingsModal
open
generatedApiKey={apiKey}
generatedApiKeyExpiration={nowISO}
/>
);
});

await user.click(screen.getByRole('switch'));
await user.click(screen.getByRole('button', { name: 'Generate' }));

expect(
screen.getByText('Are you sure you want to generate a new API key?')
).toBeVisible();
expect(screen.getByTestId('banner')).toBeVisible();
expect(screen.getByRole('button', { name: 'Generate' })).toBeVisible();
expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
});

it('should return to the form when the user clicks cancel in the confirmation modal', async () => {
const user = userEvent.setup();
const apiKey = faker.string.alpha({ length: { min: 100, max: 100 } });
const nowISO = new Date().toISOString();

await act(async () => {
render(
<ApiKeySettingsModal
open
generatedApiKey={apiKey}
generatedApiKeyExpiration={nowISO}
/>
);
});

await user.click(screen.getByRole('switch'));
await user.click(screen.getByRole('button', { name: 'Generate' }));

await user.click(screen.getByRole('button', { name: 'Cancel' }));
expect(screen.getByRole('spinbutton')).toBeVisible();
});

it('should invoke api key generation when the user clicks generate in the confirmation modal', async () => {
const user = userEvent.setup();
const apiKey = faker.string.alpha({ length: { min: 100, max: 100 } });
const nowISO = new Date().toISOString();
const onGenerate = jest.fn();

await act(async () => {
render(
<ApiKeySettingsModal
open
generatedApiKey={apiKey}
generatedApiKeyExpiration={nowISO}
onGenerate={onGenerate}
/>
);
});

await user.click(screen.getByRole('switch'));
await user.click(screen.getByRole('button', { name: 'Generate' }));

await user.click(screen.getByRole('button', { name: 'Generate' }));
expect(onGenerate).toBeCalledWith({ apiKeyExpiration: null });
});
});
});
Loading

0 comments on commit 29b4a41

Please sign in to comment.