diff --git a/.betterer.results b/.betterer.results
index a633d89cd76..47ed66a9318 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -4744,7 +4744,9 @@ exports[`better eslint`] = {
],
"public/app/features/migrate-to-cloud/onprem/NameCell.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "1"]
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "1"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "2"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "3"]
],
"public/app/features/notifications/StoredNotifications.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"]
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 d9c6153780a..ba5816d0a22 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
@@ -26,7 +26,7 @@ export const CallToAction = () => {
setModalOpen(false)}
/>
diff --git a/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/ConnectModal.tsx b/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/ConnectModal.tsx
index a3f52b27b24..5b8bf4b5961 100644
--- a/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/ConnectModal.tsx
+++ b/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/ConnectModal.tsx
@@ -3,15 +3,16 @@ import { useId } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
-import { Modal, Button, Stack, TextLink, Field, Input, Text, useStyles2, Alert } from '@grafana/ui';
+import { Modal, Button, Stack, TextLink, Field, Input, Text, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
+import { AlertWithTraceID } from 'app/features/migrate-to-cloud/shared/AlertWithTraceID';
import { CreateSessionApiArg } from '../../../api';
interface Props {
isOpen: boolean;
isLoading: boolean;
- isError: boolean;
+ error: unknown;
hideModal: () => void;
onConfirm: (connectStackData: CreateSessionApiArg) => Promise;
}
@@ -20,7 +21,7 @@ interface FormData {
token: string;
}
-export const ConnectModal = ({ isOpen, isLoading, isError, hideModal, onConfirm }: Props) => {
+export const ConnectModal = ({ isOpen, isLoading, error, hideModal, onConfirm }: Props) => {
const tokenId = useId();
const styles = useStyles2(getStyles);
@@ -94,16 +95,17 @@ export const ConnectModal = ({ isOpen, isLoading, isError, hideModal, onConfirm
- {isError && (
-
There was an error saving the token. See the Grafana server logs for more details.
-
- )}
+
+ ) : undefined}
) {
+import { ResourceTableItem } from './types';
+
+export function NameCell(props: CellProps) {
const data = props.row.original;
return (
@@ -18,12 +21,23 @@ export function NameCell(props: CellProps) {
- {data.type === 'DATASOURCE' ? : }
+
);
}
+function ResourceInfo({ data }: { data: ResourceTableItem }) {
+ switch (data.type) {
+ case 'DASHBOARD':
+ return ;
+ case 'DATASOURCE':
+ return ;
+ case 'FOLDER':
+ return ;
+ }
+}
+
function getDashboardTitle(dashboardData: object) {
if ('title' in dashboardData && typeof dashboardData.title === 'string') {
return dashboardData.title;
@@ -32,7 +46,7 @@ function getDashboardTitle(dashboardData: object) {
return undefined;
}
-function DatasourceInfo({ data }: { data: MigrateDataResponseItemDto }) {
+function DatasourceInfo({ data }: { data: ResourceTableItem }) {
const datasourceUID = data.refId;
const datasource = useDatasource(datasourceUID);
@@ -59,7 +73,7 @@ function DatasourceInfo({ data }: { data: MigrateDataResponseItemDto }) {
);
}
-function DashboardInfo({ data }: { data: MigrateDataResponseItemDto }) {
+function DashboardInfo({ data }: { data: ResourceTableItem }) {
const dashboardUID = data.refId;
// TODO: really, the API should return this directly
const { data: dashboardData, isError } = useGetDashboardByUidQuery({
@@ -92,6 +106,32 @@ function DashboardInfo({ data }: { data: MigrateDataResponseItemDto }) {
);
}
+function FolderInfo({ data }: { data: ResourceTableItem }) {
+ const { data: folderData, isLoading, isError } = useGetFolderQuery(data.refId);
+
+ if (isLoading || !folderData) {
+ return ;
+ }
+
+ if (isError) {
+ return (
+ <>
+ Unable to load dashboard
+ Dashboard {data.refId}
+ >
+ );
+ }
+
+ const parentFolderName = folderData.parents?.[folderData.parents.length - 1]?.title;
+
+ return (
+ <>
+ {folderData.title}
+ {parentFolderName ?? 'Dashboards'}
+ >
+ );
+}
+
function InfoSkeleton() {
return (
<>
@@ -101,15 +141,15 @@ function InfoSkeleton() {
);
}
-function ResourceIcon({ resource }: { resource: MigrateDataResponseItemDto }) {
+function ResourceIcon({ resource }: { resource: ResourceTableItem }) {
const styles = useStyles2(getIconStyles);
const datasource = useDatasource(resource.type === 'DATASOURCE' ? resource.refId : undefined);
if (resource.type === 'DASHBOARD') {
return ;
- }
-
- if (resource.type === 'DATASOURCE' && datasource?.meta?.info?.logos?.small) {
+ } else if (resource.type === 'FOLDER') {
+ return ;
+ } else if (resource.type === 'DATASOURCE' && datasource?.meta?.info?.logos?.small) {
return
;
} else if (resource.type === 'DATASOURCE') {
return ;
diff --git a/public/app/features/migrate-to-cloud/onprem/Page.tsx b/public/app/features/migrate-to-cloud/onprem/Page.tsx
index 0c887e65dd1..899dfeee8b7 100644
--- a/public/app/features/migrate-to-cloud/onprem/Page.tsx
+++ b/public/app/features/migrate-to-cloud/onprem/Page.tsx
@@ -1,7 +1,6 @@
import { skipToken } from '@reduxjs/toolkit/query/react';
import { useCallback, useEffect, useState } from 'react';
-import { isFetchError } from '@grafana/runtime';
import { Alert, Box, Stack, Text } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
@@ -15,6 +14,7 @@ import {
useGetSnapshotQuery,
useUploadSnapshotMutation,
} from '../api';
+import { AlertWithTraceID } from '../shared/AlertWithTraceID';
import { DisconnectModal } from './DisconnectModal';
import { EmptyState } from './EmptyState/EmptyState';
@@ -57,7 +57,7 @@ const SHOULD_POLL_STATUSES: Array = [
'PROCESSING',
];
-const SNAPSHOT_REBUILD_STATUSES: Array = ['FINISHED', 'ERROR', 'UNKNOWN'];
+const SNAPSHOT_REBUILD_STATUSES: Array = ['PENDING_PROCESSING', 'FINISHED', 'ERROR', 'UNKNOWN'];
const SNAPSHOT_BUILDING_STATUSES: Array = ['INITIALIZING', 'CREATING'];
@@ -166,7 +166,8 @@ export const Page = () => {
{/* TODO: show errors from all mutation's in a... modal? */}
{createSnapshotResult.isError && (
-
@@ -175,13 +176,7 @@ export const Page = () => {
See the Grafana server logs for more details
-
- {maybeGetTraceID(createSnapshotResult.error) && (
- // Deliberately don't want to translate 'Trace ID'
- // eslint-disable-next-line @grafana/no-untranslated-strings
- Trace ID: {maybeGetTraceID(createSnapshotResult.error)}
- )}
-
+
)}
{disconnectResult.isError && (
@@ -247,13 +242,3 @@ export const Page = () => {
>
);
};
-
-function maybeGetTraceID(err: unknown) {
- const data = isFetchError(err) ? err.data : undefined;
-
- if (typeof data === 'object' && data && 'traceID' in data && typeof data.traceID === 'string') {
- return data.traceID;
- }
-
- return undefined;
-}
diff --git a/public/app/features/migrate-to-cloud/onprem/ResourceErrorModal.tsx b/public/app/features/migrate-to-cloud/onprem/ResourceErrorModal.tsx
new file mode 100644
index 00000000000..9c95e441c16
--- /dev/null
+++ b/public/app/features/migrate-to-cloud/onprem/ResourceErrorModal.tsx
@@ -0,0 +1,55 @@
+import { Button, Modal, Stack, Text } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+
+import { prettyTypeName } from './TypeCell';
+import { ResourceTableItem } from './types';
+
+interface ResourceErrorModalProps {
+ resource: ResourceTableItem | undefined;
+ onClose: () => void;
+}
+
+export function ResourceErrorModal(props: ResourceErrorModalProps) {
+ const { resource, onClose } = props;
+
+ const refId = resource?.refId;
+ const typeName = resource && prettyTypeName(resource.type);
+
+ return (
+
+ {resource && (
+
+
+
+ {{ refId }} ({{ typeName }})
+
+
+
+ {resource.error ? (
+ <>
+
+ The specific error was:
+
+
+
+ {resource.error}
+
+ >
+ ) : (
+
+ An unknown error occurred.
+
+ )}
+
+
+
+ )}
+
+ );
+}
diff --git a/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx b/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx
index 727a05f5511..47f9a3536eb 100644
--- a/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx
+++ b/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx
@@ -1,10 +1,14 @@
+import { useCallback, useMemo, useState } from 'react';
+
import { InteractiveTable } from '@grafana/ui';
import { MigrateDataResponseItemDto } from '../api';
import { NameCell } from './NameCell';
+import { ResourceErrorModal } from './ResourceErrorModal';
import { StatusCell } from './StatusCell';
import { TypeCell } from './TypeCell';
+import { ResourceTableItem } from './types';
interface ResourcesTableProps {
resources: MigrateDataResponseItemDto[];
@@ -17,5 +21,21 @@ const columns = [
];
export function ResourcesTable({ resources }: ResourcesTableProps) {
- return r.refId} pageSize={15} />;
+ const [erroredResource, setErroredResource] = useState();
+
+ const handleShowErrorModal = useCallback((resource: ResourceTableItem) => {
+ setErroredResource(resource);
+ }, []);
+
+ const data = useMemo(() => {
+ return resources.map((r) => ({ ...r, showError: handleShowErrorModal }));
+ }, [resources, handleShowErrorModal]);
+
+ return (
+ <>
+ r.refId} pageSize={15} />
+
+ setErroredResource(undefined)} />
+ >
+ );
}
diff --git a/public/app/features/migrate-to-cloud/onprem/StatusCell.tsx b/public/app/features/migrate-to-cloud/onprem/StatusCell.tsx
index aff69dae884..caf0c7b82fc 100644
--- a/public/app/features/migrate-to-cloud/onprem/StatusCell.tsx
+++ b/public/app/features/migrate-to-cloud/onprem/StatusCell.tsx
@@ -1,32 +1,35 @@
import { CellProps, Text, Stack, Button } from '@grafana/ui';
import { t } from 'app/core/internationalization';
-import { MigrateDataResponseItemDto } from '../api';
+import { ResourceTableItem } from './types';
-export function StatusCell(props: CellProps) {
- const { status, error } = props.row.original;
+export function StatusCell(props: CellProps) {
+ const item = props.row.original;
// Keep these here to preserve the translations
// t('migrate-to-cloud.resource-status.migrating', 'Uploading...')
- if (status === 'PENDING') {
+ if (item.status === 'PENDING') {
return {t('migrate-to-cloud.resource-status.not-migrated', 'Not yet uploaded')};
- } else if (status === 'OK') {
+ } else if (item.status === 'OK') {
return {t('migrate-to-cloud.resource-status.migrated', 'Uploaded to cloud')};
- } else if (status === 'ERROR') {
- return (
-
- {t('migrate-to-cloud.resource-status.failed', 'Error')}
-
- {error && (
- // TODO: trigger a proper modal, probably from the parent, on click
-
- )}
-
- );
+ } else if (item.status === 'ERROR') {
+ return ;
}
return {t('migrate-to-cloud.resource-status.unknown', 'Unknown')};
}
+
+function ErrorCell({ item }: { item: ResourceTableItem }) {
+ return (
+
+ {t('migrate-to-cloud.resource-status.failed', 'Error')}
+
+ {item.error && (
+
+ )}
+
+ );
+}
diff --git a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx
index 769183d1bd7..2972ec65c9d 100644
--- a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx
+++ b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx
@@ -1,11 +1,9 @@
import { CellProps } from '@grafana/ui';
import { t } from 'app/core/internationalization';
-import { MigrateDataResponseItemDto } from '../api';
-
-export function TypeCell(props: CellProps) {
- const { type } = props.row.original;
+import { ResourceTableItem } from './types';
+export function prettyTypeName(type: ResourceTableItem['type']) {
switch (type) {
case 'DATASOURCE':
return t('migrate-to-cloud.resource-type.datasource', 'Data source');
@@ -17,3 +15,8 @@ export function TypeCell(props: CellProps) {
return t('migrate-to-cloud.resource-type.unknown', 'Unknown');
}
}
+
+export function TypeCell(props: CellProps) {
+ const { type } = props.row.original;
+ return <>{prettyTypeName(type)}>;
+}
diff --git a/public/app/features/migrate-to-cloud/onprem/types.ts b/public/app/features/migrate-to-cloud/onprem/types.ts
new file mode 100644
index 00000000000..63e1979dd71
--- /dev/null
+++ b/public/app/features/migrate-to-cloud/onprem/types.ts
@@ -0,0 +1,5 @@
+import { MigrateDataResponseItemDto } from '../api';
+
+export interface ResourceTableItem extends MigrateDataResponseItemDto {
+ showError: (resource: ResourceTableItem) => void;
+}
diff --git a/public/app/features/migrate-to-cloud/shared/AlertWithTraceID.tsx b/public/app/features/migrate-to-cloud/shared/AlertWithTraceID.tsx
new file mode 100644
index 00000000000..97e7745aab4
--- /dev/null
+++ b/public/app/features/migrate-to-cloud/shared/AlertWithTraceID.tsx
@@ -0,0 +1,34 @@
+import { isFetchError } from '@grafana/runtime';
+import { Alert, Stack, Text } from '@grafana/ui';
+import { Props as AlertProps } from '@grafana/ui/src/components/Alert/Alert';
+
+interface AlertWithTraceIDProps extends AlertProps {
+ error?: unknown;
+}
+
+export function AlertWithTraceID(props: AlertWithTraceIDProps) {
+ const { error, children, ...rest } = props;
+ const traceID = maybeGetTraceID(error);
+
+ return (
+
+
+ {children}
+
+ {/* Deliberately don't want to translate 'Trace ID' */}
+ {/* eslint-disable-next-line @grafana/no-untranslated-strings */}
+ {traceID && Trace ID: {traceID}}
+
+
+ );
+}
+
+function maybeGetTraceID(err: unknown) {
+ const data = isFetchError(err) ? err.data : err;
+
+ if (typeof data === 'object' && data && 'traceID' in data && typeof data.traceID === 'string') {
+ return data.traceID;
+ }
+
+ return undefined;
+}
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 33afbe97169..8e8afab441c 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -1087,6 +1087,13 @@
"message": "Help us improve this feature by providing feedback and reporting any issues.",
"title": "Migrate to Grafana Cloud is in public preview"
},
+ "resource-error": {
+ "dismiss-button": "OK",
+ "resource-summary": "{{refId}} ({{typeName}})",
+ "specific-error": "The specific error was:",
+ "title": "Unable to migrate this resource",
+ "unknown-error": "An unknown error occurred."
+ },
"resource-status": {
"error-details-button": "Details",
"failed": "Error",
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index dbdee0e3787..66924c0e8e3 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -1087,6 +1087,13 @@
"message": "Ħęľp ūş įmpřővę ŧĥįş ƒęäŧūřę þy přővįđįʼnģ ƒęęđþäčĸ äʼnđ řępőřŧįʼnģ äʼny įşşūęş.",
"title": "Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ įş įʼn pūþľįč přęvįęŵ"
},
+ "resource-error": {
+ "dismiss-button": "ØĶ",
+ "resource-summary": "{{refId}} ({{typeName}})",
+ "specific-error": "Ŧĥę şpęčįƒįč ęřřőř ŵäş:",
+ "title": "Ůʼnäþľę ŧő mįģřäŧę ŧĥįş řęşőūřčę",
+ "unknown-error": "Åʼn ūʼnĸʼnőŵʼn ęřřőř őččūřřęđ."
+ },
"resource-status": {
"error-details-button": "Đęŧäįľş",
"failed": "Ēřřőř",