E2C: Refactor on-prem ConnectModal (#85799)

* Refactor on-prem ConnectModal to not use ModalProvider

* fix
This commit is contained in:
Josh Hunt 2024-04-10 10:07:52 +01:00 committed by GitHub
parent 651223fe2d
commit f484519784
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 194 additions and 57 deletions

View File

@ -1,5 +1,5 @@
export * from './endpoints.gen';
import { BaseQueryFn, QueryDefinition } from '@reduxjs/toolkit/dist/query';
import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/dist/query';
import { generatedAPI } from './endpoints.gen';
@ -12,8 +12,9 @@ export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({
},
// Create Cloud Config
createMigration: {
invalidatesTags: ['cloud-migration-config'],
createMigration(endpoint) {
suppressErrorsOnQuery(endpoint);
endpoint.invalidatesTags = ['cloud-migration-config'];
},
// Get one Cloud Config
@ -45,7 +46,7 @@ export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({
});
function suppressErrorsOnQuery<QueryArg, BaseQuery extends BaseQueryFn, TagTypes extends string, ResultType>(
endpoint: QueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType>
endpoint: EndpointDefinition<QueryArg, BaseQuery, TagTypes, ResultType>
) {
if (!endpoint.query) {
return;

View File

@ -1,9 +1,10 @@
import { HttpResponse, http } from 'msw';
import { SetupServer, setupServer } from 'msw/node';
export function registerAPIHandlers(): SetupServer {
import { validCloudMigrationToken } from './tokens';
function createMockAPI(): SetupServer {
const server = setupServer(
// TODO
http.get('/api/dashboards/uid/:uid', ({ request, params }) => {
if (params.uid === 'dashboard-404') {
return HttpResponse.json(
@ -24,6 +25,26 @@ export function registerAPIHandlers(): SetupServer {
folderTitle: 'Dashboards',
},
});
}),
http.post('/api/cloudmigration/migration', async ({ request }) => {
const data = await request.json();
const authToken = typeof data === 'object' && data && data.authToken;
if (authToken === validCloudMigrationToken) {
return HttpResponse.json({
created: new Date().toISOString(),
id: 1,
stack: 'abc-123',
});
}
return HttpResponse.json(
{
message: 'Invalid token',
},
{ status: 500 }
);
})
);
@ -31,3 +52,15 @@ export function registerAPIHandlers(): SetupServer {
return server;
}
export function registerMockAPI() {
let server: SetupServer;
beforeAll(() => {
server = createMockAPI();
});
afterAll(() => {
server.close();
});
}

View File

@ -0,0 +1 @@
export const validCloudMigrationToken = 'valid-cloud-migration-token';

View File

@ -0,0 +1,81 @@
import 'whatwg-fetch'; // fetch polyfill
import { render as rtlRender, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { setBackendSrv } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import { registerMockAPI } from '../../../fixtures/mswAPI';
import { validCloudMigrationToken } from '../../../fixtures/tokens';
import { CallToAction } from './CallToAction';
setBackendSrv(backendSrv);
function render(...[ui, options]: Parameters<typeof rtlRender>) {
rtlRender(<TestProvider>{ui}</TestProvider>, options);
}
describe('CallToAction', () => {
registerMockAPI();
it('opens the modal when clicking on the button', async () => {
render(<CallToAction />);
const openButton = screen.getByText('Migrate this instance to Cloud');
await userEvent.click(openButton);
expect(screen.getByRole('button', { name: 'Connect to this stack' })).toBeInTheDocument();
});
it("closes the modal when clicking on the 'Cancel' button", async () => {
render(<CallToAction />);
const openButton = screen.getByText('Migrate this instance to Cloud');
await userEvent.click(openButton);
const closeButton = screen.getByText('Cancel');
await userEvent.click(closeButton);
expect(screen.queryByRole('button', { name: 'Connect to this stack' })).not.toBeInTheDocument();
});
it("disables the connect button when the 'token' field is empty", async () => {
render(<CallToAction />);
const openButton = screen.getByText('Migrate this instance to Cloud');
await userEvent.click(openButton);
expect(screen.getByRole('button', { name: 'Connect to this stack' })).toBeDisabled();
});
it('closes the modal after successfully submitting', async () => {
render(<CallToAction />);
const openButton = screen.getByText('Migrate this instance to Cloud');
await userEvent.click(openButton);
const tokenField = screen.getByRole('textbox', { name: 'Migration token *' });
await userEvent.type(tokenField, validCloudMigrationToken);
const submitButton = screen.getByRole('button', { name: 'Connect to this stack' });
await userEvent.click(submitButton);
expect(screen.queryByRole('button', { name: 'Connect to this stack' })).not.toBeInTheDocument();
});
it('shows the error', async () => {
render(<CallToAction />);
const openButton = screen.getByText('Migrate this instance to Cloud');
await userEvent.click(openButton);
const tokenField = screen.getByRole('textbox', { name: 'Migration token *' });
await userEvent.type(tokenField, 'a wrong token!!');
const submitButton = screen.getByRole('button', { name: 'Connect to this stack' });
await userEvent.click(submitButton);
expect(await screen.findByText('Error saving token')).toBeInTheDocument();
});
});

View File

@ -1,6 +1,6 @@
import React from 'react';
import React, { useState } from 'react';
import { Box, Button, ModalsController, Text } from '@grafana/ui';
import { Box, Button, Text } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { useCreateMigrationMutation } from '../../../api';
@ -8,28 +8,28 @@ import { useCreateMigrationMutation } from '../../../api';
import { ConnectModal } from './ConnectModal';
export const CallToAction = () => {
const [modalOpen, setModalOpen] = useState(false);
const [createMigration, createMigrationResponse] = useCreateMigrationMutation();
return (
<ModalsController>
{({ showModal, hideModal }) => (
<Box display="flex" padding={5} gap={2} direction="column" alignItems="center" backgroundColor="secondary">
<Text variant="h3" textAlignment="center">
<Trans i18nKey="migrate-to-cloud.cta.header">Let us manage your Grafana stack</Trans>
</Text>
<Button
disabled={createMigrationResponse.isLoading}
onClick={() =>
showModal(ConnectModal, {
hideModal,
onConfirm: createMigration,
})
}
>
<Trans i18nKey="migrate-to-cloud.cta.button">Migrate this instance to Cloud</Trans>
</Button>
</Box>
)}
</ModalsController>
<>
<Box display="flex" padding={5} gap={2} direction="column" alignItems="center" backgroundColor="secondary">
<Text variant="h3" textAlignment="center">
<Trans i18nKey="migrate-to-cloud.cta.header">Let us manage your Grafana stack</Trans>
</Text>
<Button disabled={createMigrationResponse.isLoading} onClick={() => setModalOpen(true)}>
<Trans i18nKey="migrate-to-cloud.cta.button">Migrate this instance to Cloud</Trans>
</Button>
</Box>
<ConnectModal
isOpen={modalOpen}
isLoading={createMigrationResponse.isLoading}
isError={createMigrationResponse.isError}
onConfirm={createMigration}
hideModal={() => setModalOpen(false)}
/>
</>
);
};

View File

@ -1,14 +1,17 @@
import { css } from '@emotion/css';
import React, { useId, useState } from 'react';
import React, { useId } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { Modal, Button, Stack, TextLink, Field, Input, Text, useStyles2 } from '@grafana/ui';
import { Modal, Button, Stack, TextLink, Field, Input, Text, useStyles2, Alert } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { CreateMigrationApiArg } from '../../../api';
interface Props {
isOpen: boolean;
isLoading: boolean;
isError: boolean;
hideModal: () => void;
onConfirm: (connectStackData: CreateMigrationApiArg) => Promise<unknown>;
}
@ -17,8 +20,7 @@ interface FormData {
token: string;
}
export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
const [isConnecting, setIsConnecting] = useState(false);
export const ConnectModal = ({ isOpen, isLoading, isError, hideModal, onConfirm }: Props) => {
const tokenId = useId();
const styles = useStyles2(getStyles);
@ -35,53 +37,73 @@ export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
const token = watch('token');
const onConfirmConnect: SubmitHandler<FormData> = async (formData) => {
setIsConnecting(true);
// TODO: location of this is kinda weird, making it tricky to handle errors from this.
await onConfirm({
const onConfirmConnect: SubmitHandler<FormData> = (formData) => {
onConfirm({
cloudMigrationRequest: {
authToken: formData.token,
},
}).then((resp) => {
const didError = typeof resp === 'object' && resp && 'error' in resp;
if (!didError) {
hideModal();
}
});
setIsConnecting(false);
hideModal();
};
return (
<Modal isOpen title={t('migrate-to-cloud.connect-modal.title', 'Connect to a cloud stack')} onDismiss={hideModal}>
<Modal
isOpen={isOpen}
title={t('migrate-to-cloud.connect-modal.title', 'Connect to a cloud stack')}
onDismiss={hideModal}
>
<form onSubmit={handleSubmit(onConfirmConnect)}>
<Text color="secondary">
<Stack direction="column" gap={2} alignItems="flex-start">
<Stack direction="column" gap={2}>
<Trans i18nKey="migrate-to-cloud.connect-modal.body-get-started">
To get started, you&apos;ll need a Grafana.com account.
</Trans>
<TextLink href="https://grafana.com/auth/sign-up/create-user?pg=prod-cloud" external>
{t('migrate-to-cloud.connect-modal.body-sign-up', 'Sign up for a Grafana.com account')}
</TextLink>
<div>
<TextLink href="https://grafana.com/auth/sign-up/create-user?pg=prod-cloud" external>
{t('migrate-to-cloud.connect-modal.body-sign-up', 'Sign up for a Grafana.com account')}
</TextLink>
</div>
<Trans i18nKey="migrate-to-cloud.connect-modal.body-cloud-stack">
You&apos;ll also need a cloud stack. If you just signed up, we&apos;ll automatically create your first
stack. If you have an account, you&apos;ll need to select or create a stack.
</Trans>
<TextLink href="https://grafana.com/auth/sign-in/" external>
{t('migrate-to-cloud.connect-modal.body-view-stacks', 'View my cloud stacks')}
</TextLink>
<div>
<TextLink href="https://grafana.com/auth/sign-in/" external>
{t('migrate-to-cloud.connect-modal.body-view-stacks', 'View my cloud stacks')}
</TextLink>
</div>
<span>
<div>
<Trans i18nKey="migrate-to-cloud.connect-modal.body-token">
Your self-managed Grafana installation needs special access to securely migrate content. You&apos;ll
need to create a migration token on your chosen cloud stack.
</Trans>
</span>
</div>
<span>
<div>
<Trans i18nKey="migrate-to-cloud.connect-modal.body-token-instructions">
Log into your cloud stack and navigate to Administration, General, Migrate to Grafana Cloud. Create a
migration token on that screen and paste the token here.
</Trans>
</span>
</div>
{isError && (
<Alert
severity="error"
title={t('migrate-to-cloud.connect-modal.token-error-title', 'Error saving token')}
>
<Trans i18nKey="migrate-to-cloud.connect-modal.token-error-description">
There was an error saving the token. See the Grafana server logs for more details.
</Trans>
</Alert>
)}
<Field
className={styles.field}
@ -100,12 +122,13 @@ export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
</Field>
</Stack>
</Text>
<Modal.ButtonRow>
<Button variant="secondary" onClick={hideModal}>
<Trans i18nKey="migrate-to-cloud.connect-modal.cancel">Cancel</Trans>
</Button>
<Button type="submit" disabled={isConnecting || !token}>
{isConnecting
<Button type="submit" disabled={isLoading || !token}>
{isLoading
? t('migrate-to-cloud.connect-modal.connecting', 'Connecting to this stack...')
: t('migrate-to-cloud.connect-modal.connect', 'Connect to this stack')}
</Button>

View File

@ -1,6 +1,5 @@
import 'whatwg-fetch'; // fetch polyfill
import { render as rtlRender, screen } from '@testing-library/react';
import { SetupServer } from 'msw/lib/node';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
@ -8,7 +7,7 @@ import { setBackendSrv, config } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import { wellFormedDashboardMigrationItem, wellFormedDatasourceMigrationItem } from '../fixtures/migrationItems';
import { registerAPIHandlers } from '../fixtures/mswAPI';
import { registerMockAPI } from '../fixtures/mswAPI';
import { wellFormedDatasource } from '../fixtures/others';
import { ResourcesTable } from './ResourcesTable';
@ -20,7 +19,8 @@ function render(...[ui, options]: Parameters<typeof rtlRender>) {
}
describe('ResourcesTable', () => {
let server: SetupServer;
registerMockAPI();
let originalDatasources: (typeof config)['datasources'];
const datasourceA = wellFormedDatasource(1, {
@ -29,7 +29,6 @@ describe('ResourcesTable', () => {
});
beforeAll(() => {
server = registerAPIHandlers();
originalDatasources = config.datasources;
config.datasources = {
@ -39,7 +38,6 @@ describe('ResourcesTable', () => {
});
afterAll(() => {
server.close();
config.datasources = originalDatasources;
});