Snapshots: Add loading skeleton (#79134)

* add snapshot skeleton

* add some unit tests
This commit is contained in:
Ashley Harrison 2023-12-07 10:48:06 +00:00 committed by GitHub
parent ebdffe21e6
commit 4a0c89a8fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 194 additions and 30 deletions

View File

@ -2,10 +2,12 @@ import React, { useState, useCallback } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { config } from '@grafana/runtime';
import { ConfirmModal, Button, LinkButton } from '@grafana/ui';
import { ConfirmModal } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { getDashboardSnapshotSrv, Snapshot } from 'app/features/dashboard/services/SnapshotSrv';
import { SnapshotListTableRow } from './SnapshotListTableRow';
export function getSnapshots() {
return getDashboardSnapshotSrv()
.getSnapshots()
@ -18,9 +20,12 @@ export function getSnapshots() {
}
export const SnapshotListTable = () => {
const [snapshots, setSnapshots] = useState<Snapshot[]>([]);
const [isFetching, setIsFetching] = useState(false);
const [removeSnapshot, setRemoveSnapshot] = useState<Snapshot | undefined>();
useAsync(async () => {
setIsFetching(true);
const response = await getSnapshots();
setIsFetching(false);
setSnapshots(response);
}, [setSnapshots]);
@ -58,34 +63,21 @@ export const SnapshotListTable = () => {
</tr>
</thead>
<tbody>
{snapshots.map((snapshot) => {
const url = snapshot.externalUrl || snapshot.url;
return (
<tr key={snapshot.key}>
<td>
<a href={url}>{snapshot.name}</a>
</td>
<td>
<a href={url}>{url}</a>
</td>
<td>
{snapshot.external && (
<span className="query-keyword">
<Trans i18nKey="snapshot.external-badge">External</Trans>
</span>
)}
</td>
<td className="text-center">
<LinkButton href={url} variant="secondary" size="sm" icon="eye">
<Trans i18nKey="snapshot.view-button">View</Trans>
</LinkButton>
</td>
<td className="text-right">
<Button variant="destructive" size="sm" icon="times" onClick={() => setRemoveSnapshot(snapshot)} />
</td>
</tr>
);
})}
{isFetching ? (
<>
<SnapshotListTableRow.Skeleton />
<SnapshotListTableRow.Skeleton />
<SnapshotListTableRow.Skeleton />
</>
) : (
snapshots.map((snapshot) => (
<SnapshotListTableRow
key={snapshot.key}
snapshot={snapshot}
onRemove={() => setRemoveSnapshot(snapshot)}
/>
))
)}
</tbody>
</table>

View File

@ -0,0 +1,100 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Snapshot } from 'app/features/dashboard/services/SnapshotSrv';
import { SnapshotListTableRow } from './SnapshotListTableRow';
describe('SnapshotListTableRow', () => {
const mockOnRemove = jest.fn();
const mockSnapshot = {
key: 'test',
name: 'Test Snapshot',
url: 'http://test.com',
external: false,
};
it('renders correctly', () => {
render(
<table>
<tbody>
<SnapshotListTableRow snapshot={mockSnapshot} onRemove={mockOnRemove} />
</tbody>
</table>
);
expect(screen.getByRole('row')).toBeInTheDocument();
expect(screen.getByRole('cell', { name: mockSnapshot.name })).toBeInTheDocument();
expect(screen.getByRole('cell', { name: mockSnapshot.url })).toBeInTheDocument();
expect(screen.queryByRole('cell', { name: 'External' })).not.toBeInTheDocument();
});
it('adds the correct href to the name, url and view buttons', () => {
render(
<table>
<tbody>
<SnapshotListTableRow snapshot={mockSnapshot} onRemove={mockOnRemove} />
</tbody>
</table>
);
const nameLink = screen.getByRole('link', { name: mockSnapshot.name });
const urlLink = screen.getByRole('link', { name: mockSnapshot.url });
const viewButton = screen.getByRole('link', { name: 'View' });
expect(nameLink).toHaveAttribute('href', mockSnapshot.url);
expect(urlLink).toHaveAttribute('href', mockSnapshot.url);
expect(viewButton).toHaveAttribute('href', mockSnapshot.url);
});
it('calls onRemove when delete button is clicked', async () => {
render(
<table>
<tbody>
<SnapshotListTableRow snapshot={mockSnapshot} onRemove={mockOnRemove} />
</tbody>
</table>
);
await userEvent.click(screen.getByRole('button'));
expect(mockOnRemove).toHaveBeenCalled();
});
describe('for an external snapshot', () => {
let mockSnapshotWithExternal: Snapshot;
beforeEach(() => {
mockSnapshotWithExternal = {
...mockSnapshot,
external: true,
externalUrl: 'http://external.com',
};
});
it('renders the external badge', () => {
render(
<table>
<tbody>
<SnapshotListTableRow snapshot={mockSnapshotWithExternal} onRemove={mockOnRemove} />
</tbody>
</table>
);
expect(screen.getByRole('cell', { name: 'External' })).toBeInTheDocument();
});
it('uses the external href for the name, url and view buttons', () => {
render(
<table>
<tbody>
<SnapshotListTableRow snapshot={mockSnapshotWithExternal} onRemove={mockOnRemove} />
</tbody>
</table>
);
const nameLink = screen.getByRole('link', { name: mockSnapshotWithExternal.name });
const urlLink = screen.getByRole('link', { name: mockSnapshotWithExternal.externalUrl });
const viewButton = screen.getByRole('link', { name: 'View' });
expect(nameLink).toHaveAttribute('href', mockSnapshotWithExternal.externalUrl);
expect(urlLink).toHaveAttribute('href', mockSnapshotWithExternal.externalUrl);
expect(viewButton).toHaveAttribute('href', mockSnapshotWithExternal.externalUrl);
});
});
});

View File

@ -0,0 +1,72 @@
import { css } from '@emotion/css';
import React from 'react';
import Skeleton from 'react-loading-skeleton';
import { Button, LinkButton, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { Snapshot } from 'app/features/dashboard/services/SnapshotSrv';
export interface Props {
snapshot: Snapshot;
onRemove: () => void;
}
export const SnapshotListTableRow = ({ snapshot, onRemove }: Props) => {
const url = snapshot.externalUrl || snapshot.url;
return (
<tr>
<td>
<a href={url}>{snapshot.name}</a>
</td>
<td>
<a href={url}>{url}</a>
</td>
<td>
{snapshot.external && (
<span className="query-keyword">
<Trans i18nKey="snapshot.external-badge">External</Trans>
</span>
)}
</td>
<td className="text-center">
<LinkButton href={url} variant="secondary" size="sm" icon="eye">
<Trans i18nKey="snapshot.view-button">View</Trans>
</LinkButton>
</td>
<td className="text-right">
<Button variant="destructive" size="sm" icon="times" onClick={onRemove} />
</td>
</tr>
);
};
const SnapshotListTableRowSkeleton = () => {
const styles = useStyles2(getSkeletonStyles);
return (
<tr>
<td>
<Skeleton width={80} />
</td>
<td>
<Skeleton width={240} />
</td>
<td></td>
<td>
<Skeleton width={63} height={24} containerClassName={styles.blockSkeleton} />
</td>
<td>
<Skeleton width={22} height={24} containerClassName={styles.blockSkeleton} />
</td>
</tr>
);
};
SnapshotListTableRow.Skeleton = SnapshotListTableRowSkeleton;
const getSkeletonStyles = () => ({
blockSkeleton: css({
// needed to align correctly in the table
display: 'block',
lineHeight: 1,
}),
});

View File

@ -70,7 +70,7 @@ const PlaylistCardSkeleton = () => {
<Skeleton width={140} />
</Card.Heading>
<Card.Actions>
<Stack direction="row">
<Stack direction="row" wrap="wrap">
<Skeleton containerClassName={skeletonStyles.button} width={142} height={32} />
{contextSrv.isEditor && (
<>