diff --git a/.betterer.results b/.betterer.results index de30c17fdc1..e5aaea82878 100644 --- a/.betterer.results +++ b/.betterer.results @@ -534,7 +534,8 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"], [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"] + [0, 0, 0, "Unexpected any. Specify a different type.", "10"], + [0, 0, 0, "Unexpected any. Specify a different type.", "11"] ], "packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/packages/grafana-runtime/src/services/backendSrv.ts b/packages/grafana-runtime/src/services/backendSrv.ts index 634b6553712..25512f3f76b 100644 --- a/packages/grafana-runtime/src/services/backendSrv.ts +++ b/packages/grafana-runtime/src/services/backendSrv.ts @@ -126,7 +126,7 @@ export interface FetchError { traceId?: string; } -export function isFetchError(e: unknown): e is FetchError { +export function isFetchError(e: unknown): e is FetchError { return typeof e === 'object' && e !== null && 'status' in e && 'data' in e; } diff --git a/public/app/features/migrate-to-cloud/api/index.ts b/public/app/features/migrate-to-cloud/api/index.ts index 6b05a867aef..921ff8c0f65 100644 --- a/public/app/features/migrate-to-cloud/api/index.ts +++ b/public/app/features/migrate-to-cloud/api/index.ts @@ -4,11 +4,18 @@ import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/dist/query'; import { generatedAPI } from './endpoints.gen'; export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({ - addTagTypes: ['cloud-migration-session', 'cloud-migration-snapshot'], + addTagTypes: ['cloud-migration-token', 'cloud-migration-session', 'cloud-migration-snapshot'], endpoints: { // Cloud-side - create token - createCloudMigrationToken: suppressErrorsOnQuery, + createCloudMigrationToken(endpoint) { + suppressErrorsOnQuery(endpoint); + endpoint.invalidatesTags = ['cloud-migration-token']; + }, + getCloudMigrationToken(endpoint) { + suppressErrorsOnQuery(endpoint); + endpoint.providesTags = ['cloud-migration-token']; + }, // List Cloud Configs getSessionList: { diff --git a/public/app/features/migrate-to-cloud/cloud/EmptyState/InfoPane.tsx b/public/app/features/migrate-to-cloud/cloud/EmptyState/InfoPane.tsx index c94e1a59a3f..9e8029ff64b 100644 --- a/public/app/features/migrate-to-cloud/cloud/EmptyState/InfoPane.tsx +++ b/public/app/features/migrate-to-cloud/cloud/EmptyState/InfoPane.tsx @@ -1,20 +1,15 @@ -import { Box } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { InfoItem } from '../../shared/InfoItem'; -import { MigrationTokenPane } from '../MigrationTokenPane/MigrationTokenPane'; export const InfoPane = () => { return ( - - - - You can migrate some resources from your self-managed Grafana installation to this cloud stack. To do this - securely, you'll need to generate a migration token. Your self-managed instance will use the token to - authenticate with this cloud stack. - - - - + + + You can migrate some resources from your self-managed Grafana installation to this cloud stack. To do this + securely, you'll need to generate a migration token. Your self-managed instance will use the token to + authenticate with this cloud stack. + + ); }; diff --git a/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx b/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx index 34b3cf825e6..0d39e955c57 100644 --- a/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx +++ b/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx @@ -1,22 +1,42 @@ import { useCallback, useState } from 'react'; +import { isFetchError } from '@grafana/runtime'; import { Box, Button, Text } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; -import { useCreateCloudMigrationTokenMutation } from '../../api'; +import { useCreateCloudMigrationTokenMutation, useGetCloudMigrationTokenQuery } from '../../api'; import { TokenErrorAlert } from '../TokenErrorAlert'; import { MigrationTokenModal } from './MigrationTokenModal'; import { TokenStatus } from './TokenStatus'; +// TODO: candidate to hoist and share +function maybeAPIError(err: unknown) { + if (!isFetchError(err) || typeof err.data !== 'object' || !err.data) { + return null; + } + + const data = err?.data; + const message = 'message' in data && typeof data.message === 'string' ? data.message : null; + const messageId = 'messageId' in data && typeof data.messageId === 'string' ? data.messageId : null; + const statusCode = 'statusCode' in data && typeof data.statusCode === 'number' ? data.statusCode : null; + + if (!message || !messageId || !statusCode) { + return null; + } + + return { message, messageId, statusCode }; +} + export const MigrationTokenPane = () => { const [showModal, setShowModal] = useState(false); - const isFetchingStatus = false; // TODO: No API for this yet - + const getTokenQuery = useGetCloudMigrationTokenQuery(); const [createTokenMutation, createTokenResponse] = useCreateCloudMigrationTokenMutation(); - const hasToken = Boolean(createTokenResponse.data?.token); - const isLoading = isFetchingStatus || createTokenResponse.isLoading; /* || deleteTokenResponse.isLoading */ + const getTokenQueryError = maybeAPIError(getTokenQuery.error); + + const hasToken = Boolean(createTokenResponse.data?.token) || Boolean(getTokenQuery.data?.id); + const isLoading = getTokenQuery.isFetching || createTokenResponse.isLoading; const handleGenerateToken = useCallback(async () => { const resp = await createTokenMutation(); @@ -28,20 +48,22 @@ export const MigrationTokenPane = () => { return ( <> - {createTokenResponse?.isError ? ( ) : ( - Current status: + Current status:{' '} + )} + + { +export const TokenStatus = ({ hasToken, errorMessageId, isFetching }: Props) => { if (isFetching) { return ; + } else if (hasToken) { + return ( + + Token created and active + + ); + } else if (errorMessageId === 'cloudmigrations.tokenNotFound') { + return No active token; + } else if (errorMessageId) { + return ( + + Error retrieving token + + ); } - return hasToken ? ( - - Token created and active + return ( + + Unknown - ) : ( - No active token ); }; diff --git a/public/app/features/migrate-to-cloud/cloud/Page.tsx b/public/app/features/migrate-to-cloud/cloud/Page.tsx index e2f06e25b4d..e08bd91e8cb 100644 --- a/public/app/features/migrate-to-cloud/cloud/Page.tsx +++ b/public/app/features/migrate-to-cloud/cloud/Page.tsx @@ -1,13 +1,18 @@ -import { Box } from '@grafana/ui'; +import { Box, Stack } from '@grafana/ui'; import { InfoPane } from './EmptyState/InfoPane'; import { MigrationStepsPane } from './EmptyState/MigrationStepsPane'; +import { MigrationTokenPane } from './MigrationTokenPane/MigrationTokenPane'; export const Page = () => { return ( - + + + + + diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 3d995bd627f..b9cd212efdb 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1040,7 +1040,7 @@ "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: <1>" + "status": "Current status: <2>" }, "pdc": { "body": "Exposing your data sources to the internet can raise security concerns. Private data source connect (PDC) allows Grafana Cloud to access your existing data sources over a secure network tunnel.", @@ -1088,7 +1088,9 @@ }, "token-status": { "active": "Token created and active", - "no-active": "No active token" + "no-active": "No active token", + "unknown": "Unknown", + "unknown-error": "Error retrieving 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.", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 285f9b68585..8f3c84115a4 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1040,7 +1040,7 @@ "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ŧ şŧäŧūş: <1>" + "status": "Cūřřęʼnŧ şŧäŧūş: <2>" }, "pdc": { "body": "Ēχpőşįʼnģ yőūř đäŧä şőūřčęş ŧő ŧĥę įʼnŧęřʼnęŧ čäʼn řäįşę şęčūřįŧy čőʼnčęřʼnş. Přįväŧę đäŧä şőūřčę čőʼnʼnęčŧ (PĐC) äľľőŵş Ğřäƒäʼnä Cľőūđ ŧő äččęşş yőūř ęχįşŧįʼnģ đäŧä şőūřčęş ővęř ä şęčūřę ʼnęŧŵőřĸ ŧūʼnʼnęľ.", @@ -1088,7 +1088,9 @@ }, "token-status": { "active": "Ŧőĸęʼn čřęäŧęđ äʼnđ äčŧįvę", - "no-active": "Ńő äčŧįvę ŧőĸęʼn" + "no-active": "Ńő äčŧįvę ŧőĸęʼn", + "unknown": "Ůʼnĸʼnőŵʼn", + "unknown-error": "Ēřřőř řęŧřįęvįʼnģ ŧőĸęʼ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.",