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:
Josh Hunt 2024-04-05 10:08:42 +01:00 committed by GitHub
parent c033a15aaa
commit 5ce8b60878
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 534 additions and 233 deletions

View File

@ -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"`
}

View File

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

View File

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

View File

@ -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;

View File

@ -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;
};
}

View File

@ -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,
};
}

View File

@ -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;
}

View 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,
};
}

View 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;
}

View File

@ -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]);
}

View File

@ -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();
});
});

View File

@ -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} />;
}

View 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>;
}

View 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');
}
}

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {