E2C: Empty and Loading snapshot states (#90043)

* E2C: Empty and Loading snapshot states

* fix responsive
This commit is contained in:
Josh Hunt 2024-07-05 12:32:45 +01:00 committed by GitHub
parent e9ebb6eaa4
commit 1b3597d795
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 309 additions and 64 deletions

View File

@ -0,0 +1,27 @@
import { ReactNode } from 'react';
import { Stack, Box, Text } from '@grafana/ui';
interface CTAInfoProps {
title: NonNullable<ReactNode>;
accessory?: ReactNode;
children: ReactNode;
}
export function CTAInfo(props: CTAInfoProps) {
const { title, accessory, children } = props;
return (
<Box maxWidth={44} display="flex" direction="row" gap={1} alignItems="flex-start">
{accessory && <Box>{accessory}</Box>}
<Stack gap={2} direction="column" alignItems="flex-start">
<Text element="h3" variant="h5">
{title}
</Text>
{children}
</Stack>
</Box>
);
}

View File

@ -1,19 +1,19 @@
import { ReactNode } from 'react';
import { Stack, Text } from '@grafana/ui';
import { Box, Text } from '@grafana/ui';
interface MigrationInfoProps {
title: NonNullable<ReactNode>;
value: NonNullable<ReactNode>;
children: NonNullable<ReactNode>;
}
export function MigrationInfo({ title, value }: MigrationInfoProps) {
export function MigrationInfo({ title, children }: MigrationInfoProps) {
return (
<Stack direction="column">
<Box minWidth={{ xs: 0, xxl: 16 }} display="flex" direction="column">
<Text variant="bodySmall" color="secondary">
{title}
</Text>
<Text variant="h4">{value}</Text>
</Stack>
<Text variant="h4">{children}</Text>
</Box>
);
}

View File

@ -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 (
<Box
borderColor="weak"
borderStyle="solid"
padding={2}
display="flex"
gap={4}
alignItems="center"
justifyContent="space-between"
>
<Stack gap={4} wrap="wrap">
<MigrationInfo title={t('migrate-to-cloud.summary.snapshot-date', 'Snapshot timestamp')}>
{snapshot?.created ? (
snapshot?.created
) : (
<Text color="secondary">
<Trans i18nKey="migrate-to-cloud.summary.snapshot-not-created">Not yet created</Trans>
</Text>
)}
</MigrationInfo>
<MigrationInfo title={t('migrate-to-cloud.summary.total-resource-count', 'Total resources')}>
{totalCount}
</MigrationInfo>
<MigrationInfo title={t('migrate-to-cloud.summary.errored-resource-count', 'Errors')}>
{errorCount}
</MigrationInfo>
<MigrationInfo title={t('migrate-to-cloud.summary.successful-resource-count', 'Successfully migrated')}>
{successCount}
</MigrationInfo>
<MigrationInfo title={t('migrate-to-cloud.summary.target-stack-title', 'Uploading to')}>
{session.slug}
<Space h={1} layout="inline" />
<Button
disabled={isBusy}
onClick={onDisconnect}
variant="secondary"
size="sm"
icon={disconnectIsLoading ? 'spinner' : undefined}
>
<Trans i18nKey="migrate-to-cloud.summary.disconnect">Disconnect</Trans>
</Button>
</MigrationInfo>
</Stack>
{showBuildSnapshot && (
<Button disabled={isBusy} onClick={onBuildSnapshot} icon={buildSnapshotIsLoading ? 'spinner' : undefined}>
<Trans i18nKey="migrate-to-cloud.summary.start-migration">Build snapshot</Trans>
</Button>
)}
{showUploadSnapshot && (
<Button
disabled={isBusy || uploadSnapshotIsLoading}
onClick={onUploadSnapshot}
icon={uploadSnapshotIsLoading ? 'spinner' : undefined}
>
<Trans i18nKey="migrate-to-cloud.summary.upload-migration">Upload snapshot</Trans>
</Button>
)}
</Box>
);
}

View File

@ -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<SnapshotDto['status']> = [
'PROCESSING',
];
const SNAPSHOT_BUILDING_STATUSES: Array<SnapshotDto['status']> = ['INITIALIZING', 'CREATING'];
const SNAPSHOT_UPLOADING_STATUSES: Array<SnapshotDto['status']> = ['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 <div>Loading...</div>;
} else if (!migrationMeta) {
} else if (!session.data) {
return <EmptyState />;
}
return (
<>
<Stack direction="column" gap={4}>
{/* TODO: show errors from all mutation's in a... modal? */}
{createSnapshotResult.isError && (
<Alert
severity="error"
@ -162,55 +181,45 @@ export const Page = () => {
</Alert>
)}
{migrationMeta.slug && (
<Box
borderColor="weak"
borderStyle="solid"
padding={2}
display="flex"
gap={4}
alignItems="center"
justifyContent="space-between"
>
<MigrationInfo
title={t('migrate-to-cloud.summary.target-stack-title', 'Uploading to')}
value={
<>
{migrationMeta.slug}{' '}
<Button
disabled={isBusy}
onClick={() => setDisconnectModalOpen(true)}
variant="secondary"
size="sm"
icon={disconnectResult.isLoading ? 'spinner' : undefined}
>
<Trans i18nKey="migrate-to-cloud.summary.disconnect">Disconnect</Trans>
</Button>
</>
}
/>
{session.data && (
<MigrationSummary
session={session.data}
snapshot={snapshot.data}
isBusy={isBusy}
disconnectIsLoading={disconnectResult.isLoading}
onDisconnect={handleDisconnect}
showBuildSnapshot={showBuildSnapshot}
buildSnapshotIsLoading={createSnapshotResult.isLoading}
onBuildSnapshot={handleCreateSnapshot}
showUploadSnapshot={showUploadSnapshot}
uploadSnapshotIsLoading={uploadSnapshotResult.isLoading || SNAPSHOT_UPLOADING_STATUSES.includes(status)}
onUploadSnapshot={handleUploadSnapshot}
/>
)}
<MigrationInfo title="Status" value={snapshot?.data?.status ?? 'no snapshot yet'} />
{(showBuildSnapshot || showBuildingSnapshot) && (
<Box display="flex" justifyContent="center" paddingY={10}>
{showBuildSnapshot && (
<BuildSnapshotCTA
disabled={isBusy}
isLoading={createSnapshotResult.isLoading}
onClick={handleCreateSnapshot}
/>
)}
<Button
disabled={isBusy || Boolean(snapshot.data)}
onClick={handleCreateSnapshot}
icon={createSnapshotResult.isLoading ? 'spinner' : undefined}
>
<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>
{showBuildingSnapshot && (
<CreatingSnapshotCTA
disabled={isBusy}
isLoading={cancelSnapshotResult.isLoading}
onClick={handleCancelSnapshot}
/>
)}
</Box>
)}
{resources && <ResourcesTable resources={resources} />}
{snapshot.data?.results && snapshot.data.results.length > 0 && (
<ResourcesTable resources={snapshot.data.results} />
)}
</Stack>
<DisconnectModal

View File

@ -0,0 +1,67 @@
import { Button, Icon, Spinner, Text } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { CTAInfo } from './CTAInfo';
interface SnapshotCTAProps {
disabled: boolean;
isLoading: boolean;
onClick: () => void;
}
export function BuildSnapshotCTA(props: SnapshotCTAProps) {
const { disabled, isLoading, onClick } = props;
return (
<CTAInfo
title={t('migrate-to-cloud.build-snapshot.title', 'No snapshot exists')}
accessory={<Icon name="cog" size="lg" />}
>
<Text element="p" variant="body" color="secondary">
<Trans i18nKey="migrate-to-cloud.build-snapshot.description">
This tool can migrate some resources from this installation to your cloud stack. To get started, you&apos;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.
</Trans>
</Text>
<Text element="p" variant="body" color="secondary">
<Trans i18nKey="migrate-to-cloud.build-snapshot.when-complete">
Once the snapshot is complete, you will be able to upload it to your cloud stack.
</Trans>
</Text>
<Button disabled={disabled} onClick={onClick} icon={isLoading ? 'spinner' : undefined}>
<Trans i18nKey="migrate-to-cloud.summary.start-migration">Build snapshot</Trans>
</Button>
</CTAInfo>
);
}
export function CreatingSnapshotCTA(props: SnapshotCTAProps) {
const { disabled, isLoading, onClick } = props;
return (
<CTAInfo
title={t('migrate-to-cloud.building-snapshot.title', 'Building installation snapshot')}
accessory={<Spinner inline />}
>
<Text element="p" variant="body" color="secondary">
<Trans i18nKey="migrate-to-cloud.building-snapshot.description">
We&apos;re creating a point-in-time snapshot of the current state of this installation. Once the snapshot is
complete. you&apos;ll be able to upload it to Grafana Cloud.
</Trans>
</Text>
<Text element="p" variant="body" color="secondary">
<Trans i18nKey="migrate-to-cloud.building-snapshot.description-eta">
Creating a snapshot typically takes less than two minutes.
</Trans>
</Text>
<Button disabled={disabled} onClick={onClick} icon={isLoading ? 'spinner' : undefined} variant="secondary">
<Trans i18nKey="migrate-to-cloud.summary.cancel-snapshot">Cancel snapshot</Trans>
</Button>
</CTAInfo>
);
}

View File

@ -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",

View File

@ -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ę",