Merge pull request #20932 from mario-mui/feat/core-components-i18n

add i18n for core components
This commit is contained in:
Patrik Oldsberg
2024-02-27 17:07:47 +01:00
committed by GitHub
40 changed files with 552 additions and 243 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Support i18n for core components
@@ -0,0 +1,59 @@
## API Report File for "@backstage/core-components"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
// @alpha (undocumented)
export const coreComponentsTranslationRef: TranslationRef<
'core-components',
{
readonly 'link.openNewWindow': 'Opens in a new window';
readonly 'table.filter.title': 'Filters';
readonly 'table.filter.clearAll': 'Clear all';
readonly 'signIn.title': 'Sign In';
readonly 'signIn.loginFailed': 'Login failed';
readonly 'signIn.customProvider.title': 'Custom User';
readonly 'signIn.customProvider.subtitle': 'Enter your own User ID and credentials.\n This selection will not be stored.';
readonly 'signIn.customProvider.userId': 'User ID';
readonly 'signIn.customProvider.tokenInvalid': 'Token is not a valid OpenID Connect JWT Token';
readonly 'signIn.customProvider.continue': 'Continue';
readonly 'signIn.customProvider.idToken': 'ID Token (optional)';
readonly 'signIn.guestProvider.title': 'Guest';
readonly 'signIn.guestProvider.enter': 'Enter';
readonly 'signIn.guestProvider.subtitle': 'Enter as a Guest User.\n You will not have a verified identity, meaning some features might be unavailable.';
readonly skipToContent: 'Skip to content';
readonly 'copyTextButton.tooltipText': 'Text copied to clipboard';
readonly 'simpleStepper.finish': 'Finish';
readonly 'simpleStepper.reset': 'Reset';
readonly 'simpleStepper.next': 'Next';
readonly 'simpleStepper.skip': 'Skip';
readonly 'simpleStepper.back': 'Back';
readonly 'errorPage.title': 'Looks like someone dropped the mic!';
readonly 'errorPage.subtitle': 'ERROR {{status}}: {{statusMessage}}';
readonly 'errorPage.goBack': 'Go back';
readonly 'errorPage.showMoreDetails': 'Show more details';
readonly 'errorPage.showLessDetails': 'Show less details';
readonly 'emptyState.missingAnnotation.title': 'Missing Annotation';
readonly 'emptyState.missingAnnotation.actionTitle': 'Add the annotation to your component YAML as shown in the highlighted example below:';
readonly 'emptyState.missingAnnotation.readMore': 'Read more';
readonly 'supportConfig.default.title': 'Support Not Configured';
readonly 'supportConfig.default.linkTitle': 'Add `app.support` config key';
readonly 'errorBoundary.title': 'Please contact {{slackChannel}} for help.';
readonly 'oauthRequestDialog.title': 'Login Required';
readonly 'oauthRequestDialog.authRedirectTitle': 'This will trigger a http redirect to OAuth Login.';
readonly 'oauthRequestDialog.login': 'Log in';
readonly 'oauthRequestDialog.rejectAll': 'Reject All';
readonly 'supportButton.title': 'Support';
readonly 'supportButton.close': 'Close';
readonly 'alertDisplay.message_one': '({{ count }} older message)';
readonly 'alertDisplay.message_other': '({{ count }} older messages)';
readonly 'autoLogout.stillTherePrompt.title': 'Logging out due to inactivity';
readonly 'autoLogout.stillTherePrompt.buttonText': "Yes! Don't log me out";
readonly 'proxiedSignInPage.title': 'You do not appear to be signed in. Please try reloading the browser page.';
}
>;
// (No @packageDocumentation comment for this package)
```
+4
View File
@@ -22,11 +22,15 @@
"types": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./alpha": "./src/alpha.ts",
"./testUtils": "./src/testUtils.ts",
"./package.json": "./package.json"
},
"typesVersions": {
"*": {
"alpha": [
"src/alpha.ts"
],
"testUtils": [
"src/testUtils.ts"
],
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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 './translation';
@@ -14,13 +14,14 @@
* limitations under the License.
*/
import { alertApiRef, AlertMessage, useApi } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import IconButton from '@material-ui/core/IconButton';
import Snackbar from '@material-ui/core/Snackbar';
import Typography from '@material-ui/core/Typography';
import CloseIcon from '@material-ui/icons/Close';
import { Alert } from '@material-ui/lab';
import pluralize from 'pluralize';
import React, { useEffect, useState } from 'react';
import { coreComponentsTranslationRef } from '../../translation';
/**
* Properties for {@link AlertDisplay}
@@ -63,6 +64,7 @@ export type AlertDisplayProps = {
export function AlertDisplay(props: AlertDisplayProps) {
const [messages, setMessages] = useState<Array<AlertMessage>>([]);
const alertApi = useApi(alertApiRef);
const { t } = useTranslationRef(coreComponentsTranslationRef);
const {
anchorOrigin = { vertical: 'top', horizontal: 'center' },
@@ -121,10 +123,11 @@ export function AlertDisplay(props: AlertDisplayProps) {
<Typography component="span">
{String(firstMessage.message)}
{messages.length > 1 && (
<em>{` (${messages.length - 1} older ${pluralize(
'message',
messages.length - 1,
)})`}</em>
<em>
{t('alertDisplay.message', {
count: messages.length - 1,
})}
</em>
)}
</Typography>
</Alert>
@@ -18,16 +18,18 @@ import { createMocks } from 'react-idle-timer';
import { MessageChannel } from 'worker_threads';
import { ApiProvider } from '@backstage/core-app-api';
import { identityApiRef } from '@backstage/core-plugin-api';
import { TestApiRegistry } from '@backstage/test-utils';
import { TestApiRegistry, renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { AutoLogout } from './AutoLogout';
import { cleanup, render } from '@testing-library/react';
import { cleanup } from '@testing-library/react';
// Mock the signOut function of identityApiRef
const mockSignOut = jest.fn();
const mockIdentityApi = { signOut: mockSignOut };
const mockIdentityApi = {
signOut: mockSignOut,
getCredentials: jest.fn().mockReturnValue({ token: 'xxx' }),
};
const apis = TestApiRegistry.from([identityApiRef, mockIdentityApi]);
describe('AutoLogout', () => {
@@ -44,27 +46,29 @@ describe('AutoLogout', () => {
});
it('should throw error if idleTimeoutMinutes is smaller than promptBeforeSeconds', async () => {
expect(() =>
render(
<ApiProvider apis={apis}>
<AutoLogout
enabled
idleTimeoutMinutes={0.5}
promptBeforeIdleSeconds={120}
/>
</ApiProvider>,
),
).toThrow();
await expect(
async () =>
await renderInTestApp(
<ApiProvider apis={apis}>
<AutoLogout
enabled
idleTimeoutMinutes={0.5}
promptBeforeIdleSeconds={120}
/>
</ApiProvider>,
),
).rejects.toThrow();
});
it('should throw error if idleTimeoutMinutes is smaller than 30 seconds', async () => {
expect(() =>
render(
<ApiProvider apis={apis}>
<AutoLogout enabled idleTimeoutMinutes={0.49} />
<div>Test Child</div>
</ApiProvider>,
),
).toThrow();
await expect(
async () =>
await renderInTestApp(
<ApiProvider apis={apis}>
<AutoLogout enabled idleTimeoutMinutes={0.49} />
<div>Test Child</div>
</ApiProvider>,
),
).rejects.toThrow();
});
});
@@ -22,6 +22,8 @@ import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import React, { useEffect } from 'react';
import { IIdleTimer } from 'react-idle-timer';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
export interface StillTherePromptProps {
idleTimer: IIdleTimer;
@@ -41,6 +43,7 @@ export const StillTherePrompt = (props: StillTherePromptProps) => {
remainingTime,
setRemainingTime,
} = props;
const { t } = useTranslationRef(coreComponentsTranslationRef);
useEffect(() => {
const interval = setInterval(() => {
@@ -65,7 +68,7 @@ export const StillTherePrompt = (props: StillTherePromptProps) => {
return (
<Dialog open={open} data-testid="inactivity-prompt-dialog">
<DialogTitle>Logging out due to inactivity</DialogTitle>
<DialogTitle>{t('autoLogout.stillTherePrompt.title')}</DialogTitle>
<DialogContent>
<DialogContentText>
You are about to be disconnected in{' '}
@@ -82,7 +85,7 @@ export const StillTherePrompt = (props: StillTherePromptProps) => {
variant="contained"
size="small"
>
Yes! Don't log me out
{t('autoLogout.stillTherePrompt.buttonText')}
</Button>
</DialogActions>
</Dialog>
@@ -20,6 +20,8 @@ import Tooltip from '@material-ui/core/Tooltip';
import CopyIcon from '@material-ui/icons/FileCopy';
import React, { MouseEventHandler, useEffect, useState } from 'react';
import useCopyToClipboard from 'react-use/lib/useCopyToClipboard';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/**
* Properties for {@link CopyTextButton}
@@ -78,10 +80,11 @@ export interface CopyTextButtonProps {
* ```
*/
export function CopyTextButton(props: CopyTextButtonProps) {
const { t } = useTranslationRef(coreComponentsTranslationRef);
const {
text,
tooltipDelay = 1000,
tooltipText = 'Text copied to clipboard',
tooltipText = t('copyTextButton.tooltipText'),
'aria-label': ariaLabel = 'Copy text',
} = props;
const errorApi = useApi(errorApiRef);
@@ -23,6 +23,8 @@ import React from 'react';
import { CodeSnippet } from '../CodeSnippet';
import { Link } from '../Link';
import { EmptyState } from './EmptyState';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { coreComponentsTranslationRef } from '../../translation';
const COMPONENT_YAML_TEMPLATE = `apiVersion: backstage.io/v1alpha1
kind: Component
@@ -76,7 +78,7 @@ function generateComponentYaml(annotations: string[]) {
return COMPONENT_YAML_TEMPLATE.replace(ANNOTATION_YAML, annotationYaml);
}
function generateDescription(annotations: string[]) {
function useGenerateDescription(annotations: string[]) {
const isSingular = annotations.length <= 1;
return (
<>
@@ -106,17 +108,17 @@ export function MissingAnnotationEmptyState(props: Props) {
readMoreUrl ||
'https://backstage.io/docs/features/software-catalog/well-known-annotations';
const classes = useStyles();
const { t } = useTranslationRef(coreComponentsTranslationRef);
return (
<EmptyState
missing="field"
title="Missing Annotation"
description={generateDescription(annotations)}
title={t('emptyState.missingAnnotation.title')}
description={useGenerateDescription(annotations)}
action={
<>
<Typography variant="body1">
Add the annotation to your component YAML as shown in the
highlighted example below:
{t('emptyState.missingAnnotation.actionTitle')}
</Typography>
<Box className={classes.code}>
<CodeSnippet
@@ -128,7 +130,7 @@ export function MissingAnnotationEmptyState(props: Props) {
/>
</Box>
<Button color="primary" component={Link} to={url}>
Read more
{t('emptyState.missingAnnotation.readMore')}
</Button>
</>
}
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { configApiRef, useAnalytics, useApi } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
// eslint-disable-next-line no-restricted-imports
import MaterialLink, {
LinkProps as MaterialLinkProps,
@@ -29,6 +30,7 @@ import {
LinkProps as RouterLinkProps,
Route,
} from 'react-router-dom';
import { coreComponentsTranslationRef } from '../../translation';
export function isReactRouterBeta(): boolean {
const [obj] = createRoutesFromChildren(<Route index element={<div />} />);
@@ -161,6 +163,7 @@ export const Link = React.forwardRef<any, LinkProps>(
({ onClick, noTrack, ...props }, ref) => {
const classes = useStyles();
const analytics = useAnalytics();
const { t } = useTranslationRef(coreComponentsTranslationRef);
// Adding the base path to URLs breaks react-router v6 stable, so we only
// do it for beta. The react router version won't change at runtime so it is
@@ -199,7 +202,7 @@ export const Link = React.forwardRef<any, LinkProps>(
>
{props.children}
<Typography component="span" className={classes.visuallyHidden}>
, Opens in a new window
{`, ${t('link.openNewWindow')}`}
</Typography>
</MaterialLink>
) : (
@@ -23,6 +23,8 @@ import Button from '@material-ui/core/Button';
import React, { useState } from 'react';
import { isError } from '@backstage/errors';
import { PendingOAuthRequest } from '@backstage/core-plugin-api';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
export type LoginRequestListItemClassKey = 'root';
@@ -44,6 +46,7 @@ type RowProps = {
const LoginRequestListItem = ({ request, busy, setBusy }: RowProps) => {
const classes = useItemStyles();
const [error, setError] = useState<string>();
const { t } = useTranslationRef(coreComponentsTranslationRef);
const handleContinue = async () => {
setBusy(true);
@@ -68,7 +71,7 @@ const LoginRequestListItem = ({ request, busy, setBusy }: RowProps) => {
secondary={error && <Typography color="error">{error}</Typography>}
/>
<Button color="primary" variant="contained" onClick={handleContinue}>
Log in
{t('oauthRequestDialog.login')}
</Button>
</ListItem>
);
@@ -30,6 +30,8 @@ import {
oauthRequestApiRef,
} from '@backstage/core-plugin-api';
import Typography from '@material-ui/core/Typography';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
export type OAuthRequestDialogClassKey =
| 'dialog'
@@ -63,6 +65,7 @@ export function OAuthRequestDialog(_props: {}) {
const [busy, setBusy] = useState(false);
const oauthRequestApi = useApi(oauthRequestApiRef);
const configApi = useApi(configApiRef);
const { t } = useTranslationRef(coreComponentsTranslationRef);
const authRedirect =
configApi.getOptionalBoolean('enableExperimentalRedirectFlow') ?? false;
@@ -94,12 +97,10 @@ export function OAuthRequestDialog(_props: {}) {
variant="h1"
variantMapping={{ h1: 'span' }}
>
Login Required
{t('oauthRequestDialog.title')}
</Typography>
{authRedirect ? (
<Typography>
This will trigger a http redirect to OAuth Login.
</Typography>
<Typography>{t('oauthRequestDialog.authRedirectTitle')}</Typography>
) : null}
</DialogTitle>
@@ -118,7 +119,9 @@ export function OAuthRequestDialog(_props: {}) {
</main>
<DialogActions classes={{ root: classes.actionButtons }}>
<Button onClick={handleRejectAll}>Reject All</Button>
<Button onClick={handleRejectAll}>
{t('oauthRequestDialog.rejectAll')}
</Button>
</DialogActions>
</Dialog>
);
@@ -20,6 +20,8 @@ import React, { PropsWithChildren, ReactNode, useContext } from 'react';
import { VerticalStepperContext } from './SimpleStepper';
import { StepActions } from './types';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
export type SimpleStepperFooterClassKey = 'root';
@@ -55,9 +57,12 @@ interface BackBtnProps extends CommonBtnProps {
disabled?: boolean;
stepIndex: number;
}
export const RestartBtn = ({ text, handleClick }: RestartBtnProps) => (
<Button onClick={handleClick}>{text || 'Reset'}</Button>
);
export const RestartBtn = ({ text, handleClick }: RestartBtnProps) => {
const { t } = useTranslationRef(coreComponentsTranslationRef);
return (
<Button onClick={handleClick}>{text || t('simpleStepper.reset')}</Button>
);
};
const NextBtn = ({
text,
@@ -65,39 +70,48 @@ const NextBtn = ({
disabled,
last,
stepIndex,
}: NextBtnProps) => (
<Button
variant="contained"
color="primary"
disabled={disabled}
data-testid={`nextButton-${stepIndex}`}
onClick={handleClick}
>
{text || (last ? 'Finish' : 'Next')}
</Button>
);
}: NextBtnProps) => {
const { t } = useTranslationRef(coreComponentsTranslationRef);
return (
<Button
variant="contained"
color="primary"
disabled={disabled}
data-testid={`nextButton-${stepIndex}`}
onClick={handleClick}
>
{text || (last ? t('simpleStepper.finish') : t('simpleStepper.next'))}
</Button>
);
};
const SkipBtn = ({ text, handleClick, disabled, stepIndex }: SkipBtnProps) => (
<Button
variant="outlined"
color="primary"
disabled={disabled}
data-testid={`skipButton-${stepIndex}`}
onClick={handleClick}
>
{text || 'Skip'}
</Button>
);
const SkipBtn = ({ text, handleClick, disabled, stepIndex }: SkipBtnProps) => {
const { t } = useTranslationRef(coreComponentsTranslationRef);
return (
<Button
variant="outlined"
color="primary"
disabled={disabled}
data-testid={`skipButton-${stepIndex}`}
onClick={handleClick}
>
{text || t('simpleStepper.skip')}
</Button>
);
};
const BackBtn = ({ text, handleClick, disabled, stepIndex }: BackBtnProps) => (
<Button
onClick={handleClick}
data-testid={`backButton-${stepIndex}`}
disabled={disabled}
>
{text || 'Back'}
</Button>
);
const BackBtn = ({ text, handleClick, disabled, stepIndex }: BackBtnProps) => {
const { t } = useTranslationRef(coreComponentsTranslationRef);
return (
<Button
onClick={handleClick}
data-testid={`backButton-${stepIndex}`}
disabled={disabled}
>
{text || t('simpleStepper.back')}
</Button>
);
};
export type SimpleStepperFooterProps = {
actions?: StepActions;
@@ -31,6 +31,8 @@ import React, { MouseEventHandler, useState } from 'react';
import { SupportItem, SupportItemLink, useSupportConfig } from '../../hooks';
import { HelpIcon } from '../../icons';
import { Link } from '../Link';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
type SupportButtonProps = {
title?: string;
@@ -85,6 +87,7 @@ const SupportListItem = ({ item }: { item: SupportItem }) => {
};
export function SupportButton(props: SupportButtonProps) {
const { t } = useTranslationRef(coreComponentsTranslationRef);
const { title, items, children } = props;
const { items: configItems } = useSupportConfig();
@@ -125,7 +128,7 @@ export function SupportButton(props: SupportButtonProps) {
onClick={onClickHandler}
startIcon={<HelpIcon />}
>
Support
{t('supportButton.title')}
</Button>
)}
</Box>
@@ -171,7 +174,7 @@ export function SupportButton(props: SupportButtonProps) {
onClick={popoverCloseHandler}
aria-label="Close"
>
Close
{t('supportButton.close')}
</Button>
</DialogActions>
</Popover>
@@ -21,6 +21,8 @@ import React, { useEffect, useState } from 'react';
import { Select } from '../Select';
import { SelectProps } from '../Select/Select';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
export type TableFiltersClassKey = 'root' | 'value' | 'heder' | 'filters';
@@ -76,6 +78,7 @@ export const Filters = (props: Props) => {
const classes = useFilterStyles();
const { onChangeFilters } = props;
const { t } = useTranslationRef(coreComponentsTranslationRef);
const [selectedFilters, setSelectedFilters] = useState<SelectedFilters>({
...props.selectedFilters,
@@ -96,9 +99,9 @@ export const Filters = (props: Props) => {
return (
<Box className={classes.root}>
<Box className={classes.header}>
<Box className={classes.value}>Filters</Box>
<Box className={classes.value}>{t('table.filter.title')}</Box>
<Button color="primary" onClick={handleClick}>
Clear all
{t('table.filter.clearAll')}
</Button>
</Box>
<Box className={classes.filters}>
@@ -15,6 +15,8 @@
*/
import { useApiHolder, configApiRef } from '@backstage/core-plugin-api';
import { coreComponentsTranslationRef } from '../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
export type SupportItemLink = {
url: string;
@@ -32,30 +34,34 @@ export type SupportConfig = {
items: SupportItem[];
};
const DEFAULT_SUPPORT_CONFIG: SupportConfig = {
url: 'https://github.com/backstage/backstage/issues',
items: [
{
title: 'Support Not Configured',
icon: 'warning',
links: [
{
// TODO: Update to dedicated support page on backstage.io/docs
title: 'Add `app.support` config key',
url: 'https://github.com/backstage/backstage/blob/master/app-config.yaml',
},
],
},
],
const useDefaultSupportConfig = () => {
const { t } = useTranslationRef(coreComponentsTranslationRef);
return {
url: 'https://github.com/backstage/backstage/issues',
items: [
{
title: t('supportConfig.default.title'),
icon: 'warning',
links: [
{
// TODO: Update to dedicated support page on backstage.io/docs
title: t('supportConfig.default.linkTitle'),
url: 'https://github.com/backstage/backstage/blob/master/app-config.yaml',
},
],
},
],
};
};
export function useSupportConfig(): SupportConfig {
const apiHolder = useApiHolder();
const config = apiHolder.get(configApiRef);
const supportConfig = config?.getOptionalConfig('app.support');
const defaultSupportConfig = useDefaultSupportConfig();
if (!supportConfig) {
return DEFAULT_SUPPORT_CONFIG;
return defaultSupportConfig;
}
return {
@@ -18,6 +18,8 @@ import Typography from '@material-ui/core/Typography';
import React, { ComponentClass, Component, ErrorInfo } from 'react';
import { LinkButton } from '../../components/LinkButton';
import { ErrorPanel } from '../../components/ErrorPanel';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
type SlackChannel = {
name: string;
@@ -37,14 +39,21 @@ type State = {
const SlackLink = (props: { slackChannel?: string | SlackChannel }) => {
const { slackChannel } = props;
const { t } = useTranslationRef(coreComponentsTranslationRef);
if (!slackChannel) {
return null;
} else if (typeof slackChannel === 'string') {
return <Typography>Please contact {slackChannel} for help.</Typography>;
return (
<Typography>{t('errorBoundary.title', { slackChannel })}</Typography>
);
} else if (!slackChannel.href) {
return (
<Typography>Please contact {slackChannel.name} for help.</Typography>
<Typography>
{t('errorBoundary.title', {
slackChannel: slackChannel.name,
})}
</Typography>
);
}
@@ -23,6 +23,8 @@ import { Link } from '../../components/Link';
import { useSupportConfig } from '../../hooks';
import { MicDrop } from './MicDrop';
import { StackDetails } from './StackDetails';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
interface IErrorPageProps {
status?: string;
@@ -64,10 +66,17 @@ const useStyles = makeStyles(
*
*/
export function ErrorPage(props: IErrorPageProps) {
const { status, statusMessage, additionalInfo, supportUrl, stack } = props;
const {
status = '',
statusMessage,
additionalInfo,
supportUrl,
stack,
} = props;
const classes = useStyles();
const navigate = useNavigate();
const support = useSupportConfig();
const { t } = useTranslationRef(coreComponentsTranslationRef);
return (
<Grid container className={classes.container}>
@@ -77,17 +86,17 @@ export function ErrorPage(props: IErrorPageProps) {
variant="body1"
className={classes.subtitle}
>
ERROR {status}: {statusMessage}
{t('errorPage.subtitle', { status, statusMessage })}
</Typography>
<Typography variant="body1" className={classes.subtitle}>
{additionalInfo}
</Typography>
<Typography variant="h2" className={classes.title}>
Looks like someone dropped the mic!
{t('errorPage.title')}
</Typography>
<Typography variant="h6" className={classes.title}>
<Link to="#" data-testid="go-back-link" onClick={() => navigate(-1)}>
Go back
{t('errorPage.goBack')}
</Link>
... or please{' '}
<Link to={supportUrl || support.url}>contact support</Link> if you
@@ -19,6 +19,8 @@ import { useState } from 'react';
import { Link } from '../../components/Link';
import { CodeSnippet } from '../../components';
import { makeStyles } from '@material-ui/core/styles';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { coreComponentsTranslationRef } from '../../translation';
interface IStackDetailsProps {
stack: string;
@@ -46,6 +48,7 @@ const useStyles = makeStyles(
export function StackDetails(props: IStackDetailsProps) {
const { stack } = props;
const classes = useStyles();
const { t } = useTranslationRef(coreComponentsTranslationRef);
const [detailsOpen, setDetailsOpen] = useState<boolean>(false);
@@ -53,7 +56,7 @@ export function StackDetails(props: IStackDetailsProps) {
return (
<Typography variant="h6" className={classes.title}>
<Link to="#" onClick={() => setDetailsOpen(true)}>
Show more details
{t('errorPage.showMoreDetails')}
</Link>
</Typography>
);
@@ -63,7 +66,7 @@ export function StackDetails(props: IStackDetailsProps) {
<>
<Typography variant="h6" className={classes.title}>
<Link to="#" onClick={() => setDetailsOpen(false)}>
Show less details
{t('errorPage.showLessDetails')}
</Link>
</Typography>
<CodeSnippet
@@ -24,6 +24,8 @@ import { useAsync, useMountEffect } from '@react-hookz/web';
import { ErrorPanel } from '../../components/ErrorPanel';
import { Progress } from '../../components/Progress';
import { ProxiedSignInIdentity } from './ProxiedSignInIdentity';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/**
* Props for {@link ProxiedSignInPage}.
@@ -61,6 +63,7 @@ export type ProxiedSignInPageProps = SignInPageProps & {
*/
export const ProxiedSignInPage = (props: ProxiedSignInPageProps) => {
const discoveryApi = useApi(discoveryApiRef);
const { t } = useTranslationRef(coreComponentsTranslationRef);
const [{ status, error }, { execute }] = useAsync(async () => {
const identity = new ProxiedSignInIdentity({
@@ -79,12 +82,7 @@ export const ProxiedSignInPage = (props: ProxiedSignInPageProps) => {
if (status === 'loading') {
return <Progress />;
} else if (error) {
return (
<ErrorPanel
title="You do not appear to be signed in. Please try reloading the browser page."
error={error}
/>
);
return <ErrorPanel title={t('proxiedSignInPage.title')} error={error} />;
}
return null;
@@ -34,6 +34,8 @@ import { MobileSidebar } from './MobileSidebar';
import { useContent } from './Page';
import { SidebarOpenStateProvider } from './SidebarOpenStateContext';
import { useSidebarPinState } from './SidebarPinStateContext';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { coreComponentsTranslationRef } from '../../translation';
/** @public */
export type SidebarClassKey = 'drawer' | 'drawerOpen';
@@ -250,6 +252,7 @@ function A11ySkipSidebar() {
const { sidebarConfig } = useContext(SidebarConfigContext);
const { focusContent, contentRef } = useContent();
const classes = useStyles({ sidebarConfig });
const { t } = useTranslationRef(coreComponentsTranslationRef);
if (!contentRef?.current) {
return null;
@@ -260,7 +263,7 @@ function A11ySkipSidebar() {
variant="contained"
className={classnames(classes.visuallyHidden)}
>
Skip to content
{t('skipToContent')}
</Button>
);
}
@@ -35,6 +35,8 @@ import { Page } from '../Page';
import { getSignInProviders, useSignInProviders } from './providers';
import { GridItem, useStyles } from './styles';
import { IdentityProviders, SignInProviderConfig } from './types';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
type MultiSignInPageProps = SignInPageProps & {
providers: IdentityProviders;
@@ -95,6 +97,7 @@ export const SingleSignInPage = ({
const classes = useStyles();
const authApi = useApi(provider.apiRef);
const configApi = useApi(configApiRef);
const { t } = useTranslationRef(coreComponentsTranslationRef);
const [error, setError] = useState<Error>();
@@ -174,7 +177,7 @@ export const SingleSignInPage = ({
login({ showPopup: true });
}}
>
Sign In
{t('signIn.title')}
</Button>
}
>
@@ -28,6 +28,8 @@ import { useApi, errorApiRef } from '@backstage/core-plugin-api';
import { GridItem } from './styles';
import { ForwardedError } from '@backstage/errors';
import { UserIdentity } from './UserIdentity';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
const Component: ProviderComponent = ({
config,
@@ -38,6 +40,7 @@ const Component: ProviderComponent = ({
const { apiRef, title, message } = config as SignInProviderConfig;
const authApi = useApi(apiRef);
const errorApi = useApi(errorApiRef);
const { t } = useTranslationRef(coreComponentsTranslationRef);
const handleLogin = async () => {
try {
@@ -63,7 +66,7 @@ const Component: ProviderComponent = ({
);
} catch (error) {
onSignInFailure();
errorApi.post(new ForwardedError('Login failed', error));
errorApi.post(new ForwardedError(t('signIn.loginFailed'), error));
}
};
@@ -74,7 +77,7 @@ const Component: ProviderComponent = ({
title={title}
actions={
<Button color="primary" variant="outlined" onClick={handleLogin}>
Sign In
{t('signIn.title')}
</Button>
}
>
@@ -27,6 +27,8 @@ import { InfoCard } from '../InfoCard/InfoCard';
import { ProviderComponent, ProviderLoader, SignInProvider } from './types';
import { GridItem } from './styles';
import { UserIdentity } from './UserIdentity';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
// accept base64url format according to RFC7515 (https://tools.ietf.org/html/rfc7515#section-3)
const ID_TOKEN_REGEX = /^[a-z0-9_\-]+\.[a-z0-9_\-]+\.[a-z0-9_\-]+$/i;
@@ -44,6 +46,9 @@ const useFormStyles = makeStyles(
alignSelf: 'center',
marginTop: theme.spacing(2),
},
subTitle: {
whiteSpace: 'pre-line',
},
}),
{ name: 'BackstageCustomProvider' },
);
@@ -63,6 +68,7 @@ const asInputRef = (renderResult: UseFormRegisterReturn) => {
const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => {
const classes = useFormStyles();
const { t } = useTranslationRef(coreComponentsTranslationRef);
const { register, handleSubmit, formState } = useForm<Data>({
mode: 'onChange',
});
@@ -84,18 +90,16 @@ const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => {
return (
<GridItem>
<InfoCard title="Custom User" variant="fullHeight">
<Typography variant="body1">
Enter your own User ID and credentials.
<br />
This selection will not be stored.
<InfoCard title={t('signIn.customProvider.title')} variant="fullHeight">
<Typography variant="body1" className={classes.subTitle}>
{t('signIn.customProvider.subtitle')}
</Typography>
<form className={classes.form} onSubmit={handleSubmit(handleResult)}>
<FormControl>
<TextField
{...asInputRef(register('userId', { required: true }))}
label="User ID"
label={t('signIn.customProvider.userId')}
margin="normal"
error={Boolean(errors.userId)}
/>
@@ -111,10 +115,10 @@ const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => {
validate: token =>
!token ||
ID_TOKEN_REGEX.test(token) ||
'Token is not a valid OpenID Connect JWT Token',
t('signIn.customProvider.tokenInvalid'),
}),
)}
label="ID Token (optional)"
label={t('signIn.customProvider.idToken')}
margin="normal"
autoComplete="off"
error={Boolean(errors.idToken)}
@@ -130,7 +134,7 @@ const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => {
className={classes.button}
disabled={!formState?.isDirty || !isEmpty(errors)}
>
Continue
{t('signIn.customProvider.continue')}
</Button>
</form>
</InfoCard>
@@ -25,6 +25,8 @@ import { discoveryApiRef, useApi } from '@backstage/core-plugin-api';
import { GuestUserIdentity } from './GuestUserIdentity';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { ResponseError } from '@backstage/errors';
import { coreComponentsTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
const getIdentity = async (identity: ProxiedSignInIdentity) => {
try {
@@ -48,6 +50,7 @@ const Component: ProviderComponent = ({
}) => {
const discoveryApi = useApi(discoveryApiRef);
const [_, setUseLegacyGuestToken] = useLocalStorage('enableLegacyGuestToken');
const { t } = useTranslationRef(coreComponentsTranslationRef);
const handle = async () => {
onSignInStarted();
@@ -85,11 +88,13 @@ const Component: ProviderComponent = ({
variant="fullHeight"
actions={
<Button color="primary" variant="outlined" onClick={handle}>
Enter
{t('signIn.guestProvider.enter')}
</Button>
}
>
<Typography variant="body1">Sign in as a Guest.</Typography>
<Typography variant="body1" style={{ whiteSpace: 'pre-line' }}>
{t('signIn.guestProvider.subtitle')}
</Typography>
</InfoCard>
</GridItem>
);
+111
View File
@@ -0,0 +1,111 @@
/*
* Copyright 2023 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 { createTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @alpha */
export const coreComponentsTranslationRef = createTranslationRef({
id: 'core-components',
messages: {
signIn: {
title: 'Sign In',
loginFailed: 'Login failed',
customProvider: {
title: 'Custom User',
subtitle:
'Enter your own User ID and credentials.\n This selection will not be stored.',
userId: 'User ID',
tokenInvalid: 'Token is not a valid OpenID Connect JWT Token',
continue: 'Continue',
idToken: 'ID Token (optional)',
},
guestProvider: {
title: 'Guest',
subtitle:
'Enter as a Guest User.\n You will not have a verified identity, meaning some features might be unavailable.',
enter: 'Enter',
},
},
skipToContent: 'Skip to content',
copyTextButton: {
tooltipText: 'Text copied to clipboard',
},
simpleStepper: {
reset: 'Reset',
finish: 'Finish',
next: 'Next',
skip: 'Skip',
back: 'Back',
},
errorPage: {
subtitle: 'ERROR {{status}}: {{statusMessage}}',
title: 'Looks like someone dropped the mic!',
goBack: 'Go back',
showMoreDetails: 'Show more details',
showLessDetails: 'Show less details',
},
emptyState: {
missingAnnotation: {
title: 'Missing Annotation',
actionTitle:
'Add the annotation to your component YAML as shown in the highlighted example below:',
readMore: 'Read more',
},
},
supportConfig: {
default: {
title: 'Support Not Configured',
linkTitle: 'Add `app.support` config key',
},
},
errorBoundary: {
title: 'Please contact {{slackChannel}} for help.',
},
oauthRequestDialog: {
title: 'Login Required',
authRedirectTitle: 'This will trigger a http redirect to OAuth Login.',
login: 'Log in',
rejectAll: 'Reject All',
},
link: {
openNewWindow: 'Opens in a new window',
},
supportButton: {
title: 'Support',
close: 'Close',
},
table: {
filter: {
title: 'Filters',
clearAll: 'Clear all',
},
},
alertDisplay: {
message_one: '({{ count }} older message)',
message_other: '({{ count }} older messages)',
},
autoLogout: {
stillTherePrompt: {
title: 'Logging out due to inactivity',
buttonText: "Yes! Don't log me out",
},
},
proxiedSignInPage: {
title:
'You do not appear to be signed in. Please try reloading the browser page.',
},
},
});
@@ -16,7 +16,7 @@
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { renderWithEffects, TestApiProvider } from '@backstage/test-utils';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { BadgesApi, badgesApiRef } from '../api';
import { EntityBadgesDialog } from './EntityBadgesDialog';
import { EntityProvider } from '@backstage/plugin-catalog-react';
@@ -43,7 +43,7 @@ describe('EntityBadgesDialog', () => {
kind: 'MockKind',
} as Entity;
const rendered = await renderWithEffects(
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[badgesApiRef, mockApi],
@@ -15,8 +15,8 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import { BitriseBuildsComponent } from './BitriseBuildsComponent';
import { renderInTestApp } from '@backstage/test-utils';
let entityValue: {
entity: { metadata: { annotations?: { [key: string]: string } } };
@@ -48,10 +48,10 @@ jest.mock('../BitriseBuildsTableComponent', () => ({
describe('BitriseArtifactsComponent', () => {
entityValue = { entity: { metadata: {} } };
const renderComponent = () => render(<BitriseBuildsComponent />);
const renderComponent = () => renderInTestApp(<BitriseBuildsComponent />);
it('should display an empty state if an app annotation is missing', async () => {
const rendered = renderComponent();
const rendered = await renderComponent();
expect(await rendered.findByText('Missing Annotation')).toBeInTheDocument();
});
+1
View File
@@ -52,6 +52,7 @@
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/dom": "^9.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
@@ -21,6 +21,10 @@ import { Features } from './Features';
import { mockCalverProject } from '../test-helpers/test-helpers';
import { TEST_IDS } from '../test-helpers/test-ids';
import { mockApiClient } from '../test-helpers/mock-api-client';
import { MockErrorApi, TestApiProvider } from '@backstage/test-utils';
import { translationApiRef } from '@backstage/core-plugin-api/alpha';
import { MockTranslationApi } from '@backstage/test-utils/alpha';
import { errorApiRef } from '@backstage/core-plugin-api';
jest.mock('@backstage/core-plugin-api', () => ({
...jest.requireActual('@backstage/core-plugin-api'),
@@ -35,18 +39,26 @@ jest.mock('../contexts/ProjectContext', () => ({
describe('Features', () => {
it('should omit features omitted via configuration', async () => {
const { getByTestId } = render(
<Features
features={{
info: { omit: false },
createRc: { omit: true },
promoteRc: { omit: true },
patch: { omit: true },
custom: {
// shouldn't trigger "missing key" warning in console
factory: () => [<div>Custom 1</div>, <div>Custom 2</div>],
},
}}
/>,
<TestApiProvider
apis={[
[translationApiRef, MockTranslationApi.create()],
[errorApiRef, new MockErrorApi()],
]}
>
<Features
features={{
info: { omit: false },
createRc: { omit: true },
promoteRc: { omit: true },
patch: { omit: true },
custom: {
// shouldn't trigger "missing key" warning in console
factory: () => [<div>Custom 1</div>, <div>Custom 2</div>],
},
}}
/>
,
</TestApiProvider>,
);
await waitFor(() => getByTestId(TEST_IDS.info.info));
@@ -15,14 +15,13 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import {
mockCalverProject,
mockReleaseBranch,
mockReleaseCandidateCalver,
} from '../../test-helpers/test-helpers';
import { Info } from './Info';
import { renderInTestApp } from '@backstage/test-utils';
jest.mock('../../contexts/ProjectContext', () => ({
useProjectContext: () => ({
@@ -32,7 +31,7 @@ jest.mock('../../contexts/ProjectContext', () => ({
describe('Info', () => {
it('should return early if no latestRelease exists', async () => {
const { findByText } = render(
const { findByText } = await renderInTestApp(
<Info
latestRelease={mockReleaseCandidateCalver}
releaseBranch={mockReleaseBranch}
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { render, waitFor, screen } from '@testing-library/react';
import { waitFor, screen, render } from '@testing-library/react';
import {
mockBumpedTag,
@@ -29,6 +29,10 @@ import {
import { mockApiClient } from '../../test-helpers/mock-api-client';
import { PatchBody } from './PatchBody';
import { TEST_IDS } from '../../test-helpers/test-ids';
import { MockErrorApi, TestApiProvider } from '@backstage/test-utils';
import { translationApiRef } from '@backstage/core-plugin-api/alpha';
import { MockTranslationApi } from '@backstage/test-utils/alpha';
import { errorApiRef } from '@backstage/core-plugin-api';
jest.mock('@backstage/core-plugin-api', () => ({
...jest.requireActual('@backstage/core-plugin-api'),
@@ -56,13 +60,21 @@ describe('PatchBody', () => {
});
const { getByTestId } = render(
<PatchBody
bumpedTag={mockBumpedTag}
latestRelease={mockReleaseCandidateCalver}
releaseBranch={mockReleaseBranch}
tagParts={mockTagParts}
ctaMessage={mockCtaMessage}
/>,
<TestApiProvider
apis={[
[translationApiRef, MockTranslationApi.create()],
[errorApiRef, new MockErrorApi()],
]}
>
<PatchBody
bumpedTag={mockBumpedTag}
latestRelease={mockReleaseCandidateCalver}
releaseBranch={mockReleaseBranch}
tagParts={mockTagParts}
ctaMessage={mockCtaMessage}
/>
,
</TestApiProvider>,
);
expect(getByTestId(TEST_IDS.patch.loading)).toBeInTheDocument();
@@ -74,13 +86,20 @@ describe('PatchBody', () => {
it('should render not-prerelease description', async () => {
const { getByTestId } = render(
<PatchBody
latestRelease={mockReleaseVersionCalver}
releaseBranch={mockReleaseBranch}
bumpedTag={mockBumpedTag}
tagParts={mockTagParts}
ctaMessage={mockCtaMessage}
/>,
<TestApiProvider
apis={[
[translationApiRef, MockTranslationApi.create()],
[errorApiRef, new MockErrorApi()],
]}
>
<PatchBody
latestRelease={mockReleaseVersionCalver}
releaseBranch={mockReleaseBranch}
bumpedTag={mockBumpedTag}
tagParts={mockTagParts}
ctaMessage={mockCtaMessage}
/>
</TestApiProvider>,
);
expect(getByTestId(TEST_IDS.patch.loading)).toBeInTheDocument();
@@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/core-app-api';
import { ConfigApi, configApiRef } from '@backstage/core-plugin-api';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderWithEffects, TestApiProvider } from '@backstage/test-utils';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { GoCdBuildsComponent } from './GoCdBuildsComponent';
import { gocdApiRef } from '../../plugin';
import { GoCdApi } from '../../api/gocdApi';
@@ -41,7 +41,7 @@ describe('GoCdArtifactsComponent', () => {
};
const renderComponent = () =>
renderWithEffects(
renderInTestApp(
<TestApiProvider
apis={[
[gocdApiRef, mockApiClient],
@@ -16,19 +16,18 @@
import React from 'react';
import { VisitList } from './VisitList';
import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { renderInTestApp } from '@backstage/test-utils';
describe('<VisitList/>', () => {
it('renders with mandatory parameters', async () => {
const { getByText } = await render(
const { getByText } = await renderInTestApp(
<VisitList title="My title" detailType="time-ago" />,
);
expect(getByText('My title')).toBeInTheDocument();
});
it('renders skeleton when loading is true', async () => {
const { container } = await render(
const { container } = await renderInTestApp(
<VisitList title="My title" detailType="time-ago" loading />,
);
expect(container.querySelectorAll('li')).toHaveLength(8);
@@ -36,7 +35,7 @@ describe('<VisitList/>', () => {
});
it('renders specified amount of items', async () => {
const { container } = await render(
const { container } = await renderInTestApp(
<VisitList
title="My title"
detailType="time-ago"
@@ -49,7 +48,7 @@ describe('<VisitList/>', () => {
});
it('renders some items hidden', async () => {
const { container } = await render(
const { container } = await renderInTestApp(
<VisitList
title="My title"
detailType="time-ago"
@@ -63,7 +62,7 @@ describe('<VisitList/>', () => {
});
it('renders all items when not collapsed', async () => {
const { container } = await render(
const { container } = await renderInTestApp(
<VisitList
title="My title"
detailType="time-ago"
@@ -78,23 +77,20 @@ describe('<VisitList/>', () => {
});
it('renders visit with time-ago', async () => {
const { container, getByText } = await render(
<BrowserRouter>
<VisitList
title="My title"
detailType="time-ago"
visits={[
{
id: 'explore',
name: 'Explore Backstage',
pathname: '/explore',
hits: 35,
timestamp: Date.now() - 86400_000,
},
]}
/>
,
</BrowserRouter>,
const { container, getByText } = await renderInTestApp(
<VisitList
title="My title"
detailType="time-ago"
visits={[
{
id: 'explore',
name: 'Explore Backstage',
pathname: '/explore',
hits: 35,
timestamp: Date.now() - 86400_000,
},
]}
/>,
);
expect(container.querySelectorAll('li')).toHaveLength(1);
expect(getByText('Explore Backstage')).toBeInTheDocument();
@@ -102,23 +98,20 @@ describe('<VisitList/>', () => {
});
it('renders visit with hits', async () => {
const { container, getByText } = await render(
<BrowserRouter>
<VisitList
title="My title"
detailType="hits"
visits={[
{
id: 'explore',
name: 'Explore Backstage',
pathname: '/explore',
hits: 35,
timestamp: Date.now() - 86400_000,
},
]}
/>
,
</BrowserRouter>,
const { container, getByText } = await renderInTestApp(
<VisitList
title="My title"
detailType="hits"
visits={[
{
id: 'explore',
name: 'Explore Backstage',
pathname: '/explore',
hits: 35,
timestamp: Date.now() - 86400_000,
},
]}
/>,
);
expect(container.querySelectorAll('li')).toHaveLength(1);
expect(getByText('Explore Backstage')).toBeInTheDocument();
@@ -126,23 +119,20 @@ describe('<VisitList/>', () => {
});
it('renders text warning about few items', async () => {
const { getByText } = await render(
<BrowserRouter>
<VisitList
title="My title"
detailType="hits"
visits={[
{
id: 'explore',
name: 'Explore Backstage',
pathname: '/explore',
hits: 35,
timestamp: Date.now() - 86400_000,
},
]}
/>
,
</BrowserRouter>,
const { getByText } = await renderInTestApp(
<VisitList
title="My title"
detailType="hits"
visits={[
{
id: 'explore',
name: 'Explore Backstage',
pathname: '/explore',
hits: 35,
timestamp: Date.now() - 86400_000,
},
]}
/>,
);
expect(
getByText('The more pages you visit, the more pages will appear here.'),
@@ -150,10 +140,8 @@ describe('<VisitList/>', () => {
});
it('renders text warning about no items', async () => {
const { getByText } = await render(
<BrowserRouter>
<VisitList title="My title" detailType="hits" visits={[]} />,
</BrowserRouter>,
const { getByText } = await renderInTestApp(
<VisitList title="My title" detailType="hits" visits={[]} />,
);
expect(getByText('There are no visits to show yet.')).toBeInTheDocument();
});
@@ -15,10 +15,9 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import { FixDialog } from './FixDialog';
import { Pod } from 'kubernetes-models/v1/Pod';
import { renderInTestApp } from '@backstage/test-utils';
jest.mock('../Events', () => ({
Events: () => {
@@ -33,8 +32,8 @@ jest.mock('../PodLogs', () => ({
}));
describe('FixDialog', () => {
it('docs link should render', () => {
const { getByText } = render(
it('docs link should render', async () => {
const { getByText } = await renderInTestApp(
<FixDialog
open
clusterName="some-cluster"
@@ -74,8 +73,8 @@ describe('FixDialog', () => {
expect(getByText('fix1')).toBeInTheDocument();
expect(getByText('fix2')).toBeInTheDocument();
});
it('events button should render', () => {
const { getByText } = render(
it('events button should render', async () => {
const { getByText } = await renderInTestApp(
<FixDialog
open
clusterName="some-cluster"
@@ -115,8 +114,8 @@ describe('FixDialog', () => {
expect(getByText('fix1')).toBeInTheDocument();
expect(getByText('fix2')).toBeInTheDocument();
});
it('Logs button should render', () => {
const { getByText } = render(
it('Logs button should render', async () => {
const { getByText } = await renderInTestApp(
<FixDialog
open
clusterName="some-cluster"
@@ -15,8 +15,8 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import { TechDocsSearchResultListItem } from './TechDocsSearchResultListItem';
import { renderInTestApp } from '@backstage/test-utils';
// Using canvas to render text..
jest.mock('react-text-truncate', () => {
@@ -46,7 +46,7 @@ const validResultWithTitle = {
describe('TechDocsSearchResultListItem test', () => {
it('should render search doc passed in', async () => {
const { findByText } = render(
const { findByText } = await renderInTestApp(
<TechDocsSearchResultListItem result={validResult} />,
);
@@ -61,7 +61,7 @@ describe('TechDocsSearchResultListItem test', () => {
});
it('should use title if defined', async () => {
const { findByText } = render(
const { findByText } = await renderInTestApp(
<TechDocsSearchResultListItem
result={validResult}
title="Count Dookumentation"
@@ -77,7 +77,7 @@ describe('TechDocsSearchResultListItem test', () => {
});
it('should use entity title if defined', async () => {
const { findByText } = render(
const { findByText } = await renderInTestApp(
<TechDocsSearchResultListItem result={validResultWithTitle} />,
);
@@ -16,7 +16,7 @@
import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderWithEffects, TestApiProvider } from '@backstage/test-utils';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import React from 'react';
import { TodoApi, todoApiRef } from '../../api';
import { TodoList } from './TodoList';
@@ -43,7 +43,7 @@ describe('TodoList', () => {
kind: 'MockKind',
} as Entity;
const rendered = await renderWithEffects(
const rendered = await renderInTestApp(
<TestApiProvider apis={[[todoApiRef, mockApi]]}>
<EntityProvider entity={mockEntity}>
<TodoList />
@@ -16,9 +16,12 @@
import React from 'react';
import { setupServer } from 'msw/node';
import { setupRequestMockHandlers } from '@backstage/test-utils';
import {
renderInTestApp,
setupRequestMockHandlers,
} from '@backstage/test-utils';
import { ComponentEntity } from '@backstage/catalog-model';
import { render, waitFor } from '@testing-library/react';
import { waitFor } from '@testing-library/react';
import { EntityVaultCard } from './EntityVaultCard';
import { EntityProvider } from '@backstage/plugin-catalog-react';
@@ -40,7 +43,7 @@ describe('EntityVaultCard', () => {
};
it('should render missing entity annotation', async () => {
const rendered = render(
const rendered = await renderInTestApp(
<EntityProvider entity={entityAnnotationMissing}>
<EntityVaultCard />
</EntityProvider>,
+5
View File
@@ -24,6 +24,9 @@ import {
featureFlagsApiRef,
} from '@backstage/core-plugin-api';
import { translationApiRef } from '@backstage/core-plugin-api/alpha';
import { MockTranslationApi } from '@backstage/test-utils/alpha';
const configApi = new ConfigReader({});
const featureFlagsApi = new LocalStorageFeatureFlags();
const alertApi = new AlertApiForwarder();
@@ -55,6 +58,7 @@ const oktaAuthApi = OktaAuth.create({
basePath: '/auth/',
oauthRequestApi,
});
const translationApi = MockTranslationApi.create();
export const apis = [
[configApiRef, configApi],
@@ -67,4 +71,5 @@ export const apis = [
[githubAuthApiRef, githubAuthApi],
[gitlabAuthApiRef, gitlabAuthApi],
[oktaAuthApiRef, oktaAuthApi],
[translationApiRef, translationApi],
];
+1
View File
@@ -6722,6 +6722,7 @@ __metadata:
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/integration": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@material-ui/core": ^4.12.2
"@material-ui/icons": ^4.9.1
"@material-ui/lab": 4.0.0-alpha.61