Merge pull request #15014 from RoadieHQ/entity-peek-ahead
add entity peek ahead component
This commit is contained in:
@@ -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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
+38
@@ -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 (
|
||||
<IconButton
|
||||
component={Link}
|
||||
aria-label="Email"
|
||||
title={`Email ${email}`}
|
||||
to={`mailto:${email}`}
|
||||
target="_blank"
|
||||
>
|
||||
<EmailIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
+46
@@ -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 (
|
||||
<IconButton
|
||||
component={Link}
|
||||
aria-label="Show"
|
||||
title="Show details"
|
||||
to={entityRoute({
|
||||
name: entity.metadata.name,
|
||||
namespace: entity.metadata.namespace || 'default',
|
||||
kind: entity.kind.toLocaleLowerCase('en-US'),
|
||||
})}
|
||||
>
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
+33
@@ -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 && (
|
||||
<EmailCardAction email={entity.spec.profile.email} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+33
@@ -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 && (
|
||||
<EmailCardAction email={entity.spec.profile.email} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+18
@@ -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';
|
||||
+197
@@ -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(
|
||||
<>
|
||||
<TestApiProvider
|
||||
apis={[[catalogApiRef, mockCatalogApi as any as CatalogApi]]}
|
||||
>
|
||||
<Story />
|
||||
</TestApiProvider>
|
||||
</>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const Default = (args: EntityPeekAheadPopoverProps) => (
|
||||
<EntityPeekAheadPopover {...args}>
|
||||
<Button>Hover over me to see details about this component</Button>
|
||||
</EntityPeekAheadPopover>
|
||||
);
|
||||
Default.args = defaultArgs;
|
||||
|
||||
export const User = (args: EntityPeekAheadPopoverProps) => (
|
||||
<EntityPeekAheadPopover {...args}>
|
||||
<Button>Hover over me to see details about this user</Button>
|
||||
</EntityPeekAheadPopover>
|
||||
);
|
||||
User.args = {
|
||||
entityRef: 'user:default/fname.lname',
|
||||
};
|
||||
|
||||
export const NotFound = (args: EntityPeekAheadPopoverProps) => (
|
||||
<EntityPeekAheadPopover {...args}>
|
||||
<Button>Hover over me to see details about this not found entity</Button>
|
||||
</EntityPeekAheadPopover>
|
||||
);
|
||||
NotFound.args = {
|
||||
entityRef: 'user:default/doesnt.exist',
|
||||
};
|
||||
|
||||
export const SlowCatalogItem = (args: EntityPeekAheadPopoverProps) => (
|
||||
<EntityPeekAheadPopover {...args}>
|
||||
<Button>Hover over me to see details about this slow entity</Button>
|
||||
</EntityPeekAheadPopover>
|
||||
);
|
||||
SlowCatalogItem.args = {
|
||||
entityRef: 'component:default/slow.catalog.item',
|
||||
};
|
||||
|
||||
const columns: TableColumn<CompoundEntityRef>[] = [
|
||||
{
|
||||
title: 'entity',
|
||||
render: entityRef => {
|
||||
return (
|
||||
<EntityPeekAheadPopover entityRef={stringifyEntityRef(entityRef)}>
|
||||
<EntityRefLink entityRef={entityRef} />
|
||||
</EntityPeekAheadPopover>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'owner',
|
||||
render: () => {
|
||||
return (
|
||||
<EntityPeekAheadPopover entityRef="user:default/fname.lname">
|
||||
<EntityRefLink
|
||||
entityRef={parseEntityRef('user:default/fname.lname')}
|
||||
/>
|
||||
</EntityPeekAheadPopover>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'name',
|
||||
render: entityRef => stringifyEntityRef(entityRef),
|
||||
},
|
||||
];
|
||||
export const TableOfItems = (args: { data: CompoundEntityRef[] }) => (
|
||||
<Table columns={columns} data={args.data} />
|
||||
);
|
||||
|
||||
TableOfItems.args = {
|
||||
data: [
|
||||
{
|
||||
name: 'playback',
|
||||
kind: 'component',
|
||||
namespace: 'default',
|
||||
},
|
||||
{
|
||||
name: 'playback',
|
||||
kind: 'component',
|
||||
namespace: 'default',
|
||||
},
|
||||
{
|
||||
name: 'playback',
|
||||
kind: 'component',
|
||||
namespace: 'default',
|
||||
},
|
||||
],
|
||||
};
|
||||
+84
@@ -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<CatalogApi> = {
|
||||
getEntityByRef: async (
|
||||
entityRef: CompoundEntityRef,
|
||||
): Promise<Entity | undefined> => {
|
||||
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('<EntityPeekAheadPopover/>', () => {
|
||||
it('renders all owners', async () => {
|
||||
renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityPeekAheadPopover entityRef="component:default/service1">
|
||||
<Button data-testid="popover1">s1</Button>
|
||||
</EntityPeekAheadPopover>
|
||||
<EntityPeekAheadPopover entityRef="component:default/service2">
|
||||
<Button data-testid="popover2">s2</Button>
|
||||
</EntityPeekAheadPopover>
|
||||
</ApiProvider>,
|
||||
{
|
||||
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();
|
||||
});
|
||||
});
|
||||
+208
@@ -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 (
|
||||
<>
|
||||
<span onMouseEnter={debouncedHandleMouseEnter}>
|
||||
<span data-testid="trigger" {...bindHover(popupState)}>
|
||||
{children}
|
||||
</span>
|
||||
</span>
|
||||
{isHovered && (
|
||||
<HoverPopover
|
||||
PaperProps={{
|
||||
className: classes.popoverPaper,
|
||||
}}
|
||||
{...bindPopover(popupState)}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
onMouseLeave={handleOnMouseLeave}
|
||||
>
|
||||
<>
|
||||
{error && <ResponseErrorPanel error={error} />}
|
||||
<Card>
|
||||
{loading && <Progress />}
|
||||
|
||||
<CardContent>
|
||||
{entity && (
|
||||
<>
|
||||
<Typography color="textSecondary">
|
||||
{compoundEntityRef.namespace}
|
||||
</Typography>
|
||||
<Typography variant="h5" component="div">
|
||||
{compoundEntityRef.name}
|
||||
</Typography>
|
||||
<Typography color="textSecondary">{entity.kind}</Typography>
|
||||
<Typography
|
||||
className={classes.descriptionTypography}
|
||||
paragraph
|
||||
>
|
||||
{entity.metadata.description}
|
||||
</Typography>
|
||||
<Typography>{entity.spec?.type}</Typography>
|
||||
<Box marginTop="0.5em">
|
||||
{(entity.metadata.tags || [])
|
||||
.slice(0, maxTagChips)
|
||||
.map(tag => {
|
||||
return <Chip key={tag} size="small" label={tag} />;
|
||||
})}
|
||||
{entity.metadata.tags?.length &&
|
||||
entity.metadata.tags?.length > maxTagChips && (
|
||||
<Tooltip title="Drill into the entity to see all of the tags.">
|
||||
<Chip key="other-tags" size="small" label="..." />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
{!error && (
|
||||
<CardActions>
|
||||
{entity && (
|
||||
<>
|
||||
{isUserEntity(entity) && (
|
||||
<UserCardActions entity={entity} />
|
||||
)}
|
||||
{isGroupEntity(entity) && (
|
||||
<GroupCardActions entity={entity} />
|
||||
)}
|
||||
<EntityCardActions entity={entity} />
|
||||
</>
|
||||
)}
|
||||
</CardActions>
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
</HoverPopover>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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(<Story />, {
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export const Default = (args: EntityRefLinkProps) => (
|
||||
<EntityRefLink {...args} />
|
||||
);
|
||||
Default.args = defaultArgs;
|
||||
@@ -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(<Story />, {
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export const Default = (
|
||||
args: EntityRefLinksProps<string | CompoundEntityRef>,
|
||||
) => <EntityRefLinks {...args} />;
|
||||
Default.args = defaultArgs;
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user