E2C: Create Snapshot frontend (#89901)

* First pass at using new async apis

* async api tweaks

* clean up async api usage

* Update public/app/features/migrate-to-cloud/onprem/Page.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Update public/app/features/migrate-to-cloud/onprem/Page.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* fix syntax

---------

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
Josh Hunt 2024-07-03 11:42:00 +01:00 committed by GitHub
parent bfe77ab530
commit 7448f22f91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 237 additions and 93 deletions

View File

@ -11,24 +11,48 @@ const injectedRtkApi = api.injectEndpoints({
body: queryArg.cloudMigrationSessionRequestDto,
}),
}),
getCloudMigrationRun: build.query<GetCloudMigrationRunApiResponse, GetCloudMigrationRunApiArg>({
query: (queryArg) => ({ url: `/cloudmigration/migration/run/${queryArg.runUid}` }),
}),
deleteSession: build.mutation<DeleteSessionApiResponse, DeleteSessionApiArg>({
query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}`, method: 'DELETE' }),
}),
getSession: build.query<GetSessionApiResponse, GetSessionApiArg>({
query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}` }),
}),
getCloudMigrationRunList: build.query<GetCloudMigrationRunListApiResponse, GetCloudMigrationRunListApiArg>({
query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}/run` }),
createSnapshot: build.mutation<CreateSnapshotApiResponse, CreateSnapshotApiArg>({
query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}/snapshot`, method: 'POST' }),
}),
runCloudMigration: build.mutation<RunCloudMigrationApiResponse, RunCloudMigrationApiArg>({
query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}/run`, method: 'POST' }),
getSnapshot: build.query<GetSnapshotApiResponse, GetSnapshotApiArg>({
query: (queryArg) => ({
url: `/cloudmigration/migration/${queryArg.uid}/snapshot/${queryArg.snapshotUid}`,
params: { resultPage: queryArg.resultPage, resultLimit: queryArg.resultLimit },
}),
}),
cancelSnapshot: build.mutation<CancelSnapshotApiResponse, CancelSnapshotApiArg>({
query: (queryArg) => ({
url: `/cloudmigration/migration/${queryArg.uid}/snapshot/${queryArg.snapshotUid}/cancel`,
method: 'POST',
}),
}),
uploadSnapshot: build.mutation<UploadSnapshotApiResponse, UploadSnapshotApiArg>({
query: (queryArg) => ({
url: `/cloudmigration/migration/${queryArg.uid}/snapshot/${queryArg.snapshotUid}/upload`,
method: 'POST',
}),
}),
getShapshotList: build.query<GetShapshotListApiResponse, GetShapshotListApiArg>({
query: (queryArg) => ({
url: `/cloudmigration/migration/${queryArg.uid}/snapshots`,
params: { page: queryArg.page, limit: queryArg.limit },
}),
}),
getCloudMigrationToken: build.query<GetCloudMigrationTokenApiResponse, GetCloudMigrationTokenApiArg>({
query: () => ({ url: `/cloudmigration/token` }),
}),
createCloudMigrationToken: build.mutation<CreateCloudMigrationTokenApiResponse, CreateCloudMigrationTokenApiArg>({
query: () => ({ url: `/cloudmigration/token`, method: 'POST' }),
}),
deleteCloudMigrationToken: build.mutation<DeleteCloudMigrationTokenApiResponse, DeleteCloudMigrationTokenApiArg>({
query: (queryArg) => ({ url: `/cloudmigration/token/${queryArg.uid}`, method: 'DELETE' }),
}),
getDashboardByUid: build.query<GetDashboardByUidApiResponse, GetDashboardByUidApiArg>({
query: (queryArg) => ({ url: `/dashboards/uid/${queryArg.uid}` }),
}),
@ -42,11 +66,6 @@ export type CreateSessionApiResponse = /** status 200 (empty) */ CloudMigrationS
export type CreateSessionApiArg = {
cloudMigrationSessionRequestDto: CloudMigrationSessionRequestDto;
};
export type GetCloudMigrationRunApiResponse = /** status 200 (empty) */ MigrateDataResponseDto;
export type GetCloudMigrationRunApiArg = {
/** RunUID of a migration run */
runUid: string;
};
export type DeleteSessionApiResponse = unknown;
export type DeleteSessionApiArg = {
/** UID of a migration session */
@ -57,18 +76,54 @@ export type GetSessionApiArg = {
/** UID of a migration session */
uid: string;
};
export type GetCloudMigrationRunListApiResponse = /** status 200 (empty) */ SnapshotListDto;
export type GetCloudMigrationRunListApiArg = {
/** UID of a migration */
export type CreateSnapshotApiResponse = /** status 200 (empty) */ CreateSnapshotResponseDto;
export type CreateSnapshotApiArg = {
/** UID of a session */
uid: string;
};
export type RunCloudMigrationApiResponse = /** status 200 (empty) */ MigrateDataResponseDto;
export type RunCloudMigrationApiArg = {
/** UID of a migration */
export type GetSnapshotApiResponse = /** status 200 (empty) */ GetSnapshotResponseDto;
export type GetSnapshotApiArg = {
/** ResultPage is used for pagination with ResultLimit */
resultPage?: number;
/** Max limit for snapshot results returned. */
resultLimit?: number;
/** Session UID of a session */
uid: string;
/** UID of a snapshot */
snapshotUid: string;
};
export type CancelSnapshotApiResponse = /** status 200 (empty) */ void;
export type CancelSnapshotApiArg = {
/** Session UID of a session */
uid: string;
/** UID of a snapshot */
snapshotUid: string;
};
export type UploadSnapshotApiResponse = /** status 200 (empty) */ void;
export type UploadSnapshotApiArg = {
/** Session UID of a session */
uid: string;
/** UID of a snapshot */
snapshotUid: string;
};
export type GetShapshotListApiResponse = /** status 200 (empty) */ SnapshotListResponseDto;
export type GetShapshotListApiArg = {
/** Page is used for pagination with limit */
page?: number;
/** Max limit for results returned. */
limit?: number;
/** Session UID of a session */
uid: string;
};
export type GetCloudMigrationTokenApiResponse = /** status 200 (empty) */ GetAccessTokenResponseDto;
export type GetCloudMigrationTokenApiArg = void;
export type CreateCloudMigrationTokenApiResponse = /** status 200 (empty) */ CreateAccessTokenResponseDto;
export type CreateCloudMigrationTokenApiArg = void;
export type DeleteCloudMigrationTokenApiResponse = /** status 204 (empty) */ void;
export type DeleteCloudMigrationTokenApiArg = {
/** UID of a cloud migration token */
uid: string;
};
export type GetDashboardByUidApiResponse = /** status 200 (empty) */ DashboardFullWithMeta;
export type GetDashboardByUidApiArg = {
uid: string;
@ -95,21 +150,58 @@ export type ErrorResponseBody = {
export type CloudMigrationSessionRequestDto = {
authToken?: string;
};
export type CreateSnapshotResponseDto = {
uid?: string;
};
export type MigrateDataResponseItemDto = {
error?: string;
refId: string;
status: 'OK' | 'ERROR';
status: 'OK' | 'ERROR' | 'PENDING' | 'UNKNOWN';
type: 'DASHBOARD' | 'DATASOURCE' | 'FOLDER';
};
export type MigrateDataResponseDto = {
items?: MigrateDataResponseItemDto[];
export type GetSnapshotResponseDto = {
created?: string;
finished?: string;
results?: MigrateDataResponseItemDto[];
sessionUid?: string;
status?:
| 'INITIALIZING'
| 'CREATING'
| 'PENDING_UPLOAD'
| 'UPLOADING'
| 'PENDING_PROCESSING'
| 'PROCESSING'
| 'FINISHED'
| 'ERROR'
| 'UNKNOWN';
uid?: string;
};
export type MigrateDataResponseListDto = {
export type SnapshotDto = {
created?: string;
finished?: string;
sessionUid?: string;
status?:
| 'INITIALIZING'
| 'CREATING'
| 'PENDING_UPLOAD'
| 'UPLOADING'
| 'PENDING_PROCESSING'
| 'PROCESSING'
| 'FINISHED'
| 'ERROR'
| 'UNKNOWN';
uid?: string;
};
export type SnapshotListDto = {
runs?: MigrateDataResponseListDto[];
export type SnapshotListResponseDto = {
snapshots?: SnapshotDto[];
};
export type GetAccessTokenResponseDto = {
createdAt?: string;
displayName?: string;
expiresAt?: string;
firstUsedAt?: string;
id?: string;
lastUsedAt?: string;
};
export type CreateAccessTokenResponseDto = {
token?: string;
@ -160,11 +252,15 @@ export type DashboardFullWithMeta = {
export const {
useGetSessionListQuery,
useCreateSessionMutation,
useGetCloudMigrationRunQuery,
useDeleteSessionMutation,
useGetSessionQuery,
useGetCloudMigrationRunListQuery,
useRunCloudMigrationMutation,
useCreateSnapshotMutation,
useGetSnapshotQuery,
useCancelSnapshotMutation,
useUploadSnapshotMutation,
useGetShapshotListQuery,
useGetCloudMigrationTokenQuery,
useCreateCloudMigrationTokenMutation,
useDeleteCloudMigrationTokenMutation,
useGetDashboardByUidQuery,
} = injectedRtkApi;

View File

@ -4,42 +4,45 @@ import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/dist/query';
import { generatedAPI } from './endpoints.gen';
export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({
addTagTypes: ['cloud-migration-config', 'cloud-migration-run', 'cloud-migration-run-list'],
addTagTypes: ['cloud-migration-session', 'cloud-migration-snapshot'],
endpoints: {
// Cloud-side - create token
createCloudMigrationToken: suppressErrorsOnQuery,
// List Cloud Configs
getSessionList: {
providesTags: ['cloud-migration-config'] /* should this be a -list? */,
providesTags: ['cloud-migration-session'] /* should this be a -list? */,
},
// Create Cloud Config
createSession(endpoint) {
suppressErrorsOnQuery(endpoint);
endpoint.invalidatesTags = ['cloud-migration-config'];
endpoint.invalidatesTags = ['cloud-migration-session'];
},
// Get one Cloud Config
getSession: {
providesTags: ['cloud-migration-config'],
providesTags: ['cloud-migration-session'],
},
// Delete one Cloud Config
deleteSession: {
invalidatesTags: ['cloud-migration-config'],
invalidatesTags: ['cloud-migration-session', 'cloud-migration-snapshot'],
},
getCloudMigrationRunList: {
providesTags: ['cloud-migration-run-list'],
// Snapshot management
getSnapshot: {
providesTags: ['cloud-migration-snapshot'],
},
getCloudMigrationRun: {
providesTags: ['cloud-migration-run'],
getShapshotList: {
providesTags: ['cloud-migration-snapshot'],
},
runCloudMigration: {
invalidatesTags: ['cloud-migration-run-list'],
createSnapshot: {
invalidatesTags: ['cloud-migration-snapshot'],
},
uploadSnapshot: {
invalidatesTags: ['cloud-migration-snapshot'],
},
getDashboardByUid: suppressErrorsOnQuery,

View File

@ -1,15 +1,17 @@
import { skipToken } from '@reduxjs/toolkit/query/react';
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Alert, Box, Button, Stack } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import {
SnapshotDto,
useCreateSnapshotMutation,
useDeleteSessionMutation,
useGetCloudMigrationRunListQuery,
useGetCloudMigrationRunQuery,
useGetSessionListQuery,
useRunCloudMigrationMutation,
useGetShapshotListQuery,
useGetSnapshotQuery,
useUploadSnapshotMutation,
} from '../api';
import { DisconnectModal } from './DisconnectModal';
@ -32,7 +34,7 @@ import { ResourcesTable } from './ResourcesTable';
* 2. call GetCloudMigrationRun with the ID from first step to list the result of that migration
*/
function useGetLatestMigrationDestination() {
function useGetLatestSession() {
const result = useGetSessionListQuery();
const latestMigration = result.data?.sessions?.at(-1);
@ -42,64 +44,88 @@ function useGetLatestMigrationDestination() {
};
}
function useGetLatestMigrationRun(migrationUid?: string) {
const listResult = useGetCloudMigrationRunListQuery(migrationUid ? { uid: migrationUid } : skipToken);
const latestMigrationRun = listResult.data?.runs?.at(-1);
const SHOULD_POLL_STATUSES: Array<SnapshotDto['status']> = [
'INITIALIZING',
'CREATING',
'UPLOADING',
'PENDING_PROCESSING',
'PROCESSING',
];
const runResult = useGetCloudMigrationRunQuery(
latestMigrationRun?.uid && migrationUid ? { runUid: latestMigrationRun.uid } : skipToken
);
const STATUS_POLL_INTERVAL = 5 * 1000;
function useGetLatestSnapshot(sessionUid?: string) {
const [shouldPoll, setShouldPoll] = useState(false);
const listResult = useGetShapshotListQuery(sessionUid ? { uid: sessionUid } : skipToken);
const lastItem = listResult.data?.snapshots?.at(-1); // TODO: account for pagination and ensure we're truely getting the last one
const getSnapshotQueryArgs = sessionUid && lastItem?.uid ? { uid: sessionUid, snapshotUid: lastItem.uid } : skipToken;
const snapshotResult = useGetSnapshotQuery(getSnapshotQueryArgs, {
pollingInterval: shouldPoll ? STATUS_POLL_INTERVAL : 0,
skipPollingIfUnfocused: true,
});
useEffect(() => {
const shouldPoll = SHOULD_POLL_STATUSES.includes(snapshotResult.data?.status);
setShouldPoll(shouldPoll);
}, [snapshotResult?.data?.status]);
return {
...runResult,
...snapshotResult,
data: runResult.data,
error: listResult.error || snapshotResult.error,
error: listResult.error || runResult.error,
isError: listResult.isError || runResult.isError,
isLoading: listResult.isLoading || runResult.isLoading,
isFetching: listResult.isFetching || runResult.isFetching,
// isSuccess and isUninitialised should always be from snapshotResult
// as only the 'final' values from those are important
isError: listResult.isError || snapshotResult.isError,
isLoading: listResult.isLoading || snapshotResult.isLoading,
isFetching: listResult.isFetching || snapshotResult.isFetching,
};
}
export const Page = () => {
const [disconnectModalOpen, setDisconnectModalOpen] = useState(false);
const migrationDestination = useGetLatestMigrationDestination();
const lastMigrationRun = useGetLatestMigrationRun(migrationDestination.data?.uid);
const [performRunMigration, runMigrationResult] = useRunCloudMigrationMutation();
const session = useGetLatestSession();
const snapshot = useGetLatestSnapshot(session.data?.uid);
const [performCreateSnapshot, createSnapshotResult] = useCreateSnapshotMutation();
const [performUploadSnapshot, uploadSnapshotResult] = useUploadSnapshotMutation();
const [performDisconnect, disconnectResult] = useDeleteSessionMutation();
const sessionUid = session.data?.uid;
const snapshotUid = snapshot.data?.uid;
const migrationMeta = session.data;
const isInitialLoading = session.isLoading;
// isBusy is not a loading state, but indicates that the system is doing *something*
// and all buttons should be disabled
const isBusy =
runMigrationResult.isLoading ||
migrationDestination.isFetching ||
lastMigrationRun.isFetching ||
createSnapshotResult.isLoading ||
uploadSnapshotResult.isLoading ||
session.isLoading ||
snapshot.isLoading ||
disconnectResult.isLoading;
const resources = lastMigrationRun.data?.items;
const migrationDestUID = migrationDestination.data?.uid;
const resources = snapshot.data?.results;
const handleDisconnect = useCallback(async () => {
if (!migrationDestUID) {
return;
if (sessionUid) {
performDisconnect({ uid: sessionUid });
}
}, [performDisconnect, sessionUid]);
const resp = await performDisconnect({ uid: migrationDestUID });
if (!('error' in resp)) {
setDisconnectModalOpen(false);
const handleCreateSnapshot = useCallback(() => {
if (sessionUid) {
performCreateSnapshot({ uid: sessionUid });
}
}, [migrationDestUID, performDisconnect]);
}, [performCreateSnapshot, sessionUid]);
const handleStartMigration = useCallback(() => {
if (migrationDestination.data?.uid) {
performRunMigration({ uid: migrationDestination.data?.uid });
const handleUploadSnapshot = useCallback(() => {
if (sessionUid && snapshotUid) {
performUploadSnapshot({ uid: sessionUid, snapshotUid: snapshotUid });
}
}, [performRunMigration, migrationDestination]);
const migrationMeta = migrationDestination.data;
const isInitialLoading = migrationDestination.isLoading;
}, [performUploadSnapshot, sessionUid, snapshotUid]);
if (isInitialLoading) {
// TODO: better loading state
@ -111,7 +137,7 @@ export const Page = () => {
return (
<>
<Stack direction="column" gap={4}>
{runMigrationResult.isError && (
{createSnapshotResult.isError && (
<Alert
severity="error"
title={t(
@ -164,12 +190,22 @@ export const Page = () => {
}
/>
<MigrationInfo title="Status" value={snapshot?.data?.status ?? 'no snapshot yet'} />
<Button
disabled={isBusy}
onClick={handleStartMigration}
icon={runMigrationResult.isLoading ? 'spinner' : undefined}
disabled={isBusy || Boolean(snapshot.data)}
onClick={handleCreateSnapshot}
icon={createSnapshotResult.isLoading ? 'spinner' : undefined}
>
<Trans i18nKey="migrate-to-cloud.summary.start-migration">Upload everything</Trans>
<Trans i18nKey="migrate-to-cloud.summary.start-migration">Build snapshot</Trans>
</Button>
<Button
disabled={isBusy || !(snapshot.data?.status === 'PENDING_UPLOAD')}
onClick={handleUploadSnapshot}
icon={createSnapshotResult.isLoading ? 'spinner' : undefined}
>
<Trans i18nKey="migrate-to-cloud.summary.upload-migration">Upload & migrate snapshot</Trans>
</Button>
</Box>
)}

View File

@ -968,8 +968,9 @@
"disconnect-error-title": "There was an error disconnecting",
"run-migration-error-description": "See the Grafana server logs for more details",
"run-migration-error-title": "There was an error migrating your resources",
"start-migration": "Upload everything",
"target-stack-title": "Uploading to"
"start-migration": "Build snapshot",
"target-stack-title": "Uploading to",
"upload-migration": "Upload & migrate snapshot"
},
"token-status": {
"active": "Token created and active",

View File

@ -968,8 +968,9 @@
"disconnect-error-title": "Ŧĥęřę ŵäş äʼn ęřřőř đįşčőʼnʼnęčŧįʼnģ",
"run-migration-error-description": "Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş",
"run-migration-error-title": "Ŧĥęřę ŵäş äʼn ęřřőř mįģřäŧįʼnģ yőūř řęşőūřčęş",
"start-migration": "Ůpľőäđ ęvęřyŧĥįʼnģ",
"target-stack-title": "Ůpľőäđįʼnģ ŧő"
"start-migration": "ßūįľđ şʼnäpşĥőŧ",
"target-stack-title": "Ůpľőäđįʼnģ ŧő",
"upload-migration": "Ůpľőäđ & mįģřäŧę şʼnäpşĥőŧ"
},
"token-status": {
"active": "Ŧőĸęʼn čřęäŧęđ äʼnđ äčŧįvę",

View File

@ -12,14 +12,21 @@ const config: ConfigFile = {
apiFile: '../public/app/features/migrate-to-cloud/api/baseAPI.ts',
apiImport: 'baseAPI',
filterEndpoints: [
'createCloudMigrationToken',
'getSessionList',
'getSession',
'createSession',
'deleteSession',
'runCloudMigration',
'getCloudMigrationRun',
'getCloudMigrationRunList',
'createSession',
'getShapshotList',
'getSnapshot',
'uploadSnapshot',
'createSnapshot',
'cancelSnapshot',
'createCloudMigrationToken',
'deleteCloudMigrationToken',
'getCloudMigrationToken',
'getDashboardByUid',
],
},