From 0726c034516f5abcd5a5d65db6821952d8e098d5 Mon Sep 17 00:00:00 2001 From: Frida Jacobsson Date: Fri, 11 Oct 2024 14:33:10 +0200 Subject: [PATCH] Hide ReadmeCard if backend returns 404 (#194) * Export isReadmeAvailable function from readme-plugin * Add changeset * Update docs * Update api-report * Update response error from backend * Update changeset * Fix linting problems * Use ResponseError for all errors --------- Co-authored-by: Frida Jacobsson --- .changeset/kind-avocados-smell.md | 7 +++ .../app/src/components/catalog/EntityPage.tsx | 12 +++-- plugins/readme-backend/package.json | 1 + plugins/readme-backend/src/service/router.ts | 16 +++---- plugins/readme/README.md | 19 ++++++++ plugins/readme/api-report.md | 10 ++++ plugins/readme/package.json | 1 + plugins/readme/src/api/ReadmeClient.tsx | 46 ++++++++++++++++--- .../FetchComponent/FetchComponent.tsx | 36 +++++++++------ plugins/readme/src/index.ts | 1 + yarn.lock | 2 + 11 files changed, 116 insertions(+), 35 deletions(-) create mode 100644 .changeset/kind-avocados-smell.md diff --git a/.changeset/kind-avocados-smell.md b/.changeset/kind-avocados-smell.md new file mode 100644 index 00000000..72a287a8 --- /dev/null +++ b/.changeset/kind-avocados-smell.md @@ -0,0 +1,7 @@ +--- +'@axis-backstage/plugin-readme-backend': minor +'@axis-backstage/plugin-readme': minor +'app': minor +--- + +Created the isReadmeAvailable function that returns false if no README content is found due to 404-error. If it returns false, no ReadmeCard is rendered in EntityPage. Also updated the error response from backend to be a NotFoundError. diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index b84f6b7d..8f4a0264 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -56,7 +56,7 @@ import { EntityJiraDashboardContent, isJiraDashboardAvailable, } from '@axis-backstage/plugin-jira-dashboard'; -import { ReadmeCard } from '@axis-backstage/plugin-readme'; +import { ReadmeCard, isReadmeAvailable } from '@axis-backstage/plugin-readme'; import { isStatuspageAvailable, StatuspageEntityCard, @@ -115,9 +115,13 @@ const overviewContent = ( - - - + + + + + + + diff --git a/plugins/readme-backend/package.json b/plugins/readme-backend/package.json index 053cedcd..bbd6c5e4 100644 --- a/plugins/readme-backend/package.json +++ b/plugins/readme-backend/package.json @@ -33,6 +33,7 @@ "@backstage/backend-plugin-api": "^0.8.0", "@backstage/catalog-client": "^1.6.6", "@backstage/catalog-model": "^1.6.0", + "@backstage/errors": "^1.2.4", "@backstage/integration": "^1.14.0", "@types/express": "*", "express": "^4.17.1", diff --git a/plugins/readme-backend/src/service/router.ts b/plugins/readme-backend/src/service/router.ts index 212dcad5..7c192720 100644 --- a/plugins/readme-backend/src/service/router.ts +++ b/plugins/readme-backend/src/service/router.ts @@ -15,6 +15,7 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { CatalogClient } from '@backstage/catalog-client'; +import { isError, NotFoundError } from '@backstage/errors'; import express from 'express'; import Router from 'express-promise-router'; import { isSymLink } from '../lib'; @@ -122,11 +123,9 @@ export async function createRouter( const source = getEntitySourceLocation(entity); if (!source || source.type !== 'url') { - logger.info(`Not valid location for ${source.target}`); - response.status(404).json({ - error: `Not valid location for ${source.target}`, - }); - return; + const errorMessage = `Not valid location for ${source.target}`; + logger.info(errorMessage); + throw new NotFoundError(errorMessage); } const integration = integrations.byUrl(source.target); @@ -170,8 +169,7 @@ export async function createRouter( response.send(content); return; } catch (error: unknown) { - if (error instanceof Error && error.name === 'NotFoundError') { - // Try the next readme type + if (isError(error) && error.name === 'NotFoundError') { continue; } else { response.status(500).json({ @@ -182,9 +180,7 @@ export async function createRouter( } } logger.info(`Readme not found for ${entityRef}`); - response.status(404).json({ - error: 'Readme not found.', - }); + throw new NotFoundError('Readme could not be found'); }); const middleware = MiddlewareFactory.create({ logger, config }); diff --git a/plugins/readme/README.md b/plugins/readme/README.md index bac9c0e7..f2545978 100644 --- a/plugins/readme/README.md +++ b/plugins/readme/README.md @@ -48,6 +48,24 @@ const overviewContent = ( ) ``` +If you wish to only render the ReadmeCard if a README file can be found for the entity, you can use the exported function **isReadmeAvailable**. See example below: + +```tsx +import { ReadmeCard, isReadmeAvailable } from '@axis-backstage/plugin-readme'; + +const defaultEntityPage = ( +... + + + + + + + +... +) +``` + To use `ReadmeCard` in a seperate page with full height: ```tsx @@ -59,6 +77,7 @@ const defaultEntityPage = ( ... +) ``` ## Layout diff --git a/plugins/readme/api-report.md b/plugins/readme/api-report.md index 2d0c4551..35053588 100644 --- a/plugins/readme/api-report.md +++ b/plugins/readme/api-report.md @@ -5,11 +5,21 @@ ```ts /// +import { ApiHolder } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; import { InfoCardVariants } from '@backstage/core-components'; import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; +// @public +export const isReadmeAvailable: ( + entity: Entity, + context: { + apis: ApiHolder; + }, +) => Promise; + // @public export const ReadmeCard: (props: ReadmeCardProps) => JSX_2.Element; diff --git a/plugins/readme/package.json b/plugins/readme/package.json index 6692a198..73ae1012 100644 --- a/plugins/readme/package.json +++ b/plugins/readme/package.json @@ -32,6 +32,7 @@ "@backstage/catalog-model": "^1.6.0", "@backstage/core-components": "^0.14.10", "@backstage/core-plugin-api": "^1.9.3", + "@backstage/errors": "^1.2.4", "@backstage/plugin-catalog-react": "^1.12.3", "@backstage/theme": "^0.5.6", "@mui/icons-material": "^5.15.7", diff --git a/plugins/readme/src/api/ReadmeClient.tsx b/plugins/readme/src/api/ReadmeClient.tsx index 65e78eda..2b4426d6 100644 --- a/plugins/readme/src/api/ReadmeClient.tsx +++ b/plugins/readme/src/api/ReadmeClient.tsx @@ -1,10 +1,45 @@ import { + ApiHolder, DiscoveryApi, FetchApi, IdentityApi, } from '@backstage/core-plugin-api'; -import { DEFAULT_NAMESPACE, parseEntityRef } from '@backstage/catalog-model'; -import { ReadmeApi } from './ReadmeApi'; +import { + DEFAULT_NAMESPACE, + Entity, + parseEntityRef, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { ResponseError } from '@backstage/errors'; +import { ReadmeApi, readmeApiRef } from './ReadmeApi'; + +/** + * Checks if a README is available for the given entity by making a request to the backend. +If the backend returns a 404 NotFound error, it indicates that no README is available for the entity. +@param entity - The entity for which to check the README availability. +@param context - The context providing access to the API holder. +@returns A promise that resolves to true if the README is available, or false if not found (404). + * @public + */ +export const isReadmeAvailable = async ( + entity: Entity, + context: { apis: ApiHolder }, +): Promise => { + const readmeClient = context.apis.get(readmeApiRef); + + if (readmeClient === undefined) { + return false; + } + + try { + await readmeClient.getReadmeContent(stringifyEntityRef(entity)); + } catch (error) { + if (error instanceof ResponseError && error.statusCode === 404) { + return false; + } + } + return true; +}; export class ReadmeClient implements ReadmeApi { private readonly discoveryApi: DiscoveryApi; @@ -36,15 +71,14 @@ export class ReadmeClient implements ReadmeApi { headers: { Authorization: `Bearer ${token}` }, }, ); + if (resp.ok) { return [ await resp.text(), resp.headers.get('Content-Type') || 'text/plain', ]; } - if (resp.status === 404) { - throw new Error('404'); - } - throw new Error(`${resp.status}: ${resp.statusText}`); + + throw await ResponseError.fromResponse(resp); } } diff --git a/plugins/readme/src/components/FetchComponent/FetchComponent.tsx b/plugins/readme/src/components/FetchComponent/FetchComponent.tsx index 72a37630..9998fd5c 100644 --- a/plugins/readme/src/components/FetchComponent/FetchComponent.tsx +++ b/plugins/readme/src/components/FetchComponent/FetchComponent.tsx @@ -7,6 +7,7 @@ import { Link, MarkdownContent, Progress, + ResponseErrorPanel, } from '@backstage/core-components'; import useAsync from 'react-use/lib/useAsync'; import { readmeApiRef } from '../../api/ReadmeApi'; @@ -14,6 +15,7 @@ import { stringifyEntityRef } from '@backstage/catalog-model'; import { getEntitySourceLocation } from '@backstage/catalog-model'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; +import { ResponseError } from '@backstage/errors'; export const FetchComponent = () => { const { entity } = useEntity(); @@ -40,21 +42,25 @@ export const FetchComponent = () => { if (loading) { return ; } - if (error?.message === '404') { - return ( - - - No README.md file found at source location:{' '} - {location && {location}} - - - Need help? Go to our{' '} - - documentation - - - - ); + + if (error instanceof ResponseError) { + if (error.statusCode === 404) { + return ( + + + No README.md file found at source location:{' '} + {location && {location}} + + + Need help? Go to our{' '} + + documentation + + + + ); + } + return ; } if (error) { diff --git a/plugins/readme/src/index.ts b/plugins/readme/src/index.ts index 1f3e083d..f624c6d1 100644 --- a/plugins/readme/src/index.ts +++ b/plugins/readme/src/index.ts @@ -6,3 +6,4 @@ export { readmePlugin, ReadmeCard } from './plugin'; export type { ReadmeCardProps } from './components/ReadmeCard'; +export { isReadmeAvailable } from './api/ReadmeClient'; diff --git a/yarn.lock b/yarn.lock index e7a1ba75..64333977 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1532,6 +1532,7 @@ __metadata: "@backstage/catalog-client": "npm:^1.6.6" "@backstage/catalog-model": "npm:^1.6.0" "@backstage/cli": "npm:^0.27.0" + "@backstage/errors": "npm:^1.2.4" "@backstage/integration": "npm:^1.14.0" "@types/express": "npm:*" "@types/supertest": "npm:^2.0.12" @@ -1552,6 +1553,7 @@ __metadata: "@backstage/core-components": "npm:^0.14.10" "@backstage/core-plugin-api": "npm:^1.9.3" "@backstage/dev-utils": "npm:^1.0.37" + "@backstage/errors": "npm:^1.2.4" "@backstage/plugin-catalog-react": "npm:^1.12.3" "@backstage/test-utils": "npm:^1.5.10" "@backstage/theme": "npm:^0.5.6"