From 4d248725e6eed8a7e0ef8894c9c13b4a449bef40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 25 Mar 2021 12:04:31 +0100 Subject: [PATCH] detach the badges plugin from the catalog plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/dull-badgers-pull.md | 6 ++ .changeset/eight-poems-camp.md | 5 ++ .changeset/famous-knives-cough.md | 5 ++ .changeset/selfish-apples-fly.md | 7 +++ .changeset/twelve-rings-sip.md | 5 ++ .../app/src/components/catalog/EntityPage.tsx | 62 ++++++++++++++----- .../default-backend-plugin/package.json.hbs | 2 +- .../packages/backend/package.json.hbs | 2 +- .../components/EntityBadgesDialog.test.tsx | 5 +- .../src/components/EntityBadgesDialog.tsx | 23 +++---- plugins/catalog/package.json | 1 - .../EntityContextMenu.test.tsx | 55 ++++++++++++---- .../EntityContextMenu/EntityContextMenu.tsx | 51 +++++++++------ .../components/EntityLayout/EntityLayout.tsx | 31 +++++++--- .../EntityPageLayout/EntityPageLayout.tsx | 33 ++++++---- plugins/search-backend-node/package.json | 2 +- plugins/search-backend/package.json | 4 +- yarn.lock | 2 - 18 files changed, 213 insertions(+), 88 deletions(-) create mode 100644 .changeset/dull-badgers-pull.md create mode 100644 .changeset/eight-poems-camp.md create mode 100644 .changeset/famous-knives-cough.md create mode 100644 .changeset/selfish-apples-fly.md create mode 100644 .changeset/twelve-rings-sip.md diff --git a/.changeset/dull-badgers-pull.md b/.changeset/dull-badgers-pull.md new file mode 100644 index 0000000000..ddbdeb55ca --- /dev/null +++ b/.changeset/dull-badgers-pull.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-backend': patch +'@backstage/plugin-search-backend-node': patch +--- + +Bump to use the in-repo latest `backend-common`, and the correct version of `express-promise-router` diff --git a/.changeset/eight-poems-camp.md b/.changeset/eight-poems-camp.md new file mode 100644 index 0000000000..62ee6810c9 --- /dev/null +++ b/.changeset/eight-poems-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-badges': patch +--- + +Get the current entity using `useEntity` from `@backstage/plugin-catalog-react` diff --git a/.changeset/famous-knives-cough.md b/.changeset/famous-knives-cough.md new file mode 100644 index 0000000000..28869ddf33 --- /dev/null +++ b/.changeset/famous-knives-cough.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Temporarily add `UNSTABLE_extraContextMenuItems` to the entity layout, so that we could detach the catalog plugin from the dependency on the badges plugin diff --git a/.changeset/selfish-apples-fly.md b/.changeset/selfish-apples-fly.md new file mode 100644 index 0000000000..b0be4f0423 --- /dev/null +++ b/.changeset/selfish-apples-fly.md @@ -0,0 +1,7 @@ +--- +'@backstage/create-app': patch +--- + +Update the create-app template to use the correct latest version of `express-promise-router`. + +To apply the same change in your own repository, update all of your repo's dependencies on `express-promise-router` to `"^4.1.0"`. diff --git a/.changeset/twelve-rings-sip.md b/.changeset/twelve-rings-sip.md new file mode 100644 index 0000000000..1fc2f88ec1 --- /dev/null +++ b/.changeset/twelve-rings-sip.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Make the backend plugin template use the correct latest version of `express-promise-router` diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index cacd2458b1..0f9cc98f4b 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -31,6 +31,7 @@ import { ProvidedApisCard, ProvidingComponentsCard, } from '@backstage/plugin-api-docs'; +import { EntityBadgesDialog } from '@backstage/plugin-badges'; import { AboutCard, EntityHasComponentsCard, @@ -104,7 +105,8 @@ import { RecentTravisCIBuildsWidget, Router as TravisCIRouter, } from '@roadiehq/backstage-plugin-travis-ci'; -import React, { ReactNode } from 'react'; +import React, { ReactNode, useMemo, useState } from 'react'; +import BadgeIcon from '@material-ui/icons/CallToAction'; export const CICDSwitcher = ({ entity }: { entity: Entity }) => { // This component is just an example of how you can implement your company's logic in entity page. @@ -178,6 +180,32 @@ export const ErrorsSwitcher = ({ entity }: { entity: Entity }) => { } }; +const EntityPageLayoutWrapper = (props: { children?: React.ReactNode }) => { + const [badgesDialogOpen, setBadgesDialogOpen] = useState(false); + + const extraMenuItems = useMemo(() => { + return [ + { + title: 'Badges', + Icon: BadgeIcon, + onClick: () => setBadgesDialogOpen(true), + }, + ]; + }, []); + + return ( + <> + + {props.children} + + setBadgesDialogOpen(false)} + /> + + ); +}; + const ComponentOverviewContent = ({ entity }: { entity: Entity }) => ( @@ -233,7 +261,7 @@ const ComponentApisContent = ({ entity }: { entity: Entity }) => ( ); const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( - + ( title="TODOs" element={} /> - + ); const WebsiteEntityPage = ({ entity }: { entity: Entity }) => ( - + ( title="TODOs" element={} /> - + ); const DefaultEntityPage = ({ entity }: { entity: Entity }) => ( - + ( title="TODOs" element={} /> - + ); export const ComponentEntityPage = ({ entity }: { entity: Entity }) => { @@ -393,7 +421,7 @@ const ApiDefinitionContent = ({ entity }: { entity: ApiEntity }) => ( ); const ApiEntityPage = ({ entity }: { entity: Entity }) => ( - + ( title="Definition" element={} /> - + ); const UserOverviewContent = ({ entity }: { entity: UserEntity }) => ( @@ -419,13 +447,13 @@ const UserOverviewContent = ({ entity }: { entity: UserEntity }) => ( ); const UserEntityPage = ({ entity }: { entity: Entity }) => ( - + } /> - + ); const GroupOverviewContent = ({ entity }: { entity: GroupEntity }) => ( @@ -443,13 +471,13 @@ const GroupOverviewContent = ({ entity }: { entity: GroupEntity }) => ( ); const GroupEntityPage = ({ entity }: { entity: Entity }) => ( - + } /> - + ); const SystemOverviewContent = ({ entity }: { entity: SystemEntity }) => ( @@ -467,13 +495,13 @@ const SystemOverviewContent = ({ entity }: { entity: SystemEntity }) => ( ); const SystemEntityPage = ({ entity }: { entity: Entity }) => ( - + } /> - + ); const DomainOverviewContent = ({ entity }: { entity: DomainEntity }) => ( @@ -488,13 +516,13 @@ const DomainOverviewContent = ({ entity }: { entity: DomainEntity }) => ( ); const DomainEntityPage = ({ entity }: { entity: Entity }) => ( - + } /> - + ); export const EntityPage = () => { diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index b762bb9645..d041773d3c 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -27,7 +27,7 @@ "@backstage/config": "^{{version '@backstage/config'}}", "@types/express": "^4.17.6", "express": "^4.17.1", - "express-promise-router": "^3.0.3", + "express-promise-router": "^4.1.0", "winston": "^3.2.1", "cross-fetch": "^3.0.6", "yn": "^4.0.0" diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 8c8cfae0b3..138bcaec8d 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -32,7 +32,7 @@ "@octokit/rest": "^18.0.12", "dockerode": "^3.2.1", "express": "^4.17.1", - "express-promise-router": "^3.0.3", + "express-promise-router": "^4.1.0", "knex": "^0.21.6", {{#if dbTypePG}} "pg": "^8.3.0", diff --git a/plugins/badges/src/components/EntityBadgesDialog.test.tsx b/plugins/badges/src/components/EntityBadgesDialog.test.tsx index 25a6f01c1a..62f2b55bc5 100644 --- a/plugins/badges/src/components/EntityBadgesDialog.test.tsx +++ b/plugins/badges/src/components/EntityBadgesDialog.test.tsx @@ -25,6 +25,7 @@ import { import { renderWithEffects } from '@backstage/test-utils'; import { BadgesApi, badgesApiRef } from '../api'; import { EntityBadgesDialog } from './EntityBadgesDialog'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; describe('EntityBadgesDialog', () => { it('should render', async () => { @@ -50,7 +51,9 @@ describe('EntityBadgesDialog', () => { {} as ErrorApi, )} > - + + + , ); diff --git a/plugins/badges/src/components/EntityBadgesDialog.tsx b/plugins/badges/src/components/EntityBadgesDialog.tsx index f6ba9de9fe..c0cbd6b15c 100644 --- a/plugins/badges/src/components/EntityBadgesDialog.tsx +++ b/plugins/badges/src/components/EntityBadgesDialog.tsx @@ -14,13 +14,13 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; import { CodeSnippet, Progress, ResponseErrorPanel, useApi, } from '@backstage/core'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Box, Button, @@ -39,43 +39,44 @@ import { badgesApiRef } from '../api'; type Props = { open: boolean; onClose?: () => any; - entity: Entity; }; -export const EntityBadgesDialog = ({ open, onClose, entity }: Props) => { +export const EntityBadgesDialog = ({ open, onClose }: Props) => { const theme = useTheme(); + const { entity } = useEntity(); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); const badgesApi = useApi(badgesApiRef); const { value: badges, loading, error } = useAsync(async () => { - if (open) { + if (open && entity) { return await badgesApi.getEntityBadgeSpecs(entity); } - return []; }, [badgesApi, entity, open]); const content = (badges || []).map( ({ badge: { description }, id, url, markdown }) => ( - - - {description - - + + + {description + + + ), ); return ( Entity Badges - Embed badges in other web sites that link back to this entity. Copy the relevant snippet of Markdown code to use the badge. + {loading && } {error && } + {content} diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index d2c5085035..8c05c376f9 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -35,7 +35,6 @@ "@backstage/core": "^0.7.2", "@backstage/integration": "^0.5.1", "@backstage/integration-react": "^0.1.1", - "@backstage/plugin-badges": "^0.1.1", "@backstage/plugin-catalog-react": "^0.1.2", "@backstage/theme": "^0.2.4", "@material-ui/core": "^4.11.0", diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx index 68306944a3..4d7a969542 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx @@ -14,22 +14,53 @@ * limitations under the License. */ -import { render, fireEvent } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/test-utils'; +import SearchIcon from '@material-ui/icons/Search'; +import { fireEvent, screen } from '@testing-library/react'; import * as React from 'react'; -import { act } from 'react-dom/test-utils'; import { EntityContextMenu } from './EntityContextMenu'; describe('ComponentContextMenu', () => { it('should call onUnregisterEntity on button click', async () => { - await act(async () => { - const mockCallback = jest.fn(); - const menu = render( - , - ); - const button = await menu.findByTestId('menu-button'); - fireEvent.click(button); - const unregister = await menu.findByText('Unregister entity'); - expect(unregister).toBeInTheDocument(); - }); + const mockCallback = jest.fn(); + + await renderInTestApp( + , + ); + + const button = await screen.findByTestId('menu-button'); + expect(button).toBeInTheDocument(); + fireEvent.click(button); + + const unregister = await screen.findByText('Unregister entity'); + expect(unregister).toBeInTheDocument(); + fireEvent.click(unregister); + + expect(mockCallback).toBeCalled(); + }); + + it('supports extra items', async () => { + const extra = { + title: 'HELLO', + Icon: SearchIcon, + onClick: jest.fn(), + }; + + await renderInTestApp( + , + ); + + const button = await screen.findByTestId('menu-button'); + expect(button).toBeInTheDocument(); + fireEvent.click(button); + + const item = await screen.findByText('HELLO'); + expect(item).toBeInTheDocument(); + fireEvent.click(item); + + expect(extra.onClick).toBeCalled(); }); }); diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index e4275181df..809a4fd16e 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -14,17 +14,18 @@ * limitations under the License. */ +import { IconComponent } from '@backstage/core'; import { + Divider, IconButton, ListItemIcon, + ListItemText, MenuItem, MenuList, Popover, - Typography, } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import Cancel from '@material-ui/icons/Cancel'; -import BadgeIcon from '@material-ui/icons/CallToAction'; import MoreVert from '@material-ui/icons/MoreVert'; import React, { useState } from 'react'; @@ -35,13 +36,21 @@ const useStyles = makeStyles({ }, }); +// NOTE(freben): Intentionally not exported at this point, since it's part of +// the unstable extra context menu items concept below +type ExtraContextMenuItem = { + title: string; + Icon: IconComponent; + onClick: () => void; +}; + type Props = { - onShowBadgesDialog?: () => void; + UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[]; onUnregisterEntity: () => void; }; export const EntityContextMenu = ({ - onShowBadgesDialog, + UNSTABLE_extraContextMenuItems, onUnregisterEntity, }: Props) => { const [anchorEl, setAnchorEl] = useState(); @@ -55,6 +64,24 @@ export const EntityContextMenu = ({ setAnchorEl(undefined); }; + const extraItems = UNSTABLE_extraContextMenuItems && [ + ...UNSTABLE_extraContextMenuItems.map(item => ( + { + onClose(); + item.onClick(); + }} + > + + + + + + )), + , + ]; + return ( <> - {onShowBadgesDialog && ( - { - onClose(); - onShowBadgesDialog(); - }} - > - - - - Badges - - )} + {extraItems} { onClose(); @@ -97,7 +112,7 @@ export const EntityContextMenu = ({ - Unregister entity + diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index cf7976e7ee..87a1a48e92 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -19,6 +19,7 @@ import { Content, Header, HeaderLabel, + IconComponent, Page, Progress, TabbedLayout, @@ -29,12 +30,7 @@ import { } from '@backstage/plugin-catalog-react'; import { Box } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; -import { - default as React, - PropsWithChildren, - useContext, - useState, -} from 'react'; +import { default as React, useContext, useState } from 'react'; import { useNavigate } from 'react-router'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; @@ -79,6 +75,19 @@ const headerProps = ( }; }; +// NOTE(freben): Intentionally not exported at this point, since it's part of +// the unstable extra context menu items concept below +type ExtraContextMenuItem = { + title: string; + Icon: IconComponent; + onClick: () => void; +}; + +type EntityLayoutProps = { + UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[]; + children?: React.ReactNode; +}; + /** * EntityLayout is a compound component, which allows you to define a layout for * entities using a sub-navigation mechanism. @@ -94,7 +103,10 @@ const headerProps = ( * * ``` */ -export const EntityLayout = ({ children }: PropsWithChildren<{}>) => { +export const EntityLayout = ({ + UNSTABLE_extraContextMenuItems, + children, +}: EntityLayoutProps) => { const { kind, namespace, name } = useEntityCompoundName(); const { entity, loading, error } = useContext(EntityContext); @@ -132,7 +144,10 @@ export const EntityLayout = ({ children }: PropsWithChildren<{}>) => { label="Lifecycle" value={entity.spec?.lifecycle || 'unknown'} /> - + )} diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx index 64a51f2bb8..44b1329938 100644 --- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx @@ -27,6 +27,7 @@ import { Progress, ResponseErrorPanel, WarningPanel, + IconComponent, } from '@backstage/core'; import { EntityContext, @@ -35,12 +36,11 @@ import { useEntityCompoundName, } from '@backstage/plugin-catalog-react'; import { Box } from '@material-ui/core'; -import React, { PropsWithChildren, useContext, useState } from 'react'; +import React, { useContext, useState } from 'react'; import { useNavigate } from 'react-router'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; -import { EntityBadgesDialog } from '@backstage/plugin-badges'; import { Tabbed } from './Tabbed'; const EntityPageTitle = ({ @@ -99,7 +99,23 @@ const headerProps = ( }; }; -export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => { +// NOTE(freben): Intentionally not exported at this point, since it's part of +// the unstable extra context menu items concept below +type ExtraContextMenuItem = { + title: string; + Icon: IconComponent; + onClick: () => void; +}; + +type EntityPageLayoutProps = { + UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[]; + children?: React.ReactNode; +}; + +export const EntityPageLayout = ({ + children, + UNSTABLE_extraContextMenuItems, +}: EntityPageLayoutProps) => { const { kind, namespace, name } = useEntityCompoundName(); const { entity, loading, error } = useContext(EntityContext); const { headerTitle, headerType } = headerProps( @@ -109,7 +125,6 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => { entity!, ); - const [badgesDialogOpen, setBadgesDialogOpen] = useState(false); const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); const navigate = useNavigate(); const cleanUpAfterRemoval = async () => { @@ -131,7 +146,7 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => { <> setBadgesDialogOpen(true)} + UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems} onUnregisterEntity={showRemovalDialog} /> @@ -164,14 +179,6 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => { )} - {entity && ( - setBadgesDialogOpen(false)} - /> - )} -