detach the badges plugin from the catalog plugin

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2021-03-25 12:04:31 +01:00
parent 4e1227ce2e
commit 4d248725e6
18 changed files with 213 additions and 88 deletions
+6
View File
@@ -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`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-badges': patch
---
Get the current entity using `useEntity` from `@backstage/plugin-catalog-react`
+5
View File
@@ -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
+7
View File
@@ -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"`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Make the backend plugin template use the correct latest version of `express-promise-router`
@@ -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 (
<>
<EntityPageLayout UNSTABLE_extraContextMenuItems={extraMenuItems}>
{props.children}
</EntityPageLayout>
<EntityBadgesDialog
open={badgesDialogOpen}
onClose={() => setBadgesDialogOpen(false)}
/>
</>
);
};
const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
@@ -233,7 +261,7 @@ const ComponentApisContent = ({ entity }: { entity: Entity }) => (
);
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/"
title="Overview"
@@ -284,11 +312,11 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
title="TODOs"
element={<EntityTodoContent />}
/>
</EntityPageLayout>
</EntityPageLayoutWrapper>
);
const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/"
title="Overview"
@@ -334,11 +362,11 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
title="TODOs"
element={<EntityTodoContent />}
/>
</EntityPageLayout>
</EntityPageLayoutWrapper>
);
const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
@@ -354,7 +382,7 @@ const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
title="TODOs"
element={<EntityTodoContent />}
/>
</EntityPageLayout>
</EntityPageLayoutWrapper>
);
export const ComponentEntityPage = ({ entity }: { entity: Entity }) => {
@@ -393,7 +421,7 @@ const ApiDefinitionContent = ({ entity }: { entity: ApiEntity }) => (
);
const ApiEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
@@ -404,7 +432,7 @@ const ApiEntityPage = ({ entity }: { entity: Entity }) => (
title="Definition"
element={<ApiDefinitionContent entity={entity as ApiEntity} />}
/>
</EntityPageLayout>
</EntityPageLayoutWrapper>
);
const UserOverviewContent = ({ entity }: { entity: UserEntity }) => (
@@ -419,13 +447,13 @@ const UserOverviewContent = ({ entity }: { entity: UserEntity }) => (
);
const UserEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<UserOverviewContent entity={entity as UserEntity} />}
/>
</EntityPageLayout>
</EntityPageLayoutWrapper>
);
const GroupOverviewContent = ({ entity }: { entity: GroupEntity }) => (
@@ -443,13 +471,13 @@ const GroupOverviewContent = ({ entity }: { entity: GroupEntity }) => (
);
const GroupEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<GroupOverviewContent entity={entity as GroupEntity} />}
/>
</EntityPageLayout>
</EntityPageLayoutWrapper>
);
const SystemOverviewContent = ({ entity }: { entity: SystemEntity }) => (
@@ -467,13 +495,13 @@ const SystemOverviewContent = ({ entity }: { entity: SystemEntity }) => (
);
const SystemEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<SystemOverviewContent entity={entity as SystemEntity} />}
/>
</EntityPageLayout>
</EntityPageLayoutWrapper>
);
const DomainOverviewContent = ({ entity }: { entity: DomainEntity }) => (
@@ -488,13 +516,13 @@ const DomainOverviewContent = ({ entity }: { entity: DomainEntity }) => (
);
const DomainEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<DomainOverviewContent entity={entity as DomainEntity} />}
/>
</EntityPageLayout>
</EntityPageLayoutWrapper>
);
export const EntityPage = () => {
@@ -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"
@@ -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",
@@ -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,
)}
>
<EntityBadgesDialog open entity={mockEntity} />
<EntityProvider entity={mockEntity}>
<EntityBadgesDialog open />
</EntityProvider>
</ApiProvider>,
);
@@ -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 }) => (
<DialogContentText>
<Box m={4} />
<img alt={description || id} src={url} />
<CodeSnippet language="markdown" text={markdown} showCopyCodeButton />
</DialogContentText>
<Box marginTop={4} key={id}>
<DialogContentText component="div">
<img alt={description || id} src={url} />
<CodeSnippet language="markdown" text={markdown} showCopyCodeButton />
</DialogContentText>
</Box>
),
);
return (
<Dialog fullScreen={fullScreen} open={open} onClose={onClose}>
<DialogTitle>Entity Badges</DialogTitle>
<DialogContent>
<DialogContentText>
Embed badges in other web sites that link back to this entity. Copy
the relevant snippet of Markdown code to use the badge.
</DialogContentText>
{loading && <Progress />}
{error && <ResponseErrorPanel error={error} />}
{content}
</DialogContent>
-1
View File
@@ -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",
@@ -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(
<EntityContextMenu onUnregisterEntity={mockCallback} />,
);
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(
<EntityContextMenu onUnregisterEntity={mockCallback} />,
);
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(
<EntityContextMenu
onUnregisterEntity={jest.fn()}
UNSTABLE_extraContextMenuItems={[extra]}
/>,
);
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();
});
});
@@ -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<HTMLButtonElement>();
@@ -55,6 +64,24 @@ export const EntityContextMenu = ({
setAnchorEl(undefined);
};
const extraItems = UNSTABLE_extraContextMenuItems && [
...UNSTABLE_extraContextMenuItems.map(item => (
<MenuItem
key={item.title}
onClick={() => {
onClose();
item.onClick();
}}
>
<ListItemIcon>
<item.Icon fontSize="small" />
</ListItemIcon>
<ListItemText primary={item.title} />
</MenuItem>
)),
<Divider key="the divider is here!" />,
];
return (
<>
<IconButton
@@ -75,19 +102,7 @@ export const EntityContextMenu = ({
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<MenuList>
{onShowBadgesDialog && (
<MenuItem
onClick={() => {
onClose();
onShowBadgesDialog();
}}
>
<ListItemIcon>
<BadgeIcon fontSize="small" />
</ListItemIcon>
<Typography variant="inherit">Badges</Typography>
</MenuItem>
)}
{extraItems}
<MenuItem
onClick={() => {
onClose();
@@ -97,7 +112,7 @@ export const EntityContextMenu = ({
<ListItemIcon>
<Cancel fontSize="small" />
</ListItemIcon>
<Typography variant="inherit">Unregister entity</Typography>
<ListItemText primary="Unregister entity" />
</MenuItem>
</MenuList>
</Popover>
@@ -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 = (
* </EntityLayout>
* ```
*/
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'}
/>
<EntityContextMenu onUnregisterEntity={showRemovalDialog} />
<EntityContextMenu
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
onUnregisterEntity={showRemovalDialog}
/>
</>
)}
</Header>
@@ -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<{}>) => {
<>
<EntityLabels entity={entity} />
<EntityContextMenu
onShowBadgesDialog={() => setBadgesDialogOpen(true)}
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
onUnregisterEntity={showRemovalDialog}
/>
</>
@@ -164,14 +179,6 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => {
</Content>
)}
{entity && (
<EntityBadgesDialog
open={badgesDialogOpen}
entity={entity}
onClose={() => setBadgesDialogOpen(false)}
/>
)}
<UnregisterEntityDialog
open={confirmationDialogOpen}
entity={entity!}
+1 -1
View File
@@ -23,7 +23,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/backend-common": "0.5.6",
"@backstage/backend-common": "^0.6.0",
"@backstage/cli": "^0.6.0"
},
"files": [
+2 -2
View File
@@ -19,11 +19,11 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.5.3",
"@backstage/backend-common": "^0.6.0",
"@backstage/search-common": "^0.1.1",
"@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",
"yn": "^4.0.0"
},
-2
View File
@@ -1980,7 +1980,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"
@@ -2005,7 +2004,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"