remove entity ref link popover

This also:
- adds stories for the entity ref links componet
- and moves some of the components into another file

Signed-off-by: Brian Fletcher <brian@roadie.io>
This commit is contained in:
Brian Fletcher
2022-12-15 09:06:12 +00:00
parent 8efe254e03
commit 0f9af920f4
10 changed files with 275 additions and 182 deletions
@@ -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 { Button, Tooltip } from '@material-ui/core';
import EmailIcon from '@material-ui/icons/Email';
import React from 'react';
/**
* Email Card action link
*
* @private
*/
export const EmailCardAction = ({ email }: { email: string }) => {
return (
<Tooltip title={`Email ${email}`}>
<Button target="_blank" href={`mailto:${email}`} size="small">
<EmailIcon color="action" />
</Button>
</Tooltip>
);
};
@@ -0,0 +1,48 @@
/*
* 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 { Tooltip } 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 (
<>
<Tooltip title="Show details">
<Link
component="button"
to={entityRoute({
name: entity.metadata.name,
namespace: entity.metadata.namespace || 'default',
kind: entity.kind.toLocaleLowerCase('en-US'),
})}
>
<InfoIcon color="action" />
</Link>
</Tooltip>
</>
);
};
@@ -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} />
)}
</>
);
};
@@ -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} />
)}
</>
);
};
@@ -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';
@@ -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 { Card, CardContent } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
/**
* Entity not found card
*
* @private
*/
export const EntityNotFoundCard = ({
entityRef,
error,
}: {
entityRef: string;
error?: Error;
}) => {
return (
<Card>
<CardContent>
<Alert severity="warning">
{entityRef} was not found {error?.message}
</Alert>
</CardContent>
</Card>
);
};
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { entityRouteRef } from '../../routes';
import useAsyncFn from 'react-use/lib/useAsyncFn';
import { catalogApiRef } from '../../api';
import React, { PropsWithChildren, useEffect } from 'react';
@@ -25,7 +24,6 @@ import {
} from 'material-ui-popup-state/hooks';
import {
Box,
Button,
Card,
CardActions,
CardContent,
@@ -34,19 +32,19 @@ import {
Tooltip,
Typography,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import EmailIcon from '@material-ui/icons/Email';
import InfoIcon from '@material-ui/icons/Info';
import { useApiHolder, useRouteRef } from '@backstage/core-plugin-api';
import { useApiHolder } from '@backstage/core-plugin-api';
import {
isUserEntity,
isGroupEntity,
UserEntity,
GroupEntity,
Entity,
isUserEntity,
parseEntityRef,
} from '@backstage/catalog-model';
import { Link, Progress } from '@backstage/core-components';
import { Progress } from '@backstage/core-components';
import {
EntityCardActions,
UserCardActions,
GroupCardActions,
} from './CardActionComponents';
import { EntityNotFoundCard } from './EntityNotFoundCard';
/**
* Properties for an entity popover on hover of a component.
@@ -77,80 +75,6 @@ const useStyles = makeStyles(() => {
const maxTagChips = 4;
const EmailCardAction = ({ email }: { email: string }) => {
return (
<Tooltip title={`Email ${email}`}>
<Button target="_blank" href={`mailto:${email}`} size="small">
<EmailIcon color="action" />
</Button>
</Tooltip>
);
};
const UserCardActions = ({ entity }: { entity: UserEntity }) => {
return (
<>
{entity.spec.profile?.email && (
<EmailCardAction email={entity.spec.profile.email} />
)}
</>
);
};
const GroupCardActions = ({ entity }: { entity: GroupEntity }) => {
return (
<>
{entity.spec.profile?.email && (
<EmailCardAction email={entity.spec.profile.email} />
)}
</>
);
};
/**
* Shows an entity popover on hover of a component.
*
* @public
*/
const EntityCardActions = ({ entity }: { entity: Entity }) => {
const entityRoute = useRouteRef(entityRouteRef);
return (
<>
<Tooltip title="Show details">
<Link
component="button"
to={entityRoute({
name: entity.metadata.name,
namespace: entity.metadata.namespace || 'default',
kind: entity.kind.toLocaleLowerCase('en-US'),
})}
>
<InfoIcon color="action" />
</Link>
</Tooltip>
</>
);
};
const EntityNotFoundCard = ({
entityRef,
error,
}: {
entityRef: string;
error?: Error;
}) => {
return (
<Card>
<CardContent>
<Alert severity="warning">
{entityRef} was not found {error?.message}
</Alert>
</CardContent>
</Card>
);
};
/**
* Shows an entity popover on hover of a component.
*
@@ -16,64 +16,8 @@
import React, { ComponentType } from 'react';
import { EntityRefLink, EntityRefLinkProps } from './EntityRefLink';
import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
import { catalogApiRef } from '../../api';
import { CompoundEntityRef } from '@backstage/catalog-model';
import { wrapInTestApp } from '@backstage/test-utils';
import { entityRouteRef } from '../../routes';
import { CatalogApi } from '@backstage/catalog-client';
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',
@@ -83,20 +27,11 @@ export default {
title: 'Catalog /EntityRefLink',
decorators: [
(Story: ComponentType<{}>) =>
wrapInTestApp(
<>
<TestApiProvider
apis={[[catalogApiRef, mockCatalogApi as any as CatalogApi]]}
>
<Story />
</TestApiProvider>
</>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
wrapInTestApp(<Story />, {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
),
}),
],
};
@@ -104,22 +39,3 @@ export const Default = (args: EntityRefLinkProps) => (
<EntityRefLink {...args} />
);
Default.args = defaultArgs;
export const User = (args: EntityRefLinkProps) => <EntityRefLink {...args} />;
User.args = {
entityRef: 'user:default/fname.lname',
};
export const NotFound = (args: EntityRefLinkProps) => (
<EntityRefLink {...args} />
);
NotFound.args = {
entityRef: 'user:default/doesnt.exist',
};
export const SlowCatalogItem = (args: EntityRefLinkProps) => (
<EntityRefLink {...args} />
);
SlowCatalogItem.args = {
entityRef: 'component:default/slow.catalog.item',
};
@@ -25,7 +25,8 @@ import { entityRouteRef } from '../../routes';
import { humanizeEntityRef } from './humanize';
import { Link, LinkProps } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import { EntityPeekAheadPopover } from '../EntityPeekAheadPopover';
import { Tooltip } from '@material-ui/core';
/**
* Props for {@link EntityRefLink}.
*
@@ -76,13 +77,17 @@ export const EntityRefLink = forwardRef<any, EntityRefLinkProps>(
{ defaultKind },
);
return (
<EntityPeekAheadPopover entityRef={`${kind}:${namespace}/${name}`}>
<Link {...linkProps} ref={ref} to={entityRoute(routeParams)}>
{children}
{!children && (title ?? formattedEntityRefTitle)}
</Link>
</EntityPeekAheadPopover>
const link = (
<Link {...linkProps} ref={ref} to={entityRoute(routeParams)}>
{children}
{!children && (title ?? formattedEntityRefTitle)}
</Link>
);
return title ? (
<Tooltip title={formattedEntityRefTitle}>{link}</Tooltip>
) : (
link
);
},
) as (props: EntityRefLinkProps) => JSX.Element;
@@ -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;