mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
E2C: Start connecting on-prem to real apis (#85527)
* E2C: Start connecting on-prem to real apis * actually run the migration * show resources * basic dashboards resources * show dashboard title * remove console logs * cleanup 1 * i18n * disconnect * i18n * restore type * use fixed format * fix
This commit is contained in:
parent
0ec48cfdbd
commit
aad79c9400
@ -175,7 +175,8 @@ type MigrateDataResponseDTO struct {
|
||||
}
|
||||
|
||||
type MigrateDataResponseItemDTO struct {
|
||||
RefID string `json:"refId"`
|
||||
Status ItemStatus `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Type MigrateDataType `json:"type"`
|
||||
RefID string `json:"refId"`
|
||||
Status ItemStatus `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
@ -15963,9 +15963,15 @@
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/ItemStatus"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/MigrateDataType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MigrateDataType": {
|
||||
"type": "string"
|
||||
},
|
||||
"MoveFolderCommand": {
|
||||
"description": "MoveFolderCommand captures the information required by the folder service\nto move a folder.",
|
||||
"type": "object",
|
||||
|
@ -6,6 +6,9 @@ import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
|
||||
interface RequestOptions extends BackendSrvRequest {
|
||||
manageError?: (err: unknown) => { error: unknown };
|
||||
showErrorAlert?: boolean;
|
||||
|
||||
// rtk codegen sets this
|
||||
body?: BackendSrvRequest['data'];
|
||||
}
|
||||
|
||||
function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn<RequestOptions> {
|
||||
@ -16,6 +19,7 @@ function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryF
|
||||
...requestOptions,
|
||||
url: baseURL + requestOptions.url,
|
||||
showErrorAlert: requestOptions.showErrorAlert,
|
||||
data: requestOptions.body,
|
||||
})
|
||||
);
|
||||
return { data: responseData, meta };
|
||||
|
@ -25,10 +25,13 @@ const injectedRtkApi = api.injectEndpoints({
|
||||
createCloudMigrationToken: build.mutation<CreateCloudMigrationTokenApiResponse, CreateCloudMigrationTokenApiArg>({
|
||||
query: () => ({ url: `/cloudmigration/token`, method: 'POST' }),
|
||||
}),
|
||||
getDashboardByUid: build.query<GetDashboardByUidApiResponse, GetDashboardByUidApiArg>({
|
||||
query: (queryArg) => ({ url: `/dashboards/uid/${queryArg.uid}` }),
|
||||
}),
|
||||
}),
|
||||
overrideExisting: false,
|
||||
});
|
||||
export { injectedRtkApi as enhancedApi };
|
||||
export { injectedRtkApi as generatedAPI };
|
||||
export type GetMigrationListApiResponse = /** status 200 (empty) */ CloudMigrationListResponse;
|
||||
export type GetMigrationListApiArg = void;
|
||||
export type CreateMigrationApiResponse = /** status 200 (empty) */ CloudMigrationResponse;
|
||||
@ -64,6 +67,10 @@ export type GetCloudMigrationRunApiArg = {
|
||||
};
|
||||
export type CreateCloudMigrationTokenApiResponse = /** status 200 (empty) */ CreateAccessTokenResponseDto;
|
||||
export type CreateCloudMigrationTokenApiArg = void;
|
||||
export type GetDashboardByUidApiResponse = /** status 200 (empty) */ DashboardFullWithMeta;
|
||||
export type GetDashboardByUidApiArg = {
|
||||
uid: string;
|
||||
};
|
||||
export type CloudMigrationResponse = {
|
||||
created?: string;
|
||||
id?: number;
|
||||
@ -87,10 +94,12 @@ export type CloudMigrationRequest = {
|
||||
authToken?: string;
|
||||
};
|
||||
export type ItemStatus = string;
|
||||
export type MigrateDataType = string;
|
||||
export type MigrateDataResponseItemDto = {
|
||||
error?: string;
|
||||
refId?: string;
|
||||
status?: ItemStatus;
|
||||
type?: MigrateDataType;
|
||||
};
|
||||
export type MigrateDataResponseDto = {
|
||||
id?: number;
|
||||
@ -102,6 +111,50 @@ export type CloudMigrationRunList = {
|
||||
export type CreateAccessTokenResponseDto = {
|
||||
token?: string;
|
||||
};
|
||||
export type Json = object;
|
||||
export type AnnotationActions = {
|
||||
canAdd?: boolean;
|
||||
canDelete?: boolean;
|
||||
canEdit?: boolean;
|
||||
};
|
||||
export type AnnotationPermission = {
|
||||
dashboard?: AnnotationActions;
|
||||
organization?: AnnotationActions;
|
||||
};
|
||||
export type DashboardMeta = {
|
||||
annotationsPermissions?: AnnotationPermission;
|
||||
canAdmin?: boolean;
|
||||
canDelete?: boolean;
|
||||
canEdit?: boolean;
|
||||
canSave?: boolean;
|
||||
canStar?: boolean;
|
||||
created?: string;
|
||||
createdBy?: string;
|
||||
expires?: string;
|
||||
/** Deprecated: use FolderUID instead */
|
||||
folderId?: number;
|
||||
folderTitle?: string;
|
||||
folderUid?: string;
|
||||
folderUrl?: string;
|
||||
hasAcl?: boolean;
|
||||
isFolder?: boolean;
|
||||
isSnapshot?: boolean;
|
||||
isStarred?: boolean;
|
||||
provisioned?: boolean;
|
||||
provisionedExternalId?: string;
|
||||
publicDashboardEnabled?: boolean;
|
||||
publicDashboardUid?: string;
|
||||
slug?: string;
|
||||
type?: string;
|
||||
updated?: string;
|
||||
updatedBy?: string;
|
||||
url?: string;
|
||||
version?: number;
|
||||
};
|
||||
export type DashboardFullWithMeta = {
|
||||
dashboard?: Json;
|
||||
meta?: DashboardMeta;
|
||||
};
|
||||
export const {
|
||||
useGetMigrationListQuery,
|
||||
useCreateMigrationMutation,
|
||||
@ -111,4 +164,5 @@ export const {
|
||||
useRunCloudMigrationMutation,
|
||||
useGetCloudMigrationRunQuery,
|
||||
useCreateCloudMigrationTokenMutation,
|
||||
useGetDashboardByUidQuery,
|
||||
} = injectedRtkApi;
|
||||
|
@ -1,2 +1,39 @@
|
||||
export * from './endpoints.gen';
|
||||
export { enhancedApi as cloudMigrationAPI } from './endpoints.gen';
|
||||
import { generatedAPI } from './endpoints.gen';
|
||||
|
||||
export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({
|
||||
addTagTypes: ['cloud-migration-config', 'cloud-migration-run'],
|
||||
endpoints: {
|
||||
// List Cloud Configs
|
||||
getMigrationList: {
|
||||
providesTags: ['cloud-migration-config'] /* should this be a -list? */,
|
||||
},
|
||||
|
||||
// Create Cloud Config
|
||||
createMigration: {
|
||||
invalidatesTags: ['cloud-migration-config'],
|
||||
},
|
||||
|
||||
// Get one Cloud Config
|
||||
getCloudMigration: {
|
||||
providesTags: ['cloud-migration-config'],
|
||||
},
|
||||
|
||||
// Delete one Cloud Config
|
||||
deleteCloudMigration: {
|
||||
invalidatesTags: ['cloud-migration-config'],
|
||||
},
|
||||
|
||||
getCloudMigrationRunList: {
|
||||
providesTags: ['cloud-migration-run'] /* should this be a -list? */,
|
||||
},
|
||||
|
||||
getCloudMigrationRun: {
|
||||
providesTags: ['cloud-migration-run'],
|
||||
},
|
||||
|
||||
runCloudMigration: {
|
||||
invalidatesTags: ['cloud-migration-run'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -41,11 +41,13 @@ export interface ConnectStackDTOMock {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface MigrationResourceDTOMock {
|
||||
type MigrationResourceStatus = 'not-migrated' | 'migrated' | 'migrating' | 'failed';
|
||||
|
||||
export interface MigrationResourceDatasource {
|
||||
uid: string;
|
||||
status: 'not-migrated' | 'migrated' | 'migrating' | 'failed';
|
||||
status: MigrationResourceStatus;
|
||||
statusMessage?: string;
|
||||
type: 'datasource' | 'dashboard'; // TODO: in future this would be a discriminated union with the resource details
|
||||
type: 'datasource';
|
||||
resource: {
|
||||
uid: string;
|
||||
name: string;
|
||||
@ -54,6 +56,19 @@ export interface MigrationResourceDTOMock {
|
||||
};
|
||||
}
|
||||
|
||||
export interface MigrationResourceDashboard {
|
||||
uid: string;
|
||||
status: MigrationResourceStatus;
|
||||
statusMessage?: string;
|
||||
type: 'dashboard';
|
||||
resource: {
|
||||
uid: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type MigrationResourceDTOMock = MigrationResourceDatasource | MigrationResourceDashboard;
|
||||
|
||||
const mockApplications = ['auth-service', 'web server', 'backend'];
|
||||
const mockEnvs = ['DEV', 'PROD'];
|
||||
const mockRoles = ['db', 'load-balancer', 'server', 'logs'];
|
||||
|
@ -3,13 +3,12 @@ import React from 'react';
|
||||
import { Box, Button, ModalsController, Text } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { useConnectStackMutationMock, useGetStatusQueryMock } from '../../../mockAPI';
|
||||
import { useCreateMigrationMutation } from '../../../api';
|
||||
|
||||
import { ConnectModal } from './ConnectModal';
|
||||
|
||||
export const CallToAction = () => {
|
||||
const [connectStack, connectResponse] = useConnectStackMutationMock();
|
||||
const { isFetching } = useGetStatusQueryMock();
|
||||
const [createMigration, createMigrationResponse] = useCreateMigrationMutation();
|
||||
|
||||
return (
|
||||
<ModalsController>
|
||||
@ -19,11 +18,11 @@ export const CallToAction = () => {
|
||||
<Trans i18nKey="migrate-to-cloud.cta.header">Let us manage your Grafana stack</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
disabled={isFetching || connectResponse.isLoading}
|
||||
disabled={createMigrationResponse.isLoading}
|
||||
onClick={() =>
|
||||
showModal(ConnectModal, {
|
||||
hideModal,
|
||||
onConfirm: connectStack,
|
||||
onConfirm: createMigration,
|
||||
})
|
||||
}
|
||||
>
|
||||
|
@ -6,16 +6,19 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Modal, Button, Stack, TextLink, Field, Input, Text, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { ConnectStackDTOMock } from '../../../mockAPI';
|
||||
import { CreateMigrationApiArg } from '../../../api';
|
||||
|
||||
interface Props {
|
||||
hideModal: () => void;
|
||||
onConfirm: (connectStackData: ConnectStackDTOMock) => Promise<{ data: void } | { error: unknown }>;
|
||||
onConfirm: (connectStackData: CreateMigrationApiArg) => Promise<unknown>;
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const cloudStackId = useId();
|
||||
const tokenId = useId();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
@ -24,19 +27,22 @@ export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
|
||||
register,
|
||||
formState: { errors },
|
||||
watch,
|
||||
} = useForm<ConnectStackDTOMock>({
|
||||
} = useForm<FormData>({
|
||||
defaultValues: {
|
||||
stackURL: '',
|
||||
token: '',
|
||||
},
|
||||
});
|
||||
|
||||
const stackURL = watch('stackURL');
|
||||
const token = watch('token');
|
||||
|
||||
const onConfirmConnect: SubmitHandler<ConnectStackDTOMock> = async (formData) => {
|
||||
const onConfirmConnect: SubmitHandler<FormData> = async (formData) => {
|
||||
setIsConnecting(true);
|
||||
await onConfirm(formData);
|
||||
// TODO: location of this is kinda weird, making it tricky to handle errors from this.
|
||||
await onConfirm({
|
||||
cloudMigrationRequest: {
|
||||
authToken: formData.token,
|
||||
},
|
||||
});
|
||||
setIsConnecting(false);
|
||||
hideModal();
|
||||
};
|
||||
@ -49,46 +55,34 @@ export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.body-get-started">
|
||||
To get started, you'll need a Grafana.com account.
|
||||
</Trans>
|
||||
|
||||
<TextLink href="https://grafana.com/auth/sign-up/create-user?pg=prod-cloud" external>
|
||||
{t('migrate-to-cloud.connect-modal.body-sign-up', 'Sign up for a Grafana.com account')}
|
||||
</TextLink>
|
||||
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.body-cloud-stack">
|
||||
You'll also need a cloud stack. If you just signed up, we'll automatically create your first
|
||||
stack. If you have an account, you'll need to select or create a stack.
|
||||
</Trans>
|
||||
|
||||
<TextLink href="https://grafana.com/auth/sign-in/" external>
|
||||
{t('migrate-to-cloud.connect-modal.body-view-stacks', 'View my cloud stacks')}
|
||||
</TextLink>
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.body-paste-stack">
|
||||
Once you've decided on a stack, paste the URL below.
|
||||
</Trans>
|
||||
<Field
|
||||
className={styles.field}
|
||||
invalid={!!errors.stackURL}
|
||||
error={errors.stackURL?.message}
|
||||
label={t('migrate-to-cloud.connect-modal.body-url-field', 'Cloud stack URL')}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
{...register('stackURL', {
|
||||
required: t('migrate-to-cloud.connect-modal.stack-required-error', 'Stack URL is required'),
|
||||
})}
|
||||
id={cloudStackId}
|
||||
placeholder="https://example.grafana.net/"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<span>
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.body-token">
|
||||
Your self-managed Grafana installation needs special access to securely migrate content. You'll
|
||||
need to create a migration token on your chosen cloud stack.
|
||||
</Trans>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.body-token-instructions">
|
||||
Log into your cloud stack and navigate to Administration, General, Migrate to Grafana Cloud. Create a
|
||||
migration token on that screen and paste the token here.
|
||||
</Trans>
|
||||
</span>
|
||||
|
||||
<Field
|
||||
className={styles.field}
|
||||
invalid={!!errors.token}
|
||||
@ -110,7 +104,7 @@ export const ConnectModal = ({ hideModal, onConfirm }: Props) => {
|
||||
<Button variant="secondary" onClick={hideModal}>
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.cancel">Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit" disabled={isConnecting || !(stackURL && token)}>
|
||||
<Button type="submit" disabled={isConnecting || !token}>
|
||||
{isConnecting
|
||||
? t('migrate-to-cloud.connect-modal.connecting', 'Connecting to this stack...')
|
||||
: t('migrate-to-cloud.connect-modal.connect', 'Connect to this stack')}
|
||||
|
@ -15,6 +15,7 @@ export const EmptyState = () => {
|
||||
<div className={styles.container}>
|
||||
<Stack direction="column">
|
||||
<CallToAction />
|
||||
|
||||
<Grid
|
||||
alignItems="flex-start"
|
||||
gap={1}
|
||||
|
@ -1,43 +1,139 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query/react';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert, Box, Button, Stack } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { useGetStatusQueryMock, useListResourcesQueryMock, useStartMigrationMutationMock } from '../mockAPI';
|
||||
import {
|
||||
MigrateDataResponseDto,
|
||||
useDeleteCloudMigrationMutation,
|
||||
useGetCloudMigrationRunListQuery,
|
||||
useGetCloudMigrationRunQuery,
|
||||
useGetMigrationListQuery,
|
||||
useRunCloudMigrationMutation,
|
||||
} from '../api';
|
||||
import { MigrationResourceDTOMock } from '../mockAPI';
|
||||
|
||||
import { DisconnectModal } from './DisconnectModal';
|
||||
import { EmptyState } from './EmptyState/EmptyState';
|
||||
import { MigrationInfo } from './MigrationInfo';
|
||||
import { ResourcesTable } from './ResourcesTable';
|
||||
|
||||
export const Page = () => {
|
||||
const { data: status, isFetching } = useGetStatusQueryMock();
|
||||
const { data: resources } = useListResourcesQueryMock(status?.enabled ? undefined : skipToken);
|
||||
const [startMigration, { isLoading: startMigrationIsLoading, isError: startMigrationIsError }] =
|
||||
useStartMigrationMutationMock();
|
||||
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
||||
/**
|
||||
* 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
|
||||
* - 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
|
||||
*/
|
||||
|
||||
if (!status?.enabled) {
|
||||
function useGetLatestMigrationDestination() {
|
||||
const result = useGetMigrationListQuery();
|
||||
const latestMigration = result.data?.migrations?.[0];
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: latestMigration,
|
||||
};
|
||||
}
|
||||
|
||||
function useGetLatestMigrationRun(migrationId?: number) {
|
||||
const listResult = useGetCloudMigrationRunListQuery(migrationId ? { id: migrationId } : skipToken);
|
||||
const latestMigrationRun = listResult.data?.runs?.[0];
|
||||
|
||||
const runResult = useGetCloudMigrationRunQuery(
|
||||
latestMigrationRun?.id && migrationId ? { runId: latestMigrationRun.id, id: migrationId } : skipToken
|
||||
);
|
||||
|
||||
return {
|
||||
...runResult,
|
||||
|
||||
data: runResult.data,
|
||||
|
||||
error: listResult.error || runResult.error,
|
||||
|
||||
isError: listResult.isError || runResult.isError,
|
||||
isLoading: listResult.isLoading || runResult.isLoading,
|
||||
isFetching: listResult.isFetching || runResult.isFetching,
|
||||
};
|
||||
}
|
||||
|
||||
export const Page = () => {
|
||||
const migrationDestination = useGetLatestMigrationDestination();
|
||||
const lastMigrationRun = useGetLatestMigrationRun(migrationDestination.data?.id);
|
||||
const [performRunMigration, runMigrationResult] = useRunCloudMigrationMutation();
|
||||
const [performDisconnect, disconnectResult] = useDeleteCloudMigrationMutation();
|
||||
|
||||
// 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 ||
|
||||
disconnectResult.isLoading;
|
||||
|
||||
const resources = useFixResources(lastMigrationRun.data);
|
||||
|
||||
const handleDisconnect = useCallback(() => {
|
||||
if (migrationDestination.data?.id) {
|
||||
performDisconnect({
|
||||
id: migrationDestination.data.id,
|
||||
});
|
||||
}
|
||||
}, [migrationDestination.data?.id, performDisconnect]);
|
||||
|
||||
const handleStartMigration = useCallback(() => {
|
||||
if (migrationDestination.data?.id) {
|
||||
performRunMigration({ id: migrationDestination.data?.id });
|
||||
}
|
||||
}, [performRunMigration, migrationDestination]);
|
||||
|
||||
const migrationMeta = migrationDestination.data;
|
||||
const isInitialLoading = migrationDestination.isLoading;
|
||||
|
||||
if (isInitialLoading) {
|
||||
// TODO: better loading state
|
||||
return <div>Loading...</div>;
|
||||
} else if (!migrationMeta) {
|
||||
return <EmptyState />;
|
||||
}
|
||||
|
||||
const isBusy = isFetching || isDisconnecting || startMigrationIsLoading;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="column" gap={4}>
|
||||
{startMigrationIsError && (
|
||||
{runMigrationResult.isError && (
|
||||
<Alert
|
||||
severity="error"
|
||||
title={t(
|
||||
'migrate-to-cloud.summary.error-starting-migration',
|
||||
'There was an error starting cloud migration'
|
||||
'migrate-to-cloud.summary.run-migration-error-title',
|
||||
'There was an error migrating your resources'
|
||||
)}
|
||||
/>
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.summary.run-migration-error-description">
|
||||
See the Grafana server logs for more details
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{status.stackURL && (
|
||||
{disconnectResult.isError && (
|
||||
<Alert
|
||||
severity="error"
|
||||
title={t('migrate-to-cloud.summary.disconnect-error-title', 'There was an error disconnecting')}
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.summary.disconnect-error-description">
|
||||
See the Grafana server logs for more details
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{migrationMeta.stack && (
|
||||
<Box
|
||||
borderColor="weak"
|
||||
borderStyle="solid"
|
||||
@ -51,8 +147,14 @@ export const Page = () => {
|
||||
title={t('migrate-to-cloud.summary.target-stack-title', 'Uploading to')}
|
||||
value={
|
||||
<>
|
||||
{status.stackURL}{' '}
|
||||
<Button onClick={() => setIsDisconnecting(true)} disabled={isBusy} variant="secondary" size="sm">
|
||||
{migrationMeta.stack}{' '}
|
||||
<Button
|
||||
disabled={isBusy}
|
||||
onClick={handleDisconnect}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon={disconnectResult.isLoading ? 'spinner' : undefined}
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.summary.disconnect">Disconnect</Trans>
|
||||
</Button>
|
||||
</>
|
||||
@ -61,8 +163,8 @@ export const Page = () => {
|
||||
|
||||
<Button
|
||||
disabled={isBusy}
|
||||
onClick={() => startMigration()}
|
||||
icon={startMigrationIsLoading ? 'spinner' : undefined}
|
||||
onClick={handleStartMigration}
|
||||
icon={runMigrationResult.isLoading ? 'spinner' : undefined}
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.summary.start-migration">Upload everything</Trans>
|
||||
</Button>
|
||||
@ -72,7 +174,65 @@ export const Page = () => {
|
||||
{resources && <ResourcesTable resources={resources} />}
|
||||
</Stack>
|
||||
|
||||
<DisconnectModal isOpen={isDisconnecting} onDismiss={() => setIsDisconnecting(false)} />
|
||||
{/* <DisconnectModal isOpen={isDisconnecting} onDismiss={() => setIsDisconnecting(false)} /> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// converts API status to our expected/mocked status
|
||||
function convertStatus(status: string) {
|
||||
switch (status) {
|
||||
case 'OK':
|
||||
return 'migrated';
|
||||
case 'ERROR':
|
||||
return 'failed';
|
||||
case 'failed':
|
||||
return 'failed';
|
||||
default:
|
||||
return 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
function useFixResources(data: MigrateDataResponseDto | undefined) {
|
||||
return useMemo(() => {
|
||||
if (!data?.items) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const betterResources: MigrationResourceDTOMock[] = data.items.flatMap((item) => {
|
||||
if (item.type === 'DATASOURCE') {
|
||||
const datasourceConfig = Object.values(config.datasources).find((v) => v.uid === item.refId);
|
||||
|
||||
return {
|
||||
type: 'datasource',
|
||||
uid: item.refId ?? '',
|
||||
status: convertStatus(item.status ?? ''),
|
||||
statusMessage: item.error,
|
||||
resource: {
|
||||
uid: item.refId ?? '',
|
||||
name: datasourceConfig?.name ?? 'Unknown data source',
|
||||
type: datasourceConfig?.meta?.name ?? 'Unknown type',
|
||||
icon: datasourceConfig?.meta?.info?.logos?.small,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === 'DASHBOARD') {
|
||||
return {
|
||||
type: 'dashboard',
|
||||
uid: item.refId ?? '',
|
||||
status: convertStatus(item.status ?? ''),
|
||||
statusMessage: item.error,
|
||||
resource: {
|
||||
uid: item.refId ?? '',
|
||||
name: item.refId ?? 'Unknown dashboard',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
return betterResources;
|
||||
}, [data]);
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { InteractiveTable, CellProps, Stack, Text, Icon, useStyles2, Button } from '@grafana/ui';
|
||||
import { getSvgSize } from '@grafana/ui/src/components/Icon/utils';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { MigrationResourceDTOMock } from '../mockAPI';
|
||||
import { useGetDashboardByUidQuery } from '../api';
|
||||
import { MigrationResourceDTOMock, MigrationResourceDashboard, MigrationResourceDatasource } from '../mockAPI';
|
||||
|
||||
interface ResourcesTableProps {
|
||||
resources: MigrationResourceDTOMock[];
|
||||
@ -23,18 +25,66 @@ export function ResourcesTable({ resources }: ResourcesTableProps) {
|
||||
|
||||
function NameCell(props: CellProps<MigrationResourceDTOMock>) {
|
||||
const data = props.row.original;
|
||||
|
||||
return (
|
||||
<Stack direction="row" gap={2} alignItems="center">
|
||||
<ResourceIcon resource={data} />
|
||||
|
||||
<Stack direction="column" gap={0}>
|
||||
<span>{data.resource.name}</span>
|
||||
<Text color="secondary">{data.resource.type}</Text>
|
||||
{data.type === 'datasource' ? <DatasourceInfo data={data} /> : <DashboardInfo data={data} />}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function getDashboardTitle(dashboardData: object) {
|
||||
if ('title' in dashboardData && typeof dashboardData.title === 'string') {
|
||||
return dashboardData.title;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function DatasourceInfo({ data }: { data: MigrationResourceDatasource }) {
|
||||
return (
|
||||
<>
|
||||
<span>{data.resource.name}</span>
|
||||
<Text color="secondary">{data.resource.type}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: really, the API should return this directly
|
||||
function DashboardInfo({ data }: { data: MigrationResourceDashboard }) {
|
||||
const { data: dashboardData } = useGetDashboardByUidQuery({
|
||||
uid: data.resource.uid,
|
||||
});
|
||||
|
||||
const dashboardName = useMemo(() => {
|
||||
return (dashboardData?.dashboard && getDashboardTitle(dashboardData.dashboard)) ?? data.resource.uid;
|
||||
}, [dashboardData, data.resource.uid]);
|
||||
|
||||
if (!dashboardData) {
|
||||
return (
|
||||
<>
|
||||
<span>
|
||||
<Skeleton width={250} />
|
||||
</span>
|
||||
<span>
|
||||
<Skeleton width={130} />
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>{dashboardName}</span>
|
||||
<Text color="secondary">{dashboardData.meta?.folderTitle ?? 'Dashboards'}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TypeCell(props: CellProps<MigrationResourceDTOMock>) {
|
||||
const { type } = props.row.original;
|
||||
|
||||
|
@ -703,18 +703,15 @@
|
||||
"connect-modal": {
|
||||
"body-cloud-stack": "You'll also need a cloud stack. If you just signed up, we'll automatically create your first stack. If you have an account, you'll need to select or create a stack.",
|
||||
"body-get-started": "To get started, you'll need a Grafana.com account.",
|
||||
"body-paste-stack": "Once you've decided on a stack, paste the URL below.",
|
||||
"body-sign-up": "Sign up for a Grafana.com account",
|
||||
"body-token": "Your self-managed Grafana installation needs special access to securely migrate content. You'll need to create a migration token on your chosen cloud stack.",
|
||||
"body-token-field": "Migration token",
|
||||
"body-token-field-placeholder": "Paste token here",
|
||||
"body-token-instructions": "Log into your cloud stack and navigate to Administration, General, Migrate to Grafana Cloud. Create a migration token on that screen and paste the token here.",
|
||||
"body-url-field": "Cloud stack URL",
|
||||
"body-view-stacks": "View my cloud stacks",
|
||||
"cancel": "Cancel",
|
||||
"connect": "Connect to this stack",
|
||||
"connecting": "Connecting to this stack...",
|
||||
"stack-required-error": "Stack URL is required",
|
||||
"title": "Connect to a cloud stack",
|
||||
"token-required-error": "Migration token is required"
|
||||
},
|
||||
@ -796,7 +793,10 @@
|
||||
},
|
||||
"summary": {
|
||||
"disconnect": "Disconnect",
|
||||
"error-starting-migration": "There was an error starting cloud migration",
|
||||
"disconnect-error-description": "See the Grafana server logs for more details",
|
||||
"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"
|
||||
},
|
||||
|
@ -703,18 +703,15 @@
|
||||
"connect-modal": {
|
||||
"body-cloud-stack": "Ÿőū'ľľ äľşő ʼnęęđ ä čľőūđ şŧäčĸ. Ĩƒ yőū ĵūşŧ şįģʼnęđ ūp, ŵę'ľľ äūŧőmäŧįčäľľy čřęäŧę yőūř ƒįřşŧ şŧäčĸ. Ĩƒ yőū ĥävę äʼn äččőūʼnŧ, yőū'ľľ ʼnęęđ ŧő şęľęčŧ őř čřęäŧę ä şŧäčĸ.",
|
||||
"body-get-started": "Ŧő ģęŧ şŧäřŧęđ, yőū'ľľ ʼnęęđ ä Ğřäƒäʼnä.čőm äččőūʼnŧ.",
|
||||
"body-paste-stack": "Øʼnčę yőū'vę đęčįđęđ őʼn ä şŧäčĸ, päşŧę ŧĥę ŮŖĿ þęľőŵ.",
|
||||
"body-sign-up": "Ŝįģʼn ūp ƒőř ä Ğřäƒäʼnä.čőm äččőūʼnŧ",
|
||||
"body-token": "Ÿőūř şęľƒ-mäʼnäģęđ Ğřäƒäʼnä įʼnşŧäľľäŧįőʼn ʼnęęđş şpęčįäľ äččęşş ŧő şęčūřęľy mįģřäŧę čőʼnŧęʼnŧ. Ÿőū'ľľ ʼnęęđ ŧő čřęäŧę ä mįģřäŧįőʼn ŧőĸęʼn őʼn yőūř čĥőşęʼn čľőūđ şŧäčĸ.",
|
||||
"body-token-field": "Mįģřäŧįőʼn ŧőĸęʼn",
|
||||
"body-token-field-placeholder": "Päşŧę ŧőĸęʼn ĥęřę",
|
||||
"body-token-instructions": "Ŀőģ įʼnŧő yőūř čľőūđ şŧäčĸ äʼnđ ʼnävįģäŧę ŧő Åđmįʼnįşŧřäŧįőʼn, Ğęʼnęřäľ, Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ. Cřęäŧę ä mįģřäŧįőʼn ŧőĸęʼn őʼn ŧĥäŧ şčřęęʼn äʼnđ päşŧę ŧĥę ŧőĸęʼn ĥęřę.",
|
||||
"body-url-field": "Cľőūđ şŧäčĸ ŮŖĿ",
|
||||
"body-view-stacks": "Vįęŵ my čľőūđ şŧäčĸş",
|
||||
"cancel": "Cäʼnčęľ",
|
||||
"connect": "Cőʼnʼnęčŧ ŧő ŧĥįş şŧäčĸ",
|
||||
"connecting": "Cőʼnʼnęčŧįʼnģ ŧő ŧĥįş şŧäčĸ...",
|
||||
"stack-required-error": "Ŝŧäčĸ ŮŖĿ įş řęqūįřęđ",
|
||||
"title": "Cőʼnʼnęčŧ ŧő ä čľőūđ şŧäčĸ",
|
||||
"token-required-error": "Mįģřäŧįőʼn ŧőĸęʼn įş řęqūįřęđ"
|
||||
},
|
||||
@ -796,7 +793,10 @@
|
||||
},
|
||||
"summary": {
|
||||
"disconnect": "Đįşčőʼnʼnęčŧ",
|
||||
"error-starting-migration": "Ŧĥęřę ŵäş äʼn ęřřőř şŧäřŧįʼnģ čľőūđ mįģřäŧįőʼn",
|
||||
"disconnect-error-description": "Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş",
|
||||
"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ģ ŧő"
|
||||
},
|
||||
|
@ -6740,10 +6740,16 @@
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/ItemStatus"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/MigrateDataType"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MigrateDataType": {
|
||||
"type": "string"
|
||||
},
|
||||
"MoveFolderCommand": {
|
||||
"description": "MoveFolderCommand captures the information required by the folder service\nto move a folder.",
|
||||
"properties": {
|
||||
|
@ -5,6 +5,7 @@ const config: ConfigFile = {
|
||||
schemaFile: '../public/openapi3.json',
|
||||
apiFile: '', // leave this empty, and instead populate the outputFiles object below
|
||||
hooks: true,
|
||||
exportName: 'generatedAPI',
|
||||
|
||||
outputFiles: {
|
||||
'../public/app/features/migrate-to-cloud/api/endpoints.gen.ts': {
|
||||
@ -19,6 +20,7 @@ const config: ConfigFile = {
|
||||
'getCloudMigrationRun',
|
||||
'getCloudMigrationRunList',
|
||||
'deleteCloudMigration',
|
||||
'getDashboardByUid',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user