E2C: Improvements to workflow (#91045)

* E2C: Improvements to workflow

* no eslint comment

* i18n

* i18n:

* lint
This commit is contained in:
Josh Hunt 2024-07-26 16:56:03 +01:00 committed by GitHub
parent 8e006fedda
commit d7e85354d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 224 additions and 61 deletions

View File

@ -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 <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
],
"public/app/features/notifications/StoredNotifications.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]

View File

@ -26,7 +26,7 @@ export const CallToAction = () => {
<ConnectModal
isOpen={modalOpen}
isLoading={createMigrationResponse.isLoading}
isError={createMigrationResponse.isError}
error={createMigrationResponse.error}
onConfirm={createMigration}
hideModal={() => setModalOpen(false)}
/>

View File

@ -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<unknown>;
}
@ -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
</Trans>
</div>
{isError && (
<Alert
{error ? (
<AlertWithTraceID
error={error}
severity="error"
title={t('migrate-to-cloud.connect-modal.token-error-title', 'Error saving token')}
>
<Trans i18nKey="migrate-to-cloud.connect-modal.token-error-description">
There was an error saving the token. See the Grafana server logs for more details.
</Trans>
</Alert>
)}
</AlertWithTraceID>
) : undefined}
<Field
className={styles.field}

View File

@ -7,10 +7,13 @@ 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 { useGetFolderQuery } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
import { useGetDashboardByUidQuery, MigrateDataResponseItemDto } from '../api';
import { useGetDashboardByUidQuery } from '../api';
export function NameCell(props: CellProps<MigrateDataResponseItemDto>) {
import { ResourceTableItem } from './types';
export function NameCell(props: CellProps<ResourceTableItem>) {
const data = props.row.original;
return (
@ -18,12 +21,23 @@ export function NameCell(props: CellProps<MigrateDataResponseItemDto>) {
<ResourceIcon resource={data} />
<Stack direction="column" gap={0}>
{data.type === 'DATASOURCE' ? <DatasourceInfo data={data} /> : <DashboardInfo data={data} />}
<ResourceInfo data={data} />
</Stack>
</Stack>
);
}
function ResourceInfo({ data }: { data: ResourceTableItem }) {
switch (data.type) {
case 'DASHBOARD':
return <DashboardInfo data={data} />;
case 'DATASOURCE':
return <DatasourceInfo data={data} />;
case 'FOLDER':
return <FolderInfo data={data} />;
}
}
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 <InfoSkeleton />;
}
if (isError) {
return (
<>
<Text italic>Unable to load dashboard</Text>
<Text color="secondary">Dashboard {data.refId}</Text>
</>
);
}
const parentFolderName = folderData.parents?.[folderData.parents.length - 1]?.title;
return (
<>
<span>{folderData.title}</span>
<Text color="secondary">{parentFolderName ?? 'Dashboards'}</Text>
</>
);
}
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 <Icon size="xl" name="dashboard" />;
}
if (resource.type === 'DATASOURCE' && datasource?.meta?.info?.logos?.small) {
} else if (resource.type === 'FOLDER') {
return <Icon size="xl" name="folder" />;
} else 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" />;

View File

