feat: add i18n to plugin-catalog-react

Signed-off-by: mario ma <mario.ma.node@gmail.com>
This commit is contained in:
mario ma
2024-03-05 17:24:27 +08:00
parent c324de8ccd
commit 06c0956207
39 changed files with 526 additions and 170 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-scaffolder-react': patch
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-catalog': patch
---
Support i18n for catalog and catalog-react plugins
+59
View File
@@ -13,6 +13,7 @@ import { PortableSchema } from '@backstage/frontend-plugin-api';
import { ResolvedExtensionInputs } from '@backstage/frontend-plugin-api';
import { ResourcePermission } from '@backstage/plugin-permission-common';
import { RouteRef } from '@backstage/frontend-plugin-api';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
// @alpha (undocumented)
export const catalogExtensionData: {
@@ -24,6 +25,64 @@ export const catalogExtensionData: {
entityFilterExpression: ConfigurableExtensionDataRef<string, {}>;
};
// @alpha (undocumented)
export const catalogReactTranslationRef: TranslationRef<
'catalog-react',
{
readonly 'catalogFilter.title': 'Filters';
readonly 'catalogFilter.buttonTitle': 'Filters';
readonly 'entityKindPicker.title': 'Kind';
readonly 'entityKindPicker.errorMessage': 'Failed to load entity kinds';
readonly entityLifecyclePickerTitle: 'Lifecycle';
readonly entityNamespacePickerTitle: 'Namespace';
readonly entityOwnerPickerTitle: 'Owner';
readonly 'entityPeekAheadPopover.title': 'Drill into the entity to see all of the tags.';
readonly 'entityPeekAheadPopover.entityCardActionsTitle': 'Show details';
readonly 'entityPeekAheadPopover.emailCardAction.title': 'Email {{email}}';
readonly 'entityPeekAheadPopover.emailCardAction.subTitle': 'mailto {{email}}';
readonly entityProcessingStatusPickerTitle: 'Processing Status';
readonly entitySearchBarPlaceholder: 'Search';
readonly entityTagPickerTitle: 'Tags';
readonly 'entityTypePicker.title': 'Type';
readonly 'entityTypePicker.errorMessage': 'Failed to load entity types';
readonly 'entityTypePicker.optionAllTitle': 'all';
readonly 'favoriteEntity.addToFavorites': 'Add to favorites';
readonly 'favoriteEntity.RemoveFromFavorites': 'Remove from favorites';
readonly 'inspectEntityDialog.title': 'Entity Inspector';
readonly 'inspectEntityDialog.closeButtonTitle': 'Close';
readonly 'inspectEntityDialog.ancestryPage.title': 'Ancestry';
readonly 'inspectEntityDialog.colocatedPage.title': 'Colocated';
readonly 'inspectEntityDialog.colocatedPage.description': 'These are the entities that are colocated with this entity - as in, they originated from the same data source (e.g. came from the same YAML file), or from the same origin (e.g. the originally registered URL).';
readonly 'inspectEntityDialog.colocatedPage.alertNoLocation': 'Entity had no location information.';
readonly 'inspectEntityDialog.colocatedPage.alertNoEntity': 'There were no other entities on this location.';
readonly 'inspectEntityDialog.jsonPage.title': 'Entity as JSON';
readonly 'inspectEntityDialog.jsonPage.description': 'This is the raw entity data as received from the catalog, on JSON form.';
readonly 'inspectEntityDialog.overviewPage.title': 'Overview';
readonly 'inspectEntityDialog.yamlPage.title': 'Entity as YAML';
readonly 'inspectEntityDialog.yamlPage.description': 'This is the raw entity data as received from the catalog, on YAML form.';
readonly 'unregisterEntityDialog.title': 'Are you sure you want to unregister this entity?';
readonly 'unregisterEntityDialog.cancelButtonTitle': 'Cancel';
readonly 'unregisterEntityDialog.deleteButtonTitle': 'Delete Entity';
readonly 'unregisterEntityDialog.deleteEntitySuccessMessage': 'Removed entity {{entityName}}';
readonly 'unregisterEntityDialog.onlyDeleteStateTitle': 'This entity does not seem to originate from a registered location. You therefore only have the option to delete it outright from the catalog.';
readonly 'unregisterEntityDialog.errorStateTitle': 'Internal error: Unknown state';
readonly 'unregisterEntityDialog.bootstrapState.title': 'You cannot unregister this entity, since it originates from a protected Backstage configuration (location "{{location}}"). If you believe this is in error, please contact the {{appTitle}} integrator.';
readonly 'unregisterEntityDialog.bootstrapState.advancedDescription': 'You have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.';
readonly 'unregisterEntityDialog.bootstrapState.advancedOptions': 'Advanced Options';
readonly 'unregisterEntityDialog.unregisterState.title': 'This action will unregister the following entities:';
readonly 'unregisterEntityDialog.unregisterState.description': 'To undo, just re-register the entity in {{appTitle}}.';
readonly 'unregisterEntityDialog.unregisterState.subTitle': 'Located at the following location:';
readonly 'unregisterEntityDialog.unregisterState.advancedDescription': 'You also have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.';
readonly 'unregisterEntityDialog.unregisterState.advancedOptions': 'Advanced Options';
readonly 'unregisterEntityDialog.unregisterState.unregisterButtonTitle': 'Unregister Location';
readonly 'userListPicker.defaultOrgName': 'Company';
readonly 'userListPicker.orgFilterAllLabel': 'All';
readonly 'userListPicker.personalFilter.title': 'Personal';
readonly 'userListPicker.personalFilter.ownedLabel': 'Owned';
readonly 'userListPicker.personalFilter.starredLabel': 'Starred';
}
>;
// @alpha (undocumented)
export function createEntityCardExtension<
TConfig extends {
+1
View File
@@ -32,6 +32,7 @@ import { Expand } from '../../../packages/frontend-plugin-api/src/types';
export { useEntityPermission } from './hooks/useEntityPermission';
export { isOwnerOf } from './utils';
export * from './translation';
/** @alpha */
export const catalogExtensionData = {
@@ -23,6 +23,8 @@ import Typography from '@material-ui/core/Typography';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import { Theme, useTheme } from '@material-ui/core/styles';
import FilterListIcon from '@material-ui/icons/FilterList';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export const Filters = (props: {
@@ -37,6 +39,7 @@ export const Filters = (props: {
);
const theme = useTheme();
const [filterDrawerOpen, setFilterDrawerOpen] = useState<boolean>(false);
const { t } = useTranslationRef(catalogReactTranslationRef);
return isScreenSmallerThanBreakpoint ? (
<>
@@ -45,7 +48,7 @@ export const Filters = (props: {
onClick={() => setFilterDrawerOpen(true)}
startIcon={<FilterListIcon />}
>
Filters
{t('catalogFilter.buttonTitle')}
</Button>
<Drawer
open={filterDrawerOpen}
@@ -61,7 +64,7 @@ export const Filters = (props: {
component="h2"
style={{ marginBottom: theme.spacing(1) }}
>
Filters
{t('catalogFilter.title')}
</Typography>
{props.children}
</Box>
@@ -18,7 +18,7 @@ import { GetEntityFacetsResponse } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { ApiProvider } from '@backstage/core-app-api';
import { alertApiRef } from '@backstage/core-plugin-api';
import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { fireEvent, waitFor, screen, within } from '@testing-library/react';
import { capitalize } from 'lodash';
import { default as React } from 'react';
@@ -75,7 +75,7 @@ describe('<EntityKindPicker/>', () => {
);
it('renders available entity kinds', async () => {
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{ filters: { kind: new EntityKindFilter('component') } }}
@@ -102,7 +102,7 @@ describe('<EntityKindPicker/>', () => {
it('sets the selected kind filter', async () => {
const updateFilters = jest.fn();
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -128,7 +128,7 @@ describe('<EntityKindPicker/>', () => {
it('respects the query parameter filter value', async () => {
const updateFilters = jest.fn();
const queryParameters = { kind: 'group' };
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -148,7 +148,7 @@ describe('<EntityKindPicker/>', () => {
});
it('renders unknown kinds provided in query parameters', async () => {
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{ queryParameters: { kind: 'FROb' } }}
@@ -162,7 +162,7 @@ describe('<EntityKindPicker/>', () => {
});
it('limits kinds when allowedKinds is set', async () => {
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider>
<EntityKindPicker allowedKinds={['component', 'domain']} />
@@ -183,7 +183,7 @@ describe('<EntityKindPicker/>', () => {
});
it('renders kind from the query parameter even when not in allowedKinds', async () => {
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{ queryParameters: { kind: 'Frob' } }}
@@ -21,6 +21,8 @@ import React, { useEffect, useMemo, useState } from 'react';
import { EntityKindFilter } from '../../filters';
import { useEntityList } from '../../hooks';
import { filterKinds, useAllKinds } from './kindFilterUtils';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
function useEntityKindFilter(opts: { initialFilter: string }): {
loading: boolean;
@@ -95,6 +97,7 @@ export interface EntityKindPickerProps {
/** @public */
export const EntityKindPicker = (props: EntityKindPickerProps) => {
const { allowedKinds, hidden, initialFilter = 'component' } = props;
const { t } = useTranslationRef(catalogReactTranslationRef);
const alertApi = useApi(alertApiRef);
@@ -106,11 +109,11 @@ export const EntityKindPicker = (props: EntityKindPickerProps) => {
useEffect(() => {
if (error) {
alertApi.post({
message: `Failed to load entity kinds`,
message: t('entityKindPicker.errorMessage'),
severity: 'error',
});
}
}, [error, alertApi]);
}, [error, alertApi, t]);
if (error) return null;
@@ -124,7 +127,7 @@ export const EntityKindPicker = (props: EntityKindPickerProps) => {
return hidden ? null : (
<Box pb={1} pt={1}>
<Select
label="Kind"
label={t('entityKindPicker.title')}
items={items}
selected={selectedKind.toLocaleLowerCase('en-US')}
onChange={value => setSelectedKind(String(value))}
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { MockEntityListContextProvider } from '../../testUtils/providers';
import { EntityLifecycleFilter } from '../../filters';
import { EntityLifecyclePicker } from './EntityLifecyclePicker';
import { TestApiProvider } from '@backstage/test-utils';
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
import { catalogApiRef } from '../../api';
import { CatalogApi } from '@backstage/catalog-client';
@@ -44,7 +44,7 @@ describe('<EntityLifecyclePicker/>', () => {
});
it('renders all lifecycles', async () => {
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<MockEntityListContextProvider value={{}}>
<EntityLifecyclePicker />
@@ -61,7 +61,7 @@ describe('<EntityLifecyclePicker/>', () => {
it('respects the query parameter filter value', async () => {
const updateFilters = jest.fn();
const queryParameters = { lifecycles: ['experimental'] };
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<MockEntityListContextProvider
value={{
@@ -83,7 +83,7 @@ describe('<EntityLifecyclePicker/>', () => {
it('adds lifecycles to filters', async () => {
const updateFilters = jest.fn();
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<MockEntityListContextProvider
value={{
@@ -107,7 +107,7 @@ describe('<EntityLifecyclePicker/>', () => {
it('removes lifecycles from filters', async () => {
const updateFilters = jest.fn();
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<MockEntityListContextProvider
value={{
@@ -136,7 +136,7 @@ describe('<EntityLifecyclePicker/>', () => {
it('responds to external queryParameters changes', async () => {
const updateFilters = jest.fn();
const rendered = render(
const rendered = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<MockEntityListContextProvider
value={{
@@ -180,7 +180,7 @@ describe('<EntityLifecyclePicker/>', () => {
});
const updateFilters = jest.fn();
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<MockEntityListContextProvider
value={{
@@ -201,7 +201,7 @@ describe('<EntityLifecyclePicker/>', () => {
it('responds to initialFilter prop', async () => {
const updateFilters = jest.fn();
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<MockEntityListContextProvider
value={{
@@ -18,6 +18,8 @@ import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import { EntityLifecycleFilter } from '../../filters';
import { EntityAutocompletePicker } from '../EntityAutocompletePicker';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export type CatalogReactEntityLifecyclePickerClassKey = 'input';
@@ -35,10 +37,11 @@ const useStyles = makeStyles(
export const EntityLifecyclePicker = (props: { initialFilter?: string[] }) => {
const { initialFilter = [] } = props;
const classes = useStyles();
const { t } = useTranslationRef(catalogReactTranslationRef);
return (
<EntityAutocompletePicker
label="Lifecycle"
label={t('entityLifecyclePickerTitle')}
name="lifecycles"
path="spec.lifecycle"
Filter={EntityLifecycleFilter}
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { MockEntityListContextProvider } from '../../testUtils/providers';
import { EntityNamespaceFilter } from '../../filters';
import { EntityNamespacePicker } from './EntityNamespacePicker';
import { TestApiProvider } from '@backstage/test-utils';
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
import { catalogApiRef } from '../../api';
import { CatalogApi } from '@backstage/catalog-client';
@@ -38,7 +38,7 @@ describe('<EntityNamespacePicker/>', () => {
} as unknown as CatalogApi;
it('renders all namespaces', async () => {
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider value={{}}>
<EntityNamespacePicker />
@@ -56,7 +56,7 @@ describe('<EntityNamespacePicker/>', () => {
});
it('renders unique namespaces in alphabetical order', async () => {
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider value={{}}>
<EntityNamespacePicker />
@@ -79,7 +79,7 @@ describe('<EntityNamespacePicker/>', () => {
it('respects the query parameter filter value', async () => {
const updateFilters = jest.fn();
const queryParameters = { namespace: ['namespace-1'] };
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
@@ -101,7 +101,7 @@ describe('<EntityNamespacePicker/>', () => {
it('adds namespaces to filters', async () => {
const updateFilters = jest.fn();
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
@@ -125,7 +125,7 @@ describe('<EntityNamespacePicker/>', () => {
it('removes namespaces from filters', async () => {
const updateFilters = jest.fn();
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
@@ -154,7 +154,7 @@ describe('<EntityNamespacePicker/>', () => {
it('responds to external queryParameters changes', async () => {
const updateFilters = jest.fn();
const rendered = render(
const rendered = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
@@ -197,7 +197,7 @@ describe('<EntityNamespacePicker/>', () => {
}),
} as unknown as CatalogApi;
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRefNoNamespace]]}>
<MockEntityListContextProvider
value={{
@@ -227,7 +227,7 @@ describe('<EntityNamespacePicker/>', () => {
},
}),
} as unknown as CatalogApi;
render(
await renderInTestApp(
<TestApiProvider
apis={[[catalogApiRef, mockCatalogApiRefDefaultNamespace]]}
>
@@ -19,6 +19,8 @@ import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import { EntityNamespaceFilter } from '../../filters';
import { EntityAutocompletePicker } from '../EntityAutocompletePicker';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export type CatalogReactEntityNamespacePickerClassKey = 'input';
@@ -45,9 +47,11 @@ export interface EntityNamespacePickerProps {
export const EntityNamespacePicker = (props: EntityNamespacePickerProps) => {
const { initiallySelectedNamespaces } = props;
const classes = useStyles();
const { t } = useTranslationRef(catalogReactTranslationRef);
return (
<EntityAutocompletePicker
label="Namespace"
label={t('entityNamespacePickerTitle')}
name="namespace"
path="metadata.namespace"
Filter={EntityNamespaceFilter}
@@ -23,7 +23,7 @@ import { EntityOwnerPicker } from './EntityOwnerPicker';
import { ApiProvider } from '@backstage/core-app-api';
import {
MockErrorApi,
renderWithEffects,
renderInTestApp,
TestApiRegistry,
} from '@backstage/test-utils';
import { catalogApiRef, CatalogApi } from '../..';
@@ -155,7 +155,7 @@ describe('<EntityOwnerPicker mode="all" />', () => {
});
it('renders all users and groups', async () => {
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider value={{}}>
<EntityOwnerPicker mode="all" />
@@ -201,7 +201,7 @@ describe('<EntityOwnerPicker mode="all" />', () => {
it('respects the query parameter filter value', async () => {
const updateFilters = jest.fn();
const queryParameters = { owners: ['another-owner'] };
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{
@@ -239,7 +239,7 @@ describe('<EntityOwnerPicker mode="all" />', () => {
},
],
});
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{
@@ -281,7 +281,7 @@ describe('<EntityOwnerPicker mode="all" />', () => {
it('adds owners to filters', async () => {
const updateFilters = jest.fn();
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{
@@ -308,7 +308,7 @@ describe('<EntityOwnerPicker mode="all" />', () => {
it('removes owners from filters', async () => {
const updateFilters = jest.fn();
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{
@@ -340,7 +340,7 @@ describe('<EntityOwnerPicker mode="all" />', () => {
it('responds to external queryParameters changes', async () => {
const updateFilters = jest.fn();
const rendered = await renderWithEffects(
const rendered = await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{
@@ -398,7 +398,7 @@ describe('<EntityOwnerPicker mode="owners-only" />', () => {
});
it('renders all users and groups', async () => {
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider value={{}}>
<EntityOwnerPicker mode="owners-only" />
@@ -439,7 +439,7 @@ describe('<EntityOwnerPicker mode="owners-only" />', () => {
it('respects the query parameter filter value', async () => {
const updateFilters = jest.fn();
const queryParameters = { owners: ['another-owner'] };
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{
@@ -459,7 +459,7 @@ describe('<EntityOwnerPicker mode="owners-only" />', () => {
it('adds owners to filters', async () => {
const updateFilters = jest.fn();
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{
@@ -486,7 +486,7 @@ describe('<EntityOwnerPicker mode="owners-only" />', () => {
it('removes owners from filters', async () => {
const updateFilters = jest.fn();
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{
@@ -515,7 +515,7 @@ describe('<EntityOwnerPicker mode="owners-only" />', () => {
it('responds to external queryParameters changes', async () => {
const updateFilters = jest.fn();
const rendered = await renderWithEffects(
const rendered = await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{
@@ -40,6 +40,8 @@ import { humanizeEntity, humanizeEntityRef } from '../EntityRefLink/humanize';
import { useFetchEntities } from './useFetchEntities';
import { withStyles } from '@material-ui/core/styles';
import { useEntityPresentation } from '../../apis';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export type CatalogReactEntityOwnerPickerClassKey = 'input';
@@ -130,6 +132,7 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => {
} = useEntityList();
const [text, setText] = useState('');
const { t } = useTranslationRef(catalogReactTranslationRef);
const queryParamOwners = useMemo(
() => [ownersParameter].flat().filter(Boolean) as string[],
@@ -176,7 +179,7 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => {
return (
<Box className={classes.root} pb={1} pt={1}>
<Typography className={classes.label} variant="button" component="label">
Owner
{t('entityOwnerPickerTitle')}
<Autocomplete
PopperComponent={popperProps => (
<div {...popperProps}>{popperProps.children as ReactNode}</div>
@@ -18,6 +18,8 @@ import IconButton from '@material-ui/core/IconButton';
import EmailIcon from '@material-ui/icons/Email';
import React from 'react';
import { Link } from '@backstage/core-components';
import { catalogReactTranslationRef } from '../../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/**
* Email Card action link
@@ -25,12 +27,17 @@ import { Link } from '@backstage/core-components';
* @private
*/
export const EmailCardAction = (props: { email: string }) => {
const { t } = useTranslationRef(catalogReactTranslationRef);
return (
<IconButton
component={Link}
aria-label="Email"
title={`Email ${props.email}`}
to={`mailto:${props.email}`}
title={t('entityPeekAheadPopover.emailCardAction.title', {
email: props.email,
})}
to={t('entityPeekAheadPopover.emailCardAction.subTitle', {
email: props.email,
})}
>
<EmailIcon />
</IconButton>
@@ -21,6 +21,8 @@ import React from 'react';
import { useRouteRef } from '@backstage/core-plugin-api';
import { Entity, getCompoundEntityRef } from '@backstage/catalog-model';
import { Link } from '@backstage/core-components';
import { catalogReactTranslationRef } from '../../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/**
* Card actions that show for all entities
@@ -29,12 +31,13 @@ import { Link } from '@backstage/core-components';
*/
export const EntityCardActions = (props: { entity: Entity }) => {
const entityRoute = useRouteRef(entityRouteRef);
const { t } = useTranslationRef(catalogReactTranslationRef);
return (
<IconButton
component={Link}
aria-label="Show"
title="Show details"
title={t('entityPeekAheadPopover.entityCardActionsTitle')}
to={entityRoute(getCompoundEntityRef(props.entity))}
>
<InfoIcon />
@@ -40,6 +40,8 @@ import {
GroupCardActions,
} from './CardActionComponents';
import { debounce } from 'lodash';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/**
* Properties for an entity popover on hover of a component.
@@ -75,7 +77,7 @@ const maxTagChips = 4;
*/
export const EntityPeekAheadPopover = (props: EntityPeekAheadPopoverProps) => {
const { entityRef, children, delayTime = 500 } = props;
const { t } = useTranslationRef(catalogReactTranslationRef);
const classes = useStyles();
const apiHolder = useApiHolder();
const popupState = usePopupState({
@@ -171,7 +173,7 @@ export const EntityPeekAheadPopover = (props: EntityPeekAheadPopoverProps) => {
})}
{entity.metadata.tags?.length &&
entity.metadata.tags?.length > maxTagChips && (
<Tooltip title="Drill into the entity to see all of the tags.">
<Tooltip title={t('entityPeekAheadPopover.title')}>
<Chip key="other-tags" size="small" label="..." />
</Tooltip>
)}
@@ -14,15 +14,16 @@
* limitations under the License.
*/
import { fireEvent, render, screen } from '@testing-library/react';
import { fireEvent, screen } from '@testing-library/react';
import React from 'react';
import { EntityErrorFilter, EntityOrphanFilter } from '../../filters';
import { MockEntityListContextProvider } from '../../testUtils/providers';
import { EntityProcessingStatusPicker } from './EntityProcessingStatusPicker';
import { renderInTestApp } from '@backstage/test-utils';
describe('<EntityProcessingStatusPicker/>', () => {
it('renders all processing status options', () => {
render(
it('renders all processing status options', async () => {
await renderInTestApp(
<MockEntityListContextProvider value={{}}>
<EntityProcessingStatusPicker />
</MockEntityListContextProvider>,
@@ -34,9 +35,9 @@ describe('<EntityProcessingStatusPicker/>', () => {
expect(screen.getByText('Has Error')).toBeInTheDocument();
});
it('adds orphan to orphan filter', () => {
it('adds orphan to orphan filter', async () => {
const updateFilters = jest.fn();
render(
await renderInTestApp(
<MockEntityListContextProvider
value={{
updateFilters,
@@ -53,9 +54,9 @@ describe('<EntityProcessingStatusPicker/>', () => {
});
});
it('adds error to error filter', () => {
it('adds error to error filter', async () => {
const updateFilters = jest.fn();
render(
await renderInTestApp(
<MockEntityListContextProvider
value={{
updateFilters,
@@ -72,9 +73,9 @@ describe('<EntityProcessingStatusPicker/>', () => {
});
});
it('remove orphan from orphan filter', () => {
it('remove orphan from orphan filter', async () => {
const updateFilters = jest.fn();
render(
await renderInTestApp(
<MockEntityListContextProvider
value={{
updateFilters,
@@ -91,9 +92,9 @@ describe('<EntityProcessingStatusPicker/>', () => {
});
});
it('remove error from error filter', () => {
it('remove error from error filter', async () => {
const updateFilters = jest.fn();
render(
await renderInTestApp(
<MockEntityListContextProvider
value={{
updateFilters,
@@ -27,6 +27,8 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import React, { useState, ReactNode } from 'react';
import { useEntityList } from '../../hooks';
import Autocomplete from '@material-ui/lab/Autocomplete';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export type CatalogReactEntityProcessingStatusPickerClassKey = 'input';
@@ -49,6 +51,7 @@ const checkedIcon = <CheckBoxIcon fontSize="small" />;
export const EntityProcessingStatusPicker = () => {
const classes = useStyles();
const { updateFilters } = useEntityList();
const { t } = useTranslationRef(catalogReactTranslationRef);
const [selectedAdvancedItems, setSelectedAdvancedItems] = useState<string[]>(
[],
@@ -71,7 +74,7 @@ export const EntityProcessingStatusPicker = () => {
return (
<Box className={classes.root} pb={1} pt={1}>
<Typography className={classes.label} variant="button" component="label">
Processing Status
{t('entityProcessingStatusPickerTitle')}
<Autocomplete<string, true>
PopperComponent={popperProps => (
<div {...popperProps}>{popperProps.children as ReactNode}</div>
@@ -15,16 +15,17 @@
*/
import React from 'react';
import { fireEvent, render, waitFor, screen } from '@testing-library/react';
import { fireEvent, waitFor, screen } from '@testing-library/react';
import { EntitySearchBar } from './EntitySearchBar';
import { EntityTextFilter } from '../../filters';
import { MockEntityListContextProvider } from '../../testUtils/providers';
import { renderInTestApp } from '@backstage/test-utils';
describe('EntitySearchBar', () => {
it('should display search value and execute set callback', async () => {
const updateFilters = jest.fn();
render(
renderInTestApp(
<MockEntityListContextProvider
value={{
updateFilters,
@@ -52,4 +53,4 @@ describe('EntitySearchBar', () => {
text: undefined,
});
});
});
});
@@ -26,6 +26,8 @@ import React, { useEffect, useMemo, useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import { useEntityList } from '../../hooks/useEntityListProvider';
import { EntityTextFilter } from '../../filters';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export type CatalogReactEntitySearchBarClassKey = 'searchToolbar' | 'input';
@@ -49,6 +51,7 @@ const useStyles = makeStyles(
*/
export const EntitySearchBar = () => {
const classes = useStyles();
const { t } = useTranslationRef(catalogReactTranslationRef);
const {
updateFilters,
@@ -85,7 +88,7 @@ export const EntitySearchBar = () => {
aria-label="search"
id="input-with-icon-adornment"
className={classes.input}
placeholder="Search"
placeholder={t('entitySearchBarPlaceholder')}
autoComplete="off"
onChange={event => setSearch(event.target.value)}
value={search}
@@ -29,6 +29,8 @@ import {
humanizeEntityRef,
} from '../EntityRefLink';
// TODO: column title support i18n
/** @public */
export const columnFactories = Object.freeze({
createEntityRefColumn<T extends Entity>(options: {
@@ -14,18 +14,12 @@
* limitations under the License.
*/
import {
fireEvent,
render,
waitFor,
screen,
act,
} from '@testing-library/react';
import { fireEvent, waitFor, screen, act } from '@testing-library/react';
import React from 'react';
import { MockEntityListContextProvider } from '../../testUtils/providers';
import { EntityTagFilter } from '../../filters';
import { EntityTagPicker } from './EntityTagPicker';
import { TestApiProvider } from '@backstage/test-utils';
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
import { catalogApiRef } from '../../api';
import { CatalogApi } from '@backstage/catalog-client';
@@ -41,7 +35,7 @@ describe('<EntityTagPicker/>', () => {
} as unknown as CatalogApi;
it('renders all tags', async () => {
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider value={{}}>
<EntityTagPicker />
@@ -57,7 +51,7 @@ describe('<EntityTagPicker/>', () => {
});
it('renders unique tags in alphabetical order', async () => {
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider value={{}}>
<EntityTagPicker />
@@ -77,7 +71,7 @@ describe('<EntityTagPicker/>', () => {
});
it('renders tags with counts', async () => {
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider value={{}}>
<EntityTagPicker showCounts />
@@ -99,7 +93,7 @@ describe('<EntityTagPicker/>', () => {
it('respects the query parameter filter value', async () => {
const updateFilters = jest.fn();
const queryParameters = { tags: ['tag3'] };
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
@@ -121,7 +115,7 @@ describe('<EntityTagPicker/>', () => {
it('adds tags to filters', async () => {
const updateFilters = jest.fn();
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
@@ -145,7 +139,7 @@ describe('<EntityTagPicker/>', () => {
it('removes tags from filters', async () => {
const updateFilters = jest.fn();
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
@@ -173,7 +167,7 @@ describe('<EntityTagPicker/>', () => {
it('responds to external queryParameters changes', async () => {
const updateFilters = jest.fn();
const rendered = render(
const rendered = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
@@ -209,7 +203,7 @@ describe('<EntityTagPicker/>', () => {
it('verify that user can select tags after query string has been set', async () => {
const updateFilters = jest.fn();
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
@@ -247,7 +241,7 @@ describe('<EntityTagPicker/>', () => {
}),
} as unknown as CatalogApi;
render(
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRefNoTags]]}>
<MockEntityListContextProvider
value={{
@@ -18,6 +18,8 @@ import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import { EntityTagFilter } from '../../filters';
import { EntityAutocompletePicker } from '../EntityAutocompletePicker/EntityAutocompletePicker';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export type CatalogReactEntityTagPickerClassKey = 'input';
@@ -35,10 +37,11 @@ const useStyles = makeStyles(
/** @public */
export const EntityTagPicker = (props: EntityTagPickerProps) => {
const classes = useStyles();
const { t } = useTranslationRef(catalogReactTranslationRef);
return (
<EntityAutocompletePicker
label="Tags"
label={t('entityTagPickerTitle')}
name="tags"
path="metadata.tags"
Filter={EntityTagFilter}
@@ -23,7 +23,7 @@ import { catalogApiRef } from '../../api';
import { EntityKindFilter, EntityTypeFilter } from '../../filters';
import { alertApiRef } from '@backstage/core-plugin-api';
import { ApiProvider } from '@backstage/core-app-api';
import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { GetEntityFacetsResponse } from '@backstage/catalog-client';
const entities: Entity[] = [
@@ -83,7 +83,7 @@ const apis = TestApiRegistry.from(
describe('<EntityTypePicker/>', () => {
it('renders available entity types', async () => {
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{ filters: { kind: new EntityKindFilter('component') } }}
@@ -106,7 +106,7 @@ describe('<EntityTypePicker/>', () => {
it('sets the selected type filter', async () => {
const updateFilters = jest.fn();
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -137,7 +137,7 @@ describe('<EntityTypePicker/>', () => {
it('respects the query parameter filter value', async () => {
const updateFilters = jest.fn();
const queryParameters = { type: 'tool' };
await renderWithEffects(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -158,7 +158,7 @@ describe('<EntityTypePicker/>', () => {
it('responds to external queryParameters changes', async () => {
const updateFilters = jest.fn();
const rendered = await renderWithEffects(
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -20,6 +20,8 @@ import { useEntityTypeFilter } from '../../hooks/useEntityTypeFilter';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
import { Select } from '@backstage/core-components';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/**
* Props for {@link EntityTypePicker}.
@@ -37,23 +39,24 @@ export const EntityTypePicker = (props: EntityTypePickerProps) => {
const alertApi = useApi(alertApiRef);
const { error, availableTypes, selectedTypes, setSelectedTypes } =
useEntityTypeFilter();
const { t } = useTranslationRef(catalogReactTranslationRef);
useEffect(() => {
if (error) {
alertApi.post({
message: `Failed to load entity types`,
message: t('entityTypePicker.errorMessage'),
severity: 'error',
});
}
if (initialFilter) {
setSelectedTypes([initialFilter]);
}
}, [error, alertApi, initialFilter, setSelectedTypes]);
}, [error, alertApi, initialFilter, setSelectedTypes, t]);
if (availableTypes.length === 0 || error) return null;
const items = [
{ value: 'all', label: 'all' },
{ value: 'all', label: t('entityTypePicker.optionAllTitle') },
...availableTypes.map((type: string) => ({
value: type,
label: type,
@@ -63,7 +66,7 @@ export const EntityTypePicker = (props: EntityTypePickerProps) => {
return hidden ? null : (
<Box pb={1} pt={1}>
<Select
label="Type"
label={t('entityTypePicker.title')}
items={items}
selected={(items.length > 1 ? selectedTypes[0] : undefined) ?? 'all'}
onChange={value =>
@@ -22,6 +22,8 @@ import Star from '@material-ui/icons/Star';
import StarBorder from '@material-ui/icons/StarBorder';
import React, { ComponentProps } from 'react';
import { useStarredEntity } from '../../hooks/useStarredEntity';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export type FavoriteEntityProps = ComponentProps<typeof IconButton> & {
@@ -43,6 +45,7 @@ export const FavoriteEntity = (props: FavoriteEntityProps) => {
const { toggleStarredEntity, isStarredEntity } = useStarredEntity(
props.entity,
);
const { t } = useTranslationRef(catalogReactTranslationRef);
return (
<IconButton
aria-label="favorite"
@@ -51,7 +54,11 @@ export const FavoriteEntity = (props: FavoriteEntityProps) => {
onClick={() => toggleStarredEntity()}
>
<Tooltip
title={isStarredEntity ? 'Remove from favorites' : 'Add to favorites'}
title={
isStarredEntity
? t('favoriteEntity.RemoveFromFavorites')
: t('favoriteEntity.addToFavorites')
}
>
{isStarredEntity ? <YellowStar /> : <StarBorder />}
</Tooltip>
@@ -30,6 +30,8 @@ import { ColocatedPage } from './components/ColocatedPage';
import { JsonPage } from './components/JsonPage';
import { OverviewPage } from './components/OverviewPage';
import { YamlPage } from './components/YamlPage';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
const useStyles = makeStyles(theme => ({
fullHeightDialog: {
@@ -95,6 +97,7 @@ export function InspectEntityDialog(props: {
}) {
const classes = useStyles();
const [activeTab, setActiveTab] = React.useState(0);
const { t } = useTranslationRef(catalogReactTranslationRef);
useEffect(() => {
setActiveTab(0);
@@ -114,7 +117,7 @@ export function InspectEntityDialog(props: {
PaperProps={{ className: classes.fullHeightDialog }}
>
<DialogTitle id="entity-inspector-dialog-title">
Entity Inspector
{t('inspectEntityDialog.title')}
</DialogTitle>
<DialogContent dividers>
<div className={classes.root}>
@@ -152,7 +155,7 @@ export function InspectEntityDialog(props: {
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} color="primary">
Close
{t('inspectEntityDialog.closeButtonTitle')}
</Button>
</DialogActions>
</Dialog>
@@ -38,6 +38,8 @@ import { catalogApiRef } from '../../../api';
import { humanizeEntityRef } from '../../EntityRefLink';
import { entityRouteRef } from '../../../routes';
import { EntityKindIcon } from './EntityKindIcon';
import { catalogReactTranslationRef } from '../../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
const useStyles = makeStyles(theme => ({
node: {
@@ -198,6 +200,7 @@ function CustomNode({ node }: DependencyGraphTypes.RenderNodeProps<NodeType>) {
export function AncestryPage(props: { entity: Entity }) {
const { loading, error, nodes, edges } = useAncestry(props.entity);
const { t } = useTranslationRef(catalogReactTranslationRef);
if (loading) {
return <Progress />;
} else if (error) {
@@ -206,7 +209,9 @@ export function AncestryPage(props: { entity: Entity }) {
return (
<>
<DialogContentText variant="h2">Ancestry</DialogContentText>
<DialogContentText variant="h2">
{t('inspectEntityDialog.ancestryPage.title')}
</DialogContentText>
<DialogContentText gutterBottom>
This is the ancestry of entities above the current one - as in, the
chain(s) of entities down to the current one, where{' '}
@@ -32,6 +32,8 @@ import useAsync from 'react-use/esm/useAsync';
import { catalogApiRef } from '../../../api';
import { EntityRefLink } from '../../EntityRefLink';
import { KeyValueListItem, ListItemText } from './common';
import { catalogReactTranslationRef } from '../../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
const useStyles = makeStyles({
root: {
@@ -95,6 +97,7 @@ function EntityList(props: { entities: Entity[]; header?: [string, string] }) {
function Contents(props: { entity: Entity }) {
const { entity } = props;
const { t } = useTranslationRef(catalogReactTranslationRef);
const { loading, error, location, originLocation, colocatedEntities } =
useColocated(entity);
@@ -106,12 +109,14 @@ function Contents(props: { entity: Entity }) {
if (!location && !originLocation) {
return (
<Alert severity="warning">Entity had no location information.</Alert>
<Alert severity="warning">
{t('inspectEntityDialog.colocatedPage.alertNoLocation')}
</Alert>
);
} else if (!colocatedEntities?.length) {
return (
<Alert severity="info">
There were no other entities on this location.
{t('inspectEntityDialog.colocatedPage.alertNoEntity')}
</Alert>
);
}
@@ -148,13 +153,14 @@ function Contents(props: { entity: Entity }) {
export function ColocatedPage(props: { entity: Entity }) {
const classes = useStyles();
const { t } = useTranslationRef(catalogReactTranslationRef);
return (
<>
<DialogContentText variant="h2">Colocated</DialogContentText>
<DialogContentText variant="h2">
{t('inspectEntityDialog.colocatedPage.title')}
</DialogContentText>
<DialogContentText>
These are the entities that are colocated with this entity - as in, they
originated from the same data source (e.g. came from the same YAML
file), or from the same origin (e.g. the originally registered URL).
{t('inspectEntityDialog.colocatedPage.description')}
</DialogContentText>
<div className={classes.root}>
<Contents entity={props.entity} />
@@ -19,13 +19,18 @@ import { CodeSnippet } from '@backstage/core-components';
import DialogContentText from '@material-ui/core/DialogContentText';
import React from 'react';
import { sortKeys } from './util';
import { catalogReactTranslationRef } from '../../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
export function JsonPage(props: { entity: Entity }) {
const { t } = useTranslationRef(catalogReactTranslationRef);
return (
<>
<DialogContentText variant="h2">Entity as JSON</DialogContentText>
<DialogContentText variant="h2">
{t('inspectEntityDialog.jsonPage.title')}
</DialogContentText>
<DialogContentText>
This is the raw entity data as received from the catalog, on JSON form.
{t('inspectEntityDialog.jsonPage.description')}
</DialogContentText>
<DialogContentText>
<div style={{ fontSize: '75%' }} data-testid="code-snippet">
@@ -36,6 +36,8 @@ import {
} from './common';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { CopyTextButton } from '@backstage/core-components';
import { catalogReactTranslationRef } from '../../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
const useStyles = makeStyles({
root: {
@@ -59,11 +61,14 @@ export function OverviewPage(props: { entity: AlphaEntity }) {
sortBy(relations, r => r.targetRef),
'type',
);
const { t } = useTranslationRef(catalogReactTranslationRef);
const entityRef = stringifyEntityRef(props.entity);
return (
<>
<DialogContentText variant="h2">Overview</DialogContentText>
<DialogContentText variant="h2">
{t('inspectEntityDialog.overviewPage.title')}
</DialogContentText>
<div className={classes.root}>
<Container title="Identity">
<List dense>
@@ -20,13 +20,18 @@ import DialogContentText from '@material-ui/core/DialogContentText';
import React from 'react';
import YAML from 'yaml';
import { sortKeys } from './util';
import { catalogReactTranslationRef } from '../../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
export function YamlPage(props: { entity: Entity }) {
const { t } = useTranslationRef(catalogReactTranslationRef);
return (
<>
<DialogContentText variant="h2">Entity as YAML</DialogContentText>
<DialogContentText variant="h2">
{t('inspectEntityDialog.yamlPage.title')}
</DialogContentText>
<DialogContentText>
This is the raw entity data as received from the catalog, on YAML form.
{t('inspectEntityDialog.yamlPage.description')}
</DialogContentText>
<DialogContentText>
<div style={{ fontSize: '75%' }} data-testid="code-snippet">
@@ -32,6 +32,8 @@ import { useUnregisterEntityDialogState } from './useUnregisterEntityDialogState
import { alertApiRef, configApiRef, useApi } from '@backstage/core-plugin-api';
import { Progress, ResponseErrorPanel } from '@backstage/core-components';
import { assertError } from '@backstage/errors';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
const useStyles = makeStyles({
advancedButton: {
@@ -58,6 +60,7 @@ const Contents = ({
const [showDelete, setShowDelete] = useState(false);
const [busy, setBusy] = useState(false);
const appTitle = configApi.getOptionalString('app.title') ?? 'Backstage';
const { t } = useTranslationRef(catalogReactTranslationRef);
const onUnregister = useCallback(
async function onUnregisterFn() {
@@ -86,7 +89,9 @@ const Contents = ({
const entityName = entity.metadata.title ?? entity.metadata.name;
onConfirm();
alertApi.post({
message: `Removed entity ${entityName}`,
message: t('unregisterEntityDialog.deleteEntitySuccessMessage', {
entityName,
}),
severity: 'success',
display: 'transient',
});
@@ -98,13 +103,13 @@ const Contents = ({
}
}
},
[alertApi, onConfirm, state, entity],
[alertApi, onConfirm, state, entity, t],
);
const DialogActionsPanel = () => (
<DialogActions className={classes.dialogActions}>
<Button onClick={onClose} color="primary">
Cancel
{t('unregisterEntityDialog.cancelButtonTitle')}
</Button>
</DialogActions>
);
@@ -121,10 +126,10 @@ const Contents = ({
return (
<>
<Alert severity="info">
You cannot unregister this entity, since it originates from a
protected Backstage configuration (location "{state.location}"). If
you believe this is in error, please contact the {appTitle}{' '}
integrator.
{t('unregisterEntityDialog.bootstrapState.title', {
appTitle,
location: state.location,
})}
</Alert>
<Box marginTop={2}>
@@ -137,7 +142,7 @@ const Contents = ({
className={classes.advancedButton}
onClick={() => setShowDelete(true)}
>
Advanced Options
{t('unregisterEntityDialog.bootstrapState.advancedOptions')}
</Button>
<DialogActionsPanel />
</>
@@ -146,11 +151,7 @@ const Contents = ({
{showDelete && (
<>
<DialogContentText>
You have the option to delete the entity itself from the
catalog. Note that this should only be done if you know that the
catalog file has been deleted at, or moved from, its origin
location. If that is not the case, the entity will reappear
shortly as the next refresh round is performed by the catalog.
{t('unregisterEntityDialog.bootstrapState.advancedDescription')}
</DialogContentText>
<Button
variant="contained"
@@ -158,7 +159,7 @@ const Contents = ({
disabled={busy}
onClick={onDelete}
>
Delete Entity
{t('unregisterEntityDialog.deleteButtonTitle')}
</Button>
<DialogActionsPanel />
</>
@@ -172,8 +173,7 @@ const Contents = ({
return (
<>
<DialogContentText>
This entity does not seem to originate from a registered location. You
therefore only have the option to delete it outright from the catalog.
{t('unregisterEntityDialog.onlyDeleteStateTitle')}
</DialogContentText>
<Button
variant="contained"
@@ -181,7 +181,7 @@ const Contents = ({
disabled={busy}
onClick={onDelete}
>
Delete Entity
{t('unregisterEntityDialog.deleteButtonTitle')}
</Button>
<DialogActionsPanel />
</>
@@ -192,7 +192,7 @@ const Contents = ({
return (
<>
<DialogContentText>
This action will unregister the following entities:
{t('unregisterEntityDialog.unregisterState.title')}
</DialogContentText>
<DialogContentText component="ul">
{state.colocatedEntities.map(e => (
@@ -202,13 +202,15 @@ const Contents = ({
))}
</DialogContentText>
<DialogContentText>
Located at the following location:
{t('unregisterEntityDialog.unregisterState.subTitle')}
</DialogContentText>
<DialogContentText component="ul">
<li>{state.location}</li>
</DialogContentText>
<DialogContentText>
To undo, just re-register the entity in {appTitle}.
{t('unregisterEntityDialog.unregisterState.description', {
appTitle,
})}
</DialogContentText>
<Box marginTop={2}>
<Button
@@ -217,7 +219,7 @@ const Contents = ({
disabled={busy}
onClick={onUnregister}
>
Unregister Location
{t('unregisterEntityDialog.unregisterState.unregisterButtonTitle')}
</Button>
{!showDelete && (
<Box component="span" marginLeft={2}>
@@ -228,7 +230,7 @@ const Contents = ({
className={classes.advancedButton}
onClick={() => setShowDelete(true)}
>
Advanced Options
{t('unregisterEntityDialog.unregisterState.advancedOptions')}
</Button>
</Box>
)}
@@ -240,11 +242,7 @@ const Contents = ({
<Divider />
</Box>
<DialogContentText>
You also have the option to delete the entity itself from the
catalog. Note that this should only be done if you know that the
catalog file has been deleted at, or moved from, its origin
location. If that is not the case, the entity will reappear
shortly as the next refresh round is performed by the catalog.
{t('unregisterEntityDialog.unregisterState.advancedDescription')}
</DialogContentText>
<Button
variant="contained"
@@ -252,7 +250,7 @@ const Contents = ({
disabled={busy}
onClick={onDelete}
>
Delete Entity
{t('unregisterEntityDialog.deleteButtonTitle')}
</Button>
</>
)}
@@ -260,7 +258,11 @@ const Contents = ({
);
}
return <Alert severity="error">Internal error: Unknown state</Alert>;
return (
<Alert severity="error">
{t('unregisterEntityDialog.errorStateTitle')}
</Alert>
);
};
/** @public */
@@ -274,10 +276,11 @@ export type UnregisterEntityDialogProps = {
/** @public */
export const UnregisterEntityDialog = (props: UnregisterEntityDialogProps) => {
const { open, onConfirm, onClose, entity } = props;
const { t } = useTranslationRef(catalogReactTranslationRef);
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle id="responsive-dialog-title">
Are you sure you want to unregister this entity?
{t('unregisterEntityDialog.title')}
</DialogTitle>
<DialogContent>
<Contents entity={entity} onConfirm={onConfirm} onClose={onClose} />
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { fireEvent, render, waitFor, screen } from '@testing-library/react';
import { fireEvent, waitFor, screen } from '@testing-library/react';
import { UserEntity } from '@backstage/catalog-model';
import { UserListPicker, UserListPickerProps } from './UserListPicker';
import { MockEntityListContextProvider } from '../../testUtils/providers';
@@ -30,7 +30,11 @@ import {
QueryEntitiesInitialRequest,
} from '@backstage/catalog-client';
import { catalogApiRef } from '../../api';
import { MockStorageApi, TestApiRegistry } from '@backstage/test-utils';
import {
MockStorageApi,
TestApiRegistry,
renderInTestApp,
} from '@backstage/test-utils';
import { ApiProvider } from '@backstage/core-app-api';
import {
ConfigApi,
@@ -145,7 +149,7 @@ describe('<UserListPicker />', () => {
});
it('renders filter groups', async () => {
render(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider value={{}}>
<UserListPicker />
@@ -164,7 +168,7 @@ describe('<UserListPicker />', () => {
});
it('renders filters', async () => {
render(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -208,7 +212,7 @@ describe('<UserListPicker />', () => {
});
it('respects other frontend filters in counts', async () => {
render(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -246,7 +250,7 @@ describe('<UserListPicker />', () => {
it('respects the query parameter filter value', async () => {
const updateFilters = jest.fn();
const queryParameters = { user: 'owned', kind: 'component' };
render(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -288,7 +292,7 @@ describe('<UserListPicker />', () => {
it('updates user filter when a menuitem is selected', async () => {
const updateFilters = jest.fn();
render(
await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -334,7 +338,7 @@ describe('<UserListPicker />', () => {
it('responds to external queryParameters changes', async () => {
const updateFilters = jest.fn();
const rendered = render(
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<MockEntityListContextProvider
value={{
@@ -407,7 +411,7 @@ describe('<UserListPicker />', () => {
it('does not reset the filter while entities are loading', async () => {
mockCatalogApi.queryEntities?.mockReturnValue(new Promise(() => {}));
render(<Picker initialFilter="owned" />);
await renderInTestApp(<Picker initialFilter="owned" />);
await waitFor(() =>
expect(mockCatalogApi.queryEntities).toHaveBeenCalled(),
@@ -433,7 +437,7 @@ describe('<UserListPicker />', () => {
return mockQueryEntitiesImplementation(request);
});
render(<Picker initialFilter="owned" />);
await renderInTestApp(<Picker initialFilter="owned" />);
await waitFor(() =>
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3),
@@ -444,7 +448,7 @@ describe('<UserListPicker />', () => {
});
it('does not reset the filter when request is empty', async () => {
render(<Picker initialFilter="owned" filters={{}} />);
await renderInTestApp(<Picker initialFilter="owned" filters={{}} />);
await waitFor(() => {
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(1);
@@ -473,7 +477,7 @@ describe('<UserListPicker />', () => {
return mockQueryEntitiesImplementation(request);
});
render(<Picker initialFilter="owned" />);
await renderInTestApp(<Picker initialFilter="owned" />);
await waitFor(() =>
expect(updateFilters).toHaveBeenLastCalledWith({
@@ -489,7 +493,7 @@ describe('<UserListPicker />', () => {
() => new Promise(() => {}),
);
render(<Picker initialFilter="starred" />);
await renderInTestApp(<Picker initialFilter="starred" />);
await waitFor(() =>
expect(mockCatalogApi.queryEntities).toHaveBeenCalled(),
@@ -512,7 +516,7 @@ describe('<UserListPicker />', () => {
return mockQueryEntitiesImplementation(request);
});
render(<Picker initialFilter="starred" />);
await renderInTestApp(<Picker initialFilter="starred" />);
await waitFor(() =>
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3),
@@ -537,7 +541,7 @@ describe('<UserListPicker />', () => {
return mockQueryEntitiesImplementation(request);
});
render(<Picker initialFilter="starred" />);
await renderInTestApp(<Picker initialFilter="starred" />);
await waitFor(() =>
expect(updateFilters).toHaveBeenLastCalledWith({
@@ -563,7 +567,7 @@ describe('<UserListPicker />', () => {
return mockQueryEntitiesImplementation(request);
});
render(<Picker initialFilter="owned" />);
await renderInTestApp(<Picker initialFilter="owned" />);
await waitFor(() =>
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3),
@@ -574,7 +578,7 @@ describe('<UserListPicker />', () => {
});
it('does not reset the filter when entities are loaded', async () => {
render(<Picker initialFilter="owned" />);
await renderInTestApp(<Picker initialFilter="owned" />);
await waitFor(() =>
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3),
@@ -588,7 +592,7 @@ describe('<UserListPicker />', () => {
});
it('does not reset the filter when request is empty xxxx', async () => {
render(<Picker initialFilter="owned" filters={{}} />);
await renderInTestApp(<Picker initialFilter="owned" filters={{}} />);
await waitFor(() => {
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(1);
@@ -619,7 +623,7 @@ describe('<UserListPicker />', () => {
return mockQueryEntitiesImplementation(request);
});
render(<Picker initialFilter="starred" />);
await renderInTestApp(<Picker initialFilter="starred" />);
await waitFor(() =>
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3),
@@ -630,7 +634,7 @@ describe('<UserListPicker />', () => {
});
it('does not reset the filter when entities are loaded', async () => {
render(<Picker initialFilter="starred" />);
await renderInTestApp(<Picker initialFilter="starred" />);
await waitFor(() =>
expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3),
@@ -36,6 +36,11 @@ import { UserListFilterKind } from '../../types';
import { useOwnedEntitiesCount } from './useOwnedEntitiesCount';
import { useAllEntitiesCount } from './useAllEntitiesCount';
import { useStarredEntitiesCount } from './useStarredEntitiesCount';
import {
TranslationFunction,
useTranslationRef,
} from '@backstage/core-plugin-api/alpha';
import { catalogReactTranslationRef } from '../../translation';
/** @public */
export type CatalogReactUserListPickerClassKey =
@@ -83,29 +88,32 @@ export type ButtonGroup = {
}[];
};
function getFilterGroups(orgName: string | undefined): ButtonGroup[] {
function getFilterGroups(
orgName: string,
t: TranslationFunction<typeof catalogReactTranslationRef.T>,
): ButtonGroup[] {
return [
{
name: 'Personal',
name: t('userListPicker.personalFilter.title'),
items: [
{
id: 'owned',
label: 'Owned',
label: t('userListPicker.personalFilter.ownedLabel'),
icon: SettingsIcon,
},
{
id: 'starred',
label: 'Starred',
label: t('userListPicker.personalFilter.starredLabel'),
icon: StarIcon,
},
],
},
{
name: orgName ?? 'Company',
name: orgName,
items: [
{
id: 'all',
label: 'All',
label: t('userListPicker.orgFilterAllLabel'),
},
],
},
@@ -123,7 +131,10 @@ export const UserListPicker = (props: UserListPickerProps) => {
const { initialFilter, availableFilters } = props;
const classes = useStyles();
const configApi = useApi(configApiRef);
const orgName = configApi.getOptionalString('organization.name') ?? 'Company';
const { t } = useTranslationRef(catalogReactTranslationRef);
const orgName =
configApi.getOptionalString('organization.name') ??
t('userListPicker.defaultOrgName');
const {
filters,
updateFilters,
@@ -133,7 +144,7 @@ export const UserListPicker = (props: UserListPickerProps) => {
// Remove group items that aren't in availableFilters and exclude
// any now-empty groups.
const userAndGroupFilterIds = ['starred', 'all'];
const filterGroups = getFilterGroups(orgName)
const filterGroups = getFilterGroups(orgName, t)
.map(filterGroup => ({
...filterGroup,
items: filterGroup.items.filter(({ id }) =>
@@ -20,6 +20,7 @@ import {
alertApiRef,
ConfigApi,
configApiRef,
errorApiRef,
IdentityApi,
identityApiRef,
storageApiRef,
@@ -39,6 +40,8 @@ import {
} from '../filters';
import { EntityListProvider, useEntityList } from './useEntityListProvider';
import { useMountEffect } from '@react-hookz/web';
import { translationApiRef } from '@backstage/core-plugin-api/alpha';
import { MockTranslationApi } from '@backstage/test-utils/alpha';
const entities: Entity[] = [
{
@@ -110,6 +113,8 @@ const createWrapper =
[storageApiRef, MockStorageApi.create()],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[alertApiRef, { post: jest.fn() }],
[translationApiRef, MockTranslationApi.create()],
[errorApiRef, { error$: jest.fn(), post: jest.fn() }],
]}
>
<EntityListProvider pagination={options.pagination}>
+116
View File
@@ -0,0 +1,116 @@
/*
* Copyright 2024 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 catalogReactTranslationRef = createTranslationRef({
id: 'catalog-react',
messages: {
catalogFilter: {
title: 'Filters',
buttonTitle: 'Filters',
},
entityKindPicker: {
title: 'Kind',
errorMessage: 'Failed to load entity kinds',
},
entityLifecyclePickerTitle: 'Lifecycle',
entityNamespacePickerTitle: 'Namespace',
entityOwnerPickerTitle: 'Owner',
entityPeekAheadPopover: {
title: 'Drill into the entity to see all of the tags.',
emailCardAction: {
title: 'Email {{email}}',
subTitle: 'mailto {{email}}',
},
entityCardActionsTitle: 'Show details',
},
entityProcessingStatusPickerTitle: 'Processing Status',
entitySearchBarPlaceholder: 'Search',
entityTagPickerTitle: 'Tags',
entityTypePicker: {
title: 'Type',
errorMessage: 'Failed to load entity types',
optionAllTitle: 'all',
},
favoriteEntity: {
addToFavorites: 'Add to favorites',
RemoveFromFavorites: 'Remove from favorites',
},
inspectEntityDialog: {
title: 'Entity Inspector',
closeButtonTitle: 'Close',
ancestryPage: {
title: 'Ancestry',
},
colocatedPage: {
title: 'Colocated',
description:
'These are the entities that are colocated with this entity - as in, they originated from the same data source (e.g. came from the same YAML file), or from the same origin (e.g. the originally registered URL).',
alertNoLocation: 'Entity had no location information.',
alertNoEntity: 'There were no other entities on this location.',
},
jsonPage: {
title: 'Entity as JSON',
description:
'This is the raw entity data as received from the catalog, on JSON form.',
},
overviewPage: {
title: 'Overview',
},
yamlPage: {
title: 'Entity as YAML',
description:
'This is the raw entity data as received from the catalog, on YAML form.',
},
},
unregisterEntityDialog: {
title: 'Are you sure you want to unregister this entity?',
cancelButtonTitle: 'Cancel',
deleteButtonTitle: 'Delete Entity',
deleteEntitySuccessMessage: 'Removed entity {{entityName}}',
bootstrapState: {
title:
'You cannot unregister this entity, since it originates from a protected Backstage configuration (location "{{location}}"). If you believe this is in error, please contact the {{appTitle}} integrator.',
advancedDescription:
'You have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.',
advancedOptions: 'Advanced Options',
},
onlyDeleteStateTitle:
'This entity does not seem to originate from a registered location. You therefore only have the option to delete it outright from the catalog.',
unregisterState: {
title: 'This action will unregister the following entities:',
subTitle: 'Located at the following location:',
description: 'To undo, just re-register the entity in {{appTitle}}.',
unregisterButtonTitle: 'Unregister Location',
advancedOptions: 'Advanced Options',
advancedDescription:
'You also have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.',
},
errorStateTitle: 'Internal error: Unknown state',
},
userListPicker: {
defaultOrgName: 'Company',
personalFilter: {
title: 'Personal',
ownedLabel: 'Owned',
starredLabel: 'Starred',
},
orgFilterAllLabel: 'All',
},
},
});
+75
View File
@@ -19,6 +19,81 @@ export const catalogTranslationRef: TranslationRef<
{
readonly 'indexPage.title': '{{orgName}} Catalog';
readonly 'indexPage.createButtonTitle': 'Create';
readonly 'deleteEntity.description': 'This entity is not referenced by any location and is therefore not receiving updates. Click here to delete.';
readonly 'deleteEntity.cancelButtonTitle': 'Cancel';
readonly 'deleteEntity.deleteButtonTitle': 'Delete';
readonly 'deleteEntity.dialogTitle': 'Are you sure you want to delete this entity?';
readonly 'indexPage.title': '{{orgName}} Catalog';
readonly 'indexPage.createButtonTitle': 'Create';
readonly 'indexPage.supportButtonContent': 'All your software catalog entities';
readonly 'aboutCard.title': 'About';
readonly 'aboutCard.refreshButtonTitle': 'Schedule entity refresh';
readonly 'aboutCard.editButtonTitle': 'Edit Metadata';
readonly 'aboutCard.refreshScheduledMessage': 'Refresh scheduled';
readonly 'aboutCard.launchTemplate': 'Launch Template';
readonly 'aboutCard.viewTechdocs': 'View TechDocs';
readonly 'aboutCard.viewSource': 'View Source';
readonly 'aboutCard.descriptionField.value': 'No description';
readonly 'aboutCard.descriptionField.label': 'Description';
readonly 'aboutCard.ownerField.value': 'No Owner';
readonly 'aboutCard.ownerField.label': 'Owner';
readonly 'aboutCard.domainField.value': 'No Domain';
readonly 'aboutCard.domainField.label': 'Domain';
readonly 'aboutCard.systemField.value': 'No System';
readonly 'aboutCard.systemField.label': 'System';
readonly 'aboutCard.parentComponentField.value': 'No Parent Component';
readonly 'aboutCard.parentComponentField.label': 'Parent Component';
readonly 'aboutCard.typeField.label': 'Type';
readonly 'aboutCard.lifecycleField.label': 'Lifecycle';
readonly 'aboutCard.tagsField.value': 'No Tags';
readonly 'aboutCard.tagsField.label': 'Tags';
readonly 'aboutCard.targetsField.label': 'Targets';
readonly 'searchResultItem.lifecycle': 'Lifecycle';
readonly 'searchResultItem.Owner': 'Owner';
readonly 'catalogTable.warningPanelTitle': 'Could not fetch catalog entities.';
readonly 'catalogTable.viewActionTitle': 'View';
readonly 'catalogTable.editActionTitle': 'Edit';
readonly 'catalogTable.starActionTitle': 'Add to favorites';
readonly 'catalogTable.unStarActionTitle': 'Remove from favorites';
readonly 'dependencyOfComponentsCard.title': 'Dependency of components';
readonly 'dependencyOfComponentsCard.emptyMessage': 'No component depends on this component';
readonly 'dependsOnComponentsCard.title': 'Depends on components';
readonly 'dependsOnComponentsCard.emptyMessage': 'No component is a dependency of this component';
readonly 'dependsOnResourcesCard.title': 'Depends on resources';
readonly 'dependsOnResourcesCard.emptyMessage': 'No resource is a dependency of this component';
readonly 'entityContextMenu.copiedMessage': 'Copied!';
readonly 'entityContextMenu.moreButtonTitle': 'More';
readonly 'entityContextMenu.inspectMenuTitle': 'Inspect entity';
readonly 'entityContextMenu.copyURLMenuTitle': 'Copy entity URL';
readonly 'entityContextMenu.unregisterMenuTitle': 'Unregister entity';
readonly 'entityLabelsCard.title': 'Labels';
readonly 'entityLabelsCard.emptyDescription': 'No labels defined for this entity. You can add labels to your entity YAML as shown in the highlighted example below:';
readonly 'entityLabelsCard.readMoreButtonTitle': 'Read more';
readonly 'entityLabels.warningPanelTitle': 'Entity not found';
readonly 'entityLabels.ownerLabel': 'Owner';
readonly 'entityLabels.lifecycleLabel': 'Lifecycle';
readonly 'entityLinksCard.title': 'Links';
readonly 'entityLinksCard.emptyDescription': 'No links defined for this entity. You can add links to your entity YAML as shown in the highlighted example below:';
readonly 'entityLinksCard.readMoreButtonTitle': 'Read more';
readonly 'entityNotFound.title': 'Entity was not found';
readonly 'entityNotFound.description': 'Want to help us build this? Check out our Getting Started documentation.';
readonly 'entityNotFound.docButtonTitle': 'DOCS';
readonly entityProcessingErrorsDescription: 'The error below originates from';
readonly entityRelationWarningDescription: "This entity has relations to other entities, which can't be found in the catalog.\n Entities not found are: ";
readonly 'hasComponentsCard.title': 'Has components';
readonly 'hasComponentsCard.emptyMessage': 'No component is part of this system';
readonly 'hasResourcesCard.title': 'Has resources';
readonly 'hasResourcesCard.emptyMessage': 'No resource is part of this system';
readonly 'hasSubcomponentsCard.title': 'Has subcomponents';
readonly 'hasSubcomponentsCard.emptyMessage': 'No subcomponent is part of this component';
readonly 'hasSystemsCard.title': 'Has systems';
readonly 'hasSystemsCard.emptyMessage': 'No system is part of this domain';
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';
readonly 'systemDiagramCard.edgeLabels.partOf': 'part of';
readonly 'systemDiagramCard.edgeLabels.provides': 'provides';
}
>;
+1
View File
@@ -17,3 +17,4 @@
export * from './alpha/index';
export { default } from './alpha/index';
export { catalogTranslationRef } from './translation';
export * from './translation';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { fireEvent } from '@testing-library/react';
import { CardHeader } from './CardHeader';
import { ThemeProvider } from '@material-ui/core/styles';
import { lightTheme } from '@backstage/theme';
@@ -30,7 +30,7 @@ import { stringifyEntityRef } from '@backstage/catalog-model';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
describe('CardHeader', () => {
it('should select the correct theme from the theme provider from the header', () => {
it('should select the correct theme from the theme provider from the header', async () => {
// Can't really test what we want here.
// But we can check that we call the getPage theme with the right type of template at least.
const mockTheme = {
@@ -38,7 +38,7 @@ describe('CardHeader', () => {
getPageTheme: jest.fn(lightTheme.getPageTheme),
};
render(
await renderInTestApp(
<TestApiProvider
apis={[
[