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
@@ -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"
},