From 248ed0f7d815215b34d0d1ccb4fa356cce1de110 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 5 Dec 2022 09:10:58 +0000 Subject: [PATCH 01/33] add entity peek ahead component Signed-off-by: Brian Fletcher --- plugins/catalog-react/package.json | 1 + .../EntityRefLink/EntityRefLink.tsx | 159 ++++++++++++++++-- 2 files changed, 149 insertions(+), 11 deletions(-) diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 11b9555a12..caa3ac7a56 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -48,6 +48,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", + "material-ui-popup-state": "^1.9.3", "classnames": "^2.2.6", "jwt-decode": "^3.1.0", "lodash": "^4.17.21", diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 06a632232e..62be932f44 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -19,13 +19,34 @@ import { CompoundEntityRef, DEFAULT_NAMESPACE, parseEntityRef, + isUserEntity, + isGroupEntity, } from '@backstage/catalog-model'; -import React, { forwardRef } from 'react'; +import React, { ForwardedRef, forwardRef } from 'react'; import { entityRouteRef } from '../../routes'; import { humanizeEntityRef } from './humanize'; import { Link, LinkProps } from '@backstage/core-components'; -import { useRouteRef } from '@backstage/core-plugin-api'; -import { Tooltip } from '@material-ui/core'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { + Button, + Tooltip, + Typography, + CardContent, + Card, + CardActions, + makeStyles, +} from '@material-ui/core'; +import { + usePopupState, + bindPopover, + bindHover, + PopupState, +} from 'material-ui-popup-state/hooks'; +import HoverPopover from 'material-ui-popup-state/HoverPopover'; +import EmailIcon from '@material-ui/icons/Email'; +import InfoIcon from '@material-ui/icons/Info'; +import useAsync from 'react-use/lib/useAsync'; +import { catalogApiRef } from '../../api'; /** * Props for {@link EntityRefLink}. @@ -39,6 +60,101 @@ export type EntityRefLinkProps = { children?: React.ReactNode; } & Omit; +type PeekAheadPopoverProps = { + popupState: PopupState; + entityRef: CompoundEntityRef; + ref: ForwardedRef; +}; + +const useStyles = makeStyles(() => { + return { + popover: { + width: '80em', + minWidth: '80em', + maxWidth: '80em', + }, + card: { + width: '100%', + }, + }; +}); + +export const PeekAheadPopover = ({ + popupState, + entityRef, + ref, +}: PeekAheadPopoverProps) => { + const catalogApi = useApi(catalogApiRef); + const entityRoute = useRouteRef(entityRouteRef); + const classes = useStyles(); + + const { value, loading, error } = useAsync(async () => { + if (popupState.isOpen) { + return catalogApi.getEntityByRef(entityRef); + } + return undefined; + }, [popupState]); + + if (loading) { + return null; + } + + return ( + + + + {entityRef.namespace} + + {entityRef.name} + + {entityRef.kind} + + {error && error.message} + {value && ( + <> + {value.metadata.description} +
+
+ {value.spec?.type} + + )} +
+
+ + {value && + (isUserEntity(value) || isGroupEntity(value)) && + value.spec.profile?.email && ( + + + + )} + + + + + + +
+
+ ); +}; /** * Shows a clickable link to an entity. * @@ -48,6 +164,10 @@ export const EntityRefLink = forwardRef( (props, ref) => { const { entityRef, defaultKind, title, children, ...linkProps } = props; const entityRoute = useRouteRef(entityRouteRef); + const popupState = usePopupState({ + variant: 'popover', + popupId: 'entity-peek-ahead', + }); let kind; let namespace; @@ -78,16 +198,33 @@ export const EntityRefLink = forwardRef( ); const link = ( - - {children} - {!children && (title ?? formattedEntityRefTitle)} - + <> + + {children} + {!children && (title ?? formattedEntityRefTitle)} + + ); - return title ? ( - {link} - ) : ( - link + return ( + <> + {title ? ( + {link} + ) : ( + link + )} + + + ); }, ) as (props: EntityRefLinkProps) => JSX.Element; From d4d0ffcafaa1282fc4eb34c8afe26a96b561b9a3 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 5 Dec 2022 09:17:14 +0000 Subject: [PATCH 02/33] tidy Signed-off-by: Brian Fletcher --- .../EntityRefLink/EntityRefLink.tsx | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 62be932f44..6496bc7f53 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -63,7 +63,6 @@ export type EntityRefLinkProps = { type PeekAheadPopoverProps = { popupState: PopupState; entityRef: CompoundEntityRef; - ref: ForwardedRef; }; const useStyles = makeStyles(() => { @@ -82,7 +81,6 @@ const useStyles = makeStyles(() => { export const PeekAheadPopover = ({ popupState, entityRef, - ref, }: PeekAheadPopoverProps) => { const catalogApi = useApi(catalogApiRef); const entityRoute = useRouteRef(entityRouteRef); @@ -198,17 +196,15 @@ export const EntityRefLink = forwardRef( ); const link = ( - <> - - {children} - {!children && (title ?? formattedEntityRefTitle)} - - + + {children} + {!children && (title ?? formattedEntityRefTitle)} + ); return ( @@ -220,7 +216,6 @@ export const EntityRefLink = forwardRef( )} From 516b2039b6b3e4a60dd4aad9b0f96871779c8d62 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 5 Dec 2022 09:19:43 +0000 Subject: [PATCH 03/33] changeset Signed-off-by: Brian Fletcher --- .changeset/red-tables-train.md | 5 +++++ .../src/components/EntityRefLink/EntityRefLink.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/red-tables-train.md diff --git a/.changeset/red-tables-train.md b/.changeset/red-tables-train.md new file mode 100644 index 0000000000..1aadde75cf --- /dev/null +++ b/.changeset/red-tables-train.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Add pop over on the `EntityRefLink` component. It shows a more details about the associated entity. diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 6496bc7f53..ee7d770769 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -22,7 +22,7 @@ import { isUserEntity, isGroupEntity, } from '@backstage/catalog-model'; -import React, { ForwardedRef, forwardRef } from 'react'; +import React, { forwardRef } from 'react'; import { entityRouteRef } from '../../routes'; import { humanizeEntityRef } from './humanize'; import { Link, LinkProps } from '@backstage/core-components'; From dcc65c637a919d81ed2e96b87d6ae7ed74c9e0cf Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 5 Dec 2022 09:35:29 +0000 Subject: [PATCH 04/33] yarn update Signed-off-by: Brian Fletcher --- plugins/catalog-react/package.json | 2 +- yarn.lock | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index caa3ac7a56..b74466424b 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -48,10 +48,10 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "material-ui-popup-state": "^1.9.3", "classnames": "^2.2.6", "jwt-decode": "^3.1.0", "lodash": "^4.17.21", + "material-ui-popup-state": "^1.9.3", "qs": "^6.9.4", "react-use": "^17.2.4", "yaml": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 4af9e1862a..858f0c5ea4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5469,6 +5469,7 @@ __metadata: cross-fetch: ^3.1.5 jwt-decode: ^3.1.0 lodash: ^4.17.21 + material-ui-popup-state: ^1.9.3 qs: ^6.9.4 react-test-renderer: ^16.13.1 react-use: ^17.2.4 From f68ff9a3d880113bad7da443893329f585b1aec5 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 5 Dec 2022 10:02:21 +0000 Subject: [PATCH 05/33] fix width of popover Signed-off-by: Brian Fletcher --- .../components/EntityRefLink/EntityRefLink.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index ee7d770769..329f52ea74 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -67,13 +67,8 @@ type PeekAheadPopoverProps = { const useStyles = makeStyles(() => { return { - popover: { - width: '80em', - minWidth: '80em', - maxWidth: '80em', - }, - card: { - width: '100%', + popoverPaper: { + width: '20em', }, }; }); @@ -99,8 +94,10 @@ export const PeekAheadPopover = ({ return ( - + {entityRef.namespace} From 281df37809ae2533a2e606302a1c941de24c5bd5 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 5 Dec 2022 13:10:22 +0000 Subject: [PATCH 06/33] fix tests and improve error handling Signed-off-by: Brian Fletcher --- .../examples/acme/team-a-group.yaml | 2 +- .../EntityRefLink/EntityRefLink.test.tsx | 79 +++++++++++++------ .../EntityRefLink/EntityRefLink.tsx | 33 +++++--- .../EntityRefLink/EntityRefLinks.test.tsx | 37 ++++++--- .../components/EntityTable/presets.test.tsx | 39 ++++++--- plugins/playlist/package.json | 1 + .../PlaylistCard/PlaylistCard.test.tsx | 38 +++++---- .../techdocs-addons-test-utils/package.json | 2 + .../src/test-utils.tsx | 7 ++ .../package.json | 1 + plugins/techdocs/package.json | 1 + .../TechDocsReaderPage.test.tsx | 8 +- yarn.lock | 5 ++ 13 files changed, 181 insertions(+), 72 deletions(-) diff --git a/packages/catalog-model/examples/acme/team-a-group.yaml b/packages/catalog-model/examples/acme/team-a-group.yaml index e343209d5f..6f2be963b7 100644 --- a/packages/catalog-model/examples/acme/team-a-group.yaml +++ b/packages/catalog-model/examples/acme/team-a-group.yaml @@ -21,7 +21,7 @@ spec: # Intentional no displayName for testing email: breanna-davison@example.com picture: https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg?background=%23fff - memberOf: [team-a] + memberOf: [team-a, team-not-exist] --- apiVersion: backstage.io/v1alpha1 kind: User diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.test.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.test.tsx index b0a6b46bf5..71ff25c746 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.test.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.test.tsx @@ -14,11 +14,20 @@ * limitations under the License. */ -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; import { entityRouteRef } from '../../routes'; import { EntityRefLink } from './EntityRefLink'; +import { catalogApiRef } from '../../api'; +import { CatalogApi } from '@backstage/catalog-client'; +import { ApiProvider } from '@backstage/core-app-api'; + +const catalogApi: jest.Mocked = { + getEntityByRef: jest.fn(), +} as any; + +const apis = TestApiRegistry.from([catalogApiRef, catalogApi]); describe('', () => { it('renders link for entity in default namespace', async () => { @@ -34,11 +43,16 @@ describe('', () => { lifecycle: 'production', }, }; - await renderInTestApp(, { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, + await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, + }, }, - }); + ); expect(screen.getByText('component:software')).toHaveAttribute( 'href', @@ -60,11 +74,16 @@ describe('', () => { lifecycle: 'production', }, }; - await renderInTestApp(, { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, + await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, + }, }, - }); + ); expect(screen.getByText('component:test/software')).toHaveAttribute( 'href', '/catalog/test/component/software', @@ -86,7 +105,9 @@ describe('', () => { }, }; await renderInTestApp( - , + + + , { mountedRoutes: { '/catalog/:namespace/:kind/:name/*': entityRouteRef, @@ -105,11 +126,16 @@ describe('', () => { namespace: 'default', name: 'software', }; - await renderInTestApp(, { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, + await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, + }, }, - }); + ); expect(screen.getByText('component:software')).toHaveAttribute( 'href', '/catalog/default/component/software', @@ -122,11 +148,16 @@ describe('', () => { namespace: 'test', name: 'software', }; - await renderInTestApp(, { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, + await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, + }, }, - }); + ); expect(screen.getByText('component:test/software')).toHaveAttribute( 'href', '/catalog/test/component/software', @@ -140,7 +171,9 @@ describe('', () => { name: 'software', }; await renderInTestApp( - , + + + , { mountedRoutes: { '/catalog/:namespace/:kind/:name/*': entityRouteRef, @@ -160,9 +193,11 @@ describe('', () => { name: 'software', }; await renderInTestApp( - - Custom Children - , + + + Custom Children + + , { mountedRoutes: { '/catalog/:namespace/:kind/:name/*': entityRouteRef, diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 329f52ea74..c1d699421e 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -47,6 +47,7 @@ import EmailIcon from '@material-ui/icons/Email'; import InfoIcon from '@material-ui/icons/Info'; import useAsync from 'react-use/lib/useAsync'; import { catalogApiRef } from '../../api'; +import { Alert } from '@material-ui/lab'; /** * Props for {@link EntityRefLink}. @@ -77,13 +78,21 @@ export const PeekAheadPopover = ({ popupState, entityRef, }: PeekAheadPopoverProps) => { - const catalogApi = useApi(catalogApiRef); const entityRoute = useRouteRef(entityRouteRef); const classes = useStyles(); + const catalogApi = useApi(catalogApiRef); - const { value, loading, error } = useAsync(async () => { + const { + value: entity, + loading, + error, + } = useAsync(async () => { if (popupState.isOpen) { - return catalogApi.getEntityByRef(entityRef); + const retrievedEntity = await catalogApi.getEntityByRef(entityRef); + if (!retrievedEntity) { + throw new Error(`${entityRef.name} was not found`); + } + return retrievedEntity; } return undefined; }, [popupState]); @@ -115,25 +124,25 @@ export const PeekAheadPopover = ({ {entityRef.kind} - {error && error.message} - {value && ( + {error && {error.message}} + {entity && ( <> - {value.metadata.description} + {entity.metadata.description}

- {value.spec?.type} + {entity.spec?.type} )}
- {value && - (isUserEntity(value) || isGroupEntity(value)) && - value.spec.profile?.email && ( - + {entity && + (isUserEntity(entity) || isGroupEntity(entity)) && + entity.spec.profile?.email && ( + + + )} + + + + + + +
+
+ + ); +}; diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/index.ts b/plugins/catalog-react/src/components/EntityPeekAheadPopover/index.ts new file mode 100644 index 0000000000..476767931f --- /dev/null +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 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 { EntityPeekAheadPopover } from './EntityPeekAheadPopover'; diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 7ebd828a4c..813195cb70 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -19,38 +19,13 @@ import { CompoundEntityRef, DEFAULT_NAMESPACE, parseEntityRef, - isUserEntity, - isGroupEntity, } from '@backstage/catalog-model'; -import React, { forwardRef, useEffect } from 'react'; +import React, { forwardRef } from 'react'; import { entityRouteRef } from '../../routes'; import { humanizeEntityRef } from './humanize'; -import { Link, LinkProps, Progress } from '@backstage/core-components'; -import { useApiHolder, useRouteRef } from '@backstage/core-plugin-api'; -import { - Button, - Tooltip, - Typography, - CardContent, - Card, - CardActions, - makeStyles, - Box, - Chip, -} from '@material-ui/core'; -import { - usePopupState, - bindPopover, - bindHover, - PopupState, -} from 'material-ui-popup-state/hooks'; -import HoverPopover from 'material-ui-popup-state/HoverPopover'; -import EmailIcon from '@material-ui/icons/Email'; -import InfoIcon from '@material-ui/icons/Info'; -import { catalogApiRef } from '../../api'; -import { Alert, Skeleton } from '@material-ui/lab'; -import useAsyncFn from 'react-use/lib/useAsyncFn'; - +import { Link, LinkProps } from '@backstage/core-components'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { EntityPeekAheadPopover } from '../EntityPeekAheadPopover'; /** * Props for {@link EntityRefLink}. * @@ -63,129 +38,6 @@ export type EntityRefLinkProps = { children?: React.ReactNode; } & Omit; -type PeekAheadPopoverProps = { - popupState: PopupState; - entityRef: CompoundEntityRef; -}; - -const useStyles = makeStyles(() => { - return { - popoverPaper: { - width: '30em', - }, - descriptionTypography: { - overflow: 'hidden', - textOverflow: 'ellipsis', - display: '-webkit-box', - WebkitLineClamp: 2, - WebkitBoxOrient: 'vertical', - }, - }; -}); - -const maxTagChips = 4; - -export const PeekAheadPopover = ({ - popupState, - entityRef, -}: PeekAheadPopoverProps) => { - const entityRoute = useRouteRef(entityRouteRef); - const classes = useStyles(); - const apiHolder = useApiHolder(); - - const [{ loading, error, value: entity }, load] = useAsyncFn(async () => { - const catalogApi = apiHolder.get(catalogApiRef); - if (catalogApi) { - const retrievedEntity = await catalogApi.getEntityByRef(entityRef); - if (!retrievedEntity) { - throw new Error(`${entityRef.name} was not found`); - } - return retrievedEntity; - } - return undefined; - }, [apiHolder, entityRef]); - - useEffect(() => { - if (popupState.isOpen && !entity && !error && !loading) { - load(); - } - }, [popupState.isOpen, load, entity, error, loading]); - - return ( - - - {loading && } - - {entityRef.namespace} - - {entityRef.name} - - {error && {error.message}} - {entity ? ( - <> - {entity.kind} - - {entity.metadata.description} - - {entity.spec?.type} - - {(entity.metadata.tags || []).slice(0, maxTagChips).map(tag => { - return ; - })} - {entity.metadata.tags?.length && - entity.metadata.tags?.length > maxTagChips && ( - - - - )} - - - ) : ( - <> - - - - - - )} - - - {entity && - (isUserEntity(entity) || isGroupEntity(entity)) && - entity.spec.profile?.email && ( - - - - )} - - - - - - - - - ); -}; /** * Shows a clickable link to an entity. * @@ -195,10 +47,6 @@ export const EntityRefLink = forwardRef( (props, ref) => { const { entityRef, defaultKind, title, children, ...linkProps } = props; const entityRoute = useRouteRef(entityRouteRef); - const popupState = usePopupState({ - variant: 'popover', - popupId: 'entity-peek-ahead', - }); let kind; let namespace; @@ -229,22 +77,12 @@ export const EntityRefLink = forwardRef( ); return ( - <> - + + {children} {!children && (title ?? formattedEntityRefTitle)} - - - + ); }, ) as (props: EntityRefLinkProps) => JSX.Element; diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts index c604306ead..a4380ddef0 100644 --- a/plugins/catalog-react/src/components/index.ts +++ b/plugins/catalog-react/src/components/index.ts @@ -19,6 +19,7 @@ export * from './EntityKindPicker'; export * from './EntityLifecyclePicker'; export * from './EntityOwnerPicker'; export * from './EntityRefLink'; +export * from './EntityPeekAheadPopover'; export * from './EntitySearchBar'; export * from './EntityTable'; export * from './EntityTagPicker'; From 1e1bc430eba97ca379ea003ba518a60468702b06 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 8 Dec 2022 13:38:03 +0000 Subject: [PATCH 18/33] remove testing content Signed-off-by: Brian Fletcher --- .../EntityPeekAheadPopover/EntityPeekAheadPopover.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx index c7d994628b..d50525151e 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx @@ -127,15 +127,7 @@ export const EntityPeekAheadPopover = ({ <> {entity.kind} - {/* {entity.metadata.description} */} - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed - do eiusmod tempor incididunt ut labore et dolore magna aliqua. - Ut enim ad minim veniam, quis nostrud exercitation ullamco - laboris nisi ut aliquip ex ea commodo consequat. Duis aute - irure dolor in reprehenderit in voluptate velit esse cillum - dolore eu fugiat nulla pariatur. Excepteur sint occaecat - cupidatat non proident, sunt in culpa qui officia deserunt - mollit anim id est laborum. + {entity.metadata.description} {entity.spec?.type} From 1ec7c6b055925a001e042682202ad6f52863d960 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 8 Dec 2022 14:19:27 +0000 Subject: [PATCH 19/33] small refactors and api reports Signed-off-by: Brian Fletcher --- plugins/catalog-react/api-report.md | 11 ++ .../EntityPeekAheadPopover.tsx | 101 ++++++++++++++---- .../EntityPeekAheadPopover/index.ts | 2 +- 3 files changed, 91 insertions(+), 23 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 92bfc4de96..9d30d84bb2 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -254,6 +254,17 @@ export class EntityOwnerFilter implements EntityFilter { // @public (undocumented) export const EntityOwnerPicker: () => JSX.Element | null; +// @public +export const EntityPeekAheadPopover: ({ + entityRef, + children, +}: EntityPeekAheadPopoverProps) => JSX.Element; + +// @public +export type EntityPeekAheadPopoverProps = PropsWithChildren<{ + entityRef: CompoundEntityRef; +}>; + // @public (undocumented) export const EntityProcessingStatusPicker: () => JSX.Element; diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx index d50525151e..bd41dd5b92 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx @@ -42,12 +42,20 @@ import { CompoundEntityRef, isUserEntity, isGroupEntity, + UserEntity, + GroupEntity, + Entity, } from '@backstage/catalog-model'; import { Link, Progress } from '@backstage/core-components'; -export type EntityPeekAheadPopoverProps = { +/** + * Properties for an entity popover on hover of a component. + * + * @public + */ +export type EntityPeekAheadPopoverProps = PropsWithChildren<{ entityRef: CompoundEntityRef; -}; +}>; const useStyles = makeStyles(() => { return { @@ -66,11 +74,71 @@ const useStyles = makeStyles(() => { const maxTagChips = 4; +const EmailCardAction = ({ email }: { email: string }) => { + return ( + + + + ); +}; + +const UserCardActions = ({ entity }: { entity: UserEntity }) => { + return ( + <> + {entity.spec.profile?.email && ( + + )} + + ); +}; + +const GroupCardActions = ({ entity }: { entity: GroupEntity }) => { + return ( + <> + {entity.spec.profile?.email && ( + + )} + + ); +}; + +/** + * Shows an entity popover on hover of a component. + * + * @public + */ +const EntityCardActions = ({ entity }: { entity: Entity }) => { + const entityRoute = useRouteRef(entityRouteRef); + + return ( + <> + + + + + + + ); +}; + +/** + * Shows an entity popover on hover of a component. + * + * @public + */ export const EntityPeekAheadPopover = ({ entityRef, children, -}: PropsWithChildren) => { - const entityRoute = useRouteRef(entityRouteRef); +}: EntityPeekAheadPopoverProps) => { const classes = useStyles(); const apiHolder = useApiHolder(); const popupState = usePopupState({ @@ -154,24 +222,13 @@ export const EntityPeekAheadPopover = ({ )} - {entity && - (isUserEntity(entity) || isGroupEntity(entity)) && - entity.spec.profile?.email && ( - - - - )} - - - - - + {entity && ( + <> + {isUserEntity(entity) && } + {isGroupEntity(entity) && } + + + )} diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/index.ts b/plugins/catalog-react/src/components/EntityPeekAheadPopover/index.ts index 476767931f..6630f6f555 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/index.ts +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { EntityPeekAheadPopover } from './EntityPeekAheadPopover'; +export * from './EntityPeekAheadPopover'; From f88981fb1deeba517b04df2da2ae4e0e8344f7e7 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Fri, 9 Dec 2022 14:13:57 +0000 Subject: [PATCH 20/33] add unit test for peek ahead popover Signed-off-by: Brian Fletcher --- .../EntityPeekAheadPopover.test.tsx | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx new file mode 100644 index 0000000000..7053969a09 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx @@ -0,0 +1,72 @@ +/* + * 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 { fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; +import { EntityPeekAheadPopover } from './EntityPeekAheadPopover'; +import { ApiProvider } from '@backstage/core-app-api'; +import { TestApiRegistry } from '@backstage/test-utils'; +import { catalogApiRef } from '../../api'; +import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; +import { CatalogApi } from '@backstage/catalog-client'; + +const catalogApi: Partial = { + getEntityByRef: async ( + entityRef: CompoundEntityRef, + ): Promise => { + if ( + entityRef === + { name: 'service1', namespace: 'default', kind: 'component ' } + ) { + return { + apiVersion: '', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'service1', + }, + spec: { + tags: ['java'], + }, + }; + } + return undefined; + }, +}; + +const apis = TestApiRegistry.from([catalogApiRef, catalogApi]); + +describe('', () => { + it('renders all owners', async () => { + render( + + +
asdf
+
+
, + ); + expect(screen.getByText('asdf')).toBeInTheDocument(); + expect(screen.queryByText('service1')).toBeNull(); + fireEvent.mouseOver(screen.getByTestId('popover')); + expect(screen.getByText('service1')).toBeInTheDocument(); + }); +}); From 2172ae4259fcef34a54ba2a9507469ba24351aca Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 12 Dec 2022 14:24:48 +0000 Subject: [PATCH 21/33] adds a storybook for the peek ahead popover Signed-off-by: Brian Fletcher --- .../EntityPeekAheadPopover.stories.tsx | 114 ++++++++++++++++++ storybook/.storybook/main.js | 1 + 2 files changed, 115 insertions(+) create mode 100644 plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx new file mode 100644 index 0000000000..c31e5320ef --- /dev/null +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx @@ -0,0 +1,114 @@ +/* + * 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 React, { ComponentType } from 'react'; +import { + EntityPeekAheadPopover, + EntityPeekAheadPopoverProps, +} from './EntityPeekAheadPopover'; +import Button from '@material-ui/core/Button'; +import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { catalogApiRef } from '../../api'; +import { CompoundEntityRef } from '@backstage/catalog-model'; +import { entityRouteRef } from '../../routes'; +import { CatalogApi } from '@backstage/catalog-client'; + +const mockCatalogApi = { + getEntityByRef: async (entityRef: CompoundEntityRef) => { + if ( + entityRef.namespace === 'default' && + entityRef.name === 'playback' && + entityRef.kind === 'component' + ) { + return { + kind: 'Component', + metadata: { + name: 'playback', + namespace: 'default', + description: 'Details about the playback service', + }, + }; + } + if ( + entityRef.namespace === 'default' && + entityRef.name === 'fname.lname' && + entityRef.kind === 'user' + ) { + return { + kind: 'User', + metadata: { + name: 'fname.lname', + namespace: 'default', + }, + spec: { + profile: { + email: 'fname.lname@example.com', + }, + }, + }; + } + return undefined; + }, +}; + +const defaultArgs = { + entityRef: { + namespace: 'default', + name: 'playback', + kind: 'component', + }, +}; + +export default { + title: 'Catalog /PeekAheadPopover', + decorators: [ + (Story: ComponentType<{}>) => + wrapInTestApp( + <> + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ), + ], +}; + +export const Default = (args: EntityPeekAheadPopoverProps) => ( + + + +); +Default.args = defaultArgs; + +export const User = (args: EntityPeekAheadPopoverProps) => ( + + + +); +User.args = { + entityRef: { + kind: 'user', + namespace: 'default', + name: 'fname.lname', + }, +}; diff --git a/storybook/.storybook/main.js b/storybook/.storybook/main.js index e523b68337..3d92237438 100644 --- a/storybook/.storybook/main.js +++ b/storybook/.storybook/main.js @@ -12,6 +12,7 @@ const BACKSTAGE_CORE_STORIES = [ 'plugins/search-react', 'plugins/home', 'plugins/stack-overflow', + 'plugins/catalog-react', ]; // Some configuration needs to be available directly on the exported object From 5bc78a5284773b43c5b35fa40bcc341c1ec116a5 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 13 Dec 2022 11:42:29 +0000 Subject: [PATCH 22/33] more review comments Signed-off-by: Brian Fletcher --- plugins/catalog-react/api-report.md | 9 +- .../EntityPeekAheadPopover.stories.tsx | 45 ++++++-- .../EntityPeekAheadPopover.test.tsx | 44 +++++--- .../EntityPeekAheadPopover.tsx | 106 +++++++++++------- .../EntityRefLink/EntityRefLink.tsx | 2 +- 5 files changed, 131 insertions(+), 75 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 9d30d84bb2..82f8326a17 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -255,14 +255,13 @@ export class EntityOwnerFilter implements EntityFilter { export const EntityOwnerPicker: () => JSX.Element | null; // @public -export const EntityPeekAheadPopover: ({ - entityRef, - children, -}: EntityPeekAheadPopoverProps) => JSX.Element; +export const EntityPeekAheadPopover: ( + props: EntityPeekAheadPopoverProps, +) => JSX.Element; // @public export type EntityPeekAheadPopoverProps = PropsWithChildren<{ - entityRef: CompoundEntityRef; + entityRef: string; }>; // @public (undocumented) diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx index c31e5320ef..c44a31a743 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx @@ -60,16 +60,27 @@ const mockCatalogApi = { }, }; } + if ( + entityRef.namespace === 'default' && + entityRef.name === 'slow.catalog.item' && + entityRef.kind === 'component' + ) { + await new Promise(resolve => setTimeout(resolve, 3000)); + return { + kind: 'Component', + metadata: { + name: 'slow.catalog.item', + namespace: 'default', + description: 'Details about the slow.catalog.item service', + }, + }; + } return undefined; }, }; const defaultArgs = { - entityRef: { - namespace: 'default', - name: 'playback', - kind: 'component', - }, + entityRef: 'component:default/playback', }; export default { @@ -106,9 +117,23 @@ export const User = (args: EntityPeekAheadPopoverProps) => (
); User.args = { - entityRef: { - kind: 'user', - namespace: 'default', - name: 'fname.lname', - }, + entityRef: 'user:default/fname.lname', +}; + +export const NotFound = (args: EntityPeekAheadPopoverProps) => ( + + + +); +NotFound.args = { + entityRef: 'user:default/doesnt.exist', +}; + +export const SlowCatalogItem = (args: EntityPeekAheadPopoverProps) => ( + + + +); +SlowCatalogItem.args = { + entityRef: 'component:default/slow.catalog.item', }; diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx index 7053969a09..f7c4d1cffa 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx @@ -14,22 +14,25 @@ * limitations under the License. */ -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, screen } from '@testing-library/react'; import React from 'react'; import { EntityPeekAheadPopover } from './EntityPeekAheadPopover'; import { ApiProvider } from '@backstage/core-app-api'; -import { TestApiRegistry } from '@backstage/test-utils'; +import { TestApiRegistry, renderInTestApp } from '@backstage/test-utils'; import { catalogApiRef } from '../../api'; import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; import { CatalogApi } from '@backstage/catalog-client'; +import { Button } from '@material-ui/core'; +import { entityRouteRef } from '../../routes'; const catalogApi: Partial = { getEntityByRef: async ( entityRef: CompoundEntityRef, ): Promise => { if ( - entityRef === - { name: 'service1', namespace: 'default', kind: 'component ' } + entityRef.name === 'service1' && + entityRef.namespace === 'default' && + entityRef.kind === 'component' ) { return { apiVersion: '', @@ -51,22 +54,31 @@ const apis = TestApiRegistry.from([catalogApiRef, catalogApi]); describe('', () => { it('renders all owners', async () => { - render( + renderInTestApp( - -
asdf
+ + + + +
, + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, ); - expect(screen.getByText('asdf')).toBeInTheDocument(); + expect(screen.getByText('s1')).toBeInTheDocument(); expect(screen.queryByText('service1')).toBeNull(); - fireEvent.mouseOver(screen.getByTestId('popover')); - expect(screen.getByText('service1')).toBeInTheDocument(); + fireEvent.mouseOver(screen.getByTestId('popover1')); + expect(await screen.findByText('service1')).toBeInTheDocument(); + + expect(screen.getByText('s2')).toBeInTheDocument(); + expect(screen.queryByText('service2')).toBeNull(); + fireEvent.mouseOver(screen.getByTestId('popover2')); + expect( + await screen.findByText(/service2 was not found/), + ).toBeInTheDocument(); }); }); diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx index bd41dd5b92..6e5b75ed58 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx @@ -34,17 +34,17 @@ import { Tooltip, Typography, } from '@material-ui/core'; -import { Alert, Skeleton } from '@material-ui/lab'; +import { Alert } from '@material-ui/lab'; import EmailIcon from '@material-ui/icons/Email'; import InfoIcon from '@material-ui/icons/Info'; import { useApiHolder, useRouteRef } from '@backstage/core-plugin-api'; import { - CompoundEntityRef, isUserEntity, isGroupEntity, UserEntity, GroupEntity, Entity, + parseEntityRef, } from '@backstage/catalog-model'; import { Link, Progress } from '@backstage/core-components'; @@ -54,11 +54,14 @@ import { Link, Progress } from '@backstage/core-components'; * @public */ export type EntityPeekAheadPopoverProps = PropsWithChildren<{ - entityRef: CompoundEntityRef; + entityRef: string; }>; const useStyles = makeStyles(() => { return { + trigger: { + display: 'inline-block', + }, popoverPaper: { width: '30em', }, @@ -130,33 +133,53 @@ const EntityCardActions = ({ entity }: { entity: Entity }) => { ); }; +const EntityNotFoundCard = ({ + entityRef, + error, +}: { + entityRef: string; + error?: Error; +}) => { + return ( + + + + {entityRef} was not found {error?.message} + + + + ); +}; + /** * Shows an entity popover on hover of a component. * * @public */ -export const EntityPeekAheadPopover = ({ - entityRef, - children, -}: EntityPeekAheadPopoverProps) => { +export const EntityPeekAheadPopover = (props: EntityPeekAheadPopoverProps) => { + const { entityRef, children } = props; + const classes = useStyles(); const apiHolder = useApiHolder(); const popupState = usePopupState({ variant: 'popover', popupId: 'entity-peek-ahead', }); + const compoundEntityRef = parseEntityRef(entityRef); const [{ loading, error, value: entity }, load] = useAsyncFn(async () => { const catalogApi = apiHolder.get(catalogApiRef); if (catalogApi) { - const retrievedEntity = await catalogApi.getEntityByRef(entityRef); + const retrievedEntity = await catalogApi.getEntityByRef( + compoundEntityRef, + ); if (!retrievedEntity) { - throw new Error(`${entityRef.name} was not found`); + throw new Error(`${compoundEntityRef.name} was not found`); } return retrievedEntity; } return undefined; - }, [apiHolder, entityRef]); + }, [apiHolder, compoundEntityRef]); useEffect(() => { if (popupState.isOpen && !entity && !error && !loading) { @@ -166,9 +189,9 @@ export const EntityPeekAheadPopover = ({ return ( <> -
+ {children} -
+ - + <> {loading && } - - {entityRef.namespace} - - {entityRef.name} - - {error && {error.message}} - {entity ? ( - <> + {!entity && !loading && ( + + )} + {entity && ( + + + + {compoundEntityRef.namespace} + + + {compoundEntityRef.name} + {entity.kind} {entity.metadata.description} @@ -202,35 +229,28 @@ export const EntityPeekAheadPopover = ({ {(entity.metadata.tags || []) .slice(0, maxTagChips) .map(tag => { - return ; + return ; })} {entity.metadata.tags?.length && entity.metadata.tags?.length > maxTagChips && ( - + )}
- - ) : ( - <> - - - - - - )} - - - {entity && ( - <> - {isUserEntity(entity) && } - {isGroupEntity(entity) && } - - - )} - - + + + <> + {isUserEntity(entity) && } + {isGroupEntity(entity) && ( + + )} + + + + + )} + ); diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 813195cb70..de94866bb6 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -77,7 +77,7 @@ export const EntityRefLink = forwardRef( ); return ( - + {children} {!children && (title ?? formattedEntityRefTitle)} From 416affb607d355d2568f950d0fd9279faa507006 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 13 Dec 2022 14:17:21 +0000 Subject: [PATCH 23/33] review comments Signed-off-by: Brian Fletcher --- .changeset/red-tables-train.md | 4 +- .../EntityPeekAheadPopover.stories.tsx | 2 +- .../EntityRefLink/EntityRefLink.stories.tsx | 125 ++++++++++++++++++ 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.stories.tsx diff --git a/.changeset/red-tables-train.md b/.changeset/red-tables-train.md index 1aadde75cf..c2beceb3e7 100644 --- a/.changeset/red-tables-train.md +++ b/.changeset/red-tables-train.md @@ -2,4 +2,6 @@ '@backstage/plugin-catalog-react': patch --- -Add pop over on the `EntityRefLink` component. It shows a more details about the associated entity. +Add pop over on the `EntityRefLink` component. It shows more details about the associated entity. See the playbook here https://backstage.io/storybook/?path=/story/catalog-entityreflink--default + +Add a reuseable pop over `EntityPeekAheadPopover` component. It shows more details about the associated entity. See the playbook here https://backstage.io/storybook/?path=/story/catalog-entitypeekaheadpopover--default diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx index c44a31a743..bb24f293cb 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2022 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. diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.stories.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.stories.tsx new file mode 100644 index 0000000000..e09bf89382 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.stories.tsx @@ -0,0 +1,125 @@ +/* + * Copyright 2022 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 React, { ComponentType } from 'react'; +import { EntityRefLink, EntityRefLinkProps } from './EntityRefLink'; +import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { catalogApiRef } from '../../api'; +import { CompoundEntityRef } from '@backstage/catalog-model'; +import { entityRouteRef } from '../../routes'; +import { CatalogApi } from '@backstage/catalog-client'; + +const mockCatalogApi = { + getEntityByRef: async (entityRef: CompoundEntityRef) => { + if ( + entityRef.namespace === 'default' && + entityRef.name === 'playback' && + entityRef.kind === 'component' + ) { + return { + kind: 'Component', + metadata: { + name: 'playback', + namespace: 'default', + description: 'Details about the playback service', + }, + }; + } + if ( + entityRef.namespace === 'default' && + entityRef.name === 'fname.lname' && + entityRef.kind === 'user' + ) { + return { + kind: 'User', + metadata: { + name: 'fname.lname', + namespace: 'default', + }, + spec: { + profile: { + email: 'fname.lname@example.com', + }, + }, + }; + } + if ( + entityRef.namespace === 'default' && + entityRef.name === 'slow.catalog.item' && + entityRef.kind === 'component' + ) { + await new Promise(resolve => setTimeout(resolve, 3000)); + return { + kind: 'Component', + metadata: { + name: 'slow.catalog.item', + namespace: 'default', + description: 'Details about the slow.catalog.item service', + }, + }; + } + return undefined; + }, +}; + +const defaultArgs = { + entityRef: 'component:default/playback', +}; + +export default { + title: 'Catalog /EntityRefLink', + decorators: [ + (Story: ComponentType<{}>) => + wrapInTestApp( + <> + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ), + ], +}; + +export const Default = (args: EntityRefLinkProps) => ( + +); +Default.args = defaultArgs; + +export const User = (args: EntityRefLinkProps) => ; +User.args = { + entityRef: 'user:default/fname.lname', +}; + +export const NotFound = (args: EntityRefLinkProps) => ( + +); +NotFound.args = { + entityRef: 'user:default/doesnt.exist', +}; + +export const SlowCatalogItem = (args: EntityRefLinkProps) => ( + +); +SlowCatalogItem.args = { + entityRef: 'component:default/slow.catalog.item', +}; From 8efe254e03fa70a2201d5e1826ce433ca5cfec7a Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 13 Dec 2022 14:24:06 +0000 Subject: [PATCH 24/33] fix typo Signed-off-by: Brian Fletcher --- .changeset/red-tables-train.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/red-tables-train.md b/.changeset/red-tables-train.md index c2beceb3e7..f32a0c5542 100644 --- a/.changeset/red-tables-train.md +++ b/.changeset/red-tables-train.md @@ -4,4 +4,4 @@ Add pop over on the `EntityRefLink` component. It shows more details about the associated entity. See the playbook here https://backstage.io/storybook/?path=/story/catalog-entityreflink--default -Add a reuseable pop over `EntityPeekAheadPopover` component. It shows more details about the associated entity. See the playbook here https://backstage.io/storybook/?path=/story/catalog-entitypeekaheadpopover--default +Add a reusable pop over `EntityPeekAheadPopover` component. It shows more details about the associated entity. See the playbook here https://backstage.io/storybook/?path=/story/catalog-entitypeekaheadpopover--default From 0f9af920f400349253fd81e152f81d9c8954e639 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 15 Dec 2022 09:06:12 +0000 Subject: [PATCH 25/33] remove entity ref link popover This also: - adds stories for the entity ref links componet - and moves some of the components into another file Signed-off-by: Brian Fletcher --- .../CardActionComponents/EmailCardAction.tsx | 33 +++++++ .../EntityCardActions.tsx | 48 ++++++++++ .../CardActionComponents/GroupCardActions.tsx | 33 +++++++ .../CardActionComponents/UserCardActions.tsx | 33 +++++++ .../CardActionComponents/index.ts | 18 ++++ .../EntityNotFoundCard.tsx | 41 ++++++++ .../EntityPeekAheadPopover.tsx | 94 ++----------------- .../EntityRefLink/EntityRefLink.stories.tsx | 94 +------------------ .../EntityRefLink/EntityRefLink.tsx | 21 +++-- .../EntityRefLink/EntityRefLinks.stories.tsx | 42 +++++++++ 10 files changed, 275 insertions(+), 182 deletions(-) create mode 100644 plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EmailCardAction.tsx create mode 100644 plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EntityCardActions.tsx create mode 100644 plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/GroupCardActions.tsx create mode 100644 plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/UserCardActions.tsx create mode 100644 plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/index.ts create mode 100644 plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityNotFoundCard.tsx create mode 100644 plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.stories.tsx diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EmailCardAction.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EmailCardAction.tsx new file mode 100644 index 0000000000..ed9ac7b9aa --- /dev/null +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EmailCardAction.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2022 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 { Button, Tooltip } from '@material-ui/core'; +import EmailIcon from '@material-ui/icons/Email'; +import React from 'react'; + +/** + * Email Card action link + * + * @private + */ +export const EmailCardAction = ({ email }: { email: string }) => { + return ( + + + + ); +}; diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EntityCardActions.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EntityCardActions.tsx new file mode 100644 index 0000000000..d021a9f69d --- /dev/null +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EntityCardActions.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2022 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 { entityRouteRef } from '../../../routes'; +import { Tooltip } from '@material-ui/core'; +import InfoIcon from '@material-ui/icons/Info'; +import React from 'react'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { Link } from '@backstage/core-components'; + +/** + * Card actions that show for all entities + * + * @private + */ +export const EntityCardActions = ({ entity }: { entity: Entity }) => { + const entityRoute = useRouteRef(entityRouteRef); + + return ( + <> + + + + + + + ); +}; diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/GroupCardActions.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/GroupCardActions.tsx new file mode 100644 index 0000000000..fca0763703 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/GroupCardActions.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2022 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 { EmailCardAction } from './EmailCardAction'; +import React from 'react'; +import { GroupEntity } from '@backstage/catalog-model'; + +/** + * Card actions that show for a group + * + * @private + */ +export const GroupCardActions = ({ entity }: { entity: GroupEntity }) => { + return ( + <> + {entity.spec.profile?.email && ( + + )} + + ); +}; diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/UserCardActions.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/UserCardActions.tsx new file mode 100644 index 0000000000..57801497f6 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/UserCardActions.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2022 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 { EmailCardAction } from './EmailCardAction'; +import React from 'react'; +import { UserEntity } from '@backstage/catalog-model'; + +/** + * Card actions that show for a user + * + * @private + */ +export const UserCardActions = ({ entity }: { entity: UserEntity }) => { + return ( + <> + {entity.spec.profile?.email && ( + + )} + + ); +}; diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/index.ts b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/index.ts new file mode 100644 index 0000000000..75959026f7 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 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 { EntityCardActions } from './EntityCardActions'; +export { GroupCardActions } from './GroupCardActions'; +export { UserCardActions } from './UserCardActions'; diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityNotFoundCard.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityNotFoundCard.tsx new file mode 100644 index 0000000000..08d92cab22 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityNotFoundCard.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2022 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 { Card, CardContent } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import React from 'react'; + +/** + * Entity not found card + * + * @private + */ +export const EntityNotFoundCard = ({ + entityRef, + error, +}: { + entityRef: string; + error?: Error; +}) => { + return ( + + + + {entityRef} was not found {error?.message} + + + + ); +}; diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx index 6e5b75ed58..c6cba8d421 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { entityRouteRef } from '../../routes'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import { catalogApiRef } from '../../api'; import React, { PropsWithChildren, useEffect } from 'react'; @@ -25,7 +24,6 @@ import { } from 'material-ui-popup-state/hooks'; import { Box, - Button, Card, CardActions, CardContent, @@ -34,19 +32,19 @@ import { Tooltip, Typography, } from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; -import EmailIcon from '@material-ui/icons/Email'; -import InfoIcon from '@material-ui/icons/Info'; -import { useApiHolder, useRouteRef } from '@backstage/core-plugin-api'; +import { useApiHolder } from '@backstage/core-plugin-api'; import { - isUserEntity, isGroupEntity, - UserEntity, - GroupEntity, - Entity, + isUserEntity, parseEntityRef, } from '@backstage/catalog-model'; -import { Link, Progress } from '@backstage/core-components'; +import { Progress } from '@backstage/core-components'; +import { + EntityCardActions, + UserCardActions, + GroupCardActions, +} from './CardActionComponents'; +import { EntityNotFoundCard } from './EntityNotFoundCard'; /** * Properties for an entity popover on hover of a component. @@ -77,80 +75,6 @@ const useStyles = makeStyles(() => { const maxTagChips = 4; -const EmailCardAction = ({ email }: { email: string }) => { - return ( - - - - ); -}; - -const UserCardActions = ({ entity }: { entity: UserEntity }) => { - return ( - <> - {entity.spec.profile?.email && ( - - )} - - ); -}; - -const GroupCardActions = ({ entity }: { entity: GroupEntity }) => { - return ( - <> - {entity.spec.profile?.email && ( - - )} - - ); -}; - -/** - * Shows an entity popover on hover of a component. - * - * @public - */ -const EntityCardActions = ({ entity }: { entity: Entity }) => { - const entityRoute = useRouteRef(entityRouteRef); - - return ( - <> - - - - - - - ); -}; - -const EntityNotFoundCard = ({ - entityRef, - error, -}: { - entityRef: string; - error?: Error; -}) => { - return ( - - - - {entityRef} was not found {error?.message} - - - - ); -}; - /** * Shows an entity popover on hover of a component. * diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.stories.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.stories.tsx index e09bf89382..e82c4ab963 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.stories.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.stories.tsx @@ -16,64 +16,8 @@ import React, { ComponentType } from 'react'; import { EntityRefLink, EntityRefLinkProps } from './EntityRefLink'; -import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; -import { catalogApiRef } from '../../api'; -import { CompoundEntityRef } from '@backstage/catalog-model'; +import { wrapInTestApp } from '@backstage/test-utils'; import { entityRouteRef } from '../../routes'; -import { CatalogApi } from '@backstage/catalog-client'; - -const mockCatalogApi = { - getEntityByRef: async (entityRef: CompoundEntityRef) => { - if ( - entityRef.namespace === 'default' && - entityRef.name === 'playback' && - entityRef.kind === 'component' - ) { - return { - kind: 'Component', - metadata: { - name: 'playback', - namespace: 'default', - description: 'Details about the playback service', - }, - }; - } - if ( - entityRef.namespace === 'default' && - entityRef.name === 'fname.lname' && - entityRef.kind === 'user' - ) { - return { - kind: 'User', - metadata: { - name: 'fname.lname', - namespace: 'default', - }, - spec: { - profile: { - email: 'fname.lname@example.com', - }, - }, - }; - } - if ( - entityRef.namespace === 'default' && - entityRef.name === 'slow.catalog.item' && - entityRef.kind === 'component' - ) { - await new Promise(resolve => setTimeout(resolve, 3000)); - return { - kind: 'Component', - metadata: { - name: 'slow.catalog.item', - namespace: 'default', - description: 'Details about the slow.catalog.item service', - }, - }; - } - return undefined; - }, -}; const defaultArgs = { entityRef: 'component:default/playback', @@ -83,20 +27,11 @@ export default { title: 'Catalog /EntityRefLink', decorators: [ (Story: ComponentType<{}>) => - wrapInTestApp( - <> - - - - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + wrapInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ), + }), ], }; @@ -104,22 +39,3 @@ export const Default = (args: EntityRefLinkProps) => ( ); Default.args = defaultArgs; - -export const User = (args: EntityRefLinkProps) => ; -User.args = { - entityRef: 'user:default/fname.lname', -}; - -export const NotFound = (args: EntityRefLinkProps) => ( - -); -NotFound.args = { - entityRef: 'user:default/doesnt.exist', -}; - -export const SlowCatalogItem = (args: EntityRefLinkProps) => ( - -); -SlowCatalogItem.args = { - entityRef: 'component:default/slow.catalog.item', -}; diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index de94866bb6..06a632232e 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -25,7 +25,8 @@ import { entityRouteRef } from '../../routes'; import { humanizeEntityRef } from './humanize'; import { Link, LinkProps } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; -import { EntityPeekAheadPopover } from '../EntityPeekAheadPopover'; +import { Tooltip } from '@material-ui/core'; + /** * Props for {@link EntityRefLink}. * @@ -76,13 +77,17 @@ export const EntityRefLink = forwardRef( { defaultKind }, ); - return ( - - - {children} - {!children && (title ?? formattedEntityRefTitle)} - - + const link = ( + + {children} + {!children && (title ?? formattedEntityRefTitle)} + + ); + + return title ? ( + {link} + ) : ( + link ); }, ) as (props: EntityRefLinkProps) => JSX.Element; diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.stories.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.stories.tsx new file mode 100644 index 0000000000..176dd24530 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.stories.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2022 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 React, { ComponentType } from 'react'; +import { EntityRefLinks, EntityRefLinksProps } from './EntityRefLinks'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { CompoundEntityRef } from '@backstage/catalog-model'; +import { entityRouteRef } from '../../routes'; + +const defaultArgs = { + entityRefs: ['component:default/playback', 'user:default/fname.lname'], +}; + +export default { + title: 'Catalog /EntityRefLinks', + decorators: [ + (Story: ComponentType<{}>) => + wrapInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }), + ], +}; + +export const Default = ( + args: EntityRefLinksProps, +) => ; +Default.args = defaultArgs; From b91bbaf3480123b3ab934e29dad5aa1f4e47f422 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 15 Dec 2022 09:24:32 +0000 Subject: [PATCH 26/33] remove entity ref link reference in changeset Signed-off-by: Brian Fletcher --- .changeset/red-tables-train.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.changeset/red-tables-train.md b/.changeset/red-tables-train.md index f32a0c5542..031d3720fc 100644 --- a/.changeset/red-tables-train.md +++ b/.changeset/red-tables-train.md @@ -2,6 +2,4 @@ '@backstage/plugin-catalog-react': patch --- -Add pop over on the `EntityRefLink` component. It shows more details about the associated entity. See the playbook here https://backstage.io/storybook/?path=/story/catalog-entityreflink--default - Add a reusable pop over `EntityPeekAheadPopover` component. It shows more details about the associated entity. See the playbook here https://backstage.io/storybook/?path=/story/catalog-entitypeekaheadpopover--default From 645161da45981e81632cfca1a8f56e241ecc8a1a Mon Sep 17 00:00:00 2001 From: irma12 Date: Mon, 19 Dec 2022 09:16:29 +0100 Subject: [PATCH 27/33] Add delay to popup Signed-off-by: irma12 --- .../EntityPeekAheadPopover.tsx | 152 ++++++++++-------- 1 file changed, 89 insertions(+), 63 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx index c6cba8d421..ad53182b90 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx @@ -15,7 +15,7 @@ */ import useAsyncFn from 'react-use/lib/useAsyncFn'; import { catalogApiRef } from '../../api'; -import React, { PropsWithChildren, useEffect } from 'react'; +import React, { PropsWithChildren, useEffect, useState } from 'react'; import HoverPopover from 'material-ui-popup-state/HoverPopover'; import { bindHover, @@ -45,6 +45,7 @@ import { GroupCardActions, } from './CardActionComponents'; import { EntityNotFoundCard } from './EntityNotFoundCard'; +import { debounce } from 'lodash'; /** * Properties for an entity popover on hover of a component. @@ -53,6 +54,7 @@ import { EntityNotFoundCard } from './EntityNotFoundCard'; */ export type EntityPeekAheadPopoverProps = PropsWithChildren<{ entityRef: string; + delayTime: number; }>; const useStyles = makeStyles(() => { @@ -81,7 +83,7 @@ const maxTagChips = 4; * @public */ export const EntityPeekAheadPopover = (props: EntityPeekAheadPopoverProps) => { - const { entityRef, children } = props; + const { entityRef, children, delayTime } = props; const classes = useStyles(); const apiHolder = useApiHolder(); @@ -90,6 +92,12 @@ export const EntityPeekAheadPopover = (props: EntityPeekAheadPopoverProps) => { popupId: 'entity-peek-ahead', }); const compoundEntityRef = parseEntityRef(entityRef); + const [isHovered, setIsHovered] = useState(false); + + const debouncedHandleMouseEnter = debounce( + () => setIsHovered(true), + delayTime, + ); const [{ loading, error, value: entity }, load] = useAsyncFn(async () => { const catalogApi = apiHolder.get(catalogApiRef); @@ -105,6 +113,11 @@ export const EntityPeekAheadPopover = (props: EntityPeekAheadPopoverProps) => { return undefined; }, [apiHolder, compoundEntityRef]); + const handleOnMouseLeave = () => { + setIsHovered(false); + debouncedHandleMouseEnter.cancel(); + }; + useEffect(() => { if (popupState.isOpen && !entity && !error && !loading) { load(); @@ -113,69 +126,82 @@ export const EntityPeekAheadPopover = (props: EntityPeekAheadPopoverProps) => { return ( <> - - {children} - - - <> - {loading && } - {!entity && !loading && ( - - )} - {entity && ( - - - - {compoundEntityRef.namespace} - - - {compoundEntityRef.name} - - {entity.kind} - - {entity.metadata.description} - - {entity.spec?.type} - - {(entity.metadata.tags || []) - .slice(0, maxTagChips) - .map(tag => { - return ; - })} - {entity.metadata.tags?.length && - entity.metadata.tags?.length > maxTagChips && ( - - - + + {children} + + + {isHovered && ( + + <> + {loading && } + {!entity && !loading && ( + + )} + {entity && ( + + + + {compoundEntityRef.namespace} + + + {compoundEntityRef.name} + + {entity.kind} + + {entity.metadata.description} + + {entity.spec?.type} + + {(entity.metadata.tags || []) + .slice(0, maxTagChips) + .map(tag => { + return ; + })} + {entity.metadata.tags?.length && + entity.metadata.tags?.length > maxTagChips && ( + + + + )} + + + + <> + {isUserEntity(entity) && ( + )} - - - - <> - {isUserEntity(entity) && } - {isGroupEntity(entity) && ( - - )} - - - - - )} - - + {isGroupEntity(entity) && ( + + )} + + + + + )} + + + )} ); }; From 665bbd338cdd406fd7fec2cad585d31d44a2a2e0 Mon Sep 17 00:00:00 2001 From: irma12 Date: Mon, 19 Dec 2022 10:13:33 +0100 Subject: [PATCH 28/33] Add default delayTime value Signed-off-by: irma12 --- .../EntityPeekAheadPopover/EntityPeekAheadPopover.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx index ad53182b90..41b8cfbfb2 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx @@ -54,7 +54,7 @@ import { debounce } from 'lodash'; */ export type EntityPeekAheadPopoverProps = PropsWithChildren<{ entityRef: string; - delayTime: number; + delayTime?: number; }>; const useStyles = makeStyles(() => { @@ -83,7 +83,7 @@ const maxTagChips = 4; * @public */ export const EntityPeekAheadPopover = (props: EntityPeekAheadPopoverProps) => { - const { entityRef, children, delayTime } = props; + const { entityRef, children, delayTime = 500 } = props; const classes = useStyles(); const apiHolder = useApiHolder(); From 1252ff936e47a577d6db7c58799e9f2526d2350e Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 19 Dec 2022 13:17:50 +0000 Subject: [PATCH 29/33] fix api docs Signed-off-by: Brian Fletcher --- plugins/scaffolder/api-report.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 39e58a7a42..276c225cf1 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -110,9 +110,9 @@ export type EntityPickerUiOptions = export const EntityTagsPickerFieldExtension: FieldExtensionComponent< string[], { - showCounts?: boolean | undefined; - kinds?: string[] | undefined; helperText?: string | undefined; + kinds?: string[] | undefined; + showCounts?: boolean | undefined; } >; @@ -120,9 +120,9 @@ export const EntityTagsPickerFieldExtension: FieldExtensionComponent< export const EntityTagsPickerFieldSchema: FieldSchema< string[], { - showCounts?: boolean | undefined; - kinds?: string[] | undefined; helperText?: string | undefined; + kinds?: string[] | undefined; + showCounts?: boolean | undefined; } >; From 2e633f5e51963afb86c25abd875a51ebac7cee90 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 20 Dec 2022 14:41:22 +0000 Subject: [PATCH 30/33] add delay time to api report Signed-off-by: Brian Fletcher --- plugins/catalog-react/api-report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 82f8326a17..9528c55326 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -262,6 +262,7 @@ export const EntityPeekAheadPopover: ( // @public export type EntityPeekAheadPopoverProps = PropsWithChildren<{ entityRef: string; + delayTime?: number; }>; // @public (undocumented) From e253423ae4bea08357f49e072d6d46e4ff56b433 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 20 Dec 2022 16:39:26 +0000 Subject: [PATCH 31/33] remove api report for scaffolder Signed-off-by: Brian Fletcher --- plugins/scaffolder/api-report.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 9d388d225b..3fe1208ba9 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -110,9 +110,9 @@ export type EntityPickerUiOptions = export const EntityTagsPickerFieldExtension: FieldExtensionComponent< string[], { - helperText?: string | undefined; - kinds?: string[] | undefined; showCounts?: boolean | undefined; + kinds?: string[] | undefined; + helperText?: string | undefined; } >; @@ -120,9 +120,9 @@ export const EntityTagsPickerFieldExtension: FieldExtensionComponent< export const EntityTagsPickerFieldSchema: FieldSchema< string[], { - helperText?: string | undefined; - kinds?: string[] | undefined; showCounts?: boolean | undefined; + kinds?: string[] | undefined; + helperText?: string | undefined; } >; From 3d679f83c1d17a5a145288e600e497d14649e291 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 22 Dec 2022 19:28:28 +0000 Subject: [PATCH 32/33] address review comments Signed-off-by: Brian Fletcher --- .../CardActionComponents/EmailCardAction.tsx | 17 ++- .../EntityCardActions.tsx | 28 +++-- .../EntityNotFoundCard.tsx | 41 ------- .../EntityPeekAheadPopover.stories.tsx | 60 +++++++++- .../EntityPeekAheadPopover.tsx | 105 +++++++++--------- 5 files changed, 136 insertions(+), 115 deletions(-) delete mode 100644 plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityNotFoundCard.tsx diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EmailCardAction.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EmailCardAction.tsx index ed9ac7b9aa..2fcb89eed1 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EmailCardAction.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EmailCardAction.tsx @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Button, Tooltip } from '@material-ui/core'; +import { IconButton } from '@material-ui/core'; import EmailIcon from '@material-ui/icons/Email'; import React from 'react'; +import { Link } from '@backstage/core-components'; /** * Email Card action link @@ -24,10 +25,14 @@ import React from 'react'; */ export const EmailCardAction = ({ email }: { email: string }) => { return ( - - - + + + ); }; diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EntityCardActions.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EntityCardActions.tsx index d021a9f69d..fc8b5f4007 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EntityCardActions.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EntityCardActions.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { entityRouteRef } from '../../../routes'; -import { Tooltip } from '@material-ui/core'; +import { IconButton } from '@material-ui/core'; import InfoIcon from '@material-ui/icons/Info'; import React from 'react'; import { useRouteRef } from '@backstage/core-plugin-api'; @@ -30,19 +30,17 @@ export const EntityCardActions = ({ entity }: { entity: Entity }) => { const entityRoute = useRouteRef(entityRouteRef); return ( - <> - - - - - - + + + ); }; diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityNotFoundCard.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityNotFoundCard.tsx deleted file mode 100644 index 08d92cab22..0000000000 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityNotFoundCard.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2022 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 { Card, CardContent } from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; -import React from 'react'; - -/** - * Entity not found card - * - * @private - */ -export const EntityNotFoundCard = ({ - entityRef, - error, -}: { - entityRef: string; - error?: Error; -}) => { - return ( - - - - {entityRef} was not found {error?.message} - - - - ); -}; diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx index bb24f293cb..132042452b 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx @@ -22,9 +22,15 @@ import { import Button from '@material-ui/core/Button'; import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; import { catalogApiRef } from '../../api'; -import { CompoundEntityRef } from '@backstage/catalog-model'; +import { + CompoundEntityRef, + parseEntityRef, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { entityRouteRef } from '../../routes'; import { CatalogApi } from '@backstage/catalog-client'; +import { Table, TableColumn } from '@backstage/core-components'; +import { EntityRefLink } from '../EntityRefLink'; const mockCatalogApi = { getEntityByRef: async (entityRef: CompoundEntityRef) => { @@ -137,3 +143,55 @@ export const SlowCatalogItem = (args: EntityPeekAheadPopoverProps) => ( SlowCatalogItem.args = { entityRef: 'component:default/slow.catalog.item', }; + +const columns: TableColumn[] = [ + { + title: 'entity', + render: entityRef => { + return ( + + + + ); + }, + }, + { + title: 'owner', + render: () => { + return ( + + + + ); + }, + }, + { + title: 'name', + render: entityRef => stringifyEntityRef(entityRef), + }, +]; +export const TableOfItems = (args: { data: CompoundEntityRef[] }) => ( + +); + +TableOfItems.args = { + data: [ + { + name: 'playback', + kind: 'component', + namespace: 'default', + }, + { + name: 'playback', + kind: 'component', + namespace: 'default', + }, + { + name: 'playback', + kind: 'component', + namespace: 'default', + }, + ], +}; diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx index 41b8cfbfb2..05a818fa1f 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx @@ -38,13 +38,12 @@ import { isUserEntity, parseEntityRef, } from '@backstage/catalog-model'; -import { Progress } from '@backstage/core-components'; +import { Progress, ResponseErrorPanel } from '@backstage/core-components'; import { EntityCardActions, UserCardActions, GroupCardActions, } from './CardActionComponents'; -import { EntityNotFoundCard } from './EntityNotFoundCard'; import { debounce } from 'lodash'; /** @@ -126,11 +125,7 @@ export const EntityPeekAheadPopover = (props: EntityPeekAheadPopoverProps) => { return ( <> - {' '} - + {children} @@ -149,56 +144,62 @@ export const EntityPeekAheadPopover = (props: EntityPeekAheadPopoverProps) => { vertical: 'top', horizontal: 'center', }} + onMouseLeave={handleOnMouseLeave} > <> - {loading && } - {!entity && !loading && ( - - )} - {entity && ( - - - - {compoundEntityRef.namespace} - - - {compoundEntityRef.name} - - {entity.kind} - - {entity.metadata.description} - - {entity.spec?.type} - - {(entity.metadata.tags || []) - .slice(0, maxTagChips) - .map(tag => { - return ; - })} - {entity.metadata.tags?.length && - entity.metadata.tags?.length > maxTagChips && ( - - - - )} - - - + {error && } + + {loading && } + + + {entity && ( <> - {isUserEntity(entity) && ( - - )} - {isGroupEntity(entity) && ( - - )} - + + {compoundEntityRef.namespace} + + + {compoundEntityRef.name} + + {entity.kind} + + {entity.metadata.description} + + {entity.spec?.type} + + {(entity.metadata.tags || []) + .slice(0, maxTagChips) + .map(tag => { + return ; + })} + {entity.metadata.tags?.length && + entity.metadata.tags?.length > maxTagChips && ( + + + + )} + + )} + + {!error && ( + + {entity && ( + <> + {isUserEntity(entity) && ( + + )} + {isGroupEntity(entity) && ( + + )} + + + )} - - )} + )} + )} From a06b01ee381adf4bb825d2fc58a5a214438c2c3a Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 22 Dec 2022 21:42:41 +0000 Subject: [PATCH 33/33] fixes test Signed-off-by: Brian Fletcher --- .../EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx index f7c4d1cffa..2fc630313f 100644 --- a/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx +++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx @@ -78,7 +78,7 @@ describe('', () => { expect(screen.queryByText('service2')).toBeNull(); fireEvent.mouseOver(screen.getByTestId('popover2')); expect( - await screen.findByText(/service2 was not found/), + await screen.findByText('Error: service2 was not found'), ).toBeInTheDocument(); }); });