mirror of
https://github.com/grafana/grafana.git
synced 2024-12-28 18:01:40 -06:00
E2C: Resources table refactor (#85585)
* E2C: Resources table refactor * update swagger spec with enums * use native resource item type, rather than our mock type * unit tests for resources table * update spec
This commit is contained in:
parent
c033a15aaa
commit
5ce8b60878
@ -143,6 +143,7 @@ type Base64HGInstance struct {
|
||||
|
||||
// dtos for cms api
|
||||
|
||||
// swagger:enum MigrateDataType
|
||||
type MigrateDataType string
|
||||
|
||||
const (
|
||||
@ -162,6 +163,7 @@ type MigrateDataRequestItemDTO struct {
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// swagger:enum ItemStatus
|
||||
type ItemStatus string
|
||||
|
||||
const (
|
||||
@ -175,8 +177,11 @@ type MigrateDataResponseDTO struct {
|
||||
}
|
||||
|
||||
type MigrateDataResponseItemDTO struct {
|
||||
Type MigrateDataType `json:"type"`
|
||||
RefID string `json:"refId"`
|
||||
Status ItemStatus `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
// required:true
|
||||
Type MigrateDataType `json:"type"`
|
||||
// required:true
|
||||
RefID string `json:"refId"`
|
||||
// required:true
|
||||
Status ItemStatus `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
@ -2776,6 +2776,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CloudMigrationRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authToken": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CloudMigrationResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -4539,9 +4547,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ItemStatus": {
|
||||
"type": "string"
|
||||
},
|
||||
"JSONWebKey": {
|
||||
"description": "JSONWebKey represents a public or private key in JWK format. It can be\nmarshaled into JSON and unmarshaled from JSON.",
|
||||
"type": "object",
|
||||
@ -4896,6 +4901,11 @@
|
||||
},
|
||||
"MigrateDataResponseItemDTO": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"refId",
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
@ -4904,7 +4914,19 @@
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/ItemStatus"
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"OK",
|
||||
"ERROR"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"DASHBOARD",
|
||||
"DATASOURCE",
|
||||
"FOLDER"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -15499,9 +15499,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ItemStatus": {
|
||||
"type": "string"
|
||||
},
|
||||
"JSONWebKey": {
|
||||
"description": "JSONWebKey represents a public or private key in JWK format. It can be\nmarshaled into JSON and unmarshaled from JSON.",
|
||||
"type": "object",
|
||||
@ -15954,6 +15951,11 @@
|
||||
},
|
||||
"MigrateDataResponseItemDTO": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"refId",
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
@ -15962,16 +15964,22 @@
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/ItemStatus"
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"OK",
|
||||
"ERROR"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/MigrateDataType"
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"DASHBOARD",
|
||||
"DATASOURCE",
|
||||
"FOLDER"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"MigrateDataType": {
|
||||
"type": "string"
|
||||
},
|
||||
"MoveFolderCommand": {
|
||||
"description": "MoveFolderCommand captures the information required by the folder service\nto move a folder.",
|
||||
"type": "object",
|
||||
|
@ -93,13 +93,11 @@ export type ErrorResponseBody = {
|
||||
export type CloudMigrationRequest = {
|
||||
authToken?: string;
|
||||
};
|
||||
export type ItemStatus = string;
|
||||
export type MigrateDataType = string;
|
||||
export type MigrateDataResponseItemDto = {
|
||||
error?: string;
|
||||
refId?: string;
|
||||
status?: ItemStatus;
|
||||
type?: MigrateDataType;
|
||||
refId: string;
|
||||
status: 'OK' | 'ERROR';
|
||||
type: 'DASHBOARD' | 'DATASOURCE' | 'FOLDER';
|
||||
};
|
||||
export type MigrateDataResponseDto = {
|
||||
id?: number;
|
||||
|
@ -1,4 +1,6 @@
|
||||
export * from './endpoints.gen';
|
||||
import { BaseQueryFn, QueryDefinition } from '@reduxjs/toolkit/dist/query';
|
||||
|
||||
import { generatedAPI } from './endpoints.gen';
|
||||
|
||||
export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({
|
||||
@ -35,5 +37,24 @@ export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({
|
||||
runCloudMigration: {
|
||||
invalidatesTags: ['cloud-migration-run'],
|
||||
},
|
||||
|
||||
getDashboardByUid(endpoint) {
|
||||
suppressErrorsOnQuery(endpoint);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function suppressErrorsOnQuery<QueryArg, BaseQuery extends BaseQueryFn, TagTypes extends string, ResultType>(
|
||||
endpoint: QueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType>
|
||||
) {
|
||||
if (!endpoint.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalQuery = endpoint.query;
|
||||
endpoint.query = (...args) => {
|
||||
const baseQuery = originalQuery(...args);
|
||||
baseQuery.showErrorAlert = false;
|
||||
return baseQuery;
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
import { Chance } from 'chance';
|
||||
|
||||
import { MigrateDataResponseItemDto } from '../api';
|
||||
|
||||
export function wellFormedDatasourceMigrationItem(
|
||||
seed = 1,
|
||||
partial: Partial<MigrateDataResponseItemDto> = {}
|
||||
): MigrateDataResponseItemDto {
|
||||
const random = Chance(seed);
|
||||
|
||||
return {
|
||||
type: 'DATASOURCE',
|
||||
refId: random.guid(),
|
||||
status: random.pickone(['OK', 'ERROR']),
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
export function wellFormedDashboardMigrationItem(
|
||||
seed = 1,
|
||||
partial: Partial<MigrateDataResponseItemDto> = {}
|
||||
): MigrateDataResponseItemDto {
|
||||
const random = Chance(seed);
|
||||
|
||||
return {
|
||||
type: 'DASHBOARD',
|
||||
refId: random.guid(),
|
||||
status: random.pickone(['OK', 'ERROR']),
|
||||
...partial,
|
||||
};
|
||||
}
|
@ -4,12 +4,30 @@ import { SetupServer, setupServer } from 'msw/node';
|
||||
export function registerAPIHandlers(): SetupServer {
|
||||
const server = setupServer(
|
||||
// TODO
|
||||
http.get('/api/cloudmigration/status', () => {
|
||||
http.get('/api/dashboards/uid/:uid', ({ request, params }) => {
|
||||
if (params.uid === 'dashboard-404') {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
message: 'Dashboard not found',
|
||||
},
|
||||
{
|
||||
status: 404,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
enabled: false,
|
||||
dashboard: {
|
||||
title: 'My Dashboard',
|
||||
},
|
||||
meta: {
|
||||
folderTitle: 'Dashboards',
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
server.listen();
|
||||
|
||||
return server;
|
||||
}
|
||||
|
44
public/app/features/migrate-to-cloud/fixtures/others.ts
Normal file
44
public/app/features/migrate-to-cloud/fixtures/others.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { Chance } from 'chance';
|
||||
|
||||
import { DataSourceInstanceSettings, PluginType } from '@grafana/data';
|
||||
|
||||
export function wellFormedDatasource(
|
||||
seed = 1,
|
||||
custom: Partial<DataSourceInstanceSettings> = {}
|
||||
): DataSourceInstanceSettings {
|
||||
const random = Chance(seed);
|
||||
|
||||
return {
|
||||
id: random.integer(),
|
||||
uid: random.guid(),
|
||||
type: random.word(),
|
||||
name: random.sentence({ words: 3 }),
|
||||
readOnly: false,
|
||||
jsonData: {},
|
||||
meta: {
|
||||
id: random.word(),
|
||||
name: random.word(),
|
||||
type: PluginType.datasource,
|
||||
module: random.word(),
|
||||
baseUrl: random.url(),
|
||||
|
||||
info: {
|
||||
author: {
|
||||
name: random.name(),
|
||||
},
|
||||
description: random.sentence({ words: 5 }),
|
||||
|
||||
links: [],
|
||||
logos: {
|
||||
large: random.url(),
|
||||
small: random.url(),
|
||||
},
|
||||
screenshots: [],
|
||||
updated: random.date().toISOString(),
|
||||
version: '1.0.0',
|
||||
},
|
||||
},
|
||||
access: 'direct',
|
||||
...custom,
|
||||
};
|
||||
}
|
143
public/app/features/migrate-to-cloud/onprem/NameCell.tsx
Normal file
143
public/app/features/migrate-to-cloud/onprem/NameCell.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useMemo } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { CellProps, Stack, Text, Icon, useStyles2 } from '@grafana/ui';
|
||||
import { getSvgSize } from '@grafana/ui/src/components/Icon/utils';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { useGetDashboardByUidQuery, MigrateDataResponseItemDto } from '../api';
|
||||
|
||||
export function NameCell(props: CellProps<MigrateDataResponseItemDto>) {
|
||||
const data = props.row.original;
|
||||
|
||||
return (
|
||||
<Stack direction="row" gap={2} alignItems="center">
|
||||
<ResourceIcon resource={data} />
|
||||
|
||||
<Stack direction="column" gap={0}>
|
||||
{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: MigrateDataResponseItemDto }) {
|
||||
const datasourceUID = data.refId;
|
||||
const datasource = useDatasource(datasourceUID);
|
||||
|
||||
if (!datasource) {
|
||||
return (
|
||||
<>
|
||||
<Text>
|
||||
<Trans i18nKey="migrate-to-cloud.resource-table.unknown-datasource-title">
|
||||
Data source {{ datasourceUID }}
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="migrate-to-cloud.resource-table.unknown-datasource-type">Unknown data source</Trans>
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>{datasource.name}</span>
|
||||
<Text color="secondary">{datasource.type}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardInfo({ data }: { data: MigrateDataResponseItemDto }) {
|
||||
const dashboardUID = data.refId;
|
||||
// TODO: really, the API should return this directly
|
||||
const { data: dashboardData, isError } = useGetDashboardByUidQuery({
|
||||
uid: dashboardUID,
|
||||
});
|
||||
|
||||
const dashboardName = useMemo(() => {
|
||||
return (dashboardData?.dashboard && getDashboardTitle(dashboardData.dashboard)) ?? dashboardUID;
|
||||
}, [dashboardData, dashboardUID]);
|
||||
|
||||
if (isError) {
|
||||
// Not translated because this is only temporary until the data comes through in the MigrationRun API
|
||||
return (
|
||||
<>
|
||||
<Text italic>Unable to load dashboard</Text>
|
||||
<Text color="secondary">Dashboard {dashboardUID}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!dashboardData) {
|
||||
return <InfoSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>{dashboardName}</span>
|
||||
<Text color="secondary">{dashboardData.meta?.folderTitle ?? 'Dashboards'}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoSkeleton() {
|
||||
return (
|
||||
<>
|
||||
<Skeleton width={250} />
|
||||
<Skeleton width={130} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ResourceIcon({ resource }: { resource: MigrateDataResponseItemDto }) {
|
||||
const styles = useStyles2(getIconStyles);
|
||||
const datasource = useDatasource(resource.type === 'DATASOURCE' ? resource.refId : undefined);
|
||||
|
||||
if (resource.type === 'DASHBOARD') {
|
||||
return <Icon size="xl" name="dashboard" />;
|
||||
}
|
||||
|
||||
if (resource.type === 'DATASOURCE' && datasource?.meta?.info?.logos?.small) {
|
||||
return <img className={styles.icon} src={datasource.meta.info.logos.small} alt="" />;
|
||||
} else if (resource.type === 'DATASOURCE') {
|
||||
return <Icon size="xl" name="database" />;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getIconStyles() {
|
||||
return {
|
||||
icon: css({
|
||||
display: 'block',
|
||||
width: getSvgSize('xl'),
|
||||
height: getSvgSize('xl'),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function useDatasource(datasourceUID: string | undefined): DataSourceInstanceSettings | undefined {
|
||||
const datasource = useMemo(() => {
|
||||
if (!datasourceUID) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
config.datasources[datasourceUID] || Object.values(config.datasources).find((ds) => ds.uid === datasourceUID)
|
||||
);
|
||||
}, [datasourceUID]);
|
||||
|
||||
return datasource;
|
||||
}
|
@ -1,19 +1,16 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query/react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert, Box, Button, Stack } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import {
|
||||
MigrateDataResponseDto,
|
||||
useDeleteCloudMigrationMutation,
|
||||
useGetCloudMigrationRunListQuery,
|
||||
useGetCloudMigrationRunQuery,
|
||||
useGetMigrationListQuery,
|
||||
useRunCloudMigrationMutation,
|
||||
} from '../api';
|
||||
import { MigrationResourceDTOMock } from '../mockAPI';
|
||||
|
||||
import { EmptyState } from './EmptyState/EmptyState';
|
||||
import { MigrationInfo } from './MigrationInfo';
|
||||
@ -79,7 +76,7 @@ export const Page = () => {
|
||||
lastMigrationRun.isFetching ||
|
||||
disconnectResult.isLoading;
|
||||
|
||||
const resources = useFixResources(lastMigrationRun.data);
|
||||
const resources = lastMigrationRun.data?.items;
|
||||
|
||||
const handleDisconnect = useCallback(() => {
|
||||
if (migrationDestination.data?.id) {
|
||||
@ -178,61 +175,3 @@ export const Page = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// 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]);
|
||||
}
|
||||
|
@ -0,0 +1,132 @@
|
||||
import 'whatwg-fetch'; // fetch polyfill
|
||||
import { render as rtlRender, screen } from '@testing-library/react';
|
||||
import { SetupServer } from 'msw/lib/node';
|
||||
import React from 'react';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import { setBackendSrv, config } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import { wellFormedDashboardMigrationItem, wellFormedDatasourceMigrationItem } from '../fixtures/migrationItems';
|
||||
import { registerAPIHandlers } from '../fixtures/mswAPI';
|
||||
import { wellFormedDatasource } from '../fixtures/others';
|
||||
|
||||
import { ResourcesTable } from './ResourcesTable';
|
||||
|
||||
setBackendSrv(backendSrv);
|
||||
|
||||
function render(...[ui, options]: Parameters<typeof rtlRender>) {
|
||||
rtlRender(<TestProvider>{ui}</TestProvider>, options);
|
||||
}
|
||||
|
||||
describe('ResourcesTable', () => {
|
||||
let server: SetupServer;
|
||||
let originalDatasources: (typeof config)['datasources'];
|
||||
|
||||
const datasourceA = wellFormedDatasource(1, {
|
||||
uid: 'datasource-a-uid',
|
||||
name: 'Datasource A',
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
server = registerAPIHandlers();
|
||||
originalDatasources = config.datasources;
|
||||
|
||||
config.datasources = {
|
||||
...config.datasources,
|
||||
[datasourceA.name]: datasourceA,
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
config.datasources = originalDatasources;
|
||||
});
|
||||
|
||||
it('renders data sources', async () => {
|
||||
const resources = [
|
||||
wellFormedDatasourceMigrationItem(1, {
|
||||
refId: datasourceA.uid,
|
||||
}),
|
||||
];
|
||||
|
||||
render(<ResourcesTable resources={resources} />);
|
||||
|
||||
expect(screen.getByText('Datasource A')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders data sources when their data is missing', () => {
|
||||
const item = wellFormedDatasourceMigrationItem(2);
|
||||
const resources = [item];
|
||||
|
||||
render(<ResourcesTable resources={resources} />);
|
||||
|
||||
expect(screen.getByText(`Data source ${item.refId}`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`Unknown data source`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders dashboards', async () => {
|
||||
const resources = [wellFormedDashboardMigrationItem(1)];
|
||||
|
||||
render(<ResourcesTable resources={resources} />);
|
||||
|
||||
expect(await screen.findByText('My Dashboard')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders dashboards when their data is missing', async () => {
|
||||
const resources = [
|
||||
wellFormedDashboardMigrationItem(2, {
|
||||
refId: 'dashboard-404',
|
||||
}),
|
||||
];
|
||||
|
||||
render(<ResourcesTable resources={resources} />);
|
||||
|
||||
expect(await screen.findByText('Unable to load dashboard')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Dashboard dashboard-404')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the success status correctly', () => {
|
||||
const resources = [
|
||||
wellFormedDatasourceMigrationItem(1, {
|
||||
refId: datasourceA.uid,
|
||||
status: 'OK',
|
||||
}),
|
||||
];
|
||||
|
||||
render(<ResourcesTable resources={resources} />);
|
||||
|
||||
expect(screen.getByText('Uploaded to cloud')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the success error correctly', () => {
|
||||
const resources = [
|
||||
wellFormedDatasourceMigrationItem(1, {
|
||||
refId: datasourceA.uid,
|
||||
status: 'ERROR',
|
||||
}),
|
||||
];
|
||||
|
||||
render(<ResourcesTable resources={resources} />);
|
||||
|
||||
expect(screen.getByText('Error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a details button when there's an error description", () => {
|
||||
const resources = [
|
||||
wellFormedDatasourceMigrationItem(1, {
|
||||
refId: datasourceA.uid,
|
||||
status: 'ERROR',
|
||||
error: 'Some error',
|
||||
}),
|
||||
];
|
||||
|
||||
render(<ResourcesTable resources={resources} />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: 'Details',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -1,16 +1,15 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useMemo } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
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 { InteractiveTable } from '@grafana/ui';
|
||||
|
||||
import { useGetDashboardByUidQuery } from '../api';
|
||||
import { MigrationResourceDTOMock, MigrationResourceDashboard, MigrationResourceDatasource } from '../mockAPI';
|
||||
import { MigrateDataResponseItemDto } from '../api';
|
||||
|
||||
import { NameCell } from './NameCell';
|
||||
import { StatusCell } from './StatusCell';
|
||||
import { TypeCell } from './TypeCell';
|
||||
|
||||
interface ResourcesTableProps {
|
||||
resources: MigrationResourceDTOMock[];
|
||||
resources: MigrateDataResponseItemDto[];
|
||||
}
|
||||
|
||||
const columns = [
|
||||
@ -20,134 +19,5 @@ const columns = [
|
||||
];
|
||||
|
||||
export function ResourcesTable({ resources }: ResourcesTableProps) {
|
||||
return <InteractiveTable columns={columns} data={resources} getRowId={(r) => r.uid} pageSize={15} />;
|
||||
}
|
||||
|
||||
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}>
|
||||
{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;
|
||||
|
||||
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<MigrationResourceDTOMock>) {
|
||||
const { status, statusMessage } = props.row.original;
|
||||
|
||||
if (status === 'not-migrated') {
|
||||
return <Text color="secondary">{t('migrate-to-cloud.resource-status.not-migrated', 'Not yet uploaded')}</Text>;
|
||||
} else if (status === 'migrating') {
|
||||
return <Text color="info">{t('migrate-to-cloud.resource-status.migrating', 'Uploading...')}</Text>;
|
||||
} else if (status === 'migrated') {
|
||||
return <Text color="success">{t('migrate-to-cloud.resource-status.migrated', 'Uploaded to cloud')}</Text>;
|
||||
} else if (status === 'failed') {
|
||||
return (
|
||||
<Stack alignItems="center">
|
||||
<Text color="error">{t('migrate-to-cloud.resource-status.failed', 'Error')}</Text>
|
||||
|
||||
{statusMessage && (
|
||||
// TODO: trigger a proper modal, probably from the parent, on click
|
||||
<Button size="sm" variant="secondary" onClick={() => window.alert(statusMessage)}>
|
||||
{t('migrate-to-cloud.resource-status.error-details-button', 'Details')}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return <Text color="secondary">{t('migrate-to-cloud.resource-status.unknown', 'Unknown')}</Text>;
|
||||
}
|
||||
|
||||
function ResourceIcon({ resource }: { resource: MigrationResourceDTOMock }) {
|
||||
const styles = useStyles2(getIconStyles);
|
||||
|
||||
if (resource.type === 'dashboard') {
|
||||
return <Icon size="xl" name="dashboard" />;
|
||||
}
|
||||
|
||||
if (resource.type === 'datasource' && resource.resource.icon) {
|
||||
return <img className={styles.icon} src={resource.resource.icon} alt="" />;
|
||||
} else if (resource.type === 'datasource') {
|
||||
return <Icon size="xl" name="database" />;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getIconStyles() {
|
||||
return {
|
||||
icon: css({
|
||||
display: 'block',
|
||||
width: getSvgSize('xl'),
|
||||
height: getSvgSize('xl'),
|
||||
}),
|
||||
};
|
||||
return <InteractiveTable columns={columns} data={resources} getRowId={(r) => r.refId} pageSize={15} />;
|
||||
}
|
||||
|
33
public/app/features/migrate-to-cloud/onprem/StatusCell.tsx
Normal file
33
public/app/features/migrate-to-cloud/onprem/StatusCell.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
import { CellProps, Text, Stack, Button } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { MigrateDataResponseItemDto } from '../api';
|
||||
|
||||
export function StatusCell(props: CellProps<MigrateDataResponseItemDto>) {
|
||||
const { status, error } = props.row.original;
|
||||
|
||||
// Keep these here to preserve the translations
|
||||
// t('migrate-to-cloud.resource-status.not-migrated', 'Not yet uploaded')
|
||||
// t('migrate-to-cloud.resource-status.migrating', 'Uploading...')
|
||||
|
||||
if (status === 'OK') {
|
||||
return <Text color="success">{t('migrate-to-cloud.resource-status.migrated', 'Uploaded to cloud')}</Text>;
|
||||
} else if (status === 'ERROR') {
|
||||
return (
|
||||
<Stack alignItems="center">
|
||||
<Text color="error">{t('migrate-to-cloud.resource-status.failed', 'Error')}</Text>
|
||||
|
||||
{error && (
|
||||
// TODO: trigger a proper modal, probably from the parent, on click
|
||||
<Button size="sm" variant="secondary" onClick={() => window.alert(error)}>
|
||||
{t('migrate-to-cloud.resource-status.error-details-button', 'Details')}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return <Text color="secondary">{t('migrate-to-cloud.resource-status.unknown', 'Unknown')}</Text>;
|
||||
}
|
19
public/app/features/migrate-to-cloud/onprem/TypeCell.tsx
Normal file
19
public/app/features/migrate-to-cloud/onprem/TypeCell.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { CellProps } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { MigrateDataResponseItemDto } from '../api';
|
||||
|
||||
export function TypeCell(props: CellProps<MigrateDataResponseItemDto>) {
|
||||
const { type } = props.row.original;
|
||||
|
||||
switch (type) {
|
||||
case 'DATASOURCE':
|
||||
return t('migrate-to-cloud.resource-type.datasource', 'Data source');
|
||||
case 'DASHBOARD':
|
||||
return t('migrate-to-cloud.resource-type.dashboard', 'Dashboard');
|
||||
case 'FOLDER':
|
||||
return t('migrate-to-cloud.resource-type.folder', 'Folder');
|
||||
default:
|
||||
return t('migrate-to-cloud.resource-type.unknown', 'Unknown');
|
||||
}
|
||||
}
|
@ -786,9 +786,14 @@
|
||||
"not-migrated": "Not yet uploaded",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"resource-table": {
|
||||
"unknown-datasource-title": "Data source {{datasourceUID}}",
|
||||
"unknown-datasource-type": "Unknown data source"
|
||||
},
|
||||
"resource-type": {
|
||||
"dashboard": "Dashboard",
|
||||
"datasource": "Data source",
|
||||
"folder": "Folder",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"summary": {
|
||||
|
@ -786,9 +786,14 @@
|
||||
"not-migrated": "Ńőŧ yęŧ ūpľőäđęđ",
|
||||
"unknown": "Ůʼnĸʼnőŵʼn"
|
||||
},
|
||||
"resource-table": {
|
||||
"unknown-datasource-title": "Đäŧä şőūřčę {{datasourceUID}}",
|
||||
"unknown-datasource-type": "Ůʼnĸʼnőŵʼn đäŧä şőūřčę"
|
||||
},
|
||||
"resource-type": {
|
||||
"dashboard": "Đäşĥþőäřđ",
|
||||
"datasource": "Đäŧä şőūřčę",
|
||||
"folder": "Főľđęř",
|
||||
"unknown": "Ůʼnĸʼnőŵʼn"
|
||||
},
|
||||
"summary": {
|
||||
|
@ -6277,9 +6277,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ItemStatus": {
|
||||
"type": "string"
|
||||
},
|
||||
"JSONWebKey": {
|
||||
"description": "JSONWebKey represents a public or private key in JWK format. It can be\nmarshaled into JSON and unmarshaled from JSON.",
|
||||
"properties": {
|
||||
@ -6739,17 +6736,28 @@
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/ItemStatus"
|
||||
"enum": [
|
||||
"OK",
|
||||
"ERROR"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/MigrateDataType"
|
||||
"enum": [
|
||||
"DASHBOARD",
|
||||
"DATASOURCE",
|
||||
"FOLDER"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"refId",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MigrateDataType": {
|
||||
"type": "string"
|
||||
},
|
||||
"MoveFolderCommand": {
|
||||
"description": "MoveFolderCommand captures the information required by the folder service\nto move a folder.",
|
||||
"properties": {
|
||||
|
Loading…
Reference in New Issue
Block a user