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;