From 1b3597d795c8a06b7a8d8942fbe388e865c53571 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 5 Jul 2024 12:32:45 +0100 Subject: [PATCH] E2C: Empty and Loading snapshot states (#90043) * E2C: Empty and Loading snapshot states * fix responsive --- .../migrate-to-cloud/onprem/CTAInfo.tsx | 27 ++++ .../migrate-to-cloud/onprem/MigrationInfo.tsx | 12 +- .../onprem/MigrationSummary.tsx | 110 ++++++++++++++++ .../features/migrate-to-cloud/onprem/Page.tsx | 121 ++++++++++-------- .../migrate-to-cloud/onprem/SnapshotCTAs.tsx | 67 ++++++++++ public/locales/en-US/grafana.json | 18 ++- public/locales/pseudo-LOCALE/grafana.json | 18 ++- 7 files changed, 309 insertions(+), 64 deletions(-) create mode 100644 public/app/features/migrate-to-cloud/onprem/CTAInfo.tsx create mode 100644 public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx create mode 100644 public/app/features/migrate-to-cloud/onprem/SnapshotCTAs.tsx diff --git a/public/app/features/migrate-to-cloud/onprem/CTAInfo.tsx b/public/app/features/migrate-to-cloud/onprem/CTAInfo.tsx new file mode 100644 index 00000000000..eed3befa65d --- /dev/null +++ b/public/app/features/migrate-to-cloud/onprem/CTAInfo.tsx @@ -0,0 +1,27 @@ +import { ReactNode } from 'react'; + +import { Stack, Box, Text } from '@grafana/ui'; + +interface CTAInfoProps { + title: NonNullable; + accessory?: ReactNode; + children: ReactNode; +} + +export function CTAInfo(props: CTAInfoProps) { + const { title, accessory, children } = props; + + return ( + + {accessory && {accessory}} + + + + {title} + + + {children} + + + ); +} diff --git a/public/app/features/migrate-to-cloud/onprem/MigrationInfo.tsx b/public/app/features/migrate-to-cloud/onprem/MigrationInfo.tsx index 59062c4d39f..9aa974e78f3 100644 --- a/public/app/features/migrate-to-cloud/onprem/MigrationInfo.tsx +++ b/public/app/features/migrate-to-cloud/onprem/MigrationInfo.tsx @@ -1,19 +1,19 @@ import { ReactNode } from 'react'; -import { Stack, Text } from '@grafana/ui'; +import { Box, Text } from '@grafana/ui'; interface MigrationInfoProps { title: NonNullable; - value: NonNullable; + children: NonNullable; } -export function MigrationInfo({ title, value }: MigrationInfoProps) { +export function MigrationInfo({ title, children }: MigrationInfoProps) { return ( - + {title} - {value} - + {children} + ); } diff --git a/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx b/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx new file mode 100644 index 00000000000..f27383c62ec --- /dev/null +++ b/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx @@ -0,0 +1,110 @@ +import { Box, Button, Space, Stack, Text } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; + +import { GetSessionApiResponse, GetSnapshotResponseDto } from '../api'; + +import { MigrationInfo } from './MigrationInfo'; + +interface MigrationSummaryProps { + snapshot: GetSnapshotResponseDto | undefined; + session: GetSessionApiResponse; + isBusy: boolean; + + disconnectIsLoading: boolean; + onDisconnect: () => void; + + showBuildSnapshot: boolean; + buildSnapshotIsLoading: boolean; + onBuildSnapshot: () => void; + + showUploadSnapshot: boolean; + uploadSnapshotIsLoading: boolean; + onUploadSnapshot: () => void; +} + +export function MigrationSummary(props: MigrationSummaryProps) { + const { + session, + snapshot, + isBusy, + disconnectIsLoading, + onDisconnect, + showBuildSnapshot, + buildSnapshotIsLoading, + onBuildSnapshot, + + showUploadSnapshot, + uploadSnapshotIsLoading, + onUploadSnapshot, + } = props; + + const totalCount = 0; + const errorCount = 0; + const successCount = 0; + + return ( + + + + {snapshot?.created ? ( + snapshot?.created + ) : ( + + Not yet created + + )} + + + + {totalCount} + + + + {errorCount} + + + + {successCount} + + + + {session.slug} + + + + + + {showBuildSnapshot && ( + + )} + + {showUploadSnapshot && ( + + )} + + ); +} diff --git a/public/app/features/migrate-to-cloud/onprem/Page.tsx b/public/app/features/migrate-to-cloud/onprem/Page.tsx index 91b59f801a4..b991818abff 100644 --- a/public/app/features/migrate-to-cloud/onprem/Page.tsx +++ b/public/app/features/migrate-to-cloud/onprem/Page.tsx @@ -1,11 +1,12 @@ import { skipToken } from '@reduxjs/toolkit/query/react'; import { useCallback, useEffect, useState } from 'react'; -import { Alert, Box, Button, Stack } from '@grafana/ui'; +import { Alert, Box, Stack } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; import { SnapshotDto, + useCancelSnapshotMutation, useCreateSnapshotMutation, useDeleteSessionMutation, useGetSessionListQuery, @@ -16,22 +17,25 @@ import { import { DisconnectModal } from './DisconnectModal'; import { EmptyState } from './EmptyState/EmptyState'; -import { MigrationInfo } from './MigrationInfo'; +import { MigrationSummary } from './MigrationSummary'; import { ResourcesTable } from './ResourcesTable'; +import { BuildSnapshotCTA, CreatingSnapshotCTA } from './SnapshotCTAs'; /** * Here's how migrations work: * - * A single on-prem instance can be configured to be migrated to multiple cloud instances. - * - GetMigrationList returns this the list of migration targets for the on prem instance - * - If GetMigrationList returns an empty list, then the empty state with a prompt to enter a token should be shown + * A single on-prem instance can be configured to be migrated to multiple cloud instances. We call these 'sessions'. + * - GetSessionList returns this the list of migration targets for the on prem instance + * - If GetMigrationList returns an empty list, then an empty state to prompt for token should be shown * - The UI (at the moment) only shows the most recently created migration target (the last one returned from the API) * and doesn't allow for others to be created * - * A single on-prem migration 'target' (CloudMigrationResponse) can have multiple migration runs (CloudMigrationRun) - * - To list the migration resources: - * 1. call GetCloudMigratiopnRunList to list all runs - * 2. call GetCloudMigrationRun with the ID from first step to list the result of that migration + * A single on-prem migration 'target' (CloudMigrationSession) can have multiple snapshots. + * A snapshot represents a copy of all migratable resources at a fixed point in time. + * A snapshots are created asynchronously in the background, so GetSnapshot must be polled to get the current status. + * + * After a snapshot has been created, it will be PENDING_UPLOAD. UploadSnapshot is then called which asynchronously + * uploads and migrates the snapshot to the cloud instance. */ function useGetLatestSession() { @@ -52,6 +56,10 @@ const SHOULD_POLL_STATUSES: Array = [ 'PROCESSING', ]; +const SNAPSHOT_BUILDING_STATUSES: Array = ['INITIALIZING', 'CREATING']; + +const SNAPSHOT_UPLOADING_STATUSES: Array = ['UPLOADING', 'PENDING_PROCESSING', 'PROCESSING']; + const STATUS_POLL_INTERVAL = 5 * 1000; function useGetLatestSnapshot(sessionUid?: string) { @@ -91,23 +99,27 @@ export const Page = () => { const snapshot = useGetLatestSnapshot(session.data?.uid); const [performCreateSnapshot, createSnapshotResult] = useCreateSnapshotMutation(); const [performUploadSnapshot, uploadSnapshotResult] = useUploadSnapshotMutation(); + const [performCancelSnapshot, cancelSnapshotResult] = useCancelSnapshotMutation(); const [performDisconnect, disconnectResult] = useDeleteSessionMutation(); const sessionUid = session.data?.uid; const snapshotUid = snapshot.data?.uid; - const migrationMeta = session.data; const isInitialLoading = session.isLoading; + const status = snapshot.data?.status; // isBusy is not a loading state, but indicates that the system is doing *something* // and all buttons should be disabled const isBusy = createSnapshotResult.isLoading || uploadSnapshotResult.isLoading || + cancelSnapshotResult.isLoading || session.isLoading || snapshot.isLoading || disconnectResult.isLoading; - const resources = snapshot.data?.results; + const showBuildSnapshot = !snapshot.isLoading && !snapshot.data; + const showBuildingSnapshot = SNAPSHOT_BUILDING_STATUSES.includes(status); + const showUploadSnapshot = status === 'PENDING_UPLOAD' || SNAPSHOT_UPLOADING_STATUSES.includes(status); const handleDisconnect = useCallback(async () => { if (sessionUid) { @@ -127,16 +139,23 @@ export const Page = () => { } }, [performUploadSnapshot, sessionUid, snapshotUid]); + const handleCancelSnapshot = useCallback(() => { + if (sessionUid && snapshotUid) { + performCancelSnapshot({ uid: sessionUid, snapshotUid: snapshotUid }); + } + }, [performCancelSnapshot, sessionUid, snapshotUid]); + if (isInitialLoading) { // TODO: better loading state return
Loading...
; - } else if (!migrationMeta) { + } else if (!session.data) { return ; } return ( <> + {/* TODO: show errors from all mutation's in a... modal? */} {createSnapshotResult.isError && ( { )} - {migrationMeta.slug && ( - - - {migrationMeta.slug}{' '} - - - } - /> + {session.data && ( + + )} - + {(showBuildSnapshot || showBuildingSnapshot) && ( + + {showBuildSnapshot && ( + + )} - - - + {showBuildingSnapshot && ( + + )} )} - {resources && } + {snapshot.data?.results && snapshot.data.results.length > 0 && ( + + )} void; +} + +export function BuildSnapshotCTA(props: SnapshotCTAProps) { + const { disabled, isLoading, onClick } = props; + + return ( + } + > + + + This tool can migrate some resources from this installation to your cloud stack. To get started, you'll + need to create a snapshot of this installation. Creating a snapshot typically takes less than two minutes. The + snapshot is stored alongside this Grafana installation. + + + + + + Once the snapshot is complete, you will be able to upload it to your cloud stack. + + + + + + ); +} + +export function CreatingSnapshotCTA(props: SnapshotCTAProps) { + const { disabled, isLoading, onClick } = props; + + return ( + } + > + + + We're creating a point-in-time snapshot of the current state of this installation. Once the snapshot is + complete. you'll be able to upload it to Grafana Cloud. + + + + + + Creating a snapshot typically takes less than two minutes. + + + + + + ); +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a5457b151f3..a9f224fae5b 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -901,6 +901,16 @@ } }, "migrate-to-cloud": { + "build-snapshot": { + "description": "This tool can migrate some resources from this installation to your cloud stack. To get started, you'll need to create a snapshot of this installation. Creating a snapshot typically takes less than two minutes. The snapshot is stored alongside this Grafana installation.", + "title": "No snapshot exists", + "when-complete": "Once the snapshot is complete, you will be able to upload it to your cloud stack." + }, + "building-snapshot": { + "description": "We're creating a point-in-time snapshot of the current state of this installation. Once the snapshot is complete. you'll be able to upload it to Grafana Cloud.", + "description-eta": "Creating a snapshot typically takes less than two minutes.", + "title": "Building installation snapshot" + }, "can-i-move": { "body": "Once you connect this installation to a cloud stack, you'll be able to upload data sources and dashboards.", "link-title": "Learn about migrating other settings", @@ -1003,14 +1013,20 @@ "unknown": "Unknown" }, "summary": { + "cancel-snapshot": "Cancel snapshot", "disconnect": "Disconnect", "disconnect-error-description": "See the Grafana server logs for more details", "disconnect-error-title": "There was an error disconnecting", + "errored-resource-count": "Errors", "run-migration-error-description": "See the Grafana server logs for more details", "run-migration-error-title": "There was an error migrating your resources", + "snapshot-date": "Snapshot timestamp", + "snapshot-not-created": "Not yet created", "start-migration": "Build snapshot", + "successful-resource-count": "Successfully migrated", "target-stack-title": "Uploading to", - "upload-migration": "Upload & migrate snapshot" + "total-resource-count": "Total resources", + "upload-migration": "Upload 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 a1e078eaaa6..480a53ebd9b 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -901,6 +901,16 @@ } }, "migrate-to-cloud": { + "build-snapshot": { + "description": "Ŧĥįş ŧőőľ čäʼn mįģřäŧę şőmę řęşőūřčęş ƒřőm ŧĥįş įʼnşŧäľľäŧįőʼn ŧő yőūř čľőūđ şŧäčĸ. Ŧő ģęŧ şŧäřŧęđ, yőū'ľľ ʼnęęđ ŧő čřęäŧę ä şʼnäpşĥőŧ őƒ ŧĥįş įʼnşŧäľľäŧįőʼn. Cřęäŧįʼnģ ä şʼnäpşĥőŧ ŧypįčäľľy ŧäĸęş ľęşş ŧĥäʼn ŧŵő mįʼnūŧęş. Ŧĥę şʼnäpşĥőŧ įş şŧőřęđ äľőʼnģşįđę ŧĥįş Ğřäƒäʼnä įʼnşŧäľľäŧįőʼn.", + "title": "Ńő şʼnäpşĥőŧ ęχįşŧş", + "when-complete": "Øʼnčę ŧĥę şʼnäpşĥőŧ įş čőmpľęŧę, yőū ŵįľľ þę äþľę ŧő ūpľőäđ įŧ ŧő yőūř čľőūđ şŧäčĸ." + }, + "building-snapshot": { + "description": "Ŵę'řę čřęäŧįʼnģ ä pőįʼnŧ-įʼn-ŧįmę şʼnäpşĥőŧ őƒ ŧĥę čūřřęʼnŧ şŧäŧę őƒ ŧĥįş įʼnşŧäľľäŧįőʼn. Øʼnčę ŧĥę şʼnäpşĥőŧ įş čőmpľęŧę. yőū'ľľ þę äþľę ŧő ūpľőäđ įŧ ŧő Ğřäƒäʼnä Cľőūđ.", + "description-eta": "Cřęäŧįʼnģ ä şʼnäpşĥőŧ ŧypįčäľľy ŧäĸęş ľęşş ŧĥäʼn ŧŵő mįʼnūŧęş.", + "title": "ßūįľđįʼnģ įʼnşŧäľľäŧįőʼn şʼnäpşĥőŧ" + }, "can-i-move": { "body": "Øʼnčę yőū čőʼnʼnęčŧ ŧĥįş įʼnşŧäľľäŧįőʼn ŧő ä čľőūđ şŧäčĸ, yőū'ľľ þę äþľę ŧő ūpľőäđ đäŧä şőūřčęş äʼnđ đäşĥþőäřđş.", "link-title": "Ŀęäřʼn äþőūŧ mįģřäŧįʼnģ őŧĥęř şęŧŧįʼnģş", @@ -1003,14 +1013,20 @@ "unknown": "Ůʼnĸʼnőŵʼn" }, "summary": { + "cancel-snapshot": "Cäʼnčęľ şʼnäpşĥőŧ", "disconnect": "Đįşčőʼnʼnęčŧ", "disconnect-error-description": "Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş", "disconnect-error-title": "Ŧĥęřę ŵäş äʼn ęřřőř đįşčőʼnʼnęčŧįʼnģ", + "errored-resource-count": "Ēřřőřş", "run-migration-error-description": "Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş", "run-migration-error-title": "Ŧĥęřę ŵäş äʼn ęřřőř mįģřäŧįʼnģ yőūř řęşőūřčęş", + "snapshot-date": "Ŝʼnäpşĥőŧ ŧįmęşŧämp", + "snapshot-not-created": "Ńőŧ yęŧ čřęäŧęđ", "start-migration": "ßūįľđ şʼnäpşĥőŧ", + "successful-resource-count": "Ŝūččęşşƒūľľy mįģřäŧęđ", "target-stack-title": "Ůpľőäđįʼnģ ŧő", - "upload-migration": "Ůpľőäđ & mįģřäŧę şʼnäpşĥőŧ" + "total-resource-count": "Ŧőŧäľ řęşőūřčęş", + "upload-migration": "Ůpľőäđ şʼnäpşĥőŧ" }, "token-status": { "active": "Ŧőĸęʼn čřęäŧęđ äʼnđ äčŧįvę",