Merge pull request #26631 from backstage/empty-states/descriptor-format-external-link
[UX / UI ] Empty states including learn link to descriptor format to include external link icon
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-api-docs': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Empty states updated with external link icon for learn more links
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/app-defaults': patch
|
||||
---
|
||||
|
||||
Added `externalLink` to icon defaults
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
`Link` component now accepts `externalLinkIcon` prop
|
||||
@@ -38,6 +38,7 @@ import MuiStorageIcon from '@material-ui/icons/Storage';
|
||||
import MuiFeaturedPlayListIcon from '@material-ui/icons/FeaturedPlayList';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import StarBorder from '@material-ui/icons/StarBorder';
|
||||
import OpenInNew from '@material-ui/icons/OpenInNew';
|
||||
|
||||
export const icons = {
|
||||
brokenImage: MuiBrokenImageIcon as IconComponent,
|
||||
@@ -66,4 +67,5 @@ export const icons = {
|
||||
warning: MuiWarningIcon as IconComponent,
|
||||
star: Star as IconComponent,
|
||||
unstarred: StarBorder as IconComponent,
|
||||
externalLink: OpenInNew as IconComponent,
|
||||
};
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('BackwardsCompatProvider', () => {
|
||||
expect(screen.getByTestId('ctx').textContent).toMatchInlineSnapshot(`
|
||||
"plugins: test, app
|
||||
components: NotFoundErrorPage, BootErrorPage, Progress, Router, ErrorBoundaryFallback
|
||||
icons: brokenImage, catalog, scaffolder, techdocs, search, chat, dashboard, docs, email, github, group, help, kind:api, kind:component, kind:domain, kind:group, kind:location, kind:system, kind:user, kind:resource, kind:template, user, warning, star, unstarred"
|
||||
icons: brokenImage, catalog, scaffolder, techdocs, search, chat, dashboard, docs, email, github, group, help, kind:api, kind:component, kind:domain, kind:group, kind:location, kind:system, kind:user, kind:resource, kind:template, user, warning, star, unstarred, externalLink"
|
||||
`);
|
||||
});
|
||||
|
||||
|
||||
@@ -706,6 +706,7 @@ export type LinkProps = Omit<LinkProps_2, 'to'> &
|
||||
to: string;
|
||||
component?: ElementType<any>;
|
||||
noTrack?: boolean;
|
||||
externalLinkIcon?: boolean;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LoginRequestListItemClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
||||
@@ -45,6 +45,30 @@ describe('<Link />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render external link icon if externalLinkIcon prop is not passed', async () => {
|
||||
const { container } = await renderInTestApp(
|
||||
<Link to="http://something.external">External Link</Link>,
|
||||
);
|
||||
const externalLink = screen.getByRole('link', {
|
||||
name: 'External Link , Opens in a new window',
|
||||
});
|
||||
const externalLinkIcon = container.querySelector('svg');
|
||||
expect(externalLink).not.toContainElement(externalLinkIcon);
|
||||
});
|
||||
|
||||
it('renders external link icon if externalLinkIcon prop is passed', async () => {
|
||||
const { container } = await renderInTestApp(
|
||||
<Link to="http://something.external" externalLinkIcon>
|
||||
External Link
|
||||
</Link>,
|
||||
);
|
||||
const externalLink = screen.getByRole('link', {
|
||||
name: 'External Link , Opens in a new window',
|
||||
});
|
||||
const externalLinkIcon = container.querySelector('svg');
|
||||
expect(externalLink).toContainElement(externalLinkIcon);
|
||||
});
|
||||
|
||||
it('captures click using analytics api', async () => {
|
||||
const linkText = 'Navigate!';
|
||||
const analyticsApi = new MockAnalyticsApi();
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
LinkProps as RouterLinkProps,
|
||||
Route,
|
||||
} from 'react-router-dom';
|
||||
import OpenInNew from '@material-ui/icons/OpenInNew';
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
|
||||
export function isReactRouterBeta(): boolean {
|
||||
const [obj] = createRoutesFromChildren(<Route index element={<div />} />);
|
||||
@@ -39,7 +41,7 @@ export function isReactRouterBeta(): boolean {
|
||||
export type LinkClassKey = 'visuallyHidden' | 'externalLink';
|
||||
|
||||
const useStyles = makeStyles(
|
||||
{
|
||||
theme => ({
|
||||
visuallyHidden: {
|
||||
clip: 'rect(0 0 0 0)',
|
||||
clipPath: 'inset(50%)',
|
||||
@@ -53,10 +55,21 @@ const useStyles = makeStyles(
|
||||
externalLink: {
|
||||
position: 'relative',
|
||||
},
|
||||
},
|
||||
externalLinkIcon: {
|
||||
verticalAlign: 'bottom',
|
||||
marginLeft: theme.spacing(0.5),
|
||||
},
|
||||
}),
|
||||
{ name: 'Link' },
|
||||
);
|
||||
|
||||
const ExternalLinkIcon = () => {
|
||||
const app = useApp();
|
||||
const Icon = app.getSystemIcon('externalLink') || OpenInNew;
|
||||
const classes = useStyles();
|
||||
return <Icon className={classes.externalLinkIcon} />;
|
||||
};
|
||||
|
||||
export const isExternalUri = (uri: string) => /^([a-z+.-]+):/.test(uri);
|
||||
|
||||
// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js
|
||||
@@ -90,6 +103,7 @@ export type LinkProps = Omit<MaterialLinkProps, 'to'> &
|
||||
to: string;
|
||||
component?: ElementType<any>;
|
||||
noTrack?: boolean;
|
||||
externalLinkIcon?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -161,7 +175,7 @@ const getNodeText = (node: React.ReactNode): string => {
|
||||
* - Captures Link clicks as analytics events.
|
||||
*/
|
||||
export const Link = React.forwardRef<any, LinkProps>(
|
||||
({ onClick, noTrack, ...props }, ref) => {
|
||||
({ onClick, noTrack, externalLinkIcon, ...props }, ref) => {
|
||||
const classes = useStyles();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
@@ -201,6 +215,7 @@ export const Link = React.forwardRef<any, LinkProps>(
|
||||
className={classnames(classes.externalLink, props.className)}
|
||||
>
|
||||
{props.children}
|
||||
{externalLinkIcon && <ExternalLinkIcon />}
|
||||
<Typography component="span" className={classes.visuallyHidden}>
|
||||
, Opens in a new window
|
||||
</Typography>
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('<ConsumedApisCard />', () => {
|
||||
relations: [],
|
||||
};
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
const { getByText, getByRole, container } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityProvider entity={entity}>
|
||||
<ConsumedApisCard />
|
||||
@@ -77,6 +77,17 @@ describe('<ConsumedApisCard />', () => {
|
||||
|
||||
expect(getByText(/Consumed APIs/i)).toBeInTheDocument();
|
||||
expect(getByText(/does not consume any APIs/i)).toBeInTheDocument();
|
||||
|
||||
// Also render external link icon
|
||||
const externalLink = getByRole('link');
|
||||
expect(externalLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://backstage.io/docs/features/software-catalog/descriptor-format#specconsumesapis-optional',
|
||||
);
|
||||
const externalLinkIcon: HTMLElement | null = container.querySelector(
|
||||
'svg[class*="externalLink"]',
|
||||
);
|
||||
expect(externalLink).toContainElement(externalLinkIcon);
|
||||
});
|
||||
|
||||
it('shows consumed APIs', async () => {
|
||||
|
||||
@@ -85,8 +85,11 @@ export const ConsumedApisCard = (props: {
|
||||
APIs.
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#specconsumesapis-optional">
|
||||
Learn how to change this.
|
||||
<Link
|
||||
to="https://backstage.io/docs/features/software-catalog/descriptor-format#specconsumesapis-optional"
|
||||
externalLinkIcon
|
||||
>
|
||||
Learn how to change this
|
||||
</Link>
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('<ProvidedApisCard />', () => {
|
||||
relations: [],
|
||||
};
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
const { getByText, getByRole, container } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityProvider entity={entity}>
|
||||
<ProvidedApisCard />
|
||||
@@ -77,6 +77,18 @@ describe('<ProvidedApisCard />', () => {
|
||||
|
||||
expect(getByText(/Provided APIs/i)).toBeInTheDocument();
|
||||
expect(getByText(/does not provide any APIs/i)).toBeInTheDocument();
|
||||
expect(getByText(/Learn how to change this/)).toBeInTheDocument();
|
||||
|
||||
// Also render external link icon
|
||||
const externalLink = getByRole('link');
|
||||
expect(externalLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://backstage.io/docs/features/software-catalog/descriptor-format#specprovidesapis-optional',
|
||||
);
|
||||
const externalLinkIcon: HTMLElement | null = container.querySelector(
|
||||
'svg[class*="externalLink"]',
|
||||
);
|
||||
expect(externalLink).toContainElement(externalLinkIcon);
|
||||
});
|
||||
|
||||
it('shows consumed APIs', async () => {
|
||||
|
||||
@@ -84,11 +84,12 @@ export const ProvidedApisCard = (props: {
|
||||
This {entity.kind.toLocaleLowerCase('en-US')} does not provide any
|
||||
APIs.
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#specprovidesapis-optional">
|
||||
Learn how to change this.
|
||||
</Link>
|
||||
</Typography>
|
||||
<Link
|
||||
to="https://backstage.io/docs/features/software-catalog/descriptor-format#specprovidesapis-optional"
|
||||
externalLinkIcon
|
||||
>
|
||||
Learn how to change this
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
columns={columns}
|
||||
|
||||
@@ -113,7 +113,7 @@ export const catalogTranslationRef: TranslationRef<
|
||||
readonly 'hasSubdomainsCard.emptyMessage': 'No subdomain is part of this domain';
|
||||
readonly 'hasSystemsCard.title': 'Has systems';
|
||||
readonly 'hasSystemsCard.emptyMessage': 'No system is part of this domain';
|
||||
readonly 'relatedEntitiesCard.emptyHelpLinkTitle': 'Learn how to change this.';
|
||||
readonly 'relatedEntitiesCard.emptyHelpLinkTitle': 'Learn how to change this';
|
||||
readonly 'systemDiagramCard.title': 'System Diagram';
|
||||
readonly 'systemDiagramCard.description': 'Use pinch & zoo to move around the diagram.';
|
||||
readonly 'systemDiagramCard.edgeLabels.dependsOn': 'depends on';
|
||||
|
||||
@@ -152,7 +152,7 @@ export const catalogTranslationRef = createTranslationRef({
|
||||
emptyMessage: 'No system is part of this domain',
|
||||
},
|
||||
relatedEntitiesCard: {
|
||||
emptyHelpLinkTitle: 'Learn how to change this.',
|
||||
emptyHelpLinkTitle: 'Learn how to change this',
|
||||
},
|
||||
systemDiagramCard: {
|
||||
title: 'System Diagram',
|
||||
|
||||
@@ -84,7 +84,6 @@ export const RelatedEntitiesCard = <T extends Entity>(
|
||||
asRenderableEntities,
|
||||
tableOptions = {},
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
const { entity } = useEntity();
|
||||
const { entities, loading, error } = useRelatedEntities(entity, {
|
||||
@@ -116,7 +115,7 @@ export const RelatedEntitiesCard = <T extends Entity>(
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Typography variant="body1">{emptyMessage}</Typography>
|
||||
<Typography variant="body2">
|
||||
<Link to={emptyHelpLink}>
|
||||
<Link to={emptyHelpLink} externalLinkIcon>
|
||||
{t('relatedEntitiesCard.emptyHelpLinkTitle')}
|
||||
</Link>
|
||||
</Typography>
|
||||
|
||||
Reference in New Issue
Block a user