E2C: Get Cloud Token status (#90525)

* E2C: Get Cloud Token status

* remove console.log
This commit is contained in:
Josh Hunt 2024-07-18 09:48:06 +01:00 committed by GitHub
parent fefd3faef4
commit 32232e44d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 86 additions and 39 deletions

View File

@ -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.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"], [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.", "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": [ "packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"] [0, 0, 0, "Do not use any type assertions.", "0"]

View File

@ -126,7 +126,7 @@ export interface FetchError<T = any> {
traceId?: string; traceId?: string;
} }
export function isFetchError(e: unknown): e is FetchError { export function isFetchError<T = any>(e: unknown): e is FetchError<T> {
return typeof e === 'object' && e !== null && 'status' in e && 'data' in e; return typeof e === 'object' && e !== null && 'status' in e && 'data' in e;
} }

View File

@ -4,11 +4,18 @@ import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/dist/query';
import { generatedAPI } from './endpoints.gen'; import { generatedAPI } from './endpoints.gen';
export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({ export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({
addTagTypes: ['cloud-migration-session', 'cloud-migration-snapshot'], addTagTypes: ['cloud-migration-token', 'cloud-migration-session', 'cloud-migration-snapshot'],
endpoints: { endpoints: {
// Cloud-side - create token // 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 // List Cloud Configs
getSessionList: { getSessionList: {

View File

@ -1,12 +1,9 @@
import { Box } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization'; import { t, Trans } from 'app/core/internationalization';
import { InfoItem } from '../../shared/InfoItem'; import { InfoItem } from '../../shared/InfoItem';
import { MigrationTokenPane } from '../MigrationTokenPane/MigrationTokenPane';
export const InfoPane = () => { export const InfoPane = () => {
return ( return (
<Box alignItems="flex-start" display="flex" direction="column" gap={2}>
<InfoItem title={t('migrate-to-cloud.migrate-to-this-stack.title', 'Let us help you migrate to this stack')}> <InfoItem title={t('migrate-to-cloud.migrate-to-this-stack.title', 'Let us help you migrate to this stack')}>
<Trans i18nKey="migrate-to-cloud.migrate-to-this-stack.body"> <Trans i18nKey="migrate-to-cloud.migrate-to-this-stack.body">
You can migrate some resources from your self-managed Grafana installation to this cloud stack. To do this You can migrate some resources from your self-managed Grafana installation to this cloud stack. To do this
@ -14,7 +11,5 @@ export const InfoPane = () => {
authenticate with this cloud stack. authenticate with this cloud stack.
</Trans> </Trans>
</InfoItem> </InfoItem>
<MigrationTokenPane />
</Box>
); );
}; };

View File

@ -1,22 +1,42 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { isFetchError } from '@grafana/runtime';
import { Box, Button, Text } from '@grafana/ui'; import { Box, Button, Text } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization'; import { t, Trans } from 'app/core/internationalization';
import { useCreateCloudMigrationTokenMutation } from '../../api'; import { useCreateCloudMigrationTokenMutation, useGetCloudMigrationTokenQuery } from '../../api';
import { TokenErrorAlert } from '../TokenErrorAlert'; import { TokenErrorAlert } from '../TokenErrorAlert';
import { MigrationTokenModal } from './MigrationTokenModal'; import { MigrationTokenModal } from './MigrationTokenModal';
import { TokenStatus } from './TokenStatus'; import { TokenStatus } from './TokenStatus';
// TODO: candidate to hoist and share
function maybeAPIError(err: unknown) {
if (!isFetchError<unknown>(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 = () => { export const MigrationTokenPane = () => {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const isFetchingStatus = false; // TODO: No API for this yet const getTokenQuery = useGetCloudMigrationTokenQuery();
const [createTokenMutation, createTokenResponse] = useCreateCloudMigrationTokenMutation(); 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 handleGenerateToken = useCallback(async () => {
const resp = await createTokenMutation(); const resp = await createTokenMutation();
@ -28,20 +48,22 @@ export const MigrationTokenPane = () => {
return ( return (
<> <>
<Box display="flex" alignItems="flex-start" direction="column" gap={2}> <Box display="flex" alignItems="flex-start" direction="column" gap={2}>
<Button disabled={isLoading || hasToken} onClick={handleGenerateToken}>
{createTokenResponse.isLoading
? t('migrate-to-cloud.migration-token.generate-button-loading', 'Generating a migration token...')
: t('migrate-to-cloud.migration-token.generate-button', 'Generate a migration token')}
</Button>
{createTokenResponse?.isError ? ( {createTokenResponse?.isError ? (
<TokenErrorAlert /> <TokenErrorAlert />
) : ( ) : (
<Text color="secondary"> <Text color="secondary">
<Trans i18nKey="migrate-to-cloud.migration-token.status"> <Trans i18nKey="migrate-to-cloud.migration-token.status">
Current status: <TokenStatus hasToken={hasToken} isFetching={isLoading} /> Current status:{' '}
<TokenStatus hasToken={hasToken} isFetching={isLoading} errorMessageId={getTokenQueryError?.messageId} />
</Trans> </Trans>
</Text> </Text>
)} )}
<Button disabled={isLoading || hasToken} onClick={handleGenerateToken}>
{createTokenResponse.isLoading
? t('migrate-to-cloud.migration-token.generate-button-loading', 'Generating a migration token...')
: t('migrate-to-cloud.migration-token.generate-button', 'Generate a migration token')}
</Button>
</Box> </Box>
<MigrationTokenModal <MigrationTokenModal

View File

@ -6,18 +6,31 @@ import { Trans } from 'app/core/internationalization';
interface Props { interface Props {
hasToken: boolean; hasToken: boolean;
isFetching: boolean; isFetching: boolean;
errorMessageId: string | undefined;
} }
export const TokenStatus = ({ hasToken, isFetching }: Props) => { export const TokenStatus = ({ hasToken, errorMessageId, isFetching }: Props) => {
if (isFetching) { if (isFetching) {
return <Skeleton width={100} />; return <Skeleton width={100} />;
} } else if (hasToken) {
return (
return hasToken ? (
<Text color="success"> <Text color="success">
<Trans i18nKey="migrate-to-cloud.token-status.active">Token created and active</Trans> <Trans i18nKey="migrate-to-cloud.token-status.active">Token created and active</Trans>
</Text> </Text>
) : ( );
<Trans i18nKey="migrate-to-cloud.token-status.no-active">No active token</Trans> } else if (errorMessageId === 'cloudmigrations.tokenNotFound') {
return <Trans i18nKey="migrate-to-cloud.token-status.no-active">No active token</Trans>;
} else if (errorMessageId) {
return (
<Text color="error">
<Trans i18nKey="migrate-to-cloud.token-status.unknown-error">Error retrieving token</Trans>
</Text>
);
}
return (
<Text color="warning">
<Trans i18nKey="migrate-to-cloud.token-status.unknown">Unknown</Trans>
</Text>
); );
}; };

View File

@ -1,13 +1,18 @@
import { Box } from '@grafana/ui'; import { Box, Stack } from '@grafana/ui';
import { InfoPane } from './EmptyState/InfoPane'; import { InfoPane } from './EmptyState/InfoPane';
import { MigrationStepsPane } from './EmptyState/MigrationStepsPane'; import { MigrationStepsPane } from './EmptyState/MigrationStepsPane';
import { MigrationTokenPane } from './MigrationTokenPane/MigrationTokenPane';
export const Page = () => { export const Page = () => {
return ( return (
<Box backgroundColor="secondary" display="flex" alignItems="center" direction="column"> <Box backgroundColor="secondary" display="flex" alignItems="center" direction="column">
<Box maxWidth={90} paddingY={6} paddingX={2} gap={6} direction="column" display="flex"> <Box maxWidth={90} paddingY={6} paddingX={2} gap={6} direction="column" display="flex">
<Stack gap={2} direction="column">
<InfoPane /> <InfoPane />
<MigrationTokenPane />
</Stack>
<MigrationStepsPane /> <MigrationStepsPane />
</Box> </Box>
</Box> </Box>

View File

@ -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-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-field-label": "Token",
"modal-title": "Migration token created", "modal-title": "Migration token created",
"status": "Current status: <1></1>" "status": "Current status: <2></2>"
}, },
"pdc": { "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.", "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": { "token-status": {
"active": "Token created and active", "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": { "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.", "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.",

View File

@ -1040,7 +1040,7 @@
"modal-field-description": "Cőpy ŧĥę ŧőĸęʼn ʼnőŵ äş yőū ŵįľľ ʼnőŧ þę äþľę ŧő şęę įŧ äģäįʼn. Ŀőşįʼnģ ä ŧőĸęʼn řęqūįřęş čřęäŧįʼnģ ä ʼnęŵ őʼnę.", "modal-field-description": "Cőpy ŧĥę ŧőĸęʼn ʼnőŵ äş yőū ŵįľľ ʼnőŧ þę äþľę ŧő şęę įŧ äģäįʼn. Ŀőşįʼnģ ä ŧőĸęʼn řęqūįřęş čřęäŧįʼnģ ä ʼnęŵ őʼnę.",
"modal-field-label": "Ŧőĸęʼn", "modal-field-label": "Ŧőĸęʼn",
"modal-title": "Mįģřäŧįőʼn ŧőĸęʼn čřęäŧęđ", "modal-title": "Mįģřäŧįőʼn ŧőĸęʼn čřęäŧęđ",
"status": "Cūřřęʼnŧ şŧäŧūş: <1></1>" "status": "Cūřřęʼnŧ şŧäŧūş: <2></2>"
}, },
"pdc": { "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ęľ.", "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": { "token-status": {
"active": "Ŧőĸęʼn čřęäŧęđ äʼnđ äčŧįvę", "active": "Ŧőĸęʼn čřęäŧęđ äʼnđ äčŧįvę",
"no-active": "Ńő äčŧįvę ŧőĸęʼn" "no-active": "Ńő äčŧįvę ŧőĸęʼn",
"unknown": "Ůʼnĸʼnőŵʼn",
"unknown-error": "Ēřřőř řęŧřįęvįʼnģ ŧőĸęʼn"
}, },
"what-is-cloud": { "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.", "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.",