diff --git a/.changeset/tricky-trains-change.md b/.changeset/tricky-trains-change.md new file mode 100644 index 0000000000..391dee0eb1 --- /dev/null +++ b/.changeset/tricky-trains-change.md @@ -0,0 +1,27 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-catalog': patch +--- + +Added an EntityProcessingErrorsPanel component to show any errors that occurred when refreshing an entity from its source location. If upgrading, this should be added to EntityPage in your Backstage application: + +```diff +// packages/app/src/components/catalog/EntityPage.tsx + +const overviewContent = ( +... + + + + ++ ++ ++ ++ ++ ++ ++ + +``` + +Additionally, WarningPanel now changes color based on the provided severity. diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 4e2e32cc86..d84a7341bc 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -40,6 +40,8 @@ import { isKind, EntityHasResourcesCard, EntityOrphanWarning, + EntityProcessingErrorsPanel, + hasCatalogProcessingErrors, isOrphan, } from '@backstage/plugin-catalog'; import { @@ -231,6 +233,14 @@ const overviewContent = ( + + + + + + + + diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index ee75877b1d..5ce289d8d8 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -80,7 +80,7 @@ export const Avatar: ({ // Warning: (ae-missing-release-tag) "Breadcrumbs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Breadcrumbs: ({ children, ...props }: Props_25) => JSX.Element; +export const Breadcrumbs: ({ children, ...props }: Props_24) => JSX.Element; // Warning: (ae-missing-release-tag) "BrokenImageIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -441,7 +441,7 @@ export const Content: ({ noPadding, children, ...props -}: PropsWithChildren) => JSX.Element; +}: PropsWithChildren) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "ContentHeaderProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ContentHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -695,7 +695,7 @@ export const Header: ({ tooltip, type, typeLink, -}: PropsWithChildren) => JSX.Element; +}: PropsWithChildren) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "HeaderIconLinkRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -777,7 +777,7 @@ export const InfoCard: ({ className, noPadding, titleTypographyProps, -}: Props_20) => JSX.Element; +}: Props_19) => JSX.Element; // Warning: (ae-missing-release-tag) "InfoCardVariants" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1200,7 +1200,7 @@ export const OverflowTooltip: (props: Props_12) => JSX.Element; export const Page: ({ themeId, children, -}: PropsWithChildren) => JSX.Element; +}: PropsWithChildren) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "PageWithHeaderProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PageWithHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1291,7 +1291,7 @@ export const Sidebar: ({ openDelayMs, closeDelayMs, children, -}: PropsWithChildren) => JSX.Element; +}: PropsWithChildren) => JSX.Element; // Warning: (ae-missing-release-tag) "SIDEBAR_INTRO_LOCAL_STORAGE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2448,7 +2448,7 @@ export const SidebarSpacer: React_2.ComponentType< // Warning: (ae-missing-release-tag) "SignInPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SignInPage: (props: Props_23) => JSX.Element; +export const SignInPage: (props: Props_22) => JSX.Element; // Warning: (ae-missing-release-tag) "SignInProviderConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2592,7 +2592,7 @@ export const TabbedCard: ({ deepLink, value, onChange, -}: PropsWithChildren) => JSX.Element; +}: PropsWithChildren) => JSX.Element; // Warning: (ae-missing-release-tag) "TabbedLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2715,11 +2715,17 @@ export const WarningIcon: IconComponent; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-optional-name) The @param should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets. // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "WarningProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "WarningPanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export const WarningPanel: (props: Props_17) => JSX.Element; +export const WarningPanel: ({ + severity, + title, + message, + children, + defaultExpanded, +}: WarningProps) => JSX.Element; // Warnings were encountered during analysis: // diff --git a/packages/core-components/src/components/ErrorPanel/ErrorPanel.tsx b/packages/core-components/src/components/ErrorPanel/ErrorPanel.tsx index 14b5d50325..f9996b9b1f 100644 --- a/packages/core-components/src/components/ErrorPanel/ErrorPanel.tsx +++ b/packages/core-components/src/components/ErrorPanel/ErrorPanel.tsx @@ -100,6 +100,7 @@ export const ErrorPanel = ({ }: PropsWithChildren) => { return ( diff --git a/packages/core-components/src/components/WarningPanel/WarningPanel.test.tsx b/packages/core-components/src/components/WarningPanel/WarningPanel.test.tsx index 36b5372900..4ae6423333 100644 --- a/packages/core-components/src/components/WarningPanel/WarningPanel.test.tsx +++ b/packages/core-components/src/components/WarningPanel/WarningPanel.test.tsx @@ -19,11 +19,19 @@ import { fireEvent, screen } from '@testing-library/react'; import { renderInTestApp } from '@backstage/test-utils'; import { Typography } from '@material-ui/core'; -import { WarningPanel } from './WarningPanel'; +import { WarningPanel, WarningProps } from './WarningPanel'; -const propsTitle = { title: 'Mock title' }; -const propsTitleMessage = { title: 'Mock title', message: 'Some more info' }; -const propsMessage = { message: 'Some more info' }; +const propsTitle: WarningProps = { title: 'Mock title' }; +const propsTitleMessage: WarningProps = { + title: 'Mock title', + message: 'Some more info', +}; +const propsMessage: WarningProps = { message: 'Some more info' }; +const propsErrorMessage: WarningProps = { + severity: 'error', + title: 'Mock title', + message: 'Some more info', +}; describe('', () => { it('renders without exploding', async () => { @@ -64,4 +72,8 @@ describe('', () => { expect(screen.getByText('Some more info')).toBeInTheDocument(); expect(screen.getByText('Java stacktrace')).toBeInTheDocument(); }); + it('renders message using severity', async () => { + await renderInTestApp(); + expect(screen.getByText('Error: Mock title')).toBeInTheDocument(); + }); }); diff --git a/packages/core-components/src/components/WarningPanel/WarningPanel.tsx b/packages/core-components/src/components/WarningPanel/WarningPanel.tsx index 37ae5b2c62..e6188172b7 100644 --- a/packages/core-components/src/components/WarningPanel/WarningPanel.tsx +++ b/packages/core-components/src/components/WarningPanel/WarningPanel.tsx @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { BackstageTheme } from '@backstage/theme'; import { Accordion, @@ -22,30 +21,61 @@ import { Grid, makeStyles, Typography, + darken, + lighten, } from '@material-ui/core'; import ErrorOutline from '@material-ui/icons/ErrorOutline'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import React from 'react'; +const getWarningTextColor = ( + severity: NonNullable, + theme: BackstageTheme, +) => { + const getColor = theme.palette.type === 'light' ? darken : lighten; + return getColor(theme.palette[severity].light, 0.6); +}; + +const getWarningBackgroundColor = ( + severity: NonNullable, + theme: BackstageTheme, +) => { + const getBackgroundColor = theme.palette.type === 'light' ? lighten : darken; + return getBackgroundColor(theme.palette[severity].light, 0.9); +}; + const useErrorOutlineStyles = makeStyles(theme => ({ root: { marginRight: theme.spacing(1), - fill: theme.palette.warningText, + fill: ({ severity }: WarningProps) => + getWarningTextColor( + severity as NonNullable, + theme, + ), }, })); -const ErrorOutlineStyled = () => { - const classes = useErrorOutlineStyles(); + +const ErrorOutlineStyled = ({ severity }: Pick) => { + const classes = useErrorOutlineStyles({ severity }); return ; }; -const ExpandMoreIconStyled = () => { - const classes = useErrorOutlineStyles(); +const ExpandMoreIconStyled = ({ severity }: Pick) => { + const classes = useErrorOutlineStyles({ severity }); return ; }; const useStyles = makeStyles(theme => ({ panel: { - backgroundColor: theme.palette.warningBackground, - color: theme.palette.warningText, + backgroundColor: ({ severity }: WarningProps) => + getWarningBackgroundColor( + severity as NonNullable, + theme, + ), + color: ({ severity }: WarningProps) => + getWarningTextColor( + severity as NonNullable, + theme, + ), verticalAlign: 'middle', }, summary: { @@ -53,14 +83,26 @@ const useStyles = makeStyles(theme => ({ flexDirection: 'row', }, summaryText: { - color: theme.palette.warningText, + color: ({ severity }: WarningProps) => + getWarningTextColor( + severity as NonNullable, + theme, + ), fontWeight: 'bold', }, message: { width: '100%', display: 'block', - color: theme.palette.warningText, - backgroundColor: theme.palette.warningBackground, + color: ({ severity }: WarningProps) => + getWarningTextColor( + severity as NonNullable, + theme, + ), + backgroundColor: ({ severity }: WarningProps) => + getWarningBackgroundColor( + severity as NonNullable, + theme, + ), }, details: { width: '100%', @@ -73,7 +115,7 @@ const useStyles = makeStyles(theme => ({ }, })); -type Props = { +export type WarningProps = { title?: string; severity?: 'warning' | 'error' | 'info'; message?: React.ReactNode; @@ -89,19 +131,23 @@ const capitalize = (s: string) => { * WarningPanel. Show a user friendly error message to a user similar to ErrorPanel except that the warning panel * only shows the warning message to the user. * - * @param {string} [severity=warning] Ability to change the severity of the alert. Not fully implemented. (error, warning, info) + * @param {string} [severity=warning] Ability to change the severity of the alert. * @param {string} [title] A title for the warning. If not supplied, "Warning" will be used. * @param {Object} [message] Optional more detailed user-friendly message elaborating on the cause of the error. * @param {Object} [children] Objects to provide context, such as a stack trace or detailed error reporting. * Will be available inside an unfolded accordion. */ -export const WarningPanel = (props: Props) => { - const classes = useStyles(props); - const { severity, title, message, children, defaultExpanded } = props; +export const WarningPanel = ({ + severity = 'warning', + title, + message, + children, + defaultExpanded, +}: WarningProps) => { + const classes = useStyles({ severity }); // If no severity or title provided, the heading will read simply "Warning" - const subTitle = - (severity ? capitalize(severity) : 'Warning') + (title ? `: ${title}` : ''); + const subTitle = capitalize(severity) + (title ? `: ${title}` : ''); return ( { role="alert" > } + expandIcon={} className={classes.summary} > - + {subTitle} diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index 2074a2712c..9a4659c65a 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -35,8 +35,12 @@ import { EntityLayout, EntityLinksCard, EntitySwitch, + EntityOrphanWarning, + EntityProcessingErrorsPanel, isComponentType, isKind, + hasCatalogProcessingErrors, + isOrphan, } from '@backstage/plugin-catalog'; import { isGithubActionsAvailable, @@ -80,6 +84,22 @@ const cicdContent = ( const overviewContent = ( + + + + + + + + + + + + + + + + diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index a416e70038..c030882bca 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -327,6 +327,11 @@ export const EntityPageLayout: { }) => null; }; +// Warning: (ae-missing-release-tag) "EntityProcessingErrorsPanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export const EntityProcessingErrorsPanel: () => JSX.Element; + // Warning: (ae-missing-release-tag) "EntitySwitch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -358,6 +363,11 @@ export const FilteredEntityLayout: ({ children, }: PropsWithChildren<{}>) => JSX.Element; +// Warning: (ae-missing-release-tag) "hasCatalogProcessingErrors" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const hasCatalogProcessingErrors: (entity: Entity) => boolean; + // Warning: (ae-missing-release-tag) "isComponentType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx new file mode 100644 index 0000000000..a4e284a0eb --- /dev/null +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityProvider } from '@backstage/plugin-catalog-react'; + +import { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { EntityProcessingErrorsPanel } from './EntityProcessingErrorsPanel'; +import { Entity } from '@backstage/catalog-model'; + +describe('', () => { + it('renders EntityProcessErrors if the entity has errors', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + description: 'This is the description', + }, + + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + status: { + items: [ + { + type: 'backstage.io/catalog-processing', + level: 'error', + message: + 'InputError: Policy check failed; caused by Error: Malformed envelope, /metadata/labels should be object', + error: { + name: 'InputError', + message: + 'Policy check failed; caused by Error: Malformed envelope, /metadata/labels should be object', + cause: { + name: 'Error', + message: + 'Malformed envelope, /metadata/labels should be object', + }, + }, + }, + { + type: 'foo', + level: 'error', + message: 'InputError: This should not be rendered', + error: { + name: 'InputError', + message: 'Foo', + cause: { + name: 'Error', + message: + 'Malformed envelope, /metadata/labels should be object', + }, + }, + }, + { + type: 'backstage.io/catalog-processing', + level: 'error', + message: 'InputError: Foo', + error: { + name: 'InputError', + message: 'Foo', + cause: { + name: 'Error', + message: + 'Malformed envelope, /metadata/labels should be object', + }, + }, + }, + ], + }, + }; + + const { getByText, queryByText } = await renderInTestApp( + + + , + ); + + expect( + getByText( + 'Error: Policy check failed; caused by Error: Malformed envelope, /metadata/labels should be object', + ), + ).toBeInTheDocument(); + expect(getByText('Error: Foo')).toBeInTheDocument(); + expect(queryByText('Error: This should not be rendered')).toBeNull(); + }); +}); diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx new file mode 100644 index 0000000000..833ca74daa --- /dev/null +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity, UNSTABLE_EntityStatusItem } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { Box } from '@material-ui/core'; +import React from 'react'; +import { ResponseErrorPanel } from '@backstage/core-components'; +import { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client'; + +const errorfilter = (i: UNSTABLE_EntityStatusItem) => + i.error && + i.level === 'error' && + i.type === ENTITY_STATUS_CATALOG_PROCESSING_TYPE; + +export const hasCatalogProcessingErrors = (entity: Entity) => + entity?.status?.items?.filter(errorfilter).length! > 0; + +/** + * Displays a list of errors if the entity is invalid. + */ +export const EntityProcessingErrorsPanel = () => { + const { entity } = useEntity(); + const catalogProcessingErrors = + (entity?.status?.items?.filter( + errorfilter, + ) as Required[]) || []; + + return ( + <> + {catalogProcessingErrors.map(({ error }, index) => ( + + + + ))} + + ); +}; diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/index.ts b/plugins/catalog/src/components/EntityProcessingErrorsPanel/index.ts new file mode 100644 index 0000000000..1af2c80484 --- /dev/null +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + EntityProcessingErrorsPanel, + hasCatalogProcessingErrors, +} from './EntityProcessingErrorsPanel'; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 215a18f661..fd542fafa3 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -23,6 +23,7 @@ export * from './components/CatalogTable/columns'; export * from './components/CreateComponentButton'; export * from './components/EntityLayout'; export * from './components/EntityOrphanWarning'; +export * from './components/EntityProcessingErrorsPanel'; export * from './components/EntityPageLayout'; export * from './components/EntitySwitch'; export * from './components/FilteredEntityLayout'; diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index 6953cf57a2..7b3a73673e 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -100,7 +100,7 @@ describe('', () => { const catalogError = new Error('Network timeout'); catalogApi.getEntities.mockRejectedValueOnce(catalogError); - const { getByText } = await renderInTestApp( + const { getAllByText } = await renderInTestApp( , @@ -108,7 +108,7 @@ describe('', () => { ); await waitFor(() => - expect(getByText(/Warning: Network timeout/)).toBeInTheDocument(), + expect(getAllByText(/Error: Network timeout/).length).not.toBe(0), ); }); }); diff --git a/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx b/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx index 3c771bd7d6..79b401e4a8 100644 --- a/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx +++ b/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx @@ -74,7 +74,7 @@ describe('', () => { fossaApi.getFindingSummary.mockRejectedValue(new Error('My Error')); - const { getByText } = await renderInTestApp( + const { getAllByText } = await renderInTestApp( @@ -82,7 +82,7 @@ describe('', () => { , ); - expect(getByText(/Warning: My Error/i)).toBeInTheDocument(); + expect(getAllByText(/Error: My Error/i).length).not.toBe(0); }); it('shows empty', async () => { diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 474b4a5941..4c1b7854c6 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -311,7 +311,7 @@ describe('github-deployments', () => { expect( await rendered.findByText( - 'Warning: No apiBaseUrl available for host my-github-3.com, please check your integrations config', + 'Error: No apiBaseUrl available for host my-github-3.com, please check your integrations config', ), ).toBeInTheDocument(); }); @@ -336,7 +336,7 @@ describe('github-deployments', () => { expect( await rendered.findByText( - 'Warning: No matching GitHub integration configuration for host my-github-unknown.com, please check your integrations config', + 'Error: No matching GitHub integration configuration for host my-github-unknown.com, please check your integrations config', ), ).toBeInTheDocument(); });