@backstage-community/plugin-quay 1.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +670 -0
  2. package/README.md +102 -0
  3. package/app-config.dynamic.yaml +12 -0
  4. package/config.d.ts +30 -0
  5. package/dist/api/index.esm.js +77 -0
  6. package/dist/api/index.esm.js.map +1 -0
  7. package/dist/components/PermissionAlert/PermissionAlert.esm.js +9 -0
  8. package/dist/components/PermissionAlert/PermissionAlert.esm.js.map +1 -0
  9. package/dist/components/QuayRepository/QuayRepository.esm.js +42 -0
  10. package/dist/components/QuayRepository/QuayRepository.esm.js.map +1 -0
  11. package/dist/components/QuayRepository/tableHeading.esm.js +74 -0
  12. package/dist/components/QuayRepository/tableHeading.esm.js.map +1 -0
  13. package/dist/components/QuayTagDetails/component.esm.js +107 -0
  14. package/dist/components/QuayTagDetails/component.esm.js.map +1 -0
  15. package/dist/components/QuayTagPage/component.esm.js +40 -0
  16. package/dist/components/QuayTagPage/component.esm.js.map +1 -0
  17. package/dist/components/Router.esm.js +12 -0
  18. package/dist/components/Router.esm.js.map +1 -0
  19. package/dist/hooks/quay.esm.js +113 -0
  20. package/dist/hooks/quay.esm.js.map +1 -0
  21. package/dist/hooks/useQuayViewPermission.esm.js +12 -0
  22. package/dist/hooks/useQuayViewPermission.esm.js.map +1 -0
  23. package/dist/index.d.ts +27 -0
  24. package/dist/index.esm.js +3 -0
  25. package/dist/index.esm.js.map +1 -0
  26. package/dist/lib/utils.esm.js +90 -0
  27. package/dist/lib/utils.esm.js.map +1 -0
  28. package/dist/plugin.esm.js +33 -0
  29. package/dist/plugin.esm.js.map +1 -0
  30. package/dist/routes.esm.js +13 -0
  31. package/dist/routes.esm.js.map +1 -0
  32. package/dist/types.esm.js +22 -0
  33. package/dist/types.esm.js.map +1 -0
  34. package/package.json +106 -0
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # Quay plugin for Backstage
2
+
3
+ The Quay plugin displays the information about your container images within the Quay registry in your Backstage application.
4
+
5
+ ## For administrators
6
+
7
+ ### Installation
8
+
9
+ 1. Install the Quay plugin using the following command:
10
+
11
+ ```console
12
+ yarn workspace app add @backstage-community/plugin-quay
13
+ ```
14
+
15
+ ### Configuration
16
+
17
+ 1. Set the proxy to the desired Quay server in the `app-config.yaml` file as follows:
18
+
19
+ ```yaml title="app-config.yaml"
20
+ proxy:
21
+ endpoints:
22
+ '/quay/api':
23
+ target: 'https://quay.io'
24
+ credentials: require
25
+ headers:
26
+ X-Requested-With: 'XMLHttpRequest'
27
+ # Uncomment and use the Authorization header below to access a private Quay
28
+ # Repository using a token. Refer to the "Applications and Tokens" section
29
+ # at https://docs.quay.io/api/ to find the instructions to generate a token
30
+ # Authorization: 'Bearer <YOUR TOKEN>'
31
+ changeOrigin: true
32
+ # Change to "false" in case of using self hosted quay instance with a self-signed certificate
33
+ secure: true
34
+
35
+ quay:
36
+ # The UI url for Quay, used to generate the link to Quay
37
+ uiUrl: 'https://quay.io'
38
+ ```
39
+
40
+ > [!NOTE]
41
+ > The value inside each route is either a simple URL string, or an object on the format accepted by [http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware). Additionally, it has an optional `credentials` key which can have the following values:
42
+ >
43
+ > - `require`: Callers must provide Backstage user or service credentials with each request. The credentials are not forwarded to the proxy target. This is the **default**.
44
+ > - `forward`: Callers must provide Backstage user or service credentials with each request, and those credentials are forwarded to the proxy target.
45
+ > - `dangerously-allow-unauthenticated`: No Backstage credentials are required to access this proxy target. The target can still apply its own credentials checks, but the proxy will not help block non-Backstage-blessed callers. If you also add allowedHeaders: ['Authorization'] to an endpoint configuration, then the Backstage token (if provided) WILL be forwarded.
46
+ >
47
+ > Note that if you have `backend.auth.dangerouslyDisableDefaultAuthPolicy` set to true, the credentials value does not apply; the proxy will behave as if all endpoints were set to dangerously-allow-unauthenticated.
48
+
49
+ 2. Enable an additional tab on the entity view page in `packages/app/src/components/catalog/EntityPage.tsx`:
50
+
51
+ ```tsx title="packages/app/src/components/catalog/EntityPage.tsx"
52
+ /* highlight-add-next-line */
53
+ import { isQuayAvailable, QuayPage } from '@backstage-community/plugin-quay';
54
+
55
+ const serviceEntityPage = (
56
+ <EntityLayout>
57
+ {/* ... */}
58
+ {/* highlight-add-next-line */}
59
+ <EntityLayout.Route if={isQuayAvailable} path="/quay" title="Quay">
60
+ <QuayPage />
61
+ </EntityLayout.Route>
62
+ </EntityLayout>
63
+ );
64
+ ```
65
+
66
+ 3. Annotate your entity with the following annotations:
67
+
68
+ ```yaml title="catalog-info.yaml"
69
+ metadata:
70
+ annotations:
71
+ 'quay.io/repository-slug': `<ORGANIZATION>/<REPOSITORY>',
72
+ ```
73
+
74
+ ## For users
75
+
76
+ ### Using the Quay plugin in Backstage
77
+
78
+ Quay is a front-end plugin that enables you to view the information about the container images.
79
+
80
+ #### Prerequisites
81
+
82
+ - Your Backstage application is installed and running.
83
+ - You have installed the Quay plugin. For installation process, see [Installation](#installation).
84
+
85
+ #### Procedure
86
+
87
+ 1. Open your Backstage application and select a component from the **Catalog** page.
88
+ 1. Go to the **Image Registry** tab.
89
+
90
+ The **Image Registry** tab in the Backstage UI contains a list of container images and related information, such as **TAG**, **LAST MODIFIED**, **SECURITY SCAN**, **SIZE**, **EXPIRES**, and **MANIFEST**.
91
+
92
+ ![quay-tab](./images/quay-plugin-backstage1.png)
93
+
94
+ 1. If a container image does not pass the security scan, select the security scan value of the image to check the vulnerabilities.
95
+
96
+ ![quay-tab](./images/quay-plugin-backstage2.png)
97
+
98
+ The vulnerabilities page displays the associated advisory with a link, severity, package name, and current and fixed versions.
99
+
100
+ ![quay-tab-vulnerabilities](./images/quay-plugin-backstage3.png)
101
+
102
+ The advisory link redirects to the Red Hat Security Advisory page that contains detailed information about the advisory, including the solution.
@@ -0,0 +1,12 @@
1
+ dynamicPlugins:
2
+ frontend:
3
+ backstage-community.backstage-plugin-quay:
4
+ mountPoints:
5
+ - mountPoint: entity.page.image-registry/cards
6
+ importName: QuayPage
7
+ config:
8
+ layout:
9
+ gridColumn: 1 / -1
10
+ if:
11
+ anyOf:
12
+ - isQuayAvailable
package/config.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /*
2
+ * Copyright 2024 The Backstage Authors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ export interface Config {
17
+ /** Configurations for the Quay plugin */
18
+ quay?: {
19
+ /**
20
+ * The proxy path for the Quay instance.
21
+ * @visibility frontend
22
+ */
23
+ proxyPath?: string;
24
+ /**
25
+ * The UI url of the Quay instance.
26
+ * @visibility frontend
27
+ */
28
+ uiUrl?: string;
29
+ };
30
+ }
@@ -0,0 +1,77 @@
1
+ import { createApiRef } from '@backstage/core-plugin-api';
2
+
3
+ const DEFAULT_PROXY_PATH = "/quay/api";
4
+ const quayApiRef = createApiRef({
5
+ id: "plugin.quay.service"
6
+ });
7
+ class QuayApiClient {
8
+ // @ts-ignore
9
+ discoveryApi;
10
+ configApi;
11
+ identityApi;
12
+ constructor(options) {
13
+ this.discoveryApi = options.discoveryApi;
14
+ this.configApi = options.configApi;
15
+ this.identityApi = options.identityApi;
16
+ }
17
+ async getBaseUrl() {
18
+ const proxyPath = this.configApi.getOptionalString("quay.proxyPath") || DEFAULT_PROXY_PATH;
19
+ return `${await this.discoveryApi.getBaseUrl("proxy")}${proxyPath}`;
20
+ }
21
+ async fetcher(url) {
22
+ const { token: idToken } = await this.identityApi.getCredentials();
23
+ const response = await fetch(url, {
24
+ headers: {
25
+ "Content-Type": "application/json",
26
+ ...idToken && { Authorization: `Bearer ${idToken}` }
27
+ }
28
+ });
29
+ if (!response.ok) {
30
+ throw new Error(
31
+ `failed to fetch data, status ${response.status}: ${response.statusText}`
32
+ );
33
+ }
34
+ return await response.json();
35
+ }
36
+ encodeGetParams(params) {
37
+ return Object.keys(params).filter((key) => typeof params[key] !== "undefined").map(
38
+ (k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`
39
+ ).join("&");
40
+ }
41
+ async getTags(org, repo, page, limit, specifcTag) {
42
+ const proxyUrl = await this.getBaseUrl();
43
+ const params = this.encodeGetParams({
44
+ limit,
45
+ page,
46
+ onlyActiveTags: true,
47
+ specifcTag
48
+ });
49
+ return await this.fetcher(
50
+ `${proxyUrl}/api/v1/repository/${org}/${repo}/tag/?${params}`
51
+ );
52
+ }
53
+ async getLabels(org, repo, digest) {
54
+ const proxyUrl = await this.getBaseUrl();
55
+ return await this.fetcher(
56
+ `${proxyUrl}/api/v1/repository/${org}/${repo}/manifest/${digest}/labels`
57
+ );
58
+ }
59
+ async getManifestByDigest(org, repo, digest) {
60
+ const proxyUrl = await this.getBaseUrl();
61
+ return await this.fetcher(
62
+ `${proxyUrl}/api/v1/repository/${org}/${repo}/manifest/${digest}`
63
+ );
64
+ }
65
+ async getSecurityDetails(org, repo, digest) {
66
+ const proxyUrl = await this.getBaseUrl();
67
+ const params = this.encodeGetParams({
68
+ vulnerabilities: true
69
+ });
70
+ return await this.fetcher(
71
+ `${proxyUrl}/api/v1/repository/${org}/${repo}/manifest/${digest}/security?${params}`
72
+ );
73
+ }
74
+ }
75
+
76
+ export { QuayApiClient, quayApiRef };
77
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":["../../src/api/index.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n ConfigApi,\n createApiRef,\n DiscoveryApi,\n IdentityApi,\n} from '@backstage/core-plugin-api';\n\nimport {\n LabelsResponse,\n ManifestByDigestResponse,\n SecurityDetailsResponse,\n TagsResponse,\n} from '../types';\n\nconst DEFAULT_PROXY_PATH = '/quay/api';\n\nexport interface QuayApiV1 {\n getTags(\n org: string,\n repo: string,\n page?: number,\n limit?: number,\n ): Promise<TagsResponse>;\n getLabels(org: string, repo: string, digest: string): Promise<LabelsResponse>;\n getManifestByDigest(\n org: string,\n repo: string,\n digest: string,\n ): Promise<ManifestByDigestResponse>;\n getSecurityDetails(\n org: string,\n repo: string,\n digest: string,\n ): Promise<SecurityDetailsResponse>;\n}\n\nexport const quayApiRef = createApiRef<QuayApiV1>({\n id: 'plugin.quay.service',\n});\n\nexport type Options = {\n discoveryApi: DiscoveryApi;\n configApi: ConfigApi;\n identityApi: IdentityApi;\n};\n\nexport class QuayApiClient implements QuayApiV1 {\n // @ts-ignore\n private readonly discoveryApi: DiscoveryApi;\n\n private readonly configApi: ConfigApi;\n\n private readonly identityApi: IdentityApi;\n\n constructor(options: Options) {\n this.discoveryApi = options.discoveryApi;\n this.configApi = options.configApi;\n this.identityApi = options.identityApi;\n }\n\n private async getBaseUrl() {\n const proxyPath =\n this.configApi.getOptionalString('quay.proxyPath') || DEFAULT_PROXY_PATH;\n return `${await this.discoveryApi.getBaseUrl('proxy')}${proxyPath}`;\n }\n\n private async fetcher(url: string) {\n const { token: idToken } = await this.identityApi.getCredentials();\n const response = await fetch(url, {\n headers: {\n 'Content-Type': 'application/json',\n ...(idToken && { Authorization: `Bearer ${idToken}` }),\n },\n });\n if (!response.ok) {\n throw new Error(\n `failed to fetch data, status ${response.status}: ${response.statusText}`,\n );\n }\n return await response.json();\n }\n\n private encodeGetParams(params: Record<string, any>) {\n return Object.keys(params)\n .filter(key => typeof params[key] !== 'undefined')\n .map(\n k =>\n `${encodeURIComponent(k)}=${encodeURIComponent(params[k] as string)}`,\n )\n .join('&');\n }\n\n async getTags(\n org: string,\n repo: string,\n page?: number,\n limit?: number,\n specifcTag?: string,\n ) {\n const proxyUrl = await this.getBaseUrl();\n\n const params = this.encodeGetParams({\n limit,\n page,\n onlyActiveTags: true,\n specifcTag,\n });\n\n return (await this.fetcher(\n `${proxyUrl}/api/v1/repository/${org}/${repo}/tag/?${params}`,\n )) as TagsResponse;\n }\n\n async getLabels(org: string, repo: string, digest: string) {\n const proxyUrl = await this.getBaseUrl();\n\n return (await this.fetcher(\n `${proxyUrl}/api/v1/repository/${org}/${repo}/manifest/${digest}/labels`,\n )) as LabelsResponse;\n }\n\n async getManifestByDigest(org: string, repo: string, digest: string) {\n const proxyUrl = await this.getBaseUrl();\n\n return (await this.fetcher(\n `${proxyUrl}/api/v1/repository/${org}/${repo}/manifest/${digest}`,\n )) as ManifestByDigestResponse;\n }\n\n async getSecurityDetails(org: string, repo: string, digest: string) {\n const proxyUrl = await this.getBaseUrl();\n\n const params = this.encodeGetParams({\n vulnerabilities: true,\n });\n\n return (await this.fetcher(\n `${proxyUrl}/api/v1/repository/${org}/${repo}/manifest/${digest}/security?${params}`,\n )) as SecurityDetailsResponse;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { Alert, AlertTitle } from '@material-ui/lab';
3
+
4
+ const PermissionAlert = () => {
5
+ return /* @__PURE__ */ React.createElement(Alert, { severity: "warning", "data-testid": "no-permission-alert" }, /* @__PURE__ */ React.createElement(AlertTitle, null, "Permission required"), "To view quay image registry, contact your administrator to give you the quay.view.read permission.");
6
+ };
7
+
8
+ export { PermissionAlert as default };
9
+ //# sourceMappingURL=PermissionAlert.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PermissionAlert.esm.js","sources":["../../../src/components/PermissionAlert/PermissionAlert.tsx"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport React from 'react';\n\nimport { Alert, AlertTitle } from '@material-ui/lab';\n\nconst PermissionAlert = () => {\n return (\n <Alert severity=\"warning\" data-testid=\"no-permission-alert\">\n <AlertTitle>Permission required</AlertTitle>\n To view quay image registry, contact your administrator to give you the\n quay.view.read permission.\n </Alert>\n );\n};\nexport default PermissionAlert;\n"],"names":[],"mappings":";;;AAmBA,MAAM,kBAAkB,MAAM;AAC5B,EACE,uBAAA,KAAA,CAAA,aAAA,CAAC,KAAM,EAAA,EAAA,QAAA,EAAS,SAAU,EAAA,aAAA,EAAY,yCACnC,KAAA,CAAA,aAAA,CAAA,UAAA,EAAA,IAAA,EAAW,qBAAmB,CAAA,EAAa,oGAG9C,CAAA,CAAA;AAEJ;;;;"}
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { Link, Progress, Table } from '@backstage/core-components';
3
+ import { useApi, configApiRef } from '@backstage/core-plugin-api';
4
+ import { useRepository, useTags } from '../../hooks/quay.esm.js';
5
+ import { useQuayViewPermission } from '../../hooks/useQuayViewPermission.esm.js';
6
+ import PermissionAlert from '../PermissionAlert/PermissionAlert.esm.js';
7
+ import { useStyles, columns } from './tableHeading.esm.js';
8
+
9
+ function QuayRepository(_props) {
10
+ const { repository, organization } = useRepository();
11
+ const classes = useStyles();
12
+ const configApi = useApi(configApiRef);
13
+ const quayUiUrl = configApi.getOptionalString("quay.uiUrl");
14
+ const hasViewPermission = useQuayViewPermission();
15
+ const title = quayUiUrl ? /* @__PURE__ */ React.createElement(React.Fragment, null, `Quay repository: `, /* @__PURE__ */ React.createElement(
16
+ Link,
17
+ {
18
+ to: `${quayUiUrl}/repository/${organization}/${repository}`
19
+ },
20
+ `${organization}/${repository}`
21
+ )) : `Quay repository: ${organization}/${repository}`;
22
+ const { loading, data } = useTags(organization, repository);
23
+ if (!hasViewPermission) {
24
+ return /* @__PURE__ */ React.createElement(PermissionAlert, null);
25
+ }
26
+ if (loading) {
27
+ return /* @__PURE__ */ React.createElement("div", { "data-testid": "quay-repo-progress" }, /* @__PURE__ */ React.createElement(Progress, null));
28
+ }
29
+ return /* @__PURE__ */ React.createElement("div", { "data-testid": "quay-repo-table" }, /* @__PURE__ */ React.createElement(
30
+ Table,
31
+ {
32
+ title,
33
+ options: { sorting: true, paging: true, padding: "dense" },
34
+ data,
35
+ columns,
36
+ emptyContent: /* @__PURE__ */ React.createElement("div", { "data-testid": "quay-repo-table-empty", className: classes.empty }, "There are no images available.")
37
+ }
38
+ ));
39
+ }
40
+
41
+ export { QuayRepository };
42
+ //# sourceMappingURL=QuayRepository.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QuayRepository.esm.js","sources":["../../../src/components/QuayRepository/QuayRepository.tsx"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport React from 'react';\n\nimport { Link, Progress, Table } from '@backstage/core-components';\nimport { configApiRef, useApi } from '@backstage/core-plugin-api';\n\nimport { useRepository, useTags } from '../../hooks';\nimport { useQuayViewPermission } from '../../hooks/useQuayViewPermission';\nimport PermissionAlert from '../PermissionAlert/PermissionAlert';\nimport { columns, useStyles } from './tableHeading';\n\ntype QuayRepositoryProps = Record<never, any>;\n\nexport function QuayRepository(_props: QuayRepositoryProps) {\n const { repository, organization } = useRepository();\n const classes = useStyles();\n const configApi = useApi(configApiRef);\n const quayUiUrl = configApi.getOptionalString('quay.uiUrl');\n\n const hasViewPermission = useQuayViewPermission();\n\n const title = quayUiUrl ? (\n <>\n {`Quay repository: `}\n <Link\n to={`${quayUiUrl}/repository/${organization}/${repository}`}\n >{`${organization}/${repository}`}</Link>\n </>\n ) : (\n `Quay repository: ${organization}/${repository}`\n );\n const { loading, data } = useTags(organization, repository);\n\n if (!hasViewPermission) {\n return <PermissionAlert />;\n }\n\n if (loading) {\n return (\n <div data-testid=\"quay-repo-progress\">\n <Progress />\n </div>\n );\n }\n\n return (\n <div data-testid=\"quay-repo-table\">\n <Table\n title={title}\n options={{ sorting: true, paging: true, padding: 'dense' }}\n data={data}\n columns={columns}\n emptyContent={\n <div data-testid=\"quay-repo-table-empty\" className={classes.empty}>\n There are no images available.\n </div>\n }\n />\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;AA2BO,SAAS,eAAe,MAA6B,EAAA;AAC1D,EAAA,MAAM,EAAE,UAAA,EAAY,YAAa,EAAA,GAAI,aAAc,EAAA,CAAA;AACnD,EAAA,MAAM,UAAU,SAAU,EAAA,CAAA;AAC1B,EAAM,MAAA,SAAA,GAAY,OAAO,YAAY,CAAA,CAAA;AACrC,EAAM,MAAA,SAAA,GAAY,SAAU,CAAA,iBAAA,CAAkB,YAAY,CAAA,CAAA;AAE1D,EAAA,MAAM,oBAAoB,qBAAsB,EAAA,CAAA;AAEhD,EAAM,MAAA,KAAA,GAAQ,SACZ,mBAAA,KAAA,CAAA,aAAA,CAAA,KAAA,CAAA,QAAA,EAAA,IAAA,EACG,CACD,iBAAA,CAAA,kBAAA,KAAA,CAAA,aAAA;AAAA,IAAC,IAAA;AAAA,IAAA;AAAA,MACC,IAAI,CAAG,EAAA,SAAS,CAAe,YAAA,EAAA,YAAY,IAAI,UAAU,CAAA,CAAA;AAAA,KAAA;AAAA,IACzD,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,UAAU,CAAA,CAAA;AAAA,GACjC,CAAA,GAEA,CAAoB,iBAAA,EAAA,YAAY,IAAI,UAAU,CAAA,CAAA,CAAA;AAEhD,EAAA,MAAM,EAAE,OAAS,EAAA,IAAA,EAAS,GAAA,OAAA,CAAQ,cAAc,UAAU,CAAA,CAAA;AAE1D,EAAA,IAAI,CAAC,iBAAmB,EAAA;AACtB,IAAA,2CAAQ,eAAgB,EAAA,IAAA,CAAA,CAAA;AAAA,GAC1B;AAEA,EAAA,IAAI,OAAS,EAAA;AACX,IAAA,2CACG,KAAI,EAAA,EAAA,aAAA,EAAY,oBACf,EAAA,kBAAA,KAAA,CAAA,aAAA,CAAC,cAAS,CACZ,CAAA,CAAA;AAAA,GAEJ;AAEA,EACE,uBAAA,KAAA,CAAA,aAAA,CAAC,KAAI,EAAA,EAAA,aAAA,EAAY,iBACf,EAAA,kBAAA,KAAA,CAAA,aAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,KAAA;AAAA,MACA,SAAS,EAAE,OAAA,EAAS,MAAM,MAAQ,EAAA,IAAA,EAAM,SAAS,OAAQ,EAAA;AAAA,MACzD,IAAA;AAAA,MACA,OAAA;AAAA,MACA,YAAA,sCACG,KAAI,EAAA,EAAA,aAAA,EAAY,yBAAwB,SAAW,EAAA,OAAA,CAAQ,SAAO,gCAEnE,CAAA;AAAA,KAAA;AAAA,GAGN,CAAA,CAAA;AAEJ;;;;"}
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+ import { Progress, Link } from '@backstage/core-components';
3
+ import { Tooltip } from '@material-ui/core';
4
+ import makeStyles from '@material-ui/core/styles/makeStyles';
5
+ import { vulnerabilitySummary, securityScanComparator } from '../../lib/utils.esm.js';
6
+
7
+ const columns = [
8
+ {
9
+ title: "Tag",
10
+ field: "name",
11
+ type: "string",
12
+ highlight: true
13
+ },
14
+ {
15
+ title: "Last Modified",
16
+ field: "last_modified",
17
+ type: "date"
18
+ },
19
+ {
20
+ title: "Security Scan",
21
+ field: "securityScan",
22
+ render: (rowData) => {
23
+ if (!rowData.securityStatus && !rowData.securityDetails) {
24
+ return /* @__PURE__ */ React.createElement("span", { "data-testid": "quay-repo-security-scan-progress" }, /* @__PURE__ */ React.createElement(Progress, null));
25
+ }
26
+ if (rowData.securityStatus === "queued") {
27
+ return /* @__PURE__ */ React.createElement(Tooltip, { title: "The manifest for this tag is queued to be scanned for vulnerabilities" }, /* @__PURE__ */ React.createElement("span", { "data-testid": "quay-repo-queued-for-scan" }, "Queued"));
28
+ }
29
+ if (rowData.securityStatus === "unsupported") {
30
+ return /* @__PURE__ */ React.createElement(Tooltip, { title: "The manifest for this tag has an operating system or package manager unsupported by Quay Security Scanner" }, /* @__PURE__ */ React.createElement("span", { "data-testid": "quay-repo-security-scan-unsupported" }, "Unsupported"));
31
+ }
32
+ const tagManifest = rowData.manifest_digest_raw;
33
+ const retStr = vulnerabilitySummary(rowData.securityDetails);
34
+ return /* @__PURE__ */ React.createElement(
35
+ Link,
36
+ {
37
+ "data-testid": `${rowData.name}-security-scan`,
38
+ to: `tag/${tagManifest}`
39
+ },
40
+ retStr
41
+ );
42
+ },
43
+ id: "securityScan",
44
+ customSort: (a, b) => securityScanComparator(a, b)
45
+ },
46
+ {
47
+ title: "Size",
48
+ field: "size",
49
+ type: "numeric",
50
+ customSort: (a, b) => a.rawSize - b.rawSize
51
+ },
52
+ {
53
+ title: "Expires",
54
+ field: "expiration",
55
+ type: "date",
56
+ emptyValue: /* @__PURE__ */ React.createElement("i", null, "Never")
57
+ },
58
+ {
59
+ title: "Manifest",
60
+ field: "manifest_digest",
61
+ type: "string",
62
+ customSort: (a, b) => a.manifest_digest_raw.localeCompare(b.manifest_digest_raw)
63
+ }
64
+ ];
65
+ const useStyles = makeStyles((theme) => ({
66
+ empty: {
67
+ padding: theme.spacing(2),
68
+ display: "flex",
69
+ justifyContent: "center"
70
+ }
71
+ }));
72
+
73
+ export { columns, useStyles };
74
+ //# sourceMappingURL=tableHeading.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tableHeading.esm.js","sources":["../../../src/components/QuayRepository/tableHeading.tsx"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport React from 'react';\n\nimport { Link, Progress, TableColumn } from '@backstage/core-components';\n\nimport { Tooltip } from '@material-ui/core';\nimport makeStyles from '@material-ui/core/styles/makeStyles';\n\nimport { securityScanComparator, vulnerabilitySummary } from '../../lib/utils';\nimport type { QuayTagData } from '../../types';\n\nexport const columns: TableColumn<QuayTagData>[] = [\n {\n title: 'Tag',\n field: 'name',\n type: 'string',\n highlight: true,\n },\n {\n title: 'Last Modified',\n field: 'last_modified',\n type: 'date',\n },\n {\n title: 'Security Scan',\n field: 'securityScan',\n render: (rowData: QuayTagData): React.ReactNode => {\n if (!rowData.securityStatus && !rowData.securityDetails) {\n return (\n <span data-testid=\"quay-repo-security-scan-progress\">\n <Progress />\n </span>\n );\n }\n\n if (rowData.securityStatus === 'queued') {\n return (\n <Tooltip title=\"The manifest for this tag is queued to be scanned for vulnerabilities\">\n <span data-testid=\"quay-repo-queued-for-scan\">Queued</span>\n </Tooltip>\n );\n }\n\n if (rowData.securityStatus === 'unsupported') {\n return (\n <Tooltip title=\"The manifest for this tag has an operating system or package manager unsupported by Quay Security Scanner\">\n <span data-testid=\"quay-repo-security-scan-unsupported\">\n Unsupported\n </span>\n </Tooltip>\n );\n }\n\n const tagManifest = rowData.manifest_digest_raw;\n const retStr = vulnerabilitySummary(rowData.securityDetails);\n return (\n <Link\n data-testid={`${rowData.name}-security-scan`}\n to={`tag/${tagManifest}`}\n >\n {retStr}\n </Link>\n );\n },\n id: 'securityScan',\n customSort: (a: QuayTagData, b: QuayTagData) =>\n securityScanComparator(a, b),\n },\n {\n title: 'Size',\n field: 'size',\n type: 'numeric',\n customSort: (a: QuayTagData, b: QuayTagData) => a.rawSize - b.rawSize,\n },\n {\n title: 'Expires',\n field: 'expiration',\n type: 'date',\n emptyValue: <i>Never</i>,\n },\n {\n title: 'Manifest',\n field: 'manifest_digest',\n type: 'string',\n customSort: (a: QuayTagData, b: QuayTagData) =>\n a.manifest_digest_raw.localeCompare(b.manifest_digest_raw),\n },\n];\n\nexport const useStyles = makeStyles(theme => ({\n empty: {\n padding: theme.spacing(2),\n display: 'flex',\n justifyContent: 'center',\n },\n}));\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,107 @@
1
+ import React from 'react';
2
+ import { Link, Table } from '@backstage/core-components';
3
+ import { makeStyles, TableContainer, TableHead } from '@material-ui/core';
4
+ import KeyboardBackspaceIcon from '@material-ui/icons/KeyboardBackspace';
5
+ import LinkIcon from '@material-ui/icons/Link';
6
+ import WarningIcon from '@material-ui/icons/Warning';
7
+ import { SEVERITY_COLORS } from '../../lib/utils.esm.js';
8
+ import { VulnerabilityOrder } from '../../types.esm.js';
9
+
10
+ const getVulnerabilityLink = (link) => link.split(" ")[0];
11
+ const columns = [
12
+ {
13
+ title: "Advisory",
14
+ field: "name",
15
+ render: (rowData) => {
16
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center" } }, rowData.Name, rowData.Link.trim().length > 0 ? /* @__PURE__ */ React.createElement(Link, { to: getVulnerabilityLink(rowData.Link) }, /* @__PURE__ */ React.createElement(LinkIcon, { style: { marginLeft: "0.5rem" } })) : null);
17
+ },
18
+ customSort: (a, b) => a.Name.localeCompare(b.Name, "en")
19
+ },
20
+ {
21
+ title: "Severity",
22
+ field: "Severity",
23
+ customSort: (a, b) => {
24
+ const severityA = VulnerabilityOrder[a.Severity];
25
+ const severityB = VulnerabilityOrder[b.Severity];
26
+ return severityA - severityB;
27
+ },
28
+ render: (rowData) => {
29
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(
30
+ WarningIcon,
31
+ {
32
+ htmlColor: SEVERITY_COLORS[rowData.Severity],
33
+ style: {
34
+ marginRight: "0.5rem"
35
+ }
36
+ }
37
+ ), /* @__PURE__ */ React.createElement("span", null, rowData.Severity));
38
+ }
39
+ },
40
+ {
41
+ title: "Package Name",
42
+ field: "PackageName",
43
+ type: "string"
44
+ },
45
+ {
46
+ title: "Current Version",
47
+ field: "CurrentVersion",
48
+ type: "string"
49
+ },
50
+ {
51
+ title: "Fixed By",
52
+ field: "FixedBy",
53
+ render: (rowData) => {
54
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, rowData.FixedBy.length > 0 ? /* @__PURE__ */ React.createElement("span", null, rowData.FixedBy) : "(None)");
55
+ }
56
+ }
57
+ ];
58
+ const useStyles = makeStyles({
59
+ link: {
60
+ display: "flex",
61
+ alignItems: "center"
62
+ },
63
+ linkText: {
64
+ marginLeft: "0.5rem",
65
+ fontSize: "1.1rem"
66
+ },
67
+ tableHead: {
68
+ display: "flex",
69
+ alignItems: "center",
70
+ marginBottom: "1rem"
71
+ }
72
+ });
73
+ const QuayTagDetails = ({
74
+ layer,
75
+ rootLink,
76
+ digest
77
+ }) => {
78
+ const classes = useStyles();
79
+ const vulnerabilities = layer.Features.filter(
80
+ (feat) => typeof feat.Vulnerabilities !== "undefined"
81
+ ).map((feature) => {
82
+ return feature.Vulnerabilities.map(
83
+ (v) => {
84
+ return {
85
+ ...v,
86
+ PackageName: feature.Name,
87
+ CurrentVersion: feature.Version
88
+ };
89
+ }
90
+ );
91
+ }).flat().sort((a, b) => {
92
+ const severityA = VulnerabilityOrder[a.Severity];
93
+ const severityB = VulnerabilityOrder[b.Severity];
94
+ return severityA - severityB;
95
+ });
96
+ return /* @__PURE__ */ React.createElement(TableContainer, null, /* @__PURE__ */ React.createElement(TableHead, { className: classes.tableHead }, /* @__PURE__ */ React.createElement(Link, { to: rootLink(), className: classes.link }, /* @__PURE__ */ React.createElement(KeyboardBackspaceIcon, null), /* @__PURE__ */ React.createElement("span", { className: classes.linkText }, "Back to repository"))), /* @__PURE__ */ React.createElement(
97
+ Table,
98
+ {
99
+ title: `Vulnerabilities for ${digest.substring(0, 17)}`,
100
+ data: vulnerabilities,
101
+ columns
102
+ }
103
+ ));
104
+ };
105
+
106
+ export { QuayTagDetails, QuayTagDetails as default };
107
+ //# sourceMappingURL=component.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component.esm.js","sources":["../../../src/components/QuayTagDetails/component.tsx"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport React from 'react';\n\nimport { Link, Table, TableColumn } from '@backstage/core-components';\nimport type { RouteFunc } from '@backstage/core-plugin-api';\n\nimport { makeStyles, TableContainer, TableHead } from '@material-ui/core';\nimport KeyboardBackspaceIcon from '@material-ui/icons/KeyboardBackspace';\nimport LinkIcon from '@material-ui/icons/Link';\nimport WarningIcon from '@material-ui/icons/Warning';\n\nimport { SEVERITY_COLORS } from '../../lib/utils';\nimport {\n Layer,\n Vulnerability,\n VulnerabilityListItem,\n VulnerabilityOrder,\n} from '../../types';\n\ntype QuayTagDetailsProps = {\n layer: Layer;\n digest: string;\n rootLink: RouteFunc<undefined>;\n};\n\n// from: https://github.com/quay/quay/blob/f1d85588157eababc3cbf789002c5db521dbd616/web/src/routes/TagDetails/SecurityReport/SecurityReportTable.tsx#L43\nconst getVulnerabilityLink = (link: string) => link.split(' ')[0];\n\nconst columns: TableColumn<VulnerabilityListItem>[] = [\n {\n title: 'Advisory',\n field: 'name',\n render: (rowData: VulnerabilityListItem): React.ReactNode => {\n return (\n <div style={{ display: 'flex', alignItems: 'center' }}>\n {rowData.Name}\n {rowData.Link.trim().length > 0 ? (\n <Link to={getVulnerabilityLink(rowData.Link)}>\n <LinkIcon style={{ marginLeft: '0.5rem' }} />\n </Link>\n ) : null}\n </div>\n );\n },\n customSort: (a: VulnerabilityListItem, b: VulnerabilityListItem) =>\n a.Name.localeCompare(b.Name, 'en'),\n },\n {\n title: 'Severity',\n field: 'Severity',\n customSort: (a: VulnerabilityListItem, b: VulnerabilityListItem) => {\n const severityA = VulnerabilityOrder[a.Severity];\n const severityB = VulnerabilityOrder[b.Severity];\n\n return severityA - severityB;\n },\n render: (rowData: VulnerabilityListItem): React.ReactNode => {\n return (\n <div style={{ display: 'flex', alignItems: 'center' }}>\n <WarningIcon\n htmlColor={SEVERITY_COLORS[rowData.Severity]}\n style={{\n marginRight: '0.5rem',\n }}\n />\n <span>{rowData.Severity}</span>\n </div>\n );\n },\n },\n {\n title: 'Package Name',\n field: 'PackageName',\n type: 'string',\n },\n {\n title: 'Current Version',\n field: 'CurrentVersion',\n type: 'string',\n },\n {\n title: 'Fixed By',\n field: 'FixedBy',\n render: (rowData: VulnerabilityListItem): React.ReactNode => {\n return (\n <>\n {rowData.FixedBy.length > 0 ? (\n <span>{rowData.FixedBy}</span>\n ) : (\n '(None)'\n )}\n </>\n );\n },\n },\n];\n\nconst useStyles = makeStyles({\n link: {\n display: 'flex',\n alignItems: 'center',\n },\n linkText: {\n marginLeft: '0.5rem',\n fontSize: '1.1rem',\n },\n tableHead: {\n display: 'flex',\n alignItems: 'center',\n marginBottom: '1rem',\n },\n});\n\nexport const QuayTagDetails = ({\n layer,\n rootLink,\n digest,\n}: QuayTagDetailsProps) => {\n const classes = useStyles();\n const vulnerabilities = layer.Features.filter(\n feat => typeof feat.Vulnerabilities !== 'undefined',\n )\n .map(feature => {\n // TS doesn't seem to register this list as never being undefined from the above filter\n // so we cast it into the list\n // NOSONAR - irrelevant as per above comment\n return (feature.Vulnerabilities as Vulnerability[]).map(\n (v: Vulnerability): VulnerabilityListItem => {\n return {\n ...v,\n PackageName: feature.Name,\n CurrentVersion: feature.Version,\n };\n },\n );\n })\n .flat()\n .sort((a, b) => {\n const severityA = VulnerabilityOrder[a.Severity];\n const severityB = VulnerabilityOrder[b.Severity];\n\n return severityA - severityB;\n });\n\n return (\n <TableContainer>\n <TableHead className={classes.tableHead}>\n <Link to={rootLink()} className={classes.link}>\n <KeyboardBackspaceIcon />\n <span className={classes.linkText}>Back to repository</span>\n </Link>\n </TableHead>\n <Table\n title={`Vulnerabilities for ${digest.substring(0, 17)}`}\n data={vulnerabilities}\n columns={columns}\n />\n </TableContainer>\n );\n};\n\nexport default QuayTagDetails;\n"],"names":[],"mappings":";;;;;;;;;AAwCA,MAAM,uBAAuB,CAAC,IAAA,KAAiB,KAAK,KAAM,CAAA,GAAG,EAAE,CAAC,CAAA,CAAA;AAEhE,MAAM,OAAgD,GAAA;AAAA,EACpD;AAAA,IACE,KAAO,EAAA,UAAA;AAAA,IACP,KAAO,EAAA,MAAA;AAAA,IACP,MAAA,EAAQ,CAAC,OAAoD,KAAA;AAC3D,MAAA,uBACG,KAAA,CAAA,aAAA,CAAA,KAAA,EAAA,EAAI,KAAO,EAAA,EAAE,SAAS,MAAQ,EAAA,UAAA,EAAY,QAAS,EAAA,EAAA,EACjD,OAAQ,CAAA,IAAA,EACR,OAAQ,CAAA,IAAA,CAAK,MAAO,CAAA,MAAA,GAAS,CAC5B,mBAAA,KAAA,CAAA,aAAA,CAAC,IAAK,EAAA,EAAA,EAAA,EAAI,oBAAqB,CAAA,OAAA,CAAQ,IAAI,CACzC,EAAA,kBAAA,KAAA,CAAA,aAAA,CAAC,QAAS,EAAA,EAAA,KAAA,EAAO,EAAE,UAAY,EAAA,QAAA,EAAY,EAAA,CAC7C,IACE,IACN,CAAA,CAAA;AAAA,KAEJ;AAAA,IACA,UAAA,EAAY,CAAC,CAA0B,EAAA,CAAA,KACrC,EAAE,IAAK,CAAA,aAAA,CAAc,CAAE,CAAA,IAAA,EAAM,IAAI,CAAA;AAAA,GACrC;AAAA,EACA;AAAA,IACE,KAAO,EAAA,UAAA;AAAA,IACP,KAAO,EAAA,UAAA;AAAA,IACP,UAAA,EAAY,CAAC,CAAA,EAA0B,CAA6B,KAAA;AAClE,MAAM,MAAA,SAAA,GAAY,kBAAmB,CAAA,CAAA,CAAE,QAAQ,CAAA,CAAA;AAC/C,MAAM,MAAA,SAAA,GAAY,kBAAmB,CAAA,CAAA,CAAE,QAAQ,CAAA,CAAA;AAE/C,MAAA,OAAO,SAAY,GAAA,SAAA,CAAA;AAAA,KACrB;AAAA,IACA,MAAA,EAAQ,CAAC,OAAoD,KAAA;AAC3D,MACE,uBAAA,KAAA,CAAA,aAAA,CAAC,SAAI,KAAO,EAAA,EAAE,SAAS,MAAQ,EAAA,UAAA,EAAY,UACzC,EAAA,kBAAA,KAAA,CAAA,aAAA;AAAA,QAAC,WAAA;AAAA,QAAA;AAAA,UACC,SAAA,EAAW,eAAgB,CAAA,OAAA,CAAQ,QAAQ,CAAA;AAAA,UAC3C,KAAO,EAAA;AAAA,YACL,WAAa,EAAA,QAAA;AAAA,WACf;AAAA,SAAA;AAAA,OAEF,kBAAA,KAAA,CAAA,aAAA,CAAC,MAAM,EAAA,IAAA,EAAA,OAAA,CAAQ,QAAS,CAC1B,CAAA,CAAA;AAAA,KAEJ;AAAA,GACF;AAAA,EACA;AAAA,IACE,KAAO,EAAA,cAAA;AAAA,IACP,KAAO,EAAA,aAAA;AAAA,IACP,IAAM,EAAA,QAAA;AAAA,GACR;AAAA,EACA;AAAA,IACE,KAAO,EAAA,iBAAA;AAAA,IACP,KAAO,EAAA,gBAAA;AAAA,IACP,IAAM,EAAA,QAAA;AAAA,GACR;AAAA,EACA;AAAA,IACE,KAAO,EAAA,UAAA;AAAA,IACP,KAAO,EAAA,SAAA;AAAA,IACP,MAAA,EAAQ,CAAC,OAAoD,KAAA;AAC3D,MACE,uBAAA,KAAA,CAAA,aAAA,CAAA,KAAA,CAAA,QAAA,EAAA,IAAA,EACG,OAAQ,CAAA,OAAA,CAAQ,MAAS,GAAA,CAAA,uCACvB,MAAM,EAAA,IAAA,EAAA,OAAA,CAAQ,OAAQ,CAAA,GAEvB,QAEJ,CAAA,CAAA;AAAA,KAEJ;AAAA,GACF;AACF,CAAA,CAAA;AAEA,MAAM,YAAY,UAAW,CAAA;AAAA,EAC3B,IAAM,EAAA;AAAA,IACJ,OAAS,EAAA,MAAA;AAAA,IACT,UAAY,EAAA,QAAA;AAAA,GACd;AAAA,EACA,QAAU,EAAA;AAAA,IACR,UAAY,EAAA,QAAA;AAAA,IACZ,QAAU,EAAA,QAAA;AAAA,GACZ;AAAA,EACA,SAAW,EAAA;AAAA,IACT,OAAS,EAAA,MAAA;AAAA,IACT,UAAY,EAAA,QAAA;AAAA,IACZ,YAAc,EAAA,MAAA;AAAA,GAChB;AACF,CAAC,CAAA,CAAA;AAEM,MAAM,iBAAiB,CAAC;AAAA,EAC7B,KAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AACF,CAA2B,KAAA;AACzB,EAAA,MAAM,UAAU,SAAU,EAAA,CAAA;AAC1B,EAAM,MAAA,eAAA,GAAkB,MAAM,QAAS,CAAA,MAAA;AAAA,IACrC,CAAA,IAAA,KAAQ,OAAO,IAAA,CAAK,eAAoB,KAAA,WAAA;AAAA,GAC1C,CACG,IAAI,CAAW,OAAA,KAAA;AAId,IAAA,OAAQ,QAAQ,eAAoC,CAAA,GAAA;AAAA,MAClD,CAAC,CAA4C,KAAA;AAC3C,QAAO,OAAA;AAAA,UACL,GAAG,CAAA;AAAA,UACH,aAAa,OAAQ,CAAA,IAAA;AAAA,UACrB,gBAAgB,OAAQ,CAAA,OAAA;AAAA,SAC1B,CAAA;AAAA,OACF;AAAA,KACF,CAAA;AAAA,GACD,CACA,CAAA,IAAA,GACA,IAAK,CAAA,CAAC,GAAG,CAAM,KAAA;AACd,IAAM,MAAA,SAAA,GAAY,kBAAmB,CAAA,CAAA,CAAE,QAAQ,CAAA,CAAA;AAC/C,IAAM,MAAA,SAAA,GAAY,kBAAmB,CAAA,CAAA,CAAE,QAAQ,CAAA,CAAA;AAE/C,IAAA,OAAO,SAAY,GAAA,SAAA,CAAA;AAAA,GACpB,CAAA,CAAA;AAEH,EACE,uBAAA,KAAA,CAAA,aAAA,CAAC,cACC,EAAA,IAAA,kBAAA,KAAA,CAAA,aAAA,CAAC,SAAU,EAAA,EAAA,SAAA,EAAW,OAAQ,CAAA,SAAA,EAAA,kBAC3B,KAAA,CAAA,aAAA,CAAA,IAAA,EAAA,EAAK,EAAI,EAAA,QAAA,EAAY,EAAA,SAAA,EAAW,QAAQ,IACvC,EAAA,kBAAA,KAAA,CAAA,aAAA,CAAC,qBAAsB,EAAA,IAAA,CAAA,kBACtB,KAAA,CAAA,aAAA,CAAA,MAAA,EAAA,EAAK,SAAW,EAAA,OAAA,CAAQ,QAAU,EAAA,EAAA,oBAAkB,CACvD,CACF,CACA,kBAAA,KAAA,CAAA,aAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,OAAO,CAAuB,oBAAA,EAAA,MAAA,CAAO,SAAU,CAAA,CAAA,EAAG,EAAE,CAAC,CAAA,CAAA;AAAA,MACrD,IAAM,EAAA,eAAA;AAAA,MACN,OAAA;AAAA,KAAA;AAAA,GAEJ,CAAA,CAAA;AAEJ;;;;"}
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { useParams } from 'react-router-dom';
3
+ import { Progress, ErrorPanel } from '@backstage/core-components';
4
+ import { useRouteRef } from '@backstage/core-plugin-api';
5
+ import { useRepository, useTagDetails } from '../../hooks/quay.esm.js';
6
+ import { useQuayViewPermission } from '../../hooks/useQuayViewPermission.esm.js';
7
+ import { rootRouteRef } from '../../routes.esm.js';
8
+ import PermissionAlert from '../PermissionAlert/PermissionAlert.esm.js';
9
+ import { QuayTagDetails } from '../QuayTagDetails/component.esm.js';
10
+
11
+ const QuayTagPage = () => {
12
+ const rootLink = useRouteRef(rootRouteRef);
13
+ const { repository, organization } = useRepository();
14
+ const { digest } = useParams();
15
+ const hasViewPermission = useQuayViewPermission();
16
+ if (!digest) {
17
+ throw new Error("digest is not defined");
18
+ }
19
+ const { loading, value } = useTagDetails(organization, repository, digest);
20
+ if (!hasViewPermission) {
21
+ return /* @__PURE__ */ React.createElement(PermissionAlert, null);
22
+ }
23
+ if (loading) {
24
+ return /* @__PURE__ */ React.createElement("div", { "data-testid": "quay-tag-page-progress" }, /* @__PURE__ */ React.createElement(Progress, { variant: "query" }));
25
+ }
26
+ if (!value?.data) {
27
+ return /* @__PURE__ */ React.createElement(ErrorPanel, { error: new Error("no digest") });
28
+ }
29
+ return /* @__PURE__ */ React.createElement(
30
+ QuayTagDetails,
31
+ {
32
+ rootLink,
33
+ layer: value.data.Layer,
34
+ digest
35
+ }
36
+ );
37
+ };
38
+
39
+ export { QuayTagPage, QuayTagPage as default };
40
+ //# sourceMappingURL=component.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component.esm.js","sources":["../../../src/components/QuayTagPage/component.tsx"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport React from 'react';\nimport { useParams } from 'react-router-dom';\n\nimport { ErrorPanel, Progress } from '@backstage/core-components';\nimport { useRouteRef } from '@backstage/core-plugin-api';\n\nimport { useRepository, useTagDetails } from '../../hooks';\nimport { useQuayViewPermission } from '../../hooks/useQuayViewPermission';\nimport { rootRouteRef } from '../../routes';\nimport PermissionAlert from '../PermissionAlert/PermissionAlert';\nimport { QuayTagDetails } from '../QuayTagDetails';\n\nexport const QuayTagPage = () => {\n const rootLink = useRouteRef(rootRouteRef);\n const { repository, organization } = useRepository();\n const { digest } = useParams();\n const hasViewPermission = useQuayViewPermission();\n if (!digest) {\n throw new Error('digest is not defined');\n }\n const { loading, value } = useTagDetails(organization, repository, digest);\n\n if (!hasViewPermission) {\n return <PermissionAlert />;\n }\n\n if (loading) {\n return (\n <div data-testid=\"quay-tag-page-progress\">\n <Progress variant=\"query\" />\n </div>\n );\n }\n if (!value?.data) {\n return <ErrorPanel error={new Error('no digest')} />;\n }\n\n return (\n <QuayTagDetails\n rootLink={rootLink}\n layer={value.data.Layer}\n digest={digest}\n />\n );\n};\n\nexport default QuayTagPage;\n"],"names":[],"mappings":";;;;;;;;;;AA2BO,MAAM,cAAc,MAAM;AAC/B,EAAM,MAAA,QAAA,GAAW,YAAY,YAAY,CAAA,CAAA;AACzC,EAAA,MAAM,EAAE,UAAA,EAAY,YAAa,EAAA,GAAI,aAAc,EAAA,CAAA;AACnD,EAAM,MAAA,EAAE,MAAO,EAAA,GAAI,SAAU,EAAA,CAAA;AAC7B,EAAA,MAAM,oBAAoB,qBAAsB,EAAA,CAAA;AAChD,EAAA,IAAI,CAAC,MAAQ,EAAA;AACX,IAAM,MAAA,IAAI,MAAM,uBAAuB,CAAA,CAAA;AAAA,GACzC;AACA,EAAA,MAAM,EAAE,OAAS,EAAA,KAAA,KAAU,aAAc,CAAA,YAAA,EAAc,YAAY,MAAM,CAAA,CAAA;AAEzE,EAAA,IAAI,CAAC,iBAAmB,EAAA;AACtB,IAAA,2CAAQ,eAAgB,EAAA,IAAA,CAAA,CAAA;AAAA,GAC1B;AAEA,EAAA,IAAI,OAAS,EAAA;AACX,IACE,uBAAA,KAAA,CAAA,aAAA,CAAC,SAAI,aAAY,EAAA,wBAAA,EAAA,sCACd,QAAS,EAAA,EAAA,OAAA,EAAQ,SAAQ,CAC5B,CAAA,CAAA;AAAA,GAEJ;AACA,EAAI,IAAA,CAAC,OAAO,IAAM,EAAA;AAChB,IAAA,2CAAQ,UAAW,EAAA,EAAA,KAAA,EAAO,IAAI,KAAA,CAAM,WAAW,CAAG,EAAA,CAAA,CAAA;AAAA,GACpD;AAEA,EACE,uBAAA,KAAA,CAAA,aAAA;AAAA,IAAC,cAAA;AAAA,IAAA;AAAA,MACC,QAAA;AAAA,MACA,KAAA,EAAO,MAAM,IAAK,CAAA,KAAA;AAAA,MAClB,MAAA;AAAA,KAAA;AAAA,GACF,CAAA;AAEJ;;;;"}
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { Routes, Route } from 'react-router-dom';
3
+ import { QUAY_ANNOTATION_REPOSITORY } from '../hooks/quay.esm.js';
4
+ import { tagRouteRef } from '../routes.esm.js';
5
+ import { QuayRepository } from './QuayRepository/QuayRepository.esm.js';
6
+ import { QuayTagPage } from './QuayTagPage/component.esm.js';
7
+
8
+ const isQuayAvailable = (entity) => Boolean(entity?.metadata.annotations?.[QUAY_ANNOTATION_REPOSITORY]);
9
+ const Router = () => /* @__PURE__ */ React.createElement(Routes, null, /* @__PURE__ */ React.createElement(Route, { path: "/", element: /* @__PURE__ */ React.createElement(QuayRepository, null) }), /* @__PURE__ */ React.createElement(Route, { path: tagRouteRef.path, element: /* @__PURE__ */ React.createElement(QuayTagPage, null) }));
10
+
11
+ export { Router, isQuayAvailable };
12
+ //# sourceMappingURL=Router.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Router.esm.js","sources":["../../src/components/Router.tsx"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport React from 'react';\nimport { Route, Routes } from 'react-router-dom';\n\nimport { Entity } from '@backstage/catalog-model';\n\nimport { QUAY_ANNOTATION_REPOSITORY } from '../hooks';\nimport { tagRouteRef } from '../routes';\nimport { QuayRepository } from './QuayRepository';\nimport { QuayTagPage } from './QuayTagPage';\n\n/** *\n * @public\n */\nexport const isQuayAvailable = (entity: Entity) =>\n Boolean(entity?.metadata.annotations?.[QUAY_ANNOTATION_REPOSITORY]);\n/**\n *\n * @public\n */\nexport const Router = () => (\n <Routes>\n <Route path=\"/\" element={<QuayRepository />} />\n <Route path={tagRouteRef.path} element={<QuayTagPage />} />\n </Routes>\n);\n"],"names":[],"mappings":";;;;;;;AA4Ba,MAAA,eAAA,GAAkB,CAAC,MAC9B,KAAA,OAAA,CAAQ,QAAQ,QAAS,CAAA,WAAA,GAAc,0BAA0B,CAAC,EAAA;AAKvD,MAAA,MAAA,GAAS,sBACnB,KAAA,CAAA,aAAA,CAAA,MAAA,EAAA,IAAA,sCACE,KAAM,EAAA,EAAA,IAAA,EAAK,GAAI,EAAA,OAAA,kBAAU,KAAA,CAAA,aAAA,CAAA,cAAA,EAAA,IAAe,GAAI,CAC7C,kBAAA,KAAA,CAAA,aAAA,CAAC,SAAM,IAAM,EAAA,WAAA,CAAY,MAAM,OAAS,kBAAA,KAAA,CAAA,aAAA,CAAC,WAAY,EAAA,IAAA,CAAA,EAAI,CAC3D;;;;"}