minor updates to the entity peek-ahead
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
+4
-4
@@ -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>
|
||||
|
||||
+4
-7
@@ -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>
|
||||
|
||||
+4
-8
@@ -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;
|
||||
};
|
||||
|
||||
+4
-8
@@ -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;
|
||||
};
|
||||
|
||||
+1
@@ -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';
|
||||
|
||||
+4
-16
@@ -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',
|
||||
|
||||
+8
-13
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+52
-64
@@ -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);
|
||||
@@ -146,61 +137,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>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user