From 1994d1e2c72e5c21391b599b0ee63496fd501aa5 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 29 Feb 2024 13:10:04 +0000 Subject: [PATCH] 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 --- .../features/admin/migrate-to-cloud/api.ts | 36 ++++- .../DeleteMigrationTokenModal.tsx | 12 +- .../onprem/DisconnectModal.tsx | 45 ++++++ .../onprem/EmptyState/CallToAction.tsx | 21 --- .../EmptyState/CallToAction/CallToAction.tsx | 36 +++++ .../EmptyState/CallToAction/ConnectModal.tsx | 128 ++++++++++++++++++ .../onprem/EmptyState/EmptyState.tsx | 2 +- .../admin/migrate-to-cloud/onprem/Page.tsx | 35 ++++- public/locales/en-US/grafana.json | 28 ++++ public/locales/pseudo-LOCALE/grafana.json | 28 ++++ 10 files changed, 340 insertions(+), 31 deletions(-) create mode 100644 public/app/features/admin/migrate-to-cloud/onprem/DisconnectModal.tsx delete mode 100644 public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction.tsx create mode 100644 public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction/CallToAction.tsx create mode 100644 public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction/ConnectModal.tsx diff --git a/public/app/features/admin/migrate-to-cloud/api.ts b/public/app/features/admin/migrate-to-cloud/api.ts index 405334ac9a7..9659617f0ef 100644 --- a/public/app/features/admin/migrate-to-cloud/api.ts +++ b/public/app/features/admin/migrate-to-cloud/api.ts @@ -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(data: T): Promise<{ data: T }> { return new Promise((resolve) => { @@ -49,13 +57,35 @@ function dataWithMockDelay(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({ - 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({ + invalidatesTags: ['stackDetails'], + queryFn: async ({ stackURL }) => { + HAS_STACK_DETAILS = true; + STACK_URL = stackURL; + return dataWithMockDelay(undefined); + }, + }), + disconnectStack: builder.mutation({ + invalidatesTags: ['stackDetails'], + queryFn: async () => { + HAS_STACK_DETAILS = false; + return dataWithMockDelay(undefined); + }, }), createMigrationToken: builder.mutation({ @@ -85,6 +115,8 @@ export const migrateToCloudAPI = createApi({ export const { useGetStatusQuery, + useConnectStackMutation, + useDisconnectStackMutation, useCreateMigrationTokenMutation, useDeleteMigrationTokenMutation, useHasMigrationTokenQuery, diff --git a/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/DeleteMigrationTokenModal.tsx b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/DeleteMigrationTokenModal.tsx index 6d1f3ddc8ec..3d98e6374ef 100644 --- a/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/DeleteMigrationTokenModal.tsx +++ b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/DeleteMigrationTokenModal.tsx @@ -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} > - - If you've already used this token with a self-managed installation, that installation will no longer be - able to upload content. - + + + If you've already used this token with a self-managed installation, that installation will no longer be + able to upload content. + + + + + + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction.tsx b/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction.tsx deleted file mode 100644 index 75ac9236136..00000000000 --- a/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction.tsx +++ /dev/null @@ -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 ( - - - Let us manage your Grafana stack - - - - ); -}; diff --git a/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction/CallToAction.tsx b/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction/CallToAction.tsx new file mode 100644 index 00000000000..0d3e7e55a27 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction/CallToAction.tsx @@ -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 ( + + {({ showModal, hideModal }) => ( + + + Let us manage your Grafana stack + + + + )} + + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction/ConnectModal.tsx b/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction/ConnectModal.tsx new file mode 100644 index 00000000000..24276f8cc7d --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction/ConnectModal.tsx @@ -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({ + defaultValues: { + stackURL: '', + token: '', + }, + }); + + const stackURL = watch('stackURL'); + const token = watch('token'); + + const onConfirmConnect: SubmitHandler = async (formData) => { + setIsConnecting(true); + await onConfirm(formData); + setIsConnecting(false); + hideModal(); + }; + + return ( + +
+ + + + To get started, you'll need a Grafana.com account. + + + {t('migrate-to-cloud.connect-modal.body-sign-up', 'Sign up for a Grafana.com account')} + + + 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. + + + {t('migrate-to-cloud.connect-modal.body-view-stacks', 'View my cloud stacks')} + + + Once you've decided on a stack, paste the URL below. + + + + + + + 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. + + + + + 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. + + + + + + + + + + + +
+
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + field: css({ + alignSelf: 'stretch', + }), +}); diff --git a/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx b/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx index 02183bbc95d..30d714ffd5f 100644 --- a/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx +++ b/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx @@ -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'; diff --git a/public/app/features/admin/migrate-to-cloud/onprem/Page.tsx b/public/app/features/admin/migrate-to-cloud/onprem/Page.tsx index ee3bbdde694..eddccd4103e 100644 --- a/public/app/features/admin/migrate-to-cloud/onprem/Page.tsx +++ b/public/app/features/admin/migrate-to-cloud/onprem/Page.tsx @@ -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 ; + const { data, isFetching } = useGetStatusQuery(); + const [disconnectStack, disconnectResponse] = useDisconnectStackMutation(); + if (!data?.enabled) { + return ; + } + + return ( + + {({ showModal, hideModal }) => ( + + {data.stackURL && {data.stackURL}} + + + )} + + ); }; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 6c72ea3aeea..ac5a19e6435 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index c1fe5d179e8..94594cc7b28 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -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"