diff --git a/public/app/features/admin/migrate-to-cloud/api.ts b/public/app/features/admin/migrate-to-cloud/api.ts index 9659617f0ef..b4a065d978b 100644 --- a/public/app/features/admin/migrate-to-cloud/api.ts +++ b/public/app/features/admin/migrate-to-cloud/api.ts @@ -41,12 +41,86 @@ export interface ConnectStackDTO { token: string; } +export interface MigrationResourceDTO { + uid: string; + status: 'not-migrated' | 'migrated' | 'migrating' | 'failed'; + statusMessage?: string; + type: 'datasource' | 'dashboard'; // TODO: in future this would be a discriminated union with the resource details + resource: { + uid: string; + name: string; + type: string; + icon?: string; + }; +} + +const mockApplications = ['auth-service', 'web server', 'backend']; +const mockEnvs = ['DEV', 'PROD']; +const mockRoles = ['db', 'load-balancer', 'server', 'logs']; +const mockDataSources = ['Prometheus', 'Loki', 'AWS Athena', 'AWS Cloudwatch', 'InfluxDB', 'Elasticsearch']; + +const mockDataSourceMetadata: Record = { + Prometheus: { + image: 'https://grafana.com/api/plugins/prometheus/versions/5.0.0/logos/small', + }, + + Loki: { + image: 'https://grafana.com/api/plugins/loki/versions/5.0.0/logos/small', + }, + + 'AWS Athena': { + image: 'https://grafana.com/api/plugins/grafana-athena-datasource/versions/2.13.5/logos/small', + }, + + 'AWS Cloudwatch': { + image: 'https://grafana.com/api/plugins/computest-cloudwatchalarm-datasource/versions/2.0.0/logos/small', + }, + + InfluxDB: { + image: 'https://grafana.com/api/plugins/influxdb/versions/5.0.0/logos/small', + }, + + Elasticsearch: { + image: 'https://grafana.com/api/plugins/elasticsearch/versions/5.0.0/logos/small', + }, +}; + +const mockMigrationResources: MigrationResourceDTO[] = Array.from({ length: 500 }).map((_, index) => { + const dataSource = mockDataSources[index % mockDataSources.length]; + const environment = mockEnvs[index % mockEnvs.length]; + const application = mockApplications[index % mockApplications.length]; + const role = mockRoles[index % mockRoles.length]; + + return { + status: 'not-migrated', + type: 'datasource', + uid: index.toString(16), + resource: { + uid: `${application}-${environment}-${role}-${index}`, + name: `${application} ${environment} ${role}`, + icon: mockDataSourceMetadata[dataSource]?.image, + type: dataSource, + }, + }; +}); + +mockMigrationResources[0].status = 'migrated'; +mockMigrationResources[1].status = 'failed'; +mockMigrationResources[1].statusMessage = `Source map error: Error: request failed with status 404 +Resource URL: http://localhost:3000/public/build/app.f4d0c6a0daa6a5b14892.js +Source Map URL: app.f4d0c6a0daa6a5b14892.js.map`; +mockMigrationResources[2].status = 'migrated'; +mockMigrationResources[3].status = 'migrated'; +mockMigrationResources[4].status = 'migrating'; +mockMigrationResources[5].status = 'migrating'; + // TODO remove these mock properties/functions +const queryParams = new URLSearchParams(window.location.search); const MOCK_DELAY_MS = 1000; const MOCK_TOKEN = 'TODO_thisWillBeABigLongToken'; let HAS_MIGRATION_TOKEN = false; -let HAS_STACK_DETAILS = false; -let STACK_URL: string | undefined; +let HAS_STACK_DETAILS = !!queryParams.get('mockStackURL'); +let STACK_URL: string | undefined = queryParams.get('mockStackURL') || undefined; function dataWithMockDelay(data: T): Promise<{ data: T }> { return new Promise((resolve) => { @@ -57,7 +131,7 @@ function dataWithMockDelay(data: T): Promise<{ data: T }> { } export const migrateToCloudAPI = createApi({ - tagTypes: ['migrationToken', 'stackDetails'], + tagTypes: ['migrationToken', 'stackDetails', 'resource'], reducerPath: 'migrateToCloudAPI', baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }), endpoints: (builder) => ({ @@ -72,6 +146,7 @@ export const migrateToCloudAPI = createApi({ return dataWithMockDelay(responseData); }, }), + connectStack: builder.mutation({ invalidatesTags: ['stackDetails'], queryFn: async ({ stackURL }) => { @@ -80,6 +155,7 @@ export const migrateToCloudAPI = createApi({ return dataWithMockDelay(undefined); }, }), + disconnectStack: builder.mutation({ invalidatesTags: ['stackDetails'], queryFn: async () => { @@ -110,6 +186,13 @@ export const migrateToCloudAPI = createApi({ return dataWithMockDelay(HAS_MIGRATION_TOKEN); }, }), + + listResources: builder.query({ + providesTags: ['resource'], + queryFn: async () => { + return dataWithMockDelay(mockMigrationResources); + }, + }), }), }); @@ -120,4 +203,5 @@ export const { useCreateMigrationTokenMutation, useDeleteMigrationTokenMutation, useHasMigrationTokenQuery, + useListResourcesQuery, } = migrateToCloudAPI; diff --git a/public/app/features/admin/migrate-to-cloud/onprem/Page.tsx b/public/app/features/admin/migrate-to-cloud/onprem/Page.tsx index faf78386581..4fc0614bed1 100644 --- a/public/app/features/admin/migrate-to-cloud/onprem/Page.tsx +++ b/public/app/features/admin/migrate-to-cloud/onprem/Page.tsx @@ -1,29 +1,34 @@ +import { skipToken } from '@reduxjs/toolkit/query/react'; import React, { useState } from 'react'; import { Button, Stack, Text } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; -import { useGetStatusQuery } from '../api'; +import { useGetStatusQuery, useListResourcesQuery } from '../api'; import { DisconnectModal } from './DisconnectModal'; import { EmptyState } from './EmptyState/EmptyState'; +import { ResourcesTable } from './ResourcesTable'; export const Page = () => { - const { data, isFetching } = useGetStatusQuery(); + const { data: status, isFetching } = useGetStatusQuery(); + const { data: resources } = useListResourcesQuery(status?.enabled ? undefined : skipToken); const [isDisconnecting, setIsDisconnecting] = useState(false); - if (!data?.enabled) { + if (!status?.enabled) { return ; } return ( <> - - {data.stackURL && {data.stackURL}} + + {status.stackURL && {status.stackURL}} + + {resources && } setIsDisconnecting(false)} /> diff --git a/public/app/features/admin/migrate-to-cloud/onprem/ResourcesTable.tsx b/public/app/features/admin/migrate-to-cloud/onprem/ResourcesTable.tsx new file mode 100644 index 00000000000..8806dce5ef5 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/onprem/ResourcesTable.tsx @@ -0,0 +1,103 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +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 { MigrationResourceDTO } from '../api'; + +interface ResourcesTableProps { + resources: MigrationResourceDTO[]; +} + +const columns = [ + { id: 'name', header: 'Name', cell: NameCell }, + { id: 'type', header: 'Type', cell: TypeCell }, + { id: 'status', header: 'Status', cell: StatusCell }, +]; + +export function ResourcesTable({ resources }: ResourcesTableProps) { + return r.uid} pageSize={15} />; +} + +function NameCell(props: CellProps) { + const data = props.row.original; + return ( + + + + + {data.resource.name} + {data.resource.type} + + + ); +} + +function TypeCell(props: CellProps) { + const { type } = props.row.original; + + if (type === 'datasource') { + return t('migrate-to-cloud.resource-type.datasource', 'Data source'); + } + + if (type === 'dashboard') { + return t('migrate-to-cloud.resource-type.dashboard', 'Dashboard'); + } + + return t('migrate-to-cloud.resource-type.unknown', 'Unknown'); +} + +function StatusCell(props: CellProps) { + const { status, statusMessage } = props.row.original; + + if (status === 'not-migrated') { + return {t('migrate-to-cloud.resource-status.not-migrated', 'Not yet uploaded')}; + } else if (status === 'migrating') { + return {t('migrate-to-cloud.resource-status.migrating', 'Uploading...')}; + } else if (status === 'migrated') { + return {t('migrate-to-cloud.resource-status.migrated', 'Uploaded to cloud')}; + } else if (status === 'failed') { + return ( + + {t('migrate-to-cloud.resource-status.failed', 'Error')} + + {statusMessage && ( + // TODO: trigger a proper modal, probably from the parent, on click + + )} + + ); + } + + return {t('migrate-to-cloud.resource-status.unknown', 'Unknown')}; +} + +function ResourceIcon({ resource }: { resource: MigrationResourceDTO }) { + const styles = useStyles2(getIconStyles); + + if (resource.type === 'dashboard') { + return ; + } + + if (resource.type === 'datasource' && resource.resource.icon) { + return ; + } else if (resource.type === 'datasource') { + return ; + } + + return undefined; +} + +function getIconStyles() { + return { + icon: css({ + display: 'block', + width: getSvgSize('xl'), + height: getSvgSize('xl'), + }), + }; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a02aa3d22a1..75b56554755 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -724,6 +724,7 @@ "cancel": "Cancel", "disconnect": "Disconnect", "disconnecting": "Disconnecting...", + "error": "There was an error disconnecting", "title": "Disconnect from cloud stack" }, "get-started": { @@ -776,6 +777,19 @@ "link-title": "Grafana Cloud pricing", "title": "How much does it cost?" }, + "resource-status": { + "error-details-button": "Details", + "failed": "Error", + "migrated": "Uploaded to cloud", + "migrating": "Uploading...", + "not-migrated": "Not yet uploaded", + "unknown": "Unknown" + }, + "resource-type": { + "dashboard": "Dashboard", + "datasource": "Data source", + "unknown": "Unknown" + }, "resources": { "disconnect": "Disconnect" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 900fa2e4b34..548641ea6fe 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -724,6 +724,7 @@ "cancel": "Cäʼnčęľ", "disconnect": "Đįşčőʼnʼnęčŧ", "disconnecting": "Đįşčőʼnʼnęčŧįʼnģ...", + "error": "Ŧĥęřę ŵäş äʼn ęřřőř đįşčőʼnʼnęčŧįʼnģ", "title": "Đįşčőʼnʼnęčŧ ƒřőm čľőūđ şŧäčĸ" }, "get-started": { @@ -776,6 +777,19 @@ "link-title": "Ğřäƒäʼnä Cľőūđ přįčįʼnģ", "title": "Ħőŵ mūčĥ đőęş įŧ čőşŧ?" }, + "resource-status": { + "error-details-button": "Đęŧäįľş", + "failed": "Ēřřőř", + "migrated": "Ůpľőäđęđ ŧő čľőūđ", + "migrating": "Ůpľőäđįʼnģ...", + "not-migrated": "Ńőŧ yęŧ ūpľőäđęđ", + "unknown": "Ůʼnĸʼnőŵʼn" + }, + "resource-type": { + "dashboard": "Đäşĥþőäřđ", + "datasource": "Đäŧä şőūřčę", + "unknown": "Ůʼnĸʼnőŵʼn" + }, "resources": { "disconnect": "Đįşčőʼnʼnęčŧ" },