From faaf4dc1e33ec47ba226d675b89ddfed747ffe88 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 27 Feb 2024 09:54:06 +0000 Subject: [PATCH] E2C: Implement cloud auth flow (#83409) * implement cloud auth * move logic into MigrationTokenPane folder * update PDC link * add missed translations --- .../features/admin/migrate-to-cloud/api.ts | 49 ++++++++++++- .../admin/migrate-to-cloud/cloud/InfoPane.tsx | 2 +- .../cloud/MigrationTokenPane.tsx | 30 -------- .../DeleteMigrationTokenModal.tsx | 43 +++++++++++ .../MigrationTokenModal.tsx | 45 ++++++++++++ .../MigrationTokenPane/MigrationTokenPane.tsx | 72 +++++++++++++++++++ .../cloud/MigrationTokenPane/TokenStatus.tsx | 24 +++++++ .../admin/migrate-to-cloud/cloud/Page.tsx | 2 +- public/app/store/configureStore.ts | 4 +- public/locales/en-US/grafana.json | 19 ++++- public/locales/pseudo-LOCALE/grafana.json | 19 ++++- 11 files changed, 273 insertions(+), 36 deletions(-) delete mode 100644 public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane.tsx create mode 100644 public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/DeleteMigrationTokenModal.tsx create mode 100644 public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenModal.tsx create mode 100644 public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx create mode 100644 public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/TokenStatus.tsx diff --git a/public/app/features/admin/migrate-to-cloud/api.ts b/public/app/features/admin/migrate-to-cloud/api.ts index d1972ce85c9..b144fce904c 100644 --- a/public/app/features/admin/migrate-to-cloud/api.ts +++ b/public/app/features/admin/migrate-to-cloud/api.ts @@ -31,7 +31,17 @@ interface MigrateToCloudStatusDTO { enabled: boolean; } +interface CreateMigrationTokenResponseDTO { + token: string; +} + +// TODO remove these mock properties/functions +const MOCK_DELAY_MS = 1000; +const MOCK_TOKEN = 'TODO_thisWillBeABigLongToken'; +let HAS_MIGRATION_TOKEN = false; + export const migrateToCloudAPI = createApi({ + tagTypes: ['migrationToken'], reducerPath: 'migrateToCloudAPI', baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }), endpoints: (builder) => ({ @@ -39,7 +49,44 @@ export const migrateToCloudAPI = createApi({ getStatus: builder.query({ queryFn: () => ({ data: { enabled: false } }), }), + createMigrationToken: builder.mutation({ + invalidatesTags: ['migrationToken'], + queryFn: async () => { + return new Promise((resolve) => { + setTimeout(() => { + HAS_MIGRATION_TOKEN = true; + resolve({ data: { token: MOCK_TOKEN } }); + }, MOCK_DELAY_MS); + }); + }, + }), + deleteMigrationToken: builder.mutation({ + invalidatesTags: ['migrationToken'], + queryFn: async () => { + return new Promise((resolve) => { + setTimeout(() => { + HAS_MIGRATION_TOKEN = false; + resolve({ data: undefined }); + }, MOCK_DELAY_MS); + }); + }, + }), + hasMigrationToken: builder.query({ + providesTags: ['migrationToken'], + queryFn: async () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ data: HAS_MIGRATION_TOKEN }); + }, MOCK_DELAY_MS); + }); + }, + }), }), }); -export const { useGetStatusQuery } = migrateToCloudAPI; +export const { + useGetStatusQuery, + useCreateMigrationTokenMutation, + useDeleteMigrationTokenMutation, + useHasMigrationTokenQuery, +} = migrateToCloudAPI; diff --git a/public/app/features/admin/migrate-to-cloud/cloud/InfoPane.tsx b/public/app/features/admin/migrate-to-cloud/cloud/InfoPane.tsx index 929daaa642b..a15b5212864 100644 --- a/public/app/features/admin/migrate-to-cloud/cloud/InfoPane.tsx +++ b/public/app/features/admin/migrate-to-cloud/cloud/InfoPane.tsx @@ -61,7 +61,7 @@ export const InfoPane = () => { - + {t('migrate-to-cloud.get-started.configure-pdc-link', 'Configure PDC for this stack')} diff --git a/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane.tsx b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane.tsx deleted file mode 100644 index d70f219d1c5..00000000000 --- a/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; - -import { Box, Button, Text } from '@grafana/ui'; -import { t, Trans } from 'app/core/internationalization'; - -import { InfoItem } from '../shared/InfoItem'; - -export const MigrationTokenPane = () => { - const onGenerateToken = () => { - console.log('TODO: generate token!'); - }; - const tokenStatus = 'TODO'; - - return ( - - - - Your self-managed Grafana instance will require a special authentication token to securely connect to this - cloud stack. - - - - Current status: {{ tokenStatus }} - - - - ); -}; 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 new file mode 100644 index 00000000000..6d1f3ddc8ec --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/DeleteMigrationTokenModal.tsx @@ -0,0 +1,43 @@ +import React, { useState } from 'react'; + +import { Modal, Button } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; + +interface Props { + hideModal: () => void; + onConfirm: () => Promise<{ data: void } | { error: unknown }>; +} + +export const DeleteMigrationTokenModal = ({ hideModal, onConfirm }: Props) => { + const [isDeleting, setIsDeleting] = useState(false); + + const onConfirmDelete = async () => { + setIsDeleting(true); + await onConfirm(); + setIsDeleting(false); + hideModal(); + }; + + return ( + + + 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/cloud/MigrationTokenPane/MigrationTokenModal.tsx b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenModal.tsx new file mode 100644 index 00000000000..70a61593e01 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenModal.tsx @@ -0,0 +1,45 @@ +import React, { useId } from 'react'; + +import { Modal, Button, Input, Stack, ClipboardButton, Field } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; + +interface Props { + hideModal: () => void; + migrationToken: string; +} + +export const MigrationTokenModal = ({ hideModal, migrationToken }: Props) => { + const inputId = useId(); + + return ( + + + + + migrationToken}> + Copy to clipboard + + + + + + migrationToken} onClipboardCopy={hideModal}> + Copy to clipboard and close + + + + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx new file mode 100644 index 00000000000..884eaf46bc9 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx @@ -0,0 +1,72 @@ +import React from 'react'; + +import { Box, Button, ModalsController, Text } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; + +import { useCreateMigrationTokenMutation, useDeleteMigrationTokenMutation, useHasMigrationTokenQuery } from '../../api'; +import { InfoItem } from '../../shared/InfoItem'; + +import { DeleteMigrationTokenModal } from './DeleteMigrationTokenModal'; +import { MigrationTokenModal } from './MigrationTokenModal'; +import { TokenStatus } from './TokenStatus'; + +export const MigrationTokenPane = () => { + const { data: hasToken, isFetching } = useHasMigrationTokenQuery(); + const [createToken, createTokenResponse] = useCreateMigrationTokenMutation(); + const [deleteToken, deleteTokenResponse] = useDeleteMigrationTokenMutation(); + + return ( + + {({ showModal, hideModal }) => ( + + + + Your self-managed Grafana instance will require a special authentication token to securely connect to this + cloud stack. + + + + + Current status:{' '} + + + + {hasToken ? ( + + ) : ( + + )} + + )} + + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/TokenStatus.tsx b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/TokenStatus.tsx new file mode 100644 index 00000000000..a907d06dfe2 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/TokenStatus.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import Skeleton from 'react-loading-skeleton'; + +import { Text } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; + +interface Props { + hasToken: boolean; + isFetching: boolean; +} + +export const TokenStatus = ({ hasToken, isFetching }: Props) => { + if (isFetching) { + return ; + } + + return hasToken ? ( + + Token created and active + + ) : ( + No active token + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/cloud/Page.tsx b/public/app/features/admin/migrate-to-cloud/cloud/Page.tsx index 765e8cdb84b..4ae3676ae67 100644 --- a/public/app/features/admin/migrate-to-cloud/cloud/Page.tsx +++ b/public/app/features/admin/migrate-to-cloud/cloud/Page.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Grid, useStyles2 } from '@grafana/ui'; import { InfoPane } from './InfoPane'; -import { MigrationTokenPane } from './MigrationTokenPane'; +import { MigrationTokenPane } from './MigrationTokenPane/MigrationTokenPane'; export const Page = () => { const styles = useStyles2(getStyles); diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index a7a462bf3a8..4794d46d7e0 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -1,6 +1,7 @@ import { configureStore as reduxConfigureStore, createListenerMiddleware } from '@reduxjs/toolkit'; import { setupListeners } from '@reduxjs/toolkit/query'; +import { migrateToCloudAPI } from 'app/features/admin/migrate-to-cloud/api'; import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { publicDashboardApi } from 'app/features/dashboard/api/publicDashboardApi'; import { StoreState } from 'app/types/store'; @@ -28,7 +29,8 @@ export function configureStore(initialState?: Partial) { listenerMiddleware.middleware, alertingApi.middleware, publicDashboardApi.middleware, - browseDashboardsAPI.middleware + browseDashboardsAPI.middleware, + migrateToCloudAPI.middleware ), devTools: process.env.NODE_ENV !== 'production', preloadedState: { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index bfb19353212..e93f39c9c33 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -724,8 +724,21 @@ }, "migration-token": { "body": "Your self-managed Grafana instance will require a special authentication token to securely connect to this cloud stack.", + "delete-button": "Delete this migration token", + "delete-modal-body": "If you've already used this token with a self-managed installation, that installation will no longer be able to upload content.", + "delete-modal-cancel": "Cancel", + "delete-modal-confirm": "Delete", + "delete-modal-deleting": "Deleting...", + "delete-modal-title": "Delete migration token", "generate-button": "Generate a migration token", - "status": "Current status: {{tokenStatus}}", + "generate-button-loading": "Generating a migration token...", + "modal-close": "Close", + "modal-copy-and-close": "Copy to clipboard and close", + "modal-copy-button": "Copy to clipboard", + "modal-field-description": "Copy the token now as you will not be able to see it again. Losing a token requires creating a new one.", + "modal-field-label": "Token", + "modal-title": "Migration token created", + "status": "Current status: <2>", "title": "Migration token" }, "pdc": { @@ -738,6 +751,10 @@ "link-title": "Grafana Cloud pricing", "title": "How much does it cost?" }, + "token-status": { + "active": "Token created and active", + "no-active": "No active token" + }, "what-is-cloud": { "body": "Grafana cloud is a fully managed cloud-hosted observability platform ideal for cloud native environments. It's everything you love about Grafana without the overhead of maintaining, upgrading, and supporting an installation.", "link-title": "Learn about cloud features", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 2608eb4fef9..a41468f8858 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -724,8 +724,21 @@ }, "migration-token": { "body": "Ÿőūř şęľƒ-mäʼnäģęđ Ğřäƒäʼnä įʼnşŧäʼnčę ŵįľľ řęqūįřę ä şpęčįäľ äūŧĥęʼnŧįčäŧįőʼn ŧőĸęʼn ŧő şęčūřęľy čőʼnʼnęčŧ ŧő ŧĥįş čľőūđ şŧäčĸ.", + "delete-button": "Đęľęŧę ŧĥįş mįģřäŧįőʼn ŧőĸęʼn", + "delete-modal-body": "Ĩƒ yőū'vę äľřęäđy ūşęđ ŧĥįş ŧőĸęʼn ŵįŧĥ ä şęľƒ-mäʼnäģęđ įʼnşŧäľľäŧįőʼn, ŧĥäŧ įʼnşŧäľľäŧįőʼn ŵįľľ ʼnő ľőʼnģęř þę äþľę ŧő ūpľőäđ čőʼnŧęʼnŧ.", + "delete-modal-cancel": "Cäʼnčęľ", + "delete-modal-confirm": "Đęľęŧę", + "delete-modal-deleting": "Đęľęŧįʼnģ...", + "delete-modal-title": "Đęľęŧę mįģřäŧįőʼn ŧőĸęʼn", "generate-button": "Ğęʼnęřäŧę ä mįģřäŧįőʼn ŧőĸęʼn", - "status": "Cūřřęʼnŧ şŧäŧūş: {{tokenStatus}}", + "generate-button-loading": "Ğęʼnęřäŧįʼnģ ä mįģřäŧįőʼn ŧőĸęʼn...", + "modal-close": "Cľőşę", + "modal-copy-and-close": "Cőpy ŧő čľįpþőäřđ äʼnđ čľőşę", + "modal-copy-button": "Cőpy ŧő čľįpþőäřđ", + "modal-field-description": "Cőpy ŧĥę ŧőĸęʼn ʼnőŵ äş yőū ŵįľľ ʼnőŧ þę äþľę ŧő şęę įŧ äģäįʼn. Ŀőşįʼnģ ä ŧőĸęʼn řęqūįřęş čřęäŧįʼnģ ä ʼnęŵ őʼnę.", + "modal-field-label": "Ŧőĸęʼn", + "modal-title": "Mįģřäŧįőʼn ŧőĸęʼn čřęäŧęđ", + "status": "Cūřřęʼnŧ şŧäŧūş: <2>", "title": "Mįģřäŧįőʼn ŧőĸęʼn" }, "pdc": { @@ -738,6 +751,10 @@ "link-title": "Ğřäƒäʼnä Cľőūđ přįčįʼnģ", "title": "Ħőŵ mūčĥ đőęş įŧ čőşŧ?" }, + "token-status": { + "active": "Ŧőĸęʼn čřęäŧęđ äʼnđ äčŧįvę", + "no-active": "Ńő äčŧįvę ŧőĸęʼn" + }, "what-is-cloud": { "body": "Ğřäƒäʼnä čľőūđ įş ä ƒūľľy mäʼnäģęđ čľőūđ-ĥőşŧęđ őþşęřväþįľįŧy pľäŧƒőřm įđęäľ ƒőř čľőūđ ʼnäŧįvę ęʼnvįřőʼnmęʼnŧş. Ĩŧ'ş ęvęřyŧĥįʼnģ yőū ľővę äþőūŧ Ğřäƒäʼnä ŵįŧĥőūŧ ŧĥę ővęřĥęäđ őƒ mäįʼnŧäįʼnįʼnģ, ūpģřäđįʼnģ, äʼnđ şūppőřŧįʼnģ äʼn įʼnşŧäľľäŧįőʼn.", "link-title": "Ŀęäřʼn äþőūŧ čľőūđ ƒęäŧūřęş",