Merge pull request #6586 from ntgussoni/feature/show-catalog-errors

Show catalog-parsing related errors in entity page
This commit is contained in:
Tim Hansen
2021-07-28 14:08:17 -06:00
committed by GitHub
15 changed files with 347 additions and 40 deletions
+27
View File
@@ -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 = (
...
<EntityOrphanWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
+ <EntitySwitch>
+ <EntitySwitch.Case if={hasCatalogProcessingErrors}>
+ <Grid item xs={12}>
+ <EntityProcessingErrorsPanel />
+ </Grid>
+ </EntitySwitch.Case>
+ </EntitySwitch>
```
Additionally, WarningPanel now changes color based on the provided severity.
@@ -40,6 +40,8 @@ import {
isKind,
EntityHasResourcesCard,
EntityOrphanWarning,
EntityProcessingErrorsPanel,
hasCatalogProcessingErrors,
isOrphan,
} from '@backstage/plugin-catalog';
import {
@@ -231,6 +233,14 @@ const overviewContent = (
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasCatalogProcessingErrors}>
<Grid item xs={12}>
<EntityProcessingErrorsPanel />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={8} xs={12}>
<EntityAboutCard variant="gridItem" />
</Grid>
+16 -10
View File
@@ -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<Props_18>) => JSX.Element;
}: PropsWithChildren<Props_17>) => 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<Props_19>) => JSX.Element;
}: PropsWithChildren<Props_18>) => 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<Props_21>) => JSX.Element;
}: PropsWithChildren<Props_20>) => 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<Props_22>) => JSX.Element;
}: PropsWithChildren<Props_21>) => 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<Props_24>) => JSX.Element;
}: PropsWithChildren<Props_23>) => 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:
//
@@ -100,6 +100,7 @@ export const ErrorPanel = ({
}: PropsWithChildren<ErrorPanelProps>) => {
return (
<WarningPanel
severity="error"
title={title ?? error.message}
defaultExpanded={defaultExpanded}
>
@@ -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('<WarningPanel />', () => {
it('renders without exploding', async () => {
@@ -64,4 +72,8 @@ describe('<WarningPanel />', () => {
expect(screen.getByText('Some more info')).toBeInTheDocument();
expect(screen.getByText('Java stacktrace')).toBeInTheDocument();
});
it('renders message using severity', async () => {
await renderInTestApp(<WarningPanel {...propsErrorMessage} />);
expect(screen.getByText('Error: Mock title')).toBeInTheDocument();
});
});
@@ -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<WarningProps['severity']>,
theme: BackstageTheme,
) => {
const getColor = theme.palette.type === 'light' ? darken : lighten;
return getColor(theme.palette[severity].light, 0.6);
};
const getWarningBackgroundColor = (
severity: NonNullable<WarningProps['severity']>,
theme: BackstageTheme,
) => {
const getBackgroundColor = theme.palette.type === 'light' ? lighten : darken;
return getBackgroundColor(theme.palette[severity].light, 0.9);
};
const useErrorOutlineStyles = makeStyles<BackstageTheme>(theme => ({
root: {
marginRight: theme.spacing(1),
fill: theme.palette.warningText,
fill: ({ severity }: WarningProps) =>
getWarningTextColor(
severity as NonNullable<WarningProps['severity']>,
theme,
),
},
}));
const ErrorOutlineStyled = () => {
const classes = useErrorOutlineStyles();
const ErrorOutlineStyled = ({ severity }: Pick<WarningProps, 'severity'>) => {
const classes = useErrorOutlineStyles({ severity });
return <ErrorOutline classes={classes} />;
};
const ExpandMoreIconStyled = () => {
const classes = useErrorOutlineStyles();
const ExpandMoreIconStyled = ({ severity }: Pick<WarningProps, 'severity'>) => {
const classes = useErrorOutlineStyles({ severity });
return <ExpandMoreIcon classes={classes} />;
};
const useStyles = makeStyles<BackstageTheme>(theme => ({
panel: {
backgroundColor: theme.palette.warningBackground,
color: theme.palette.warningText,
backgroundColor: ({ severity }: WarningProps) =>
getWarningBackgroundColor(
severity as NonNullable<WarningProps['severity']>,
theme,
),
color: ({ severity }: WarningProps) =>
getWarningTextColor(
severity as NonNullable<WarningProps['severity']>,
theme,
),
verticalAlign: 'middle',
},
summary: {
@@ -53,14 +83,26 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
flexDirection: 'row',
},
summaryText: {
color: theme.palette.warningText,
color: ({ severity }: WarningProps) =>
getWarningTextColor(
severity as NonNullable<WarningProps['severity']>,
theme,
),
fontWeight: 'bold',
},
message: {
width: '100%',
display: 'block',
color: theme.palette.warningText,
backgroundColor: theme.palette.warningBackground,
color: ({ severity }: WarningProps) =>
getWarningTextColor(
severity as NonNullable<WarningProps['severity']>,
theme,
),
backgroundColor: ({ severity }: WarningProps) =>
getWarningBackgroundColor(
severity as NonNullable<WarningProps['severity']>,
theme,
),
},
details: {
width: '100%',
@@ -73,7 +115,7 @@ const useStyles = makeStyles<BackstageTheme>(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 (
<Accordion
@@ -110,10 +156,10 @@ export const WarningPanel = (props: Props) => {
role="alert"
>
<AccordionSummary
expandIcon={<ExpandMoreIconStyled />}
expandIcon={<ExpandMoreIconStyled severity={severity} />}
className={classes.summary}
>
<ErrorOutlineStyled />
<ErrorOutlineStyled severity={severity} />
<Typography className={classes.summaryText} variant="subtitle1">
{subTitle}
</Typography>
@@ -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 = (
<Grid container spacing={3} alignItems="stretch">
<EntitySwitch>
<EntitySwitch.Case if={isOrphan}>
<Grid item xs={12}>
<EntityOrphanWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasCatalogProcessingErrors}>
<Grid item xs={12}>
<EntityProcessingErrorsPanel />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
+10
View File
@@ -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)
@@ -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('<EntityProcessErrors />', () => {
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(
<EntityProvider entity={entity}>
<EntityProcessingErrorsPanel />
</EntityProvider>,
);
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();
});
});
@@ -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<UNSTABLE_EntityStatusItem>[]) || [];
return (
<>
{catalogProcessingErrors.map(({ error }, index) => (
<Box key={index} mb={1}>
<ResponseErrorPanel error={error} />
</Box>
))}
</>
);
};
@@ -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';
+1
View File
@@ -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';
@@ -100,7 +100,7 @@ describe('<GroupsExplorerContent />', () => {
const catalogError = new Error('Network timeout');
catalogApi.getEntities.mockRejectedValueOnce(catalogError);
const { getByText } = await renderInTestApp(
const { getAllByText } = await renderInTestApp(
<Wrapper>
<GroupsExplorerContent />
</Wrapper>,
@@ -108,7 +108,7 @@ describe('<GroupsExplorerContent />', () => {
);
await waitFor(() =>
expect(getByText(/Warning: Network timeout/)).toBeInTheDocument(),
expect(getAllByText(/Error: Network timeout/).length).not.toBe(0),
);
});
});
@@ -74,7 +74,7 @@ describe('<FossaCard />', () => {
fossaApi.getFindingSummary.mockRejectedValue(new Error('My Error'));
const { getByText } = await renderInTestApp(
const { getAllByText } = await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<FossaCard />
@@ -82,7 +82,7 @@ describe('<FossaCard />', () => {
</Wrapper>,
);
expect(getByText(/Warning: My Error/i)).toBeInTheDocument();
expect(getAllByText(/Error: My Error/i).length).not.toBe(0);
});
it('shows empty', async () => {
@@ -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();
});