Skip to content

Commit 2c050db

Browse files
authored
fix(quay): fix infinite progress bar when scan is unsupported (#1031)
1 parent 1544d1f commit 2c050db

File tree

4 files changed

+91
-7
lines changed

4 files changed

+91
-7
lines changed

plugins/quay/src/components/QuayRepository/QuayRepository.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,28 @@ describe('QuayRepository', () => {
9696
expect(queryByText(/Quay repository/i)).toBeInTheDocument();
9797
expect(queryByTestId('quay-repo-security-scan-progress')).not.toBeNull();
9898
});
99+
100+
it('should show table if loaded and data is present but shows unsupported if security scan is not supported', () => {
101+
(useTags as jest.Mock).mockReturnValue({
102+
loading: false,
103+
data: [
104+
{
105+
name: 'latest',
106+
manifest_digest: undefined,
107+
securityStatus: 'unsupported',
108+
size: null,
109+
last_modified: 'Wed, 15 Mar 2023 18:22:18 -0000',
110+
},
111+
],
112+
});
113+
const { queryByTestId, queryByText } = render(
114+
<BrowserRouter>
115+
<QuayRepository />
116+
</BrowserRouter>,
117+
);
118+
expect(queryByTestId('quay-repo-table')).not.toBeNull();
119+
expect(queryByTestId('quay-repo-table-empty')).toBeNull();
120+
expect(queryByText(/Quay repository/i)).toBeInTheDocument();
121+
expect(queryByTestId('quay-repo-security-scan-unsupported')).not.toBeNull();
122+
});
99123
});

plugins/quay/src/components/QuayRepository/tableHeading.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22

33
import { Link, Progress, TableColumn } from '@backstage/core-components';
44

5+
import { Tooltip } from '@material-ui/core';
56
import makeStyles from '@material-ui/core/styles/makeStyles';
67

78
import type { Layer } from '../../types';
@@ -41,13 +42,24 @@ export const columns: TableColumn[] = [
4142
title: 'Security Scan',
4243
field: 'securityScan',
4344
render: (rowData: any): React.ReactNode => {
44-
if (!rowData.securityDetails) {
45+
if (!rowData.securityStatus && !rowData.securityDetails) {
4546
return (
4647
<span data-testid="quay-repo-security-scan-progress">
4748
<Progress />
4849
</span>
4950
);
5051
}
52+
53+
if (rowData.securityStatus === 'unsupported') {
54+
return (
55+
<Tooltip title="The manifest for this tag has an operating system or package manager unsupported by Quay Security Scanner">
56+
<span data-testid="quay-repo-security-scan-unsupported">
57+
Unsupported
58+
</span>
59+
</Tooltip>
60+
);
61+
}
62+
5163
const tagManifest = rowData.manifest_digest_raw;
5264
const retStr = vulnerabilitySummary(rowData.securityDetails as Layer);
5365
return <Link to={`tag/${tagManifest}`}>{retStr}</Link>;

plugins/quay/src/hooks/quay.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export const useTags = (organization: string, repository: string) => {
2929
const [tagManifestLayers, setTagManifestLayers] = React.useState<
3030
Record<string, Layer>
3131
>({});
32+
const [tagManifestStatuses, setTagManifestStatuses] = React.useState<
33+
Record<string, string>
34+
>({});
3235
const localClasses = useLocalStyles();
3336

3437
const fetchSecurityDetails = async (tag: Tag) => {
@@ -46,13 +49,19 @@ export const useTags = (organization: string, repository: string) => {
4649
tagsResponse.tags.map(async tag => {
4750
const securityDetails = await fetchSecurityDetails(tag);
4851
const securityData = securityDetails.data;
49-
if (!securityData) {
50-
return;
51-
}
52-
setTagManifestLayers(prevState => ({
52+
const securityStatus = securityDetails.status;
53+
54+
setTagManifestStatuses(prevState => ({
5355
...prevState,
54-
[tag.manifest_digest]: securityData.Layer,
56+
[tag.manifest_digest]: securityStatus,
5557
}));
58+
59+
if (securityData) {
60+
setTagManifestLayers(prevState => ({
61+
...prevState,
62+
[tag.manifest_digest]: securityData.Layer,
63+
}));
64+
}
5665
}),
5766
);
5867
setTags(prevTags => [...prevTags, ...tagsResponse.tags]);
@@ -76,6 +85,7 @@ export const useTags = (organization: string, repository: string) => {
7685
),
7786
expiration: tag.expiration,
7887
securityDetails: tagManifestLayers[tag.manifest_digest],
88+
securityStatus: tagManifestStatuses[tag.manifest_digest],
7989
manifest_digest_raw: tag.manifest_digest,
8090
// is_manifest_list: tag.is_manifest_list,
8191
// reversion: tag.reversion,
@@ -84,7 +94,7 @@ export const useTags = (organization: string, repository: string) => {
8494
// manifest_list: tag.manifest_list,
8595
};
8696
});
87-
}, [tags, tagManifestLayers, localClasses.chip]);
97+
}, [tags, localClasses.chip, tagManifestLayers, tagManifestStatuses]);
8898

8999
return { loading, data };
90100
};

plugins/quay/src/hooks/useTags.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useApi } from '@backstage/core-plugin-api';
2+
13
import { waitFor } from '@testing-library/react';
24
import { renderHook } from '@testing-library/react-hooks';
35

@@ -21,4 +23,40 @@ describe('useTags', () => {
2123
expect(result.current.data).toHaveLength(1);
2224
});
2325
});
26+
27+
it('should return security status for tags', async () => {
28+
(useApi as jest.Mock).mockReturnValue({
29+
getSecurityDetails: jest
30+
.fn()
31+
.mockReturnValue({ data: null, status: 'unsupported' }),
32+
getTags: jest.fn().mockReturnValue({
33+
tags: [{ name: 'tag1', manifest_digest: 'manifestDigest' }],
34+
}),
35+
});
36+
const { result } = renderHook(() => useTags('foo', 'bar'));
37+
await waitFor(() => {
38+
expect(result.current.loading).toBeFalsy();
39+
expect(result.current.data).toHaveLength(1);
40+
expect(result.current.data[0].securityStatus).toBe('unsupported');
41+
expect(result.current.data[0].securityDetails).toBeUndefined();
42+
});
43+
});
44+
45+
it('should return tag layers as security details for tags', async () => {
46+
(useApi as jest.Mock).mockReturnValue({
47+
getSecurityDetails: jest
48+
.fn()
49+
.mockReturnValue({ data: { Layer: {} }, status: 'scanned' }),
50+
getTags: jest.fn().mockReturnValue({
51+
tags: [{ name: 'tag1', manifest_digest: 'manifestDigest' }],
52+
}),
53+
});
54+
const { result } = renderHook(() => useTags('foo', 'bar'));
55+
await waitFor(() => {
56+
expect(result.current.loading).toBeFalsy();
57+
expect(result.current.data).toHaveLength(1);
58+
expect(result.current.data[0].securityStatus).toBe('scanned');
59+
expect(result.current.data[0].securityDetails).toEqual({});
60+
});
61+
});
2462
});

0 commit comments

Comments
 (0)