From 7448f22f91946ab240f33e5f188c710962d778e0 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 3 Jul 2024 11:42:00 +0100 Subject: [PATCH] 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 * Update public/app/features/migrate-to-cloud/onprem/Page.tsx Co-authored-by: Alex Khomenko * fix syntax --------- Co-authored-by: Alex Khomenko --- .../migrate-to-cloud/api/endpoints.gen.ts | 150 ++++++++++++++---- .../features/migrate-to-cloud/api/index.ts | 29 ++-- .../features/migrate-to-cloud/onprem/Page.tsx | 124 ++++++++++----- public/locales/en-US/grafana.json | 5 +- public/locales/pseudo-LOCALE/grafana.json | 5 +- scripts/generate-rtk-apis.ts | 17 +- 6 files changed, 237 insertions(+), 93 deletions(-) diff --git a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts index 54beb3847ef..537535a7920 100644 --- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts +++ b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts @@ -11,24 +11,48 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.cloudMigrationSessionRequestDto, }), }), - getCloudMigrationRun: build.query({ - query: (queryArg) => ({ url: `/cloudmigration/migration/run/${queryArg.runUid}` }), - }), deleteSession: build.mutation({ query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}`, method: 'DELETE' }), }), getSession: build.query({ query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}` }), }), - getCloudMigrationRunList: build.query({ - query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}/run` }), + createSnapshot: build.mutation({ + query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}/snapshot`, method: 'POST' }), }), - runCloudMigration: build.mutation({ - query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}/run`, method: 'POST' }), + getSnapshot: build.query({ + query: (queryArg) => ({ + url: `/cloudmigration/migration/${queryArg.uid}/snapshot/${queryArg.snapshotUid}`, + params: { resultPage: queryArg.resultPage, resultLimit: queryArg.resultLimit }, + }), + }), + cancelSnapshot: build.mutation({ + query: (queryArg) => ({ + url: `/cloudmigration/migration/${queryArg.uid}/snapshot/${queryArg.snapshotUid}/cancel`, + method: 'POST', + }), + }), + uploadSnapshot: build.mutation({ + query: (queryArg) => ({ + url: `/cloudmigration/migration/${queryArg.uid}/snapshot/${queryArg.snapshotUid}/upload`, + method: 'POST', + }), + }), + getShapshotList: build.query({ + query: (queryArg) => ({ + url: `/cloudmigration/migration/${queryArg.uid}/snapshots`, + params: { page: queryArg.page, limit: queryArg.limit }, + }), + }), + getCloudMigrationToken: build.query({ + query: () => ({ url: `/cloudmigration/token` }), }), createCloudMigrationToken: build.mutation({ query: () => ({ url: `/cloudmigration/token`, method: 'POST' }), }), + deleteCloudMigrationToken: build.mutation({ + query: (queryArg) => ({ url: `/cloudmigration/token/${queryArg.uid}`, method: 'DELETE' }), + }), getDashboardByUid: build.query({ 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; diff --git a/public/app/features/migrate-to-cloud/api/index.ts b/public/app/features/migrate-to-cloud/api/index.ts index 696a5a8505b..6b05a867aef 100644 --- a/public/app/features/migrate-to-cloud/api/index.ts +++ b/public/app/features/migrate-to-cloud/api/index.ts @@ -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, diff --git a/public/app/features/migrate-to-cloud/onprem/Page.tsx b/public/app/features/migrate-to-cloud/onprem/Page.tsx index c92a1de23c0..91b59f801a4 100644 --- a/public/app/features/migrate-to-cloud/onprem/Page.tsx +++ b/public/app/features/migrate-to-cloud/onprem/Page.tsx @@ -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 = [ + '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 ( <> - {runMigrationResult.isError && ( + {createSnapshotResult.isError && ( { } /> + + + + )} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 92d54e9c3ae..fdcdb1efc8f 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 9a3df4cb747..2aee9750ec0 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -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ę", diff --git a/scripts/generate-rtk-apis.ts b/scripts/generate-rtk-apis.ts index af419895beb..72724c99a84 100644 --- a/scripts/generate-rtk-apis.ts +++ b/scripts/generate-rtk-apis.ts @@ -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', ], },