diff --git a/.changeset/red-tables-train.md b/.changeset/red-tables-train.md
new file mode 100644
index 0000000000..031d3720fc
--- /dev/null
+++ b/.changeset/red-tables-train.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-react': patch
+---
+
+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
diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md
index 92bfc4de96..9528c55326 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: (
+ props: EntityPeekAheadPopoverProps,
+) => JSX.Element;
+
+// @public
+export type EntityPeekAheadPopoverProps = PropsWithChildren<{
+ entityRef: string;
+ delayTime?: number;
+}>;
+
// @public (undocumented)
export const EntityProcessingStatusPicker: () => JSX.Element;
diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json
index 9c9757f161..14a3ce8df5 100644
--- a/plugins/catalog-react/package.json
+++ b/plugins/catalog-react/package.json
@@ -51,6 +51,7 @@
"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/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..2fcb89eed1
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EmailCardAction.tsx
@@ -0,0 +1,38 @@
+/*
+ * 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 { 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
+ *
+ * @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..fc8b5f4007
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/CardActionComponents/EntityCardActions.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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 { IconButton } 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/EntityPeekAheadPopover.stories.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx
new file mode 100644
index 0000000000..132042452b
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.stories.tsx
@@ -0,0 +1,197 @@
+/*
+ * 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 {
+ EntityPeekAheadPopover,
+ EntityPeekAheadPopoverProps,
+} from './EntityPeekAheadPopover';
+import Button from '@material-ui/core/Button';
+import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
+import { catalogApiRef } from '../../api';
+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) => {
+ 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 /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: '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',
+};
+
+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.test.tsx b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx
new file mode 100644
index 0000000000..2fc630313f
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.test.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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, screen } from '@testing-library/react';
+import React from 'react';
+import { EntityPeekAheadPopover } from './EntityPeekAheadPopover';
+import { ApiProvider } from '@backstage/core-app-api';
+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' &&
+ entityRef.namespace === 'default' &&
+ entityRef.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 () => {
+ renderInTestApp(
+
+
+
+
+
+
+
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+ },
+ );
+ expect(screen.getByText('s1')).toBeInTheDocument();
+ expect(screen.queryByText('service1')).toBeNull();
+ 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('Error: 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
new file mode 100644
index 0000000000..05a818fa1f
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityPeekAheadPopover/EntityPeekAheadPopover.tsx
@@ -0,0 +1,208 @@
+/*
+ * 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 useAsyncFn from 'react-use/lib/useAsyncFn';
+import { catalogApiRef } from '../../api';
+import React, { PropsWithChildren, useEffect, useState } from 'react';
+import HoverPopover from 'material-ui-popup-state/HoverPopover';
+import {
+ bindHover,
+ bindPopover,
+ usePopupState,
+} from 'material-ui-popup-state/hooks';
+import {
+ Box,
+ Card,
+ CardActions,
+ CardContent,
+ Chip,
+ makeStyles,
+ Tooltip,
+ Typography,
+} from '@material-ui/core';
+import { useApiHolder } from '@backstage/core-plugin-api';
+import {
+ isGroupEntity,
+ isUserEntity,
+ parseEntityRef,
+} from '@backstage/catalog-model';
+import { Progress, ResponseErrorPanel } from '@backstage/core-components';
+import {
+ EntityCardActions,
+ UserCardActions,
+ GroupCardActions,
+} from './CardActionComponents';
+import { debounce } from 'lodash';
+
+/**
+ * Properties for an entity popover on hover of a component.
+ *
+ * @public
+ */
+export type EntityPeekAheadPopoverProps = PropsWithChildren<{
+ entityRef: string;
+ delayTime?: number;
+}>;
+
+const useStyles = makeStyles(() => {
+ return {
+ trigger: {
+ display: 'inline-block',
+ },
+ popoverPaper: {
+ width: '30em',
+ },
+ descriptionTypography: {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ display: '-webkit-box',
+ WebkitLineClamp: 2,
+ WebkitBoxOrient: 'vertical',
+ },
+ };
+});
+
+const maxTagChips = 4;
+
+/**
+ * Shows an entity popover on hover of a component.
+ *
+ * @public
+ */
+export const EntityPeekAheadPopover = (props: EntityPeekAheadPopoverProps) => {
+ const { entityRef, children, delayTime = 500 } = props;
+
+ const classes = useStyles();
+ const apiHolder = useApiHolder();
+ const popupState = usePopupState({
+ variant: 'popover',
+ 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);
+ if (catalogApi) {
+ const retrievedEntity = await catalogApi.getEntityByRef(
+ compoundEntityRef,
+ );
+ if (!retrievedEntity) {
+ throw new Error(`${compoundEntityRef.name} was not found`);
+ }
+ return retrievedEntity;
+ }
+ return undefined;
+ }, [apiHolder, compoundEntityRef]);
+
+ const handleOnMouseLeave = () => {
+ setIsHovered(false);
+ debouncedHandleMouseEnter.cancel();
+ };
+
+ useEffect(() => {
+ if (popupState.isOpen && !entity && !error && !loading) {
+ load();
+ }
+ }, [popupState.isOpen, load, entity, error, loading]);
+
+ return (
+ <>
+
+
+ {children}
+
+
+ {isHovered && (
+
+ <>
+ {error && }
+
+ {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 && (
+
+ {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
new file mode 100644
index 0000000000..6630f6f555
--- /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 * from './EntityPeekAheadPopover';
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..e82c4ab963
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.stories.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 React, { ComponentType } from 'react';
+import { EntityRefLink, EntityRefLinkProps } from './EntityRefLink';
+import { wrapInTestApp } from '@backstage/test-utils';
+import { entityRouteRef } from '../../routes';
+
+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;
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;
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';
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
diff --git a/yarn.lock b/yarn.lock
index fa39818052..a47f5bad21 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5477,6 +5477,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