E2C: Resources table (#84317)
* first pass at mock api and table * finish up resources table * i18n * centre icon:
This commit is contained in:
@@ -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'),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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ęčŧ"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user