diff --git a/ui/src/plugin/PluginList.jsx b/ui/src/plugin/PluginList.jsx index 67af85b81..346e1ceae 100644 --- a/ui/src/plugin/PluginList.jsx +++ b/ui/src/plugin/PluginList.jsx @@ -2,6 +2,7 @@ import React, { useMemo, useState, useCallback } from 'react' import { Button, Datagrid, + Empty, TextField, TopToolbar, useNotify, @@ -10,7 +11,13 @@ import { useTranslate, } from 'react-admin' import { makeStyles } from '@material-ui/core/styles' -import { useMediaQuery, Tooltip, Chip, Typography } from '@material-ui/core' +import { + useMediaQuery, + Tooltip, + Chip, + Typography, + Box, +} from '@material-ui/core' import { MdError, MdRefresh } from 'react-icons/md' import { List, DateField, SimpleList, useResourceRefresh } from '../common' import { httpClient } from '../dataProvider' @@ -72,8 +79,7 @@ const ManifestField = ({ source }) => { return {manifest[source] || '-'} } -const PluginListActions = () => { - const translate = useTranslate() +const RescanButton = () => { const notify = useNotify() const refresh = useRefresh() const [loading, setLoading] = useState(false) @@ -92,20 +98,37 @@ const PluginListActions = () => { }) }, [notify, refresh]) + return ( + + ) +} + +const PluginListActions = () => { return ( - + ) } +const PluginEmpty = () => { + return ( + <> + + + + + + ) +} + const PluginList = (props) => { const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs')) const translate = useTranslate() @@ -118,6 +141,7 @@ const PluginList = (props) => { exporter={false} bulkActionButtons={false} actions={} + empty={} > {isXsmall ? ( { TopToolbar: ({ children }) => (
{children}
), + Empty: () =>
No resources
, Datagrid: ({ children }) =>
{children}
, TextField: ({ source }) => , } @@ -42,9 +49,10 @@ vi.mock('react-admin', async () => { // Mock common components vi.mock('../common', async () => { return { - List: ({ children, actions, ...props }) => ( + List: ({ children, actions, empty, ...props }) => (
{actions} + {empty &&
{empty}
} {children}
), @@ -94,14 +102,16 @@ describe('PluginList', () => { expect(screen.getByTestId('datagrid')).toBeInTheDocument() }) - it('renders the rescan button', () => { + it('renders the rescan button in the toolbar', () => { render() - expect(screen.getByTestId('rescan-button')).toBeInTheDocument() + const toolbar = screen.getByTestId('top-toolbar') + expect(within(toolbar).getByTestId('rescan-button')).toBeInTheDocument() }) it('calls rescan endpoint when rescan button is clicked', async () => { render() - const rescanButton = screen.getByTestId('rescan-button') + const toolbar = screen.getByTestId('top-toolbar') + const rescanButton = within(toolbar).getByTestId('rescan-button') fireEvent.click(rescanButton) @@ -114,7 +124,8 @@ describe('PluginList', () => { it('calls refresh after successful rescan', async () => { render() - const rescanButton = screen.getByTestId('rescan-button') + const toolbar = screen.getByTestId('top-toolbar') + const rescanButton = within(toolbar).getByTestId('rescan-button') fireEvent.click(rescanButton) @@ -127,7 +138,8 @@ describe('PluginList', () => { mockHttpClient.mockRejectedValue(new Error('Network error')) render() - const rescanButton = screen.getByTestId('rescan-button') + const toolbar = screen.getByTestId('top-toolbar') + const rescanButton = within(toolbar).getByTestId('rescan-button') fireEvent.click(rescanButton) @@ -137,4 +149,25 @@ describe('PluginList', () => { }) }) }) + + it('renders a rescan button in the empty state', () => { + render() + const emptyState = screen.getByTestId('empty-state') + expect(emptyState).toBeInTheDocument() + expect(within(emptyState).getByTestId('rescan-button')).toBeInTheDocument() + }) + + it('empty state rescan button triggers rescan', async () => { + render() + const emptyState = screen.getByTestId('empty-state') + const rescanButton = within(emptyState).getByTestId('rescan-button') + + fireEvent.click(rescanButton) + + await waitFor(() => { + expect(mockHttpClient).toHaveBeenCalledWith('/api/plugin/rescan', { + method: 'POST', + }) + }) + }) })