diff --git a/pkg/services/cloudmigration/model.go b/pkg/services/cloudmigration/model.go index 7b07f32b653..d77e2fc7492 100644 --- a/pkg/services/cloudmigration/model.go +++ b/pkg/services/cloudmigration/model.go @@ -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"` } diff --git a/public/api-merged.json b/public/api-merged.json index 80ae39ac437..bef6be380cd 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -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", diff --git a/public/app/features/migrate-to-cloud/api/baseAPI.ts b/public/app/features/migrate-to-cloud/api/baseAPI.ts index 62d97c20b9e..7e1b154581a 100644 --- a/public/app/features/migrate-to-cloud/api/baseAPI.ts +++ b/public/app/features/migrate-to-cloud/api/baseAPI.ts @@ -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 { @@ -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 }; 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 110b4ae30b7..19de279b1df 100644 --- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts +++ b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts @@ -25,10 +25,13 @@ const injectedRtkApi = api.injectEndpoints({ createCloudMigrationToken: build.mutation({ query: () => ({ url: `/cloudmigration/token`, method: 'POST' }), }), + getDashboardByUid: build.query({ + 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; diff --git a/public/app/features/migrate-to-cloud/api/index.ts b/public/app/features/migrate-to-cloud/api/index.ts index 3c0c7cab8d9..ee3bd1aa218 100644 --- a/public/app/features/migrate-to-cloud/api/index.ts +++ b/public/app/features/migrate-to-cloud/api/index.ts @@ -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'], + }, + }, +}); diff --git a/public/app/features/migrate-to-cloud/mockAPI.ts b/public/app/features/migrate-to-cloud/mockAPI.ts index a4ff9d547c5..93d1110ccb7 100644 --- a/public/app/features/migrate-to-cloud/mockAPI.ts +++ b/public/app/features/migrate-to-cloud/mockAPI.ts @@ -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']; diff --git a/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/CallToAction.tsx b/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/CallToAction.tsx index f3dee4ba4d4..71a21eb32cc 100644 --- a/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/CallToAction.tsx +++ b/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/CallToAction.tsx @@ -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 ( @@ -19,11 +18,11 @@ export const CallToAction = () => { Let us manage your Grafana stack - @@ -61,8 +163,8 @@ export const Page = () => { @@ -72,7 +174,65 @@ export const Page = () => { {resources && } - setIsDisconnecting(false)} /> + {/* 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]); +} diff --git a/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx b/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx index 65aa992b41d..1835047b1b8 100644 --- a/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx +++ b/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx @@ -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) { const data = props.row.original; + return ( - {data.resource.name} - {data.resource.type} + {data.type === 'datasource' ? : } ); } +function getDashboardTitle(dashboardData: object) { + if ('title' in dashboardData && typeof dashboardData.title === 'string') { + return dashboardData.title; + } + + return undefined; +} + +function DatasourceInfo({ data }: { data: MigrationResourceDatasource }) { + return ( + <> + {data.resource.name} + {data.resource.type} + + ); +} + +// 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 ( + <> + + + + + + + + ); + } + + return ( + <> + {dashboardName} + {dashboardData.meta?.folderTitle ?? 'Dashboards'} + + ); +} + function TypeCell(props: CellProps) { const { type } = props.row.original; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 5cf2fdc5e90..d5456911eac 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index b6c7e819674..c0220152110 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -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ģ ŧő" }, diff --git a/public/openapi3.json b/public/openapi3.json index fbc17b006c7..c9aad593102 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -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": { diff --git a/scripts/generate-rtk-apis.ts b/scripts/generate-rtk-apis.ts index 61e80c33d28..1e380e63200 100644 --- a/scripts/generate-rtk-apis.ts +++ b/scripts/generate-rtk-apis.ts @@ -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', ], }, },