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.", "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"]

View File

@ -126,7 +126,7 @@ export interface FetchError<T = any> {
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;
}

View File

@ -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: {

View File

@ -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 (
<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')}>
<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
securely, you&apos;ll need to generate a migration token. Your self-managed instance will use the token to
authenticate with this cloud stack.
</Trans>
</InfoItem>
<MigrationTokenPane />
</Box>
<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">
You can migrate some resources from your self-managed Grafana installation to this cloud stack. To do this
securely, you&apos;ll need to generate a migration token. Your self-managed instance will use the token to
authenticate with this cloud stack.
</Trans>
</InfoItem>
);
};

View File

@ -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<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 = () => {
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 (
<>
<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 ? (
<TokenErrorAlert />
) : (
<Text color="secondary">
<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>
</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>
<MigrationTokenModal

View File

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

View File

@ -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 (
<Box backgroundColor="secondary" display="flex" alignItems="center" direction="column">
<Box maxWidth={90} paddingY={6} paddingX={2} gap={6} direction="column" display="flex">
<InfoPane />
<Stack gap={2} direction="column">
<InfoPane />
<MigrationTokenPane />
</Stack>
<MigrationStepsPane />
</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-label": "Token",
"modal-title": "Migration token created",
"status": "Current status: <1></1>"
"status": "Current status: <2></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.",

View File

@ -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></1>"
"status": "Cūřřęʼnŧ şŧäŧūş: <2></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.",