E2C: Resources table (#84317)

* first pass at mock api and table

* finish up resources table

* i18n

* centre icon:
This commit is contained in:
Josh Hunt
2024-03-14 09:46:18 +00:00
committed by GitHub
parent 5d23bc48b4
commit e6949d9838
5 changed files with 228 additions and 8 deletions
@@ -41,12 +41,86 @@ export interface ConnectStackDTO {
token: string;
}
export interface MigrationResourceDTO {
uid: string;
status: 'not-migrated' | 'migrated' | 'migrating' | 'failed';
statusMessage?: string;
type: 'datasource' | 'dashboard'; // TODO: in future this would be a discriminated union with the resource details
resource: {
uid: string;
name: string;
type: string;
icon?: string;
};
}
const mockApplications = ['auth-service', 'web server', 'backend'];
const mockEnvs = ['DEV', 'PROD'];
const mockRoles = ['db', 'load-balancer', 'server', 'logs'];
const mockDataSources = ['Prometheus', 'Loki', 'AWS Athena', 'AWS Cloudwatch', 'InfluxDB', 'Elasticsearch'];
const mockDataSourceMetadata: Record<string, { image: string }> = {
Prometheus: {
image: 'https://grafana.com/api/plugins/prometheus/versions/5.0.0/logos/small',
},
Loki: {
image: 'https://grafana.com/api/plugins/loki/versions/5.0.0/logos/small',
},
'AWS Athena': {
image: 'https://grafana.com/api/plugins/grafana-athena-datasource/versions/2.13.5/logos/small',
},
'AWS Cloudwatch': {
image: 'https://grafana.com/api/plugins/computest-cloudwatchalarm-datasource/versions/2.0.0/logos/small',
},
InfluxDB: {
image: 'https://grafana.com/api/plugins/influxdb/versions/5.0.0/logos/small',
},
Elasticsearch: {
image: 'https://grafana.com/api/plugins/elasticsearch/versions/5.0.0/logos/small',
},
};
const mockMigrationResources: MigrationResourceDTO[] = Array.from({ length: 500 }).map((_, index) => {
const dataSource = mockDataSources[index % mockDataSources.length];
const environment = mockEnvs[index % mockEnvs.length];
const application = mockApplications[index % mockApplications.length];
const role = mockRoles[index % mockRoles.length];
return {
status: 'not-migrated',
type: 'datasource',
uid: index.toString(16),
resource: {
uid: `${application}-${environment}-${role}-${index}`,
name: `${application} ${environment} ${role}`,
icon: mockDataSourceMetadata[dataSource]?.image,
type: dataSource,
},
};
});
mockMigrationResources[0].status = 'migrated';
mockMigrationResources[1].status = 'failed';
mockMigrationResources[1].statusMessage = `Source map error: Error: request failed with status 404
Resource URL: http://localhost:3000/public/build/app.f4d0c6a0daa6a5b14892.js
Source Map URL: app.f4d0c6a0daa6a5b14892.js.map`;
mockMigrationResources[2].status = 'migrated';
mockMigrationResources[3].status = 'migrated';
mockMigrationResources[4].status = 'migrating';
mockMigrationResources[5].status = 'migrating';
// TODO remove these mock properties/functions
const queryParams = new URLSearchParams(window.location.search);
const MOCK_DELAY_MS = 1000;
const MOCK_TOKEN = 'TODO_thisWillBeABigLongToken';
let HAS_MIGRATION_TOKEN = false;
let HAS_STACK_DETAILS = false;
let STACK_URL: string | undefined;
let HAS_STACK_DETAILS = !!queryParams.get('mockStackURL');
let STACK_URL: string | undefined = queryParams.get('mockStackURL') || undefined;
function dataWithMockDelay<T>(data: T): Promise<{ data: T }> {
return new Promise((resolve) => {
@@ -57,7 +131,7 @@ function dataWithMockDelay<T>(data: T): Promise<{ data: T }> {
}
export const migrateToCloudAPI = createApi({
tagTypes: ['migrationToken', 'stackDetails'],
tagTypes: ['migrationToken', 'stackDetails', 'resource'],
reducerPath: 'migrateToCloudAPI',
baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }),
endpoints: (builder) => ({
@@ -72,6 +146,7 @@ export const migrateToCloudAPI = createApi({
return dataWithMockDelay(responseData);
},
}),
connectStack: builder.mutation<void, ConnectStackDTO>({
invalidatesTags: ['stackDetails'],
queryFn: async ({ stackURL }) => {
@@ -80,6 +155,7 @@ export const migrateToCloudAPI = createApi({
return dataWithMockDelay(undefined);
},
}),
disconnectStack: builder.mutation<void, void>({
invalidatesTags: ['stackDetails'],
queryFn: async () => {
@@ -110,6 +186,13 @@ export const migrateToCloudAPI = createApi({
return dataWithMockDelay(HAS_MIGRATION_TOKEN);
},
}),
listResources: builder.query<MigrationResourceDTO[], void>({
providesTags: ['resource'],
queryFn: async () => {
return dataWithMockDelay(mockMigrationResources);
},
}),
}),
});
@@ -120,4 +203,5 @@ export const {
useCreateMigrationTokenMutation,
useDeleteMigrationTokenMutation,
useHasMigrationTokenQuery,
useListResourcesQuery,
} = migrateToCloudAPI;
@@ -1,29 +1,34 @@
import { skipToken } from '@reduxjs/toolkit/query/react';
import React, { useState } from 'react';
import { Button, Stack, Text } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { useGetStatusQuery } from '../api';
import { useGetStatusQuery, useListResourcesQuery } from '../api';
import { DisconnectModal } from './DisconnectModal';
import { EmptyState } from './EmptyState/EmptyState';
import { ResourcesTable } from './ResourcesTable';
export const Page = () => {
const { data, isFetching } = useGetStatusQuery();
const { data: status, isFetching } = useGetStatusQuery();
const { data: resources } = useListResourcesQuery(status?.enabled ? undefined : skipToken);
const [isDisconnecting, setIsDisconnecting] = useState(false);
if (!data?.enabled) {
if (!status?.enabled) {
return <EmptyState />;
}
return (
<>
<Stack alignItems="center">
{data.stackURL && <Text variant="h4">{data.stackURL}</Text>}
<Stack direction="column" alignItems="flex-start">
{status.stackURL && <Text variant="h4">{status.stackURL}</Text>}
<Button disabled={isFetching || isDisconnecting} variant="secondary" onClick={() => setIsDisconnecting(true)}>
<Trans i18nKey="migrate-to-cloud.resources.disconnect">Disconnect</Trans>
</Button>
{resources && <ResourcesTable resources={resources} />}
</Stack>
<DisconnectModal isOpen={isDisconnecting} onDismiss={() => setIsDisconnecting(false)} />
@@ -0,0 +1,103 @@
import { css } from '@emotion/css';
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 { MigrationResourceDTO } from '../api';
interface ResourcesTableProps {
resources: MigrationResourceDTO[];
}
const columns = [
{ id: 'name', header: 'Name', cell: NameCell },
{ id: 'type', header: 'Type', cell: TypeCell },
{ id: 'status', header: 'Status', cell: StatusCell },
];
export function ResourcesTable({ resources }: ResourcesTableProps) {
return <InteractiveTable columns={columns} data={resources} getRowId={(r) => r.uid} pageSize={15} />;
}
function NameCell(props: CellProps<MigrationResourceDTO>) {
const data = props.row.original;
return (
<Stack direction="row" gap={2} alignItems="center">
<ResourceIcon resource={data} />
<Stack direction="column" gap={0}>
<span>{data.resource.name}</span>
<Text color="secondary">{data.resource.type}</Text>
</Stack>
</Stack>
);
}
function TypeCell(props: CellProps<MigrationResourceDTO>) {
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<MigrationResourceDTO>) {
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: MigrationResourceDTO }) {
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'),
}),
};
}
+14
View File
@@ -724,6 +724,7 @@
"cancel": "Cancel",
"disconnect": "Disconnect",
"disconnecting": "Disconnecting...",
"error": "There was an error disconnecting",
"title": "Disconnect from cloud stack"
},
"get-started": {
@@ -776,6 +777,19 @@
"link-title": "Grafana Cloud pricing",
"title": "How much does it cost?"
},
"resource-status": {
"error-details-button": "Details",
"failed": "Error",
"migrated": "Uploaded to cloud",
"migrating": "Uploading...",
"not-migrated": "Not yet uploaded",
"unknown": "Unknown"
},
"resource-type": {
"dashboard": "Dashboard",
"datasource": "Data source",
"unknown": "Unknown"
},
"resources": {
"disconnect": "Disconnect"
},
+14
View File
@@ -724,6 +724,7 @@
"cancel": "Cäʼnčęľ",
"disconnect": "Đįşčőʼnʼnęčŧ",
"disconnecting": "Đįşčőʼnʼnęčŧįʼnģ...",
"error": "Ŧĥęřę ŵäş äʼn ęřřőř đįşčőʼnʼnęčŧįʼnģ",
"title": "Đįşčőʼnʼnęčŧ ƒřőm čľőūđ şŧäčĸ"
},
"get-started": {
@@ -776,6 +777,19 @@
"link-title": "Ğřäƒäʼnä Cľőūđ přįčįʼnģ",
"title": "Ħőŵ mūčĥ đőęş įŧ čőşŧ?"
},
"resource-status": {
"error-details-button": "Đęŧäįľş",
"failed": "Ēřřőř",
"migrated": "Ůpľőäđęđ ŧő čľőūđ",
"migrating": "Ůpľőäđįʼnģ...",
"not-migrated": "Ńőŧ yęŧ ūpľőäđęđ",
"unknown": "Ůʼnĸʼnőŵʼn"
},
"resource-type": {
"dashboard": "Đäşĥþőäřđ",
"datasource": "Đäŧä şőūřčę",
"unknown": "Ůʼnĸʼnőŵʼn"
},
"resources": {
"disconnect": "Đįşčőʼnʼnęčŧ"
},