@ -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<SnapshotDto['status']> = [
'PROCESSING',
];
const SNAPSHOT_REBUILD_STATUSES: Array<SnapshotDto['status']> = ['FINISHED', 'ERROR', 'UNKNOWN'];
const SNAPSHOT_REBUILD_STATUSES: Array<SnapshotDto['status']> = ['PENDING_PROCESSING', 'FINISHED', 'ERROR', 'UNKNOWN'];
const SNAPSHOT_BUILDING_STATUSES: Array<SnapshotDto['status']> = ['INITIALIZING', 'CREATING'];
@ -166,7 +166,8 @@ export const Page = () => {
{/* TODO: show errors from all mutation's in a... modal? */}
{createSnapshotResult.isError && (
<Alert
<AlertWithTraceID
error={createSnapshotResult.error}
severity="error"
title={t('migrate-to-cloud.summary.run-migration-error-title', 'Error creating snapshot')}
>
@ -175,13 +176,7 @@ export const Page = () => {
See the Grafana server logs for more details
</Trans>
</Text>
{maybeGetTraceID(createSnapshotResult.error) && (
// Deliberately don't want to translate 'Trace ID'
// eslint-disable-next-line @grafana/no-untranslated-strings
<Text element="p">Trace ID: {maybeGetTraceID(createSnapshotResult.error)}</Text>
)}
</Alert>
</AlertWithTraceID>
)}
{disconnectResult.isError && (
@ -247,13 +242,3 @@ export const Page = () => {
</>
);
};
function maybeGetTraceID(err: unknown) {
const data = isFetchError<unknown>(err) ? err.data : undefined;
if (typeof data === 'object' && data && 'traceID' in data && typeof data.traceID === 'string') {
return data.traceID;
}
return undefined;
}

View File

@ -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 (
<Modal
title={t('migrate-to-cloud.resource-error.title', 'Unable to migrate this resource')}
isOpen={Boolean(resource)}
onDismiss={onClose}
>
{resource && (
<Stack direction="column" gap={2} alignItems="flex-start">
<Text element="p" weight="bold">
<Trans i18nKey="migrate-to-cloud.resource-error.resource-summary">
{{ refId }} ({{ typeName }})
</Trans>
</Text>
{resource.error ? (
<>
<Text element="p">
<Trans i18nKey="migrate-to-cloud.resource-error.specific-error">The specific error was:</Trans>
</Text>
<Text element="p" weight="bold">
{resource.error}
</Text>
</>
) : (
<Text element="p">
<Trans i18nKey="migrate-to-cloud.resource-error.unknown-error">An unknown error occurred.</Trans>
</Text>
)}
<Button onClick={onClose}>
<Trans i18nKey="migrate-to-cloud.resource-error.dismiss-button">OK</Trans>
</Button>
</Stack>
)}
</Modal>
);
}

View File

@ -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 <InteractiveTable columns={columns} data={resources} getRowId={(r) => r.refId} pageSize={15} />;
const [erroredResource, setErroredResource] = useState<ResourceTableItem | undefined>();
const handleShowErrorModal = useCallback((resource: ResourceTableItem) => {
setErroredResource(resource);
}, []);
const data = useMemo(() => {
return resources.map((r) => ({ ...r, showError: handleShowErrorModal }));
}, [resources, handleShowErrorModal]);
return (
<>
<InteractiveTable columns={columns} data={data} getRowId={(r) => r.refId} pageSize={15} />
<ResourceErrorModal resource={erroredResource} onClose={() => setErroredResource(undefined)} />
</>
);
}

View File

@ -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<MigrateDataResponseItemDto>) {
const { status, error } = props.row.original;
export function StatusCell(props: CellProps<ResourceTableItem>) {
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 <Text color="secondary">{t('migrate-to-cloud.resource-status.not-migrated', 'Not yet uploaded')}</Text>;
} else if (status === 'OK') {
} else if (item.status === 'OK') {
return <Text color="success">{t('migrate-to-cloud.resource-status.migrated', 'Uploaded to cloud')}</Text>;
} else if (status === 'ERROR') {
} else if (item.status === 'ERROR') {
return <ErrorCell item={item} />;
}
return <Text color="secondary">{t('migrate-to-cloud.resource-status.unknown', 'Unknown')}</Text>;
}
function ErrorCell({ item }: { item: ResourceTableItem }) {
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)}>
{item.error && (
<Button size="sm" variant="secondary" onClick={() => item.showError(item)}>
{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

@ -1,11 +1,9 @@
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;
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<MigrateDataResponseItemDto>) {
return t('migrate-to-cloud.resource-type.unknown', 'Unknown');
}
}
export function TypeCell(props: CellProps<ResourceTableItem>) {
const { type } = props.row.original;
return <>{prettyTypeName(type)}</>;
}

View File

@ -0,0 +1,5 @@
import { MigrateDataResponseItemDto } from '../api';
export interface ResourceTableItem extends MigrateDataResponseItemDto {
showError: (resource: ResourceTableItem) => void;
}

View File

@ -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 (
<Alert {...rest}>
<Stack direction="column" gap={1}>
{children}
{/* Deliberately don't want to translate 'Trace ID' */}
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
{traceID && <Text element="p">Trace ID: {traceID}</Text>}
</Stack>
</Alert>
);
}
function maybeGetTraceID(err: unknown) {
const data = isFetchError<unknown>(err) ? err.data : err;
if (typeof data === 'object' && data && 'traceID' in data && typeof data.traceID === 'string') {
return data.traceID;
}
return undefined;
}

View File

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

View File

@ -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": "Ēřřőř",