mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
E2C: Refactor on-prem ConnectModal (#85799)
* Refactor on-prem ConnectModal to not use ModalProvider * fix
This commit is contained in:
parent
651223fe2d
commit
f484519784
@ -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;
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
1
public/app/features/migrate-to-cloud/fixtures/tokens.ts
Normal file
1
public/app/features/migrate-to-cloud/fixtures/tokens.ts
Normal file
@ -0,0 +1 @@
|
||||
export const validCloudMigrationToken = 'valid-cloud-migration-token';
|
@ -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();
|
||||
});
|
||||
});
|
@ -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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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'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'll also need a cloud stack. If you just signed up, we'll automatically create your first
|
||||
stack. If you have an account, you'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'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>
|
||||
|
@ -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;
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user