E2C: Implement on-prem auth flow (#83513)

* add connection modal

* extend api with connect/disconnect endpoints

* extract translations

* display stack url

* use react-hook-form

* fix links spanning whole modal

* review comments
This commit is contained in:
Ashley Harrison 2024-02-29 13:10:04 +00:00 committed by GitHub
parent 036e19037e
commit 1994d1e2c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 340 additions and 31 deletions

View File

@ -29,16 +29,24 @@ function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryF
interface MigrateToCloudStatusDTO {
enabled: boolean;
stackURL?: string;
}
interface CreateMigrationTokenResponseDTO {
token: string;
}
export interface ConnectStackDTO {
stackURL: string;
token: string;
}
// TODO remove these mock properties/functions
const MOCK_DELAY_MS = 1000;
const MOCK_TOKEN = 'TODO_thisWillBeABigLongToken';
let HAS_MIGRATION_TOKEN = false;
let HAS_STACK_DETAILS = false;
let STACK_URL: string | undefined;
function dataWithMockDelay<T>(data: T): Promise<{ data: T }> {
return new Promise((resolve) => {
@ -49,13 +57,35 @@ function dataWithMockDelay<T>(data: T): Promise<{ data: T }> {
}
export const migrateToCloudAPI = createApi({
tagTypes: ['migrationToken'],
tagTypes: ['migrationToken', 'stackDetails'],
reducerPath: 'migrateToCloudAPI',
baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }),
endpoints: (builder) => ({
// TODO :)
getStatus: builder.query<MigrateToCloudStatusDTO, void>({
queryFn: () => dataWithMockDelay({ enabled: true }),
providesTags: ['stackDetails'],
queryFn: () => {
const responseData: MigrateToCloudStatusDTO = { enabled: HAS_STACK_DETAILS };
if (STACK_URL) {
responseData.stackURL = STACK_URL;
}
return dataWithMockDelay(responseData);
},
}),
connectStack: builder.mutation<void, ConnectStackDTO>({
invalidatesTags: ['stackDetails'],
queryFn: async ({ stackURL }) => {
HAS_STACK_DETAILS = true;
STACK_URL = stackURL;
return dataWithMockDelay(undefined);
},
}),
disconnectStack: builder.mutation<void, void>({
invalidatesTags: ['stackDetails'],
queryFn: async () => {
HAS_STACK_DETAILS = false;
return dataWithMockDelay(undefined);
},
}),
createMigrationToken: builder.mutation<CreateMigrationTokenResponseDTO, void>({
@ -85,6 +115,8 @@ export const migrateToCloudAPI = createApi({
export const {
useGetStatusQuery,
useConnectStackMutation,
useDisconnectStackMutation,
useCreateMigrationTokenMutation,
useDeleteMigrationTokenMutation,
useHasMigrationTokenQuery,

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Modal, Button } from '@grafana/ui';
import { Modal, Button, Text } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
interface Props {
@ -24,10 +24,12 @@ export const DeleteMigrationTokenModal = ({ hideModal, onConfirm }: Props) => {
title={t('migrate-to-cloud.migration-token.delete-modal-title', 'Delete migration token')}
onDismiss={hideModal}
>
<Trans i18nKey="migrate-to-cloud.migration-token.delete-modal-body">
If you&apos;ve already used this token with a self-managed installation, that installation will no longer be
able to upload content.
</Trans>
<Text color="secondary">
<Trans i18nKey="migrate-to-cloud.migration-token.delete-modal-body">
If you&apos;ve already used this token with a self-managed installation, that installation will no longer be
able to upload content.
</Trans>
</Text>
<Modal.ButtonRow>
<Button variant="secondary" onClick={hideModal}>
<Trans i18nKey="migrate-to-cloud.migration-token.delete-modal-cancel">Cancel</Trans>

View File

@ -0,0 +1,45 @@
import React, { useState } from 'react';
import { Modal, Button, Text } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
interface Props {
hideModal: () => void;
onConfirm: () => Promise<{ data: void } | { error: unknown }>;
}
export const DisconnectModal = ({ hideModal, onConfirm }: Props) => {
const [isDisconnecting, setIsDisconnecting] = useState(false);
const onConfirmDisconnect = async () => {
setIsDisconnecting(true);
await onConfirm();
setIsDisconnecting(false);
hideModal();
};
return (
<Modal
isOpen
title={t('migrate-to-cloud.disconnect-modal.title', 'Disconnect from cloud stack')}
onDismiss={hideModal}
>
<Text color="secondary">
<Trans i18nKey="migrate-to-cloud.disconnect-modal.body">
This will remove the migration token from this installation. If you wish to upload more resources in the
future, you will need to enter a new migration token.
</Trans>
</Text>
<Modal.ButtonRow>
<Button variant="secondary" onClick={hideModal}>
<Trans i18nKey="migrate-to-cloud.disconnect-modal.cancel">Cancel</Trans>
</Button>
<Button disabled={isDisconnecting} onClick={onConfirmDisconnect}>
{isDisconnecting
? t('migrate-to-cloud.disconnect-modal.disconnecting', 'Disconnecting...')
: t('migrate-to-cloud.disconnect-modal.disconnect', 'Disconnect')}
</Button>
</Modal.ButtonRow>
</Modal>
);
};

View File

@ -1,21 +0,0 @@
import React from 'react';
import { Box, Button, Text } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
export const CallToAction = () => {
const onClickMigrate = () => {
console.log('TODO migration!');
};
return (
<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 onClick={onClickMigrate}>
<Trans i18nKey="migrate-to-cloud.cta.button">Migrate this instance to Cloud</Trans>
</Button>
</Box>
);
};

View File

@ -0,0 +1,36 @@
import React from 'react';
import { Box, Button, ModalsController, Text } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { useConnectStackMutation, useGetStatusQuery } from '../../../api';
import { ConnectModal } from './ConnectModal';
export const CallToAction = () => {
const [connectStack, connectResponse] = useConnectStackMutation();
const { isFetching } = useGetStatusQuery();
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={isFetching || connectResponse.isLoading}
onClick={() =>
showModal(ConnectModal, {
hideModal,
onConfirm: connectStack,
})
}
>
<Trans i18nKey="migrate-to-cloud.cta.button">Migrate this instance to Cloud</Trans>
</Button>
</Box>
)}
</ModalsController>
);
};

View File

@ -0,0 +1,128 @@
import { css } from '@emotion/css';
import React, { useId, useState } 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 { Trans, t } from 'app/core/internationalization';
import { ConnectStackDTO } from '../../../api';
interface Props {
hideModal: () => void;
onConfirm: (connectStackData: ConnectStackDTO) => Promise<{ data: void } | { error: unknown }>;
}
export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
const [isConnecting, setIsConnecting] = useState(false);
const cloudStackId = useId();
const tokenId = useId();
const styles = useStyles2(getStyles);
const {
handleSubmit,
register,
formState: { errors },
watch,
} = useForm<ConnectStackDTO>({
defaultValues: {
stackURL: '',
token: '',
},
});
const stackURL = watch('stackURL');
const token = watch('token');
const onConfirmConnect: SubmitHandler<ConnectStackDTO> = async (formData) => {
setIsConnecting(true);
await onConfirm(formData);
setIsConnecting(false);
hideModal();
};
return (
<Modal 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">
<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>
<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>
<Trans i18nKey="migrate-to-cloud.connect-modal.body-paste-stack">
Once you&apos;ve decided on a stack, paste the URL below.
</Trans>
<Field
className={styles.field}
invalid={!!errors.stackURL}
error={errors.stackURL?.message}
label={t('migrate-to-cloud.connect-modal.body-url-field', 'Cloud stack URL')}
required
>
<Input
{...register('stackURL', {
required: t('migrate-to-cloud.connect-modal.stack-required-error', 'Stack URL is required'),
})}
id={cloudStackId}
placeholder="https://example.grafana.net/"
/>
</Field>
<span>
<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>
<span>
<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>
<Field
className={styles.field}
invalid={!!errors.token}
error={errors.token?.message}
label={t('migrate-to-cloud.connect-modal.body-token-field', 'Migration token')}
required
>
<Input
{...register('token', {
required: t('migrate-to-cloud.connect-modal.token-required-error', 'Migration token is required'),
})}
id={tokenId}
placeholder={t('migrate-to-cloud.connect-modal.body-token-field-placeholder', 'Paste token here')}
/>
</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 || !(stackURL && token)}>
{isConnecting
? t('migrate-to-cloud.connect-modal.connecting', 'Connecting to this stack...')
: t('migrate-to-cloud.connect-modal.connect', 'Connect to this stack')}
</Button>
</Modal.ButtonRow>
</form>
</Modal>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
field: css({
alignSelf: 'stretch',
}),
});

View File

@ -4,7 +4,7 @@ import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Grid, Stack, useStyles2 } from '@grafana/ui';
import { CallToAction } from './CallToAction';
import { CallToAction } from './CallToAction/CallToAction';
import { InfoPaneLeft } from './InfoPaneLeft';
import { InfoPaneRight } from './InfoPaneRight';

View File

@ -1,8 +1,39 @@
import React from 'react';
import { Button, ModalsController, Stack, Text } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { useDisconnectStackMutation, useGetStatusQuery } from '../api';
import { DisconnectModal } from './DisconnectModal';
import { EmptyState } from './EmptyState/EmptyState';
export const Page = () => {
// TODO logic to determine whether to show the empty state or the resource table
return <EmptyState />;
const { data, isFetching } = useGetStatusQuery();
const [disconnectStack, disconnectResponse] = useDisconnectStackMutation();
if (!data?.enabled) {
return <EmptyState />;
}
return (
<ModalsController>
{({ showModal, hideModal }) => (
<Stack alignItems="center">
{data.stackURL && <Text variant="h4">{data.stackURL}</Text>}
<Button
disabled={isFetching || disconnectResponse.isLoading}
variant="secondary"
onClick={() =>
showModal(DisconnectModal, {
hideModal,
onConfirm: disconnectStack,
})
}
>
<Trans i18nKey="migrate-to-cloud.resources.disconnect">Disconnect</Trans>
</Button>
</Stack>
)}
</ModalsController>
);
};

View File

@ -697,10 +697,35 @@
"link-title": "Learn about migrating other settings",
"title": "Can I move this installation to Grafana 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.",
"body-get-started": "To get started, you'll need a Grafana.com account.",
"body-paste-stack": "Once you've decided on a stack, paste the URL below.",
"body-sign-up": "Sign up for a Grafana.com account",
"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.",
"body-token-field": "Migration token",
"body-token-field-placeholder": "Paste token here",
"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.",
"body-url-field": "Cloud stack URL",
"body-view-stacks": "View my cloud stacks",
"cancel": "Cancel",
"connect": "Connect to this stack",
"connecting": "Connecting to this stack...",
"stack-required-error": "Stack URL is required",
"title": "Connect to a cloud stack",
"token-required-error": "Migration token is required"
},
"cta": {
"button": "Migrate this instance to Cloud",
"header": "Let us manage your Grafana stack"
},
"disconnect-modal": {
"body": "This will remove the migration token from this installation. If you wish to upload more resources in the future, you will need to enter a new migration token.",
"cancel": "Cancel",
"disconnect": "Disconnect",
"disconnecting": "Disconnecting...",
"title": "Disconnect from cloud stack"
},
"get-started": {
"body": "The migration process must be started from your self-managed Grafana instance.",
"configure-pdc-link": "Configure PDC for this stack",
@ -751,6 +776,9 @@
"link-title": "Grafana Cloud pricing",
"title": "How much does it cost?"
},
"resources": {
"disconnect": "Disconnect"
},
"token-status": {
"active": "Token created and active",
"no-active": "No active token"

View File

@ -697,10 +697,35 @@
"link-title": "Ŀęäřʼn äþőūŧ mįģřäŧįʼnģ őŧĥęř şęŧŧįʼnģş",
"title": "Cäʼn Ĩ mővę ŧĥįş įʼnşŧäľľäŧįőʼn ŧő Ğřäƒäʼnä Cľőūđ?"
},
"connect-modal": {
"body-cloud-stack": "Ÿőū'ľľ äľşő ʼnęęđ ä čľőūđ şŧäčĸ. Ĩƒ yőū ĵūşŧ şįģʼnęđ ūp, ŵę'ľľ äūŧőmäŧįčäľľy čřęäŧę yőūř ƒįřşŧ şŧäčĸ. Ĩƒ yőū ĥävę äʼn äččőūʼnŧ, yőū'ľľ ʼnęęđ ŧő şęľęčŧ őř čřęäŧę ä şŧäčĸ.",
"body-get-started": "Ŧő ģęŧ şŧäřŧęđ, yőū'ľľ ʼnęęđ ä Ğřäƒäʼnä.čőm äččőūʼnŧ.",
"body-paste-stack": "Øʼnčę yőū'vę đęčįđęđ őʼn ä şŧäčĸ, päşŧę ŧĥę ŮŖĿ þęľőŵ.",
"body-sign-up": "Ŝįģʼn ūp ƒőř ä Ğřäƒäʼnä.čőm äččőūʼnŧ",
"body-token": "Ÿőūř şęľƒ-mäʼnäģęđ Ğřäƒäʼnä įʼnşŧäľľäŧįőʼn ʼnęęđş şpęčįäľ äččęşş ŧő şęčūřęľy mįģřäŧę čőʼnŧęʼnŧ. Ÿőū'ľľ ʼnęęđ ŧő čřęäŧę ä mįģřäŧįőʼn ŧőĸęʼn őʼn yőūř čĥőşęʼn čľőūđ şŧäčĸ.",
"body-token-field": "Mįģřäŧįőʼn ŧőĸęʼn",
"body-token-field-placeholder": "Päşŧę ŧőĸęʼn ĥęřę",
"body-token-instructions": "Ŀőģ įʼnŧő yőūř čľőūđ şŧäčĸ äʼnđ ʼnävįģäŧę ŧő Åđmįʼnįşŧřäŧįőʼn, Ğęʼnęřäľ, Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ. Cřęäŧę ä mįģřäŧįőʼn ŧőĸęʼn őʼn ŧĥäŧ şčřęęʼn äʼnđ päşŧę ŧĥę ŧőĸęʼn ĥęřę.",
"body-url-field": "Cľőūđ şŧäčĸ ŮŖĿ",
"body-view-stacks": "Vįęŵ my čľőūđ şŧäčĸş",
"cancel": "Cäʼnčęľ",
"connect": "Cőʼnʼnęčŧ ŧő ŧĥįş şŧäčĸ",
"connecting": "Cőʼnʼnęčŧįʼnģ ŧő ŧĥįş şŧäčĸ...",
"stack-required-error": "Ŝŧäčĸ ŮŖĿ įş řęqūįřęđ",
"title": "Cőʼnʼnęčŧ ŧő ä čľőūđ şŧäčĸ",
"token-required-error": "Mįģřäŧįőʼn ŧőĸęʼn įş řęqūįřęđ"
},
"cta": {
"button": "Mįģřäŧę ŧĥįş įʼnşŧäʼnčę ŧő Cľőūđ",
"header": "Ŀęŧ ūş mäʼnäģę yőūř Ğřäƒäʼnä şŧäčĸ"
},
"disconnect-modal": {
"body": "Ŧĥįş ŵįľľ řęmővę ŧĥę mįģřäŧįőʼn ŧőĸęʼn ƒřőm ŧĥįş įʼnşŧäľľäŧįőʼn. Ĩƒ yőū ŵįşĥ ŧő ūpľőäđ mőřę řęşőūřčęş įʼn ŧĥę ƒūŧūřę, yőū ŵįľľ ʼnęęđ ŧő ęʼnŧęř ä ʼnęŵ mįģřäŧįőʼn ŧőĸęʼn.",
"cancel": "Cäʼnčęľ",
"disconnect": "Đįşčőʼnʼnęčŧ",
"disconnecting": "Đįşčőʼnʼnęčŧįʼnģ...",
"title": "Đįşčőʼnʼnęčŧ ƒřőm čľőūđ şŧäčĸ"
},
"get-started": {
"body": "Ŧĥę mįģřäŧįőʼn přőčęşş mūşŧ þę şŧäřŧęđ ƒřőm yőūř şęľƒ-mäʼnäģęđ Ğřäƒäʼnä įʼnşŧäʼnčę.",
"configure-pdc-link": "Cőʼnƒįģūřę PĐC ƒőř ŧĥįş şŧäčĸ",
@ -751,6 +776,9 @@
"link-title": "Ğřäƒäʼnä Cľőūđ přįčįʼnģ",
"title": "Ħőŵ mūčĥ đőęş įŧ čőşŧ?"
},
"resources": {
"disconnect": "Đįşčőʼnʼnęčŧ"
},
"token-status": {
"active": "Ŧőĸęʼn čřęäŧęđ äʼnđ äčŧįvę",
"no-active": "Ńő äčŧįvę ŧőĸęʼn"