Merge pull request #15595 from backstage/freben/popover-tweaks

minor updates to the entity peek-ahead
This commit is contained in:
Fredrik Adelöw
2023-01-16 09:51:30 +01:00
committed by GitHub
8 changed files with 81 additions and 120 deletions
@@ -13,6 +13,7 @@
* 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';
@@ -23,14 +24,13 @@ import { Link } from '@backstage/core-components';
*
* @private
*/
export const EmailCardAction = ({ email }: { email: string }) => {
export const EmailCardAction = (props: { email: string }) => {
return (
<IconButton
component={Link}
aria-label="Email"
title={`Email ${email}`}
to={`mailto:${email}`}
target="_blank"
title={`Email ${props.email}`}
to={`mailto:${props.email}`}
>
<EmailIcon />
</IconButton>
@@ -13,12 +13,13 @@
* 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 { Entity, getCompoundEntityRef } from '@backstage/catalog-model';
import { Link } from '@backstage/core-components';
/**
@@ -26,7 +27,7 @@ import { Link } from '@backstage/core-components';
*
* @private
*/
export const EntityCardActions = ({ entity }: { entity: Entity }) => {
export const EntityCardActions = (props: { entity: Entity }) => {
const entityRoute = useRouteRef(entityRouteRef);
return (
@@ -34,11 +35,7 @@ export const EntityCardActions = ({ entity }: { entity: Entity }) => {
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'),
})}
to={entityRoute(getCompoundEntityRef(props.entity))}
>
<InfoIcon />
</IconButton>
@@ -13,6 +13,7 @@
* 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';
@@ -22,12 +23,7 @@ import { GroupEntity } from '@backstage/catalog-model';
*
* @private
*/
export const GroupCardActions = ({ entity }: { entity: GroupEntity }) => {
return (
<>
{entity.spec.profile?.email && (
<EmailCardAction email={entity.spec.profile.email} />
)}
</>
);
export const GroupCardActions = (props: { entity: GroupEntity }) => {
const email = props.entity.spec.profile?.email;
return email ? <EmailCardAction email={email} /> : null;
};
@@ -13,6 +13,7 @@
* 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';
@@ -22,12 +23,7 @@ import { UserEntity } from '@backstage/catalog-model';
*
* @private
*/
export const UserCardActions = ({ entity }: { entity: UserEntity }) => {
return (
<>
{entity.spec.profile?.email && (
<EmailCardAction email={entity.spec.profile.email} />
)}
</>
);
export const UserCardActions = (props: { entity: UserEntity }) => {
const email = props.entity.spec.profile?.email;
return email ? <EmailCardAction email={email} /> : null;
};
@@ -13,6 +13,7 @@
* 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';
@@ -33,12 +33,8 @@ 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'
) {
getEntityByRef: async (entityRef: string) => {
if (entityRef === 'component:default/playback') {
return {
kind: 'Component',
metadata: {
@@ -48,11 +44,7 @@ const mockCatalogApi = {
},
};
}
if (
entityRef.namespace === 'default' &&
entityRef.name === 'fname.lname' &&
entityRef.kind === 'user'
) {
if (entityRef === 'user:default/fname.lname') {
return {
kind: 'User',
metadata: {
@@ -66,11 +58,7 @@ const mockCatalogApi = {
},
};
}
if (
entityRef.namespace === 'default' &&
entityRef.name === 'slow.catalog.item' &&
entityRef.kind === 'component'
) {
if (entityRef === 'component:default/slow.catalog.item') {
await new Promise(resolve => setTimeout(resolve, 3000));
return {
kind: 'Component',
@@ -14,26 +14,21 @@
* limitations under the License.
*/
import { fireEvent, screen } from '@testing-library/react';
import { screen } from '@testing-library/react';
import user from '@testing-library/user-event';
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 { 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'
) {
getEntityByRef: async (entityRef: string): Promise<Entity | undefined> => {
if (entityRef === 'component:default/service1') {
return {
apiVersion: '',
kind: 'Component',
@@ -71,14 +66,14 @@ describe('<EntityPeekAheadPopover/>', () => {
);
expect(screen.getByText('s1')).toBeInTheDocument();
expect(screen.queryByText('service1')).toBeNull();
fireEvent.mouseOver(screen.getByTestId('popover1'));
user.hover(screen.getByTestId('popover1'));
expect(await screen.findByText('service1')).toBeInTheDocument();
expect(screen.getByText('s2')).toBeInTheDocument();
expect(screen.queryByText('service2')).toBeNull();
fireEvent.mouseOver(screen.getByTestId('popover2'));
user.hover(screen.getByTestId('popover2'));
expect(
await screen.findByText('Error: service2 was not found'),
await screen.findByText('Error: component:default/service2 not found'),
).toBeInTheDocument();
});
});
@@ -13,9 +13,10 @@
* 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 React, { PropsWithChildren, useEffect, useMemo, useState } from 'react';
import HoverPopover from 'material-ui-popup-state/HoverPopover';
import {
bindHover,
@@ -33,11 +34,7 @@ import {
Typography,
} from '@material-ui/core';
import { useApiHolder } from '@backstage/core-plugin-api';
import {
isGroupEntity,
isUserEntity,
parseEntityRef,
} from '@backstage/catalog-model';
import { isGroupEntity, isUserEntity } from '@backstage/catalog-model';
import { Progress, ResponseErrorPanel } from '@backstage/core-components';
import {
EntityCardActions,
@@ -58,9 +55,6 @@ export type EntityPeekAheadPopoverProps = PropsWithChildren<{
const useStyles = makeStyles(() => {
return {
trigger: {
display: 'inline-block',
},
popoverPaper: {
width: '30em',
},
@@ -90,27 +84,24 @@ export const EntityPeekAheadPopover = (props: EntityPeekAheadPopoverProps) => {
variant: 'popover',
popupId: 'entity-peek-ahead',
});
const compoundEntityRef = parseEntityRef(entityRef);
const [isHovered, setIsHovered] = useState(false);
const debouncedHandleMouseEnter = debounce(
() => setIsHovered(true),
delayTime,
const debouncedHandleMouseEnter = useMemo(
() => debounce(() => setIsHovered(true), delayTime),
[delayTime],
);
const [{ loading, error, value: entity }, load] = useAsyncFn(async () => {
const catalogApi = apiHolder.get(catalogApiRef);
if (catalogApi) {
const retrievedEntity = await catalogApi.getEntityByRef(
compoundEntityRef,
);
const retrievedEntity = await catalogApi.getEntityByRef(entityRef);
if (!retrievedEntity) {
throw new Error(`${compoundEntityRef.name} was not found`);
throw new Error(`${entityRef} not found`);
}
return retrievedEntity;
}
return undefined;
}, [apiHolder, compoundEntityRef]);
}, [apiHolder, entityRef]);
const handleOnMouseLeave = () => {
setIsHovered(false);
@@ -150,61 +141,58 @@ export const EntityPeekAheadPopover = (props: EntityPeekAheadPopoverProps) => {
}}
onMouseLeave={handleOnMouseLeave}
>
<>
{error && <ResponseErrorPanel error={error} />}
<Card>
<Card>
<CardContent>
{error && <ResponseErrorPanel error={error} />}
{loading && <Progress />}
<CardContent>
{entity && (
<>
<Typography color="textSecondary">
{compoundEntityRef.namespace}
</Typography>
<Typography variant="h5" component="div">
{compoundEntityRef.name}
</Typography>
<Typography color="textSecondary">{entity.kind}</Typography>
{entity && (
<>
<Typography color="textSecondary">
{entity.metadata.namespace}
</Typography>
<Typography variant="h5" component="div">
{entity.metadata.name}
</Typography>
<Typography color="textSecondary" gutterBottom>
{entity.kind}
</Typography>
{entity.metadata.description && (
<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>
<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>
</>
)}
</Card>
</>
</CardContent>
{!error && entity && (
<CardActions>
<>
{isUserEntity(entity) && <UserCardActions entity={entity} />}
{isGroupEntity(entity) && (
<GroupCardActions entity={entity} />
)}
<EntityCardActions entity={entity} />
</>
</CardActions>
)}
</Card>
</HoverPopover>
)}
</>