From d7e85354d1175825da489caf6e2b5b2812788820 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 26 Jul 2024 16:56:03 +0100 Subject: [PATCH] E2C: Improvements to workflow (#91045) * E2C: Improvements to workflow * no eslint comment * i18n * i18n: * lint --- .betterer.results | 4 +- .../EmptyState/CallToAction/CallToAction.tsx | 2 +- .../EmptyState/CallToAction/ConnectModal.tsx | 16 ++--- .../migrate-to-cloud/onprem/NameCell.tsx | 58 ++++++++++++++++--- .../features/migrate-to-cloud/onprem/Page.tsx | 25 ++------ .../onprem/ResourceErrorModal.tsx | 55 ++++++++++++++++++ .../onprem/ResourcesTable.tsx | 22 ++++++- .../migrate-to-cloud/onprem/StatusCell.tsx | 39 +++++++------ .../migrate-to-cloud/onprem/TypeCell.tsx | 11 ++-- .../features/migrate-to-cloud/onprem/types.ts | 5 ++ .../shared/AlertWithTraceID.tsx | 34 +++++++++++ public/locales/en-US/grafana.json | 7 +++ public/locales/pseudo-LOCALE/grafana.json | 7 +++ 13 files changed, 224 insertions(+), 61 deletions(-) create mode 100644 public/app/features/migrate-to-cloud/onprem/ResourceErrorModal.tsx create mode 100644 public/app/features/migrate-to-cloud/onprem/types.ts create mode 100644 public/app/features/migrate-to-cloud/shared/AlertWithTraceID.tsx 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": "Ēřřőř",