mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
E2C: Improvements to workflow (#91045)
* E2C: Improvements to workflow * no eslint comment * i18n * i18n: * lint
This commit is contained in:
parent
8e006fedda
commit
d7e85354d1
@ -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"]
|
||||
|
@ -26,7 +26,7 @@ export const CallToAction = () => {
|
||||
<ConnectModal
|
||||
isOpen={modalOpen}
|
||||
isLoading={createMigrationResponse.isLoading}
|
||||
isError={createMigrationResponse.isError}
|
||||
error={createMigrationResponse.error}
|
||||
onConfirm={createMigration}
|
||||
hideModal={() => setModalOpen(false)}
|
||||
/>
|
||||
|
@ -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}
|
||||
|
@ -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" />;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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)}</>;
|
||||
}
|
||||
|
5
public/app/features/migrate-to-cloud/onprem/types.ts
Normal file
5
public/app/features/migrate-to-cloud/onprem/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { MigrateDataResponseItemDto } from '../api';
|
||||
|
||||
export interface ResourceTableItem extends MigrateDataResponseItemDto {
|
||||
showError: (resource: ResourceTableItem) => void;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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",
|
||||
|
@ -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": "Ēřřőř",
|
||||
|
Loading…
Reference in New Issue
Block a user