diff --git a/.changeset/lemon-cycles-enjoy.md b/.changeset/lemon-cycles-enjoy.md new file mode 100644 index 0000000000..1df7905030 --- /dev/null +++ b/.changeset/lemon-cycles-enjoy.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-lighthouse': patch +--- + +Added more verbose components (used to render `null`) when no audits for a website corresponding to the provided url were found. +Added `Create New Audit` button for the `AuditListForEntity` component used by `EntityLighthouseContent` and `EmbeddedRouter`. +Removed error alert from `errorApi` if error was due to no audits being found for a website (empty database query result). diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index 901e890318..627e0aa0f3 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -409,6 +409,7 @@ unregistration untracked upsert upvote +url URIs URLs utils diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx index d1ed72c3fb..42b02692a9 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx @@ -26,16 +26,31 @@ import { lighthouseApiRef } from '../../api'; import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; import * as data from '../../__fixtures__/website-list-response.json'; import { AuditListForEntity } from './AuditListForEntity'; +import { rootRouteRef } from '../../plugin'; +import { fireEvent, screen } from '@testing-library/react'; jest.mock('../../hooks/useWebsiteForEntity', () => ({ useWebsiteForEntity: jest.fn(), })); +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + const mockNavigation = jest.fn(); + return { + ...actual, + useNavigate: jest.fn(() => mockNavigation), + }; +}); + const useWebsiteForEntityMock = useWebsiteForEntity as jest.Mock; const websiteListResponse = data as WebsiteListResponse; const entityWebsite = websiteListResponse.items[0]; +const testAppOptions = { + mountedRoutes: { '/': rootRouteRef }, +}; + describe('', () => { afterEach(() => { jest.resetAllMocks(); @@ -73,39 +88,77 @@ describe('', () => { loading: false, error: null, }); - const { findByText } = await renderInTestApp(subject()); - expect(await findByText(entityWebsite.url)).toBeInTheDocument(); + const rendered = await renderInTestApp(subject(), testAppOptions); + const create_audit_button = await rendered.findByText('Create New Audit'); + const support_button = await rendered.findByText('Support'); + expect(await rendered.findByText(entityWebsite.url)).toBeInTheDocument(); + expect(await rendered.findByText('Latest Audit')).toBeInTheDocument(); + expect(create_audit_button).toBeInTheDocument(); + expect(support_button).toBeInTheDocument(); }); - it('renders a Progress element where the data is loading', async () => { + it('renders a Progress element when the data is loading', async () => { useWebsiteForEntityMock.mockReturnValue({ value: null, loading: true, error: null, }); - const { findByTestId } = await renderInTestApp(subject()); + const { findByTestId } = await renderInTestApp(subject(), testAppOptions); expect(await findByTestId('progress')).toBeInTheDocument(); }); - it('renders nothing where there is an error loading data', async () => { + it('renders a WarningPanel when there is an error loading data', async () => { useWebsiteForEntityMock.mockReturnValue({ value: null, loading: false, - error: 'error', + error: { name: 'error', message: 'error loading data' }, }); - const { queryByTestId } = await renderInTestApp(subject()); - expect(queryByTestId('AuditListTable')).toBeNull(); + await renderInTestApp(subject(), testAppOptions); + const expandIcon = screen.getByText('Error: Could not load audit list.'); + fireEvent.click(expandIcon); + expect( + screen.getByText('Error: Could not load audit list.'), + ).toBeInTheDocument(); + expect(screen.getByText('error loading data')).toBeInTheDocument(); }); - it('renders nothing where there is not data', async () => { + it('renders an empty table when there is no data', async () => { useWebsiteForEntityMock.mockReturnValue({ value: null, loading: false, error: null, }); - const { queryByTestId } = await renderInTestApp(subject()); - expect(queryByTestId('AuditListTable')).toBeNull(); + const rendered = await renderInTestApp(subject(), testAppOptions); + const create_audit_button = await rendered.findByText('Create New Audit'); + const support_button = await rendered.findByText('Support'); + expect( + await rendered.findByText('No records to display'), + ).toBeInTheDocument(); + expect(await rendered.findByText('Latest Audit')).toBeInTheDocument(); + expect(create_audit_button).toBeInTheDocument(); + expect(support_button).toBeInTheDocument(); + }); + + it('renders an empty table when there is no data and error loading data due to empty database query result', async () => { + useWebsiteForEntityMock.mockReturnValue({ + value: null, + loading: false, + error: { + name: 'error', + message: 'no audited website found for url unit-test-url', + }, + }); + + const rendered = await renderInTestApp(subject(), testAppOptions); + const create_audit_button = await rendered.findByText('Create New Audit'); + const support_button = await rendered.findByText('Support'); + expect( + await rendered.findByText('No records to display'), + ).toBeInTheDocument(); + expect(await rendered.findByText('Latest Audit')).toBeInTheDocument(); + expect(create_audit_button).toBeInTheDocument(); + expect(support_button).toBeInTheDocument(); }); }); diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx index 6251a9ba34..4c8de4d786 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx @@ -13,19 +13,65 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { AuditListTable } from './AuditListTable'; import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; -import { Progress } from '@backstage/core-components'; +import { LIGHTHOUSE_WEBSITE_URL_ANNOTATION } from '../../../constants'; +import { + Content, + ContentHeader, + InfoCard, + Progress, + WarningPanel, +} from '@backstage/core-components'; +import { Button } from '@material-ui/core'; +import { resolvePath, useNavigate } from 'react-router-dom'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { rootRouteRef } from '../../plugin'; +import LighthouseSupportButton from '../SupportButton'; export const AuditListForEntity = () => { const { value, loading, error } = useWebsiteForEntity(); - if (loading) { - return ; - } - if (error || !value) { - return null; + const { entity } = useEntity(); + const navigate = useNavigate(); + const fromPath = useRouteRef(rootRouteRef)?.() ?? '../'; + + let createAuditButtonUrl = 'create-audit'; + const websiteUrl = + entity.metadata.annotations?.[LIGHTHOUSE_WEBSITE_URL_ANNOTATION] ?? ''; + if (websiteUrl) { + createAuditButtonUrl += `?url=${encodeURIComponent(websiteUrl)}`; } - return ; + let content: ReactNode = null; + content = ( + + + + + + + + ); + + if (loading) { + content = ; + } + if (error && !error.message.includes('no audited website found for url')) { + // We only want to display this warning panel when its caused by an error other than no audits for the website + content = ( + + {error.message} + + ); + } + + return {content}; }; diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx index 09b21fd028..fd54dd99c8 100644 --- a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx +++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx @@ -26,6 +26,7 @@ import { import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; import * as data from '../../__fixtures__/website-list-response.json'; import { LastLighthouseAuditCard } from './LastLighthouseAuditCard'; +import { fireEvent, screen } from '@testing-library/react'; jest.mock('../../hooks/useWebsiteForEntity', () => ({ useWebsiteForEntity: jest.fn(), @@ -147,17 +148,22 @@ describe('', () => { (useWebsiteForEntity as jest.Mock).mockReturnValue({ value: null, loading: false, - error: 'error', + error: { name: 'error', message: 'error loading data' }, }); }); - it('renders nothing', async () => { - const { queryByTestId } = await renderInTestApp( + it('renders a WarningPanel', async () => { + await renderInTestApp( , ); - expect(queryByTestId('AuditListTable')).toBeNull(); + const expandIcon = screen.getByText('Error: Could not load audit list.'); + fireEvent.click(expandIcon); + expect( + screen.getByText('Error: Could not load audit list.'), + ).toBeInTheDocument(); + expect(screen.getByText('error loading data')).toBeInTheDocument(); }); }); @@ -179,4 +185,26 @@ describe('', () => { expect(queryByTestId('AuditListTable')).toBeNull(); }); }); + + describe('where error was due to an empty database query result', () => { + beforeEach(() => { + (useWebsiteForEntity as jest.Mock).mockReturnValue({ + value: null, + loading: false, + error: { + name: 'error', + message: 'no audited website found for url unit-test-url', + }, + }); + }); + + it('renders EmptyState card', async () => { + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText('No Audits Found')).toBeInTheDocument(); + }); + }); }); diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx index af5ce94487..8245d558e1 100644 --- a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx +++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx @@ -23,6 +23,7 @@ import { import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; import AuditStatusIcon from '../AuditStatusIcon'; import { + EmptyState, InfoCard, InfoCardVariants, Progress, @@ -30,6 +31,7 @@ import { StatusOK, StatusWarning, StructuredMetadataTable, + WarningPanel, } from '@backstage/core-components'; const LighthouseCategoryScoreStatus = (props: { score: number }) => { @@ -105,7 +107,14 @@ export const LastLighthouseAuditCard = (props: { content = ; } if (error) { - content = null; + // We only want to display this warning panel when its caused by an error other than no audits being found for the website + content = error.message.includes('no audited website found for url') ? ( + + ) : ( + + {error.message} + + ); } if (website) { content = ( diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx index 7ab717c57d..199ede3049 100644 --- a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx +++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx @@ -98,4 +98,21 @@ describe('useWebsiteForEntity', () => { expect(mockErrorApi.post).toHaveBeenCalledWith(error); }); }); + + describe('where there is an error regarding "no audited websites for url"', () => { + const error = new Error('no audited website found for url unit-test-url'); + + beforeEach(() => { + (mockLighthouseApi.getWebsiteByUrl as jest.Mock).mockRejectedValueOnce( + error, + ); + }); + + it('does not post the error to the error api and returns the error to the caller', async () => { + const { result, waitForNextUpdate } = subject(); + await waitForNextUpdate(); + expect(result.current?.error).toBe(error); + expect(mockErrorApi.post).not.toHaveBeenCalledWith(error); + }); + }); }); diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts b/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts index 3f7709fee0..1237209749 100644 --- a/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts +++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts @@ -31,7 +31,11 @@ export const useWebsiteForEntity = () => { () => lighthouseApi.getWebsiteByUrl(websiteUrl), [websiteUrl], ); - if (response.error) { + // Do not display error alert if its due to no audits found for a website + if ( + response.error && + !response.error.message.includes('no audited website found for url') + ) { errorApi.post(response.error); } return response;