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';
|
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';
|
import { generatedAPI } from './endpoints.gen';
|
||||||
|
|
||||||
@ -12,8 +12,9 @@ export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Create Cloud Config
|
// Create Cloud Config
|
||||||
createMigration: {
|
createMigration(endpoint) {
|
||||||
invalidatesTags: ['cloud-migration-config'],
|
suppressErrorsOnQuery(endpoint);
|
||||||
|
endpoint.invalidatesTags = ['cloud-migration-config'];
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get one Cloud Config
|
// Get one Cloud Config
|
||||||
@ -45,7 +46,7 @@ export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function suppressErrorsOnQuery<QueryArg, BaseQuery extends BaseQueryFn, TagTypes extends string, ResultType>(
|
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) {
|
if (!endpoint.query) {
|
||||||
return;
|
return;
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { HttpResponse, http } from 'msw';
|
import { HttpResponse, http } from 'msw';
|
||||||
import { SetupServer, setupServer } from 'msw/node';
|
import { SetupServer, setupServer } from 'msw/node';
|
||||||
|
|
||||||
export function registerAPIHandlers(): SetupServer {
|
import { validCloudMigrationToken } from './tokens';
|
||||||
|
|
||||||
|
function createMockAPI(): SetupServer {
|
||||||
const server = setupServer(
|
const server = setupServer(
|
||||||
// TODO
|
|
||||||
http.get('/api/dashboards/uid/:uid', ({ request, params }) => {
|
http.get('/api/dashboards/uid/:uid', ({ request, params }) => {
|
||||||
if (params.uid === 'dashboard-404') {
|
if (params.uid === 'dashboard-404') {
|
||||||
return HttpResponse.json(
|
return HttpResponse.json(
|
||||||
@ -24,6 +25,26 @@ export function registerAPIHandlers(): SetupServer {
|
|||||||
folderTitle: 'Dashboards',
|
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;
|
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 { Trans } from 'app/core/internationalization';
|
||||||
|
|
||||||
import { useCreateMigrationMutation } from '../../../api';
|
import { useCreateMigrationMutation } from '../../../api';
|
||||||
@ -8,28 +8,28 @@ import { useCreateMigrationMutation } from '../../../api';
|
|||||||
import { ConnectModal } from './ConnectModal';
|
import { ConnectModal } from './ConnectModal';
|
||||||
|
|
||||||
export const CallToAction = () => {
|
export const CallToAction = () => {
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [createMigration, createMigrationResponse] = useCreateMigrationMutation();
|
const [createMigration, createMigrationResponse] = useCreateMigrationMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalsController>
|
<>
|
||||||
{({ showModal, hideModal }) => (
|
<Box display="flex" padding={5} gap={2} direction="column" alignItems="center" backgroundColor="secondary">
|
||||||
<Box display="flex" padding={5} gap={2} direction="column" alignItems="center" backgroundColor="secondary">
|
<Text variant="h3" textAlignment="center">
|
||||||
<Text variant="h3" textAlignment="center">
|
<Trans i18nKey="migrate-to-cloud.cta.header">Let us manage your Grafana stack</Trans>
|
||||||
<Trans i18nKey="migrate-to-cloud.cta.header">Let us manage your Grafana stack</Trans>
|
</Text>
|
||||||
</Text>
|
|
||||||
<Button
|
<Button disabled={createMigrationResponse.isLoading} onClick={() => setModalOpen(true)}>
|
||||||
disabled={createMigrationResponse.isLoading}
|
<Trans i18nKey="migrate-to-cloud.cta.button">Migrate this instance to Cloud</Trans>
|
||||||
onClick={() =>
|
</Button>
|
||||||
showModal(ConnectModal, {
|
</Box>
|
||||||
hideModal,
|
|
||||||
onConfirm: createMigration,
|
<ConnectModal
|
||||||
})
|
isOpen={modalOpen}
|
||||||
}
|
isLoading={createMigrationResponse.isLoading}
|
||||||
>
|
isError={createMigrationResponse.isError}
|
||||||
<Trans i18nKey="migrate-to-cloud.cta.button">Migrate this instance to Cloud</Trans>
|
onConfirm={createMigration}
|
||||||
</Button>
|
hideModal={() => setModalOpen(false)}
|
||||||
</Box>
|
/>
|
||||||
)}
|
</>
|
||||||
</ModalsController>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useId, useState } from 'react';
|
import React, { useId } from 'react';
|
||||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
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 { Trans, t } from 'app/core/internationalization';
|
||||||
|
|
||||||
import { CreateMigrationApiArg } from '../../../api';
|
import { CreateMigrationApiArg } from '../../../api';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
isError: boolean;
|
||||||
hideModal: () => void;
|
hideModal: () => void;
|
||||||
onConfirm: (connectStackData: CreateMigrationApiArg) => Promise<unknown>;
|
onConfirm: (connectStackData: CreateMigrationApiArg) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
@ -17,8 +20,7 @@ interface FormData {
|
|||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
|
export const ConnectModal = ({ isOpen, isLoading, isError, hideModal, onConfirm }: Props) => {
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
|
||||||
const tokenId = useId();
|
const tokenId = useId();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
@ -35,53 +37,73 @@ export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
|
|||||||
|
|
||||||
const token = watch('token');
|
const token = watch('token');
|
||||||
|
|
||||||
const onConfirmConnect: SubmitHandler<FormData> = async (formData) => {
|
const onConfirmConnect: SubmitHandler<FormData> = (formData) => {
|
||||||
setIsConnecting(true);
|
onConfirm({
|
||||||
// TODO: location of this is kinda weird, making it tricky to handle errors from this.
|
|
||||||
await onConfirm({
|
|
||||||
cloudMigrationRequest: {
|
cloudMigrationRequest: {
|
||||||
authToken: formData.token,
|
authToken: formData.token,
|
||||||
},
|
},
|
||||||
|
}).then((resp) => {
|
||||||
|
const didError = typeof resp === 'object' && resp && 'error' in resp;
|
||||||
|
if (!didError) {
|
||||||
|
hideModal();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
setIsConnecting(false);
|
|
||||||
hideModal();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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)}>
|
<form onSubmit={handleSubmit(onConfirmConnect)}>
|
||||||
<Text color="secondary">
|
<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">
|
<Trans i18nKey="migrate-to-cloud.connect-modal.body-get-started">
|
||||||
To get started, you'll need a Grafana.com account.
|
To get started, you'll need a Grafana.com account.
|
||||||
</Trans>
|
</Trans>
|
||||||
|
|
||||||
<TextLink href="https://grafana.com/auth/sign-up/create-user?pg=prod-cloud" external>
|
<div>
|
||||||
{t('migrate-to-cloud.connect-modal.body-sign-up', 'Sign up for a Grafana.com account')}
|
<TextLink href="https://grafana.com/auth/sign-up/create-user?pg=prod-cloud" external>
|
||||||
</TextLink>
|
{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">
|
<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
|
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.
|
stack. If you have an account, you'll need to select or create a stack.
|
||||||
</Trans>
|
</Trans>
|
||||||
|
|
||||||
<TextLink href="https://grafana.com/auth/sign-in/" external>
|
<div>
|
||||||
{t('migrate-to-cloud.connect-modal.body-view-stacks', 'View my cloud stacks')}
|
<TextLink href="https://grafana.com/auth/sign-in/" external>
|
||||||
</TextLink>
|
{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">
|
<Trans i18nKey="migrate-to-cloud.connect-modal.body-token">
|
||||||
Your self-managed Grafana installation needs special access to securely migrate content. You'll
|
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.
|
need to create a migration token on your chosen cloud stack.
|
||||||
</Trans>
|
</Trans>
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
<span>
|
<div>
|
||||||
<Trans i18nKey="migrate-to-cloud.connect-modal.body-token-instructions">
|
<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
|
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.
|
migration token on that screen and paste the token here.
|
||||||
</Trans>
|
</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
|
<Field
|
||||||
className={styles.field}
|
className={styles.field}
|
||||||
@ -100,12 +122,13 @@ export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
|
|||||||
</Field>
|
</Field>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Modal.ButtonRow>
|
<Modal.ButtonRow>
|
||||||
<Button variant="secondary" onClick={hideModal}>
|
<Button variant="secondary" onClick={hideModal}>
|
||||||
<Trans i18nKey="migrate-to-cloud.connect-modal.cancel">Cancel</Trans>
|
<Trans i18nKey="migrate-to-cloud.connect-modal.cancel">Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isConnecting || !token}>
|
<Button type="submit" disabled={isLoading || !token}>
|
||||||
{isConnecting
|
{isLoading
|
||||||
? t('migrate-to-cloud.connect-modal.connecting', 'Connecting to this stack...')
|
? t('migrate-to-cloud.connect-modal.connecting', 'Connecting to this stack...')
|
||||||
: t('migrate-to-cloud.connect-modal.connect', 'Connect to this stack')}
|
: t('migrate-to-cloud.connect-modal.connect', 'Connect to this stack')}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'whatwg-fetch'; // fetch polyfill
|
import 'whatwg-fetch'; // fetch polyfill
|
||||||
import { render as rtlRender, screen } from '@testing-library/react';
|
import { render as rtlRender, screen } from '@testing-library/react';
|
||||||
import { SetupServer } from 'msw/lib/node';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TestProvider } from 'test/helpers/TestProvider';
|
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 { backendSrv } from 'app/core/services/backend_srv';
|
||||||
|
|
||||||
import { wellFormedDashboardMigrationItem, wellFormedDatasourceMigrationItem } from '../fixtures/migrationItems';
|
import { wellFormedDashboardMigrationItem, wellFormedDatasourceMigrationItem } from '../fixtures/migrationItems';
|
||||||
import { registerAPIHandlers } from '../fixtures/mswAPI';
|
import { registerMockAPI } from '../fixtures/mswAPI';
|
||||||
import { wellFormedDatasource } from '../fixtures/others';
|
import { wellFormedDatasource } from '../fixtures/others';
|
||||||
|
|
||||||
import { ResourcesTable } from './ResourcesTable';
|
import { ResourcesTable } from './ResourcesTable';
|
||||||
@ -20,7 +19,8 @@ function render(...[ui, options]: Parameters<typeof rtlRender>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('ResourcesTable', () => {
|
describe('ResourcesTable', () => {
|
||||||
let server: SetupServer;
|
registerMockAPI();
|
||||||
|
|
||||||
let originalDatasources: (typeof config)['datasources'];
|
let originalDatasources: (typeof config)['datasources'];
|
||||||
|
|
||||||
const datasourceA = wellFormedDatasource(1, {
|
const datasourceA = wellFormedDatasource(1, {
|
||||||
@ -29,7 +29,6 @@ describe('ResourcesTable', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
server = registerAPIHandlers();
|
|
||||||
originalDatasources = config.datasources;
|
originalDatasources = config.datasources;
|
||||||
|
|
||||||
config.datasources = {
|
config.datasources = {
|
||||||
@ -39,7 +38,6 @@ describe('ResourcesTable', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
server.close();
|
|
||||||
config.datasources = originalDatasources;
|
config.datasources = originalDatasources;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user