diff --git a/plugins/quay/src/components/QuayRepository/QuayRepository.test.tsx b/plugins/quay/src/components/QuayRepository/QuayRepository.test.tsx index 236a409c02..40c12c54a3 100644 --- a/plugins/quay/src/components/QuayRepository/QuayRepository.test.tsx +++ b/plugins/quay/src/components/QuayRepository/QuayRepository.test.tsx @@ -96,4 +96,28 @@ describe('QuayRepository', () => { expect(queryByText(/Quay repository/i)).toBeInTheDocument(); expect(queryByTestId('quay-repo-security-scan-progress')).not.toBeNull(); }); + + it('should show table if loaded and data is present but shows unsupported if security scan is not supported', () => { + (useTags as jest.Mock).mockReturnValue({ + loading: false, + data: [ + { + name: 'latest', + manifest_digest: undefined, + securityStatus: 'unsupported', + size: null, + last_modified: 'Wed, 15 Mar 2023 18:22:18 -0000', + }, + ], + }); + const { queryByTestId, queryByText } = render( + + + , + ); + expect(queryByTestId('quay-repo-table')).not.toBeNull(); + expect(queryByTestId('quay-repo-table-empty')).toBeNull(); + expect(queryByText(/Quay repository/i)).toBeInTheDocument(); + expect(queryByTestId('quay-repo-security-scan-unsupported')).not.toBeNull(); + }); }); diff --git a/plugins/quay/src/components/QuayRepository/tableHeading.tsx b/plugins/quay/src/components/QuayRepository/tableHeading.tsx index 0573baa92f..c455771d96 100644 --- a/plugins/quay/src/components/QuayRepository/tableHeading.tsx +++ b/plugins/quay/src/components/QuayRepository/tableHeading.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Link, Progress, TableColumn } from '@backstage/core-components'; +import { Tooltip } from '@material-ui/core'; import makeStyles from '@material-ui/core/styles/makeStyles'; import type { Layer } from '../../types'; @@ -41,13 +42,24 @@ export const columns: TableColumn[] = [ title: 'Security Scan', field: 'securityScan', render: (rowData: any): React.ReactNode => { - if (!rowData.securityDetails) { + if (!rowData.securityStatus && !rowData.securityDetails) { return ( ); } + + if (rowData.securityStatus === 'unsupported') { + return ( + + + Unsupported + + + ); + } + const tagManifest = rowData.manifest_digest_raw; const retStr = vulnerabilitySummary(rowData.securityDetails as Layer); return {retStr}; diff --git a/plugins/quay/src/hooks/quay.tsx b/plugins/quay/src/hooks/quay.tsx index 5035d20891..3f48c60d70 100644 --- a/plugins/quay/src/hooks/quay.tsx +++ b/plugins/quay/src/hooks/quay.tsx @@ -29,6 +29,9 @@ export const useTags = (organization: string, repository: string) => { const [tagManifestLayers, setTagManifestLayers] = React.useState< Record >({}); + const [tagManifestStatuses, setTagManifestStatuses] = React.useState< + Record + >({}); const localClasses = useLocalStyles(); const fetchSecurityDetails = async (tag: Tag) => { @@ -46,13 +49,19 @@ export const useTags = (organization: string, repository: string) => { tagsResponse.tags.map(async tag => { const securityDetails = await fetchSecurityDetails(tag); const securityData = securityDetails.data; - if (!securityData) { - return; - } - setTagManifestLayers(prevState => ({ + const securityStatus = securityDetails.status; + + setTagManifestStatuses(prevState => ({ ...prevState, - [tag.manifest_digest]: securityData.Layer, + [tag.manifest_digest]: securityStatus, })); + + if (securityData) { + setTagManifestLayers(prevState => ({ + ...prevState, + [tag.manifest_digest]: securityData.Layer, + })); + } }), ); setTags(prevTags => [...prevTags, ...tagsResponse.tags]); @@ -76,6 +85,7 @@ export const useTags = (organization: string, repository: string) => { ), expiration: tag.expiration, securityDetails: tagManifestLayers[tag.manifest_digest], + securityStatus: tagManifestStatuses[tag.manifest_digest], manifest_digest_raw: tag.manifest_digest, // is_manifest_list: tag.is_manifest_list, // reversion: tag.reversion, @@ -84,7 +94,7 @@ export const useTags = (organization: string, repository: string) => { // manifest_list: tag.manifest_list, }; }); - }, [tags, tagManifestLayers, localClasses.chip]); + }, [tags, localClasses.chip, tagManifestLayers, tagManifestStatuses]); return { loading, data }; }; diff --git a/plugins/quay/src/hooks/useTags.test.ts b/plugins/quay/src/hooks/useTags.test.ts index 61dc7871f3..15c398877a 100644 --- a/plugins/quay/src/hooks/useTags.test.ts +++ b/plugins/quay/src/hooks/useTags.test.ts @@ -1,3 +1,5 @@ +import { useApi } from '@backstage/core-plugin-api'; + import { waitFor } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; @@ -21,4 +23,40 @@ describe('useTags', () => { expect(result.current.data).toHaveLength(1); }); }); + + it('should return security status for tags', async () => { + (useApi as jest.Mock).mockReturnValue({ + getSecurityDetails: jest + .fn() + .mockReturnValue({ data: null, status: 'unsupported' }), + getTags: jest.fn().mockReturnValue({ + tags: [{ name: 'tag1', manifest_digest: 'manifestDigest' }], + }), + }); + const { result } = renderHook(() => useTags('foo', 'bar')); + await waitFor(() => { + expect(result.current.loading).toBeFalsy(); + expect(result.current.data).toHaveLength(1); + expect(result.current.data[0].securityStatus).toBe('unsupported'); + expect(result.current.data[0].securityDetails).toBeUndefined(); + }); + }); + + it('should return tag layers as security details for tags', async () => { + (useApi as jest.Mock).mockReturnValue({ + getSecurityDetails: jest + .fn() + .mockReturnValue({ data: { Layer: {} }, status: 'scanned' }), + getTags: jest.fn().mockReturnValue({ + tags: [{ name: 'tag1', manifest_digest: 'manifestDigest' }], + }), + }); + const { result } = renderHook(() => useTags('foo', 'bar')); + await waitFor(() => { + expect(result.current.loading).toBeFalsy(); + expect(result.current.data).toHaveLength(1); + expect(result.current.data[0].securityStatus).toBe('scanned'); + expect(result.current.data[0].securityDetails).toEqual({}); + }); + }); });