mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Snapshots: Add loading skeleton (#79134)
* add snapshot skeleton * add some unit tests
This commit is contained in:
parent
ebdffe21e6
commit
4a0c89a8fd
@ -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>
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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,
|
||||
}),
|
||||
});
|
@ -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 && (
|
||||
<>
|
||||
|
Loading…
Reference in New Issue
Block a user