Merge pull request #23392 from mario-mui/feat/add-catalog-i18n
Feat/add catalog i18n
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Support i18n for catalog and catalog-react plugins
|
||||
@@ -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 'entityLifecyclePicker.title': 'Lifecycle';
|
||||
readonly 'entityNamespacePicker.title': 'Namespace';
|
||||
readonly 'entityOwnerPicker.title': 'Owner';
|
||||
readonly 'entityProcessingStatusPicker.title': 'Processing Status';
|
||||
readonly 'entityTagPicker.title': 'Tags';
|
||||
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 'entitySearchBar.placeholder': 'Search';
|
||||
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 {
|
||||
|
||||
@@ -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))}
|
||||
|
||||
+9
-9
@@ -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={{
|
||||
|
||||
+4
-1
@@ -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('entityLifecyclePicker.title')}
|
||||
name="lifecycles"
|
||||
path="spec.lifecycle"
|
||||
Filter={EntityLifecycleFilter}
|
||||
|
||||
+33
-12
@@ -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={{
|
||||
@@ -215,7 +215,7 @@ describe('<EntityNamespacePicker/>', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
it('namespace picker is invisible if there are only 1 available option', async () => {
|
||||
it('namespace picker is visible if there are only 1 available option', async () => {
|
||||
const defaultNamespaces = ['default', 'default', 'default'];
|
||||
const mockCatalogApiRefDefaultNamespace = {
|
||||
getEntityFacets: async () => ({
|
||||
@@ -227,7 +227,28 @@ describe('<EntityNamespacePicker/>', () => {
|
||||
},
|
||||
}),
|
||||
} as unknown as CatalogApi;
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[[catalogApiRef, mockCatalogApiRefDefaultNamespace]]}
|
||||
>
|
||||
<MockEntityListContextProvider value={{}}>
|
||||
<EntityNamespacePicker />
|
||||
</MockEntityListContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText('Namespace')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
it('namespace picker is invisible if there is zero available option', async () => {
|
||||
const mockCatalogApiRefDefaultNamespace = {
|
||||
getEntityFacets: async () => ({
|
||||
facets: {
|
||||
'metadata.namespace': [],
|
||||
},
|
||||
}),
|
||||
} as unknown as CatalogApi;
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[[catalogApiRef, mockCatalogApiRefDefaultNamespace]]}
|
||||
>
|
||||
@@ -241,7 +262,7 @@ describe('<EntityNamespacePicker/>', () => {
|
||||
);
|
||||
});
|
||||
it('renders initially selected namespaces', async () => {
|
||||
render(
|
||||
renderInTestApp(
|
||||
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
|
||||
<MockEntityListContextProvider value={{}}>
|
||||
<EntityNamespacePicker
|
||||
|
||||
+5
-1
@@ -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('entityNamespacePicker.title')}
|
||||
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={{
|
||||
@@ -402,7 +402,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" />
|
||||
@@ -445,7 +445,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={{
|
||||
@@ -465,7 +465,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={{
|
||||
@@ -496,7 +496,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={{
|
||||
@@ -525,7 +525,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('entityOwnerPicker.title')}
|
||||
<Autocomplete
|
||||
PopperComponent={popperProps => (
|
||||
<div {...popperProps}>{popperProps.children as ReactNode}</div>
|
||||
|
||||
+9
-2
@@ -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>
|
||||
|
||||
+4
-1
@@ -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 />
|
||||
|
||||
+4
-2
@@ -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>
|
||||
)}
|
||||
|
||||
+12
-11
@@ -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,
|
||||
|
||||
+4
-1
@@ -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('entityProcessingStatusPicker.title')}
|
||||
<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(
|
||||
await renderInTestApp(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
|
||||
@@ -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('entitySearchBar.placeholder')}
|
||||
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('entityTagPicker.title')}
|
||||
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>
|
||||
|
||||
+6
-1
@@ -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{' '}
|
||||
|
||||
+12
-6
@@ -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">
|
||||
|
||||
+6
-1
@@ -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">
|
||||
|
||||
+33
-30
@@ -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}>
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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',
|
||||
},
|
||||
entityLifecyclePicker: {
|
||||
title: 'Lifecycle',
|
||||
},
|
||||
entityNamespacePicker: {
|
||||
title: 'Namespace',
|
||||
},
|
||||
entityOwnerPicker: {
|
||||
title: 'Owner',
|
||||
},
|
||||
entityProcessingStatusPicker: {
|
||||
title: 'Processing Status',
|
||||
},
|
||||
entityTagPicker: {
|
||||
title: 'Tags',
|
||||
},
|
||||
entityPeekAheadPopover: {
|
||||
title: 'Drill into the entity to see all of the tags.',
|
||||
emailCardAction: {
|
||||
title: 'Email {{email}}',
|
||||
subTitle: 'mailto {{email}}',
|
||||
},
|
||||
entityCardActionsTitle: 'Show details',
|
||||
},
|
||||
entitySearchBar: {
|
||||
placeholder: 'Search',
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -17,8 +17,82 @@ import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
export const catalogTranslationRef: TranslationRef<
|
||||
'catalog',
|
||||
{
|
||||
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.createSimilarButtonTitle': 'Create something similar';
|
||||
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';
|
||||
}
|
||||
>;
|
||||
|
||||
|
||||
@@ -17,3 +17,4 @@
|
||||
export * from './alpha/index';
|
||||
export { default } from './alpha/index';
|
||||
export { catalogTranslationRef } from './translation';
|
||||
export * from './translation';
|
||||
|
||||
@@ -64,6 +64,8 @@ import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common
|
||||
import { useSourceTemplateCompoundEntityRef } from './hooks';
|
||||
import { taskCreatePermission } from '@backstage/plugin-scaffolder-common/alpha';
|
||||
import { usePermission } from '@backstage/plugin-permission-react';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref';
|
||||
|
||||
@@ -120,6 +122,7 @@ export function AboutCard(props: AboutCardProps) {
|
||||
const { allowed: canRefresh } = useEntityPermission(
|
||||
catalogEntityRefreshPermission,
|
||||
);
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
const { allowed: canCreateTemplateTask } = usePermission({
|
||||
permission: taskCreatePermission,
|
||||
@@ -146,13 +149,13 @@ export function AboutCard(props: AboutCardProps) {
|
||||
}
|
||||
|
||||
const viewInSource: IconLinkVerticalProps = {
|
||||
label: 'View Source',
|
||||
label: t('aboutCard.viewSource'),
|
||||
disabled: !entitySourceLocation,
|
||||
icon: <ScmIntegrationIcon type={entitySourceLocation?.integrationType} />,
|
||||
href: entitySourceLocation?.locationTargetUrl,
|
||||
};
|
||||
const viewInTechDocs: IconLinkVerticalProps = {
|
||||
label: 'View TechDocs',
|
||||
label: t('aboutCard.viewTechdocs'),
|
||||
disabled:
|
||||
!(
|
||||
entity.metadata.annotations?.[TECHDOCS_ANNOTATION] ||
|
||||
@@ -180,7 +183,7 @@ export function AboutCard(props: AboutCardProps) {
|
||||
const Icon = app.getSystemIcon('scaffolder') ?? CreateComponentIcon;
|
||||
|
||||
const launchTemplate: IconLinkVerticalProps = {
|
||||
label: 'Launch Template',
|
||||
label: t('aboutCard.launchTemplate'),
|
||||
icon: <Icon />,
|
||||
disabled: !templateRoute || !canCreateTemplateTask,
|
||||
href:
|
||||
@@ -216,25 +219,25 @@ export function AboutCard(props: AboutCardProps) {
|
||||
try {
|
||||
await catalogApi.refreshEntity(stringifyEntityRef(entity));
|
||||
alertApi.post({
|
||||
message: 'Refresh scheduled',
|
||||
message: t('aboutCard.refreshScheduledMessage'),
|
||||
severity: 'info',
|
||||
display: 'transient',
|
||||
});
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
}, [catalogApi, alertApi, errorApi, entity]);
|
||||
}, [catalogApi, entity, alertApi, t, errorApi]);
|
||||
|
||||
return (
|
||||
<Card className={cardClass}>
|
||||
<CardHeader
|
||||
title="About"
|
||||
title={t('aboutCard.title')}
|
||||
action={
|
||||
<>
|
||||
{allowRefresh && canRefresh && (
|
||||
<IconButton
|
||||
aria-label="Refresh"
|
||||
title="Schedule entity refresh"
|
||||
title={t('aboutCard.refreshButtonTitle')}
|
||||
onClick={refreshEntity}
|
||||
>
|
||||
<CachedIcon />
|
||||
@@ -244,7 +247,7 @@ export function AboutCard(props: AboutCardProps) {
|
||||
component={Link}
|
||||
aria-label="Edit"
|
||||
disabled={!entityMetadataEditUrl}
|
||||
title="Edit Metadata"
|
||||
title={t('aboutCard.editButtonTitle')}
|
||||
to={entityMetadataEditUrl ?? '#'}
|
||||
>
|
||||
<EditIcon />
|
||||
@@ -252,7 +255,7 @@ export function AboutCard(props: AboutCardProps) {
|
||||
{sourceTemplateRef && templateRoute && (
|
||||
<IconButton
|
||||
component={Link}
|
||||
title="Create something similar"
|
||||
title={t('aboutCard.createSimilarButtonTitle')}
|
||||
to={templateRoute({
|
||||
namespace: sourceTemplateRef.namespace,
|
||||
templateName: sourceTemplateRef.name,
|
||||
|
||||
@@ -32,6 +32,8 @@ import { MarkdownContent } from '@backstage/core-components';
|
||||
import React from 'react';
|
||||
import { AboutField } from './AboutField';
|
||||
import { LinksGridList } from '../EntityLinksCard/LinksGridList';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
description: {
|
||||
@@ -76,6 +78,8 @@ function getLocationTargetHref(
|
||||
export function AboutContent(props: AboutContentProps) {
|
||||
const { entity } = props;
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
const isSystem = entity.kind.toLocaleLowerCase('en-US') === 'system';
|
||||
const isResource = entity.kind.toLocaleLowerCase('en-US') === 'resource';
|
||||
const isComponent = entity.kind.toLocaleLowerCase('en-US') === 'component';
|
||||
@@ -113,15 +117,21 @@ export function AboutContent(props: AboutContentProps) {
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
<AboutField label="Description" gridSizes={{ xs: 12 }}>
|
||||
<AboutField
|
||||
label={t('aboutCard.descriptionField.label')}
|
||||
gridSizes={{ xs: 12 }}
|
||||
>
|
||||
<MarkdownContent
|
||||
className={classes.description}
|
||||
content={entity?.metadata?.description || 'No description'}
|
||||
content={
|
||||
entity?.metadata?.description ||
|
||||
t('aboutCard.descriptionField.value')
|
||||
}
|
||||
/>
|
||||
</AboutField>
|
||||
<AboutField
|
||||
label="Owner"
|
||||
value="No Owner"
|
||||
label={t('aboutCard.ownerField.label')}
|
||||
value={t('aboutCard.ownerField.value')}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
{ownedByRelations.length > 0 && (
|
||||
@@ -130,8 +140,8 @@ export function AboutContent(props: AboutContentProps) {
|
||||
</AboutField>
|
||||
{(isSystem || partOfDomainRelations.length > 0) && (
|
||||
<AboutField
|
||||
label="Domain"
|
||||
value="No Domain"
|
||||
label={t('aboutCard.domainField.label')}
|
||||
value={t('aboutCard.domainField.value')}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
{partOfDomainRelations.length > 0 && (
|
||||
@@ -147,8 +157,8 @@ export function AboutContent(props: AboutContentProps) {
|
||||
isResource ||
|
||||
partOfSystemRelations.length > 0) && (
|
||||
<AboutField
|
||||
label="System"
|
||||
value="No System"
|
||||
label={t('aboutCard.systemField.label')}
|
||||
value={t('aboutCard.systemField.value')}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
{partOfSystemRelations.length > 0 && (
|
||||
@@ -161,8 +171,8 @@ export function AboutContent(props: AboutContentProps) {
|
||||
)}
|
||||
{isComponent && partOfComponentRelations.length > 0 && (
|
||||
<AboutField
|
||||
label="Parent Component"
|
||||
value="No Parent Component"
|
||||
label={t('aboutCard.parentComponentField.label')}
|
||||
value={t('aboutCard.parentComponentField.value')}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
<EntityRefLinks
|
||||
@@ -179,7 +189,7 @@ export function AboutContent(props: AboutContentProps) {
|
||||
isLocation ||
|
||||
typeof entity?.spec?.type === 'string') && (
|
||||
<AboutField
|
||||
label="Type"
|
||||
label={t('aboutCard.typeField.label')}
|
||||
value={entity?.spec?.type as string}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
/>
|
||||
@@ -188,22 +198,25 @@ export function AboutContent(props: AboutContentProps) {
|
||||
isComponent ||
|
||||
typeof entity?.spec?.lifecycle === 'string') && (
|
||||
<AboutField
|
||||
label="Lifecycle"
|
||||
label={t('aboutCard.lifecycleField.label')}
|
||||
value={entity?.spec?.lifecycle as string}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
/>
|
||||
)}
|
||||
<AboutField
|
||||
label="Tags"
|
||||
value="No Tags"
|
||||
label={t('aboutCard.tagsField.label')}
|
||||
value={t('aboutCard.tagsField.value')}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
{(entity?.metadata?.tags || []).map(t => (
|
||||
<Chip key={t} size="small" label={t} />
|
||||
{(entity?.metadata?.tags || []).map(tag => (
|
||||
<Chip key={tag} size="small" label={tag} />
|
||||
))}
|
||||
</AboutField>
|
||||
{isLocation && (entity?.spec?.targets || entity?.spec?.target) && (
|
||||
<AboutField label="Targets" gridSizes={{ xs: 12 }}>
|
||||
<AboutField
|
||||
label={t('aboutCard.targetsField.label')}
|
||||
gridSizes={{ xs: 12 }}
|
||||
>
|
||||
<LinksGridList
|
||||
cols={1}
|
||||
items={((entity.spec.targets as JsonArray) || [entity.spec.target])
|
||||
|
||||
@@ -69,7 +69,7 @@ export function BaseCatalogPage(props: BaseCatalogPageProps) {
|
||||
to={createComponentLink && createComponentLink()}
|
||||
/>
|
||||
)}
|
||||
<SupportButton>All your software catalog entities</SupportButton>
|
||||
<SupportButton>{t('indexPage.supportButtonContent')}</SupportButton>
|
||||
</ContentHeader>
|
||||
<EntityListProvider pagination={pagination}>
|
||||
<CatalogFilterLayout>
|
||||
|
||||
+11
-2
@@ -27,6 +27,8 @@ import {
|
||||
ResultHighlight,
|
||||
} from '@backstage/plugin-search-common';
|
||||
import { HighlightedSearchResultText } from '@backstage/plugin-search-react';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
const useStyles = makeStyles(
|
||||
{
|
||||
@@ -66,6 +68,7 @@ export function CatalogSearchResultListItem(
|
||||
const highlight = props.highlight as ResultHighlight;
|
||||
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
if (!result) return null;
|
||||
|
||||
@@ -121,10 +124,16 @@ export function CatalogSearchResultListItem(
|
||||
{result.kind && <Chip label={`Kind: ${result.kind}`} size="small" />}
|
||||
{result.type && <Chip label={`Type: ${result.type}`} size="small" />}
|
||||
{result.lifecycle && (
|
||||
<Chip label={`Lifecycle: ${result.lifecycle}`} size="small" />
|
||||
<Chip
|
||||
label={`${t('searchResultItem.lifecycle')}: ${result.lifecycle}`}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
{result.owner && (
|
||||
<Chip label={`Owner: ${result.owner}`} size="small" />
|
||||
<Chip
|
||||
label={`${t('searchResultItem.Owner')}: ${result.owner}`}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
@@ -48,6 +48,8 @@ import { columnFactories } from './columns';
|
||||
import { CatalogTableColumnsFunc, CatalogTableRow } from './types';
|
||||
import { PaginatedCatalogTable } from './PaginatedCatalogTable';
|
||||
import { defaultCatalogTableColumnsFunc } from './defaultCatalogTableColumnsFunc';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
|
||||
/**
|
||||
* Props for {@link CatalogTable}.
|
||||
@@ -96,13 +98,14 @@ export const CatalogTable = (props: CatalogTableProps) => {
|
||||
typeof columns === 'function' ? columns(entityListContext) : columns,
|
||||
[columns, entityListContext],
|
||||
);
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
<WarningPanel
|
||||
severity="error"
|
||||
title="Could not fetch catalog entities."
|
||||
title={t('catalogTable.warningPanelTitle')}
|
||||
>
|
||||
<CodeSnippet language="text" text={error.toString()} />
|
||||
</WarningPanel>
|
||||
@@ -113,7 +116,7 @@ export const CatalogTable = (props: CatalogTableProps) => {
|
||||
const defaultActions: TableProps<CatalogTableRow>['actions'] = [
|
||||
({ entity }) => {
|
||||
const url = entity.metadata.annotations?.[ANNOTATION_VIEW_URL];
|
||||
const title = 'View';
|
||||
const title = t('catalogTable.viewActionTitle');
|
||||
|
||||
return {
|
||||
icon: () => (
|
||||
@@ -132,7 +135,7 @@ export const CatalogTable = (props: CatalogTableProps) => {
|
||||
},
|
||||
({ entity }) => {
|
||||
const url = entity.metadata.annotations?.[ANNOTATION_EDIT_URL];
|
||||
const title = 'Edit';
|
||||
const title = t('catalogTable.editActionTitle');
|
||||
|
||||
return {
|
||||
icon: () => (
|
||||
@@ -151,7 +154,9 @@ export const CatalogTable = (props: CatalogTableProps) => {
|
||||
},
|
||||
({ entity }) => {
|
||||
const isStarred = isStarredEntity(entity);
|
||||
const title = isStarred ? 'Remove from favorites' : 'Add to favorites';
|
||||
const title = isStarred
|
||||
? t('catalogTable.unStarActionTitle')
|
||||
: t('catalogTable.starActionTitle');
|
||||
|
||||
return {
|
||||
cellStyle: { paddingLeft: '1em' },
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { ReactNode } from 'react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import { PaginatedCatalogTable } from './PaginatedCatalogTable';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { CatalogTableRow } from './types';
|
||||
@@ -63,8 +63,8 @@ describe('PaginatedCatalogTable', () => {
|
||||
);
|
||||
};
|
||||
|
||||
it('should display all the items', () => {
|
||||
render(
|
||||
it('should display all the items', async () => {
|
||||
await renderInTestApp(
|
||||
wrapInContext(<PaginatedCatalogTable data={data} columns={columns} />),
|
||||
);
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('PaginatedCatalogTable', () => {
|
||||
});
|
||||
|
||||
it('should display and invoke the next button', async () => {
|
||||
const { rerender } = render(
|
||||
const { rerender } = await renderInTestApp(
|
||||
wrapInContext(
|
||||
<PaginatedCatalogTable
|
||||
data={data}
|
||||
@@ -106,7 +106,7 @@ describe('PaginatedCatalogTable', () => {
|
||||
});
|
||||
|
||||
it('should display and invoke the prev button', async () => {
|
||||
const { rerender } = render(
|
||||
const { rerender } = await renderInTestApp(
|
||||
wrapInContext(
|
||||
<PaginatedCatalogTable
|
||||
data={data}
|
||||
|
||||
+8
-2
@@ -23,6 +23,8 @@ import {
|
||||
componentEntityHelpLink,
|
||||
RelatedEntitiesCard,
|
||||
} from '../RelatedEntitiesCard';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @public */
|
||||
export interface DependencyOfComponentsCardProps {
|
||||
@@ -33,7 +35,11 @@ export interface DependencyOfComponentsCardProps {
|
||||
export function DependencyOfComponentsCard(
|
||||
props: DependencyOfComponentsCardProps,
|
||||
) {
|
||||
const { variant = 'gridItem', title = 'Dependency of components' } = props;
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
const {
|
||||
variant = 'gridItem',
|
||||
title = t('dependencyOfComponentsCard.title'),
|
||||
} = props;
|
||||
return (
|
||||
<RelatedEntitiesCard
|
||||
variant={variant}
|
||||
@@ -41,7 +47,7 @@ export function DependencyOfComponentsCard(
|
||||
entityKind="Component"
|
||||
relationType={RELATION_DEPENDENCY_OF}
|
||||
columns={componentEntityColumns}
|
||||
emptyMessage="No component depends on this component"
|
||||
emptyMessage={t('dependencyOfComponentsCard.emptyMessage')}
|
||||
emptyHelpLink={componentEntityHelpLink}
|
||||
asRenderableEntities={asComponentEntities}
|
||||
/>
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
componentEntityHelpLink,
|
||||
RelatedEntitiesCard,
|
||||
} from '../RelatedEntitiesCard';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @public */
|
||||
export interface DependsOnComponentsCardProps {
|
||||
@@ -37,9 +39,10 @@ export interface DependsOnComponentsCardProps {
|
||||
}
|
||||
|
||||
export function DependsOnComponentsCard(props: DependsOnComponentsCardProps) {
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
const {
|
||||
variant = 'gridItem',
|
||||
title = 'Depends on components',
|
||||
title = t('dependsOnComponentsCard.title'),
|
||||
columns = componentEntityColumns,
|
||||
tableOptions = {},
|
||||
} = props;
|
||||
@@ -50,7 +53,7 @@ export function DependsOnComponentsCard(props: DependsOnComponentsCardProps) {
|
||||
entityKind="Component"
|
||||
relationType={RELATION_DEPENDS_ON}
|
||||
columns={columns}
|
||||
emptyMessage="No component is a dependency of this component"
|
||||
emptyMessage={t('dependsOnComponentsCard.emptyMessage')}
|
||||
emptyHelpLink={componentEntityHelpLink}
|
||||
asRenderableEntities={asComponentEntities}
|
||||
tableOptions={tableOptions}
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
RelatedEntitiesCard,
|
||||
resourceEntityColumns,
|
||||
} from '../RelatedEntitiesCard';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @public */
|
||||
export interface DependsOnResourcesCardProps {
|
||||
@@ -37,9 +39,10 @@ export interface DependsOnResourcesCardProps {
|
||||
}
|
||||
|
||||
export function DependsOnResourcesCard(props: DependsOnResourcesCardProps) {
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
const {
|
||||
variant = 'gridItem',
|
||||
title = 'Depends on resources',
|
||||
title = t('dependsOnResourcesCard.title'),
|
||||
columns = resourceEntityColumns,
|
||||
tableOptions = {},
|
||||
} = props;
|
||||
@@ -50,7 +53,7 @@ export function DependsOnResourcesCard(props: DependsOnResourcesCardProps) {
|
||||
entityKind="Resource"
|
||||
relationType={RELATION_DEPENDS_ON}
|
||||
columns={columns}
|
||||
emptyMessage="No resource is a dependency of this component"
|
||||
emptyMessage={t('dependsOnResourcesCard.emptyMessage')}
|
||||
emptyHelpLink={componentEntityHelpLink}
|
||||
asRenderableEntities={asResourceEntities}
|
||||
tableOptions={tableOptions}
|
||||
|
||||
@@ -33,6 +33,8 @@ import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common/
|
||||
import { UnregisterEntity, UnregisterEntityOptions } from './UnregisterEntity';
|
||||
import { useApi, alertApiRef } from '@backstage/core-plugin-api';
|
||||
import useCopyToClipboard from 'react-use/esm/useCopyToClipboard';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @public */
|
||||
export type EntityContextMenuClassKey = 'button';
|
||||
@@ -70,6 +72,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) {
|
||||
onUnregisterEntity,
|
||||
onInspectEntity,
|
||||
} = props;
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>();
|
||||
const classes = useStyles();
|
||||
const unregisterPermission = useEntityPermission(
|
||||
@@ -90,12 +93,12 @@ export function EntityContextMenu(props: EntityContextMenuProps) {
|
||||
useEffect(() => {
|
||||
if (!copyState.error && copyState.value) {
|
||||
alertApi.post({
|
||||
message: 'Copied!',
|
||||
message: t('entityContextMenu.copiedMessage'),
|
||||
severity: 'info',
|
||||
display: 'transient',
|
||||
});
|
||||
}
|
||||
}, [copyState, alertApi]);
|
||||
}, [copyState, alertApi, t]);
|
||||
|
||||
const extraItems = UNSTABLE_extraContextMenuItems && [
|
||||
...UNSTABLE_extraContextMenuItems.map(item => (
|
||||
@@ -117,7 +120,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="More" arrow>
|
||||
<Tooltip title={t('entityContextMenu.moreButtonTitle')} arrow>
|
||||
<IconButton
|
||||
aria-label="more"
|
||||
aria-controls="long-menu"
|
||||
@@ -157,7 +160,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) {
|
||||
<ListItemIcon>
|
||||
<BugReportIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Inspect entity" />
|
||||
<ListItemText primary={t('entityContextMenu.inspectMenuTitle')} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
@@ -168,7 +171,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) {
|
||||
<ListItemIcon>
|
||||
<FileCopyTwoToneIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Copy entity URL" />
|
||||
<ListItemText primary={t('entityContextMenu.copyURLMenuTitle')} />
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Popover>
|
||||
|
||||
@@ -19,6 +19,8 @@ import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import CancelIcon from '@material-ui/icons/Cancel';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
type VisibleType = 'visible' | 'hidden' | 'disable';
|
||||
|
||||
@@ -40,6 +42,7 @@ export function UnregisterEntity(props: UnregisterEntityProps) {
|
||||
onUnregisterEntity,
|
||||
onClose,
|
||||
} = props;
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
const isBoolean =
|
||||
typeof unregisterEntityOptions?.disableUnregister === 'boolean';
|
||||
@@ -65,7 +68,7 @@ export function UnregisterEntity(props: UnregisterEntityProps) {
|
||||
<ListItemIcon>
|
||||
<CancelIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Unregister entity" />
|
||||
<ListItemText primary={t('entityContextMenu.unregisterMenuTitle')} />
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
import { EntityLabelsEmptyState } from './EntityLabelsEmptyState';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @public */
|
||||
export interface EntityLabelsCardProps {
|
||||
@@ -42,6 +44,7 @@ export const EntityLabelsCard = (props: EntityLabelsCardProps) => {
|
||||
const { variant, title } = props;
|
||||
const { entity } = useEntity();
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
const columns: TableColumn<{ key: string; value: string }>[] = [
|
||||
{
|
||||
@@ -61,7 +64,7 @@ export const EntityLabelsCard = (props: EntityLabelsCardProps) => {
|
||||
const labels = entity?.metadata?.labels;
|
||||
|
||||
return (
|
||||
<InfoCard title={title || 'Labels'} variant={variant}>
|
||||
<InfoCard title={title || t('entityLabelsCard.title')} variant={variant}>
|
||||
{!labels || Object.keys(labels).length === 0 ? (
|
||||
<EntityLabelsEmptyState />
|
||||
) : (
|
||||
|
||||
@@ -19,6 +19,8 @@ import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
import { CodeSnippet } from '@backstage/core-components';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
const ENTITY_YAML = `metadata:
|
||||
name: example
|
||||
@@ -39,12 +41,12 @@ const useStyles = makeStyles(
|
||||
|
||||
export function EntityLabelsEmptyState() {
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1">
|
||||
No labels defined for this entity. You can add labels to your entity
|
||||
YAML as shown in the highlighted example below:
|
||||
{t('entityLabelsCard.emptyDescription')}
|
||||
</Typography>
|
||||
<div className={classes.code}>
|
||||
<CodeSnippet
|
||||
@@ -61,7 +63,7 @@ export function EntityLabelsEmptyState() {
|
||||
target="_blank"
|
||||
href="https://backstage.io/docs/features/software-catalog/descriptor-format#labels-optional"
|
||||
>
|
||||
Read more
|
||||
{t('entityLabelsCard.readMoreButtonTitle')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -53,6 +53,8 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
|
||||
import { rootRouteRef, unregisterRedirectRouteRef } from '../../routes';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @public */
|
||||
export type EntityLayoutRouteProps = {
|
||||
@@ -117,11 +119,12 @@ function headerProps(
|
||||
function EntityLabels(props: { entity: Entity }) {
|
||||
const { entity } = props;
|
||||
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
return (
|
||||
<>
|
||||
{ownedByRelations.length > 0 && (
|
||||
<HeaderLabel
|
||||
label="Owner"
|
||||
label={t('entityLabels.ownerLabel')}
|
||||
contentTypograpyRootComponent="p"
|
||||
value={
|
||||
<EntityRefLinks
|
||||
@@ -134,7 +137,7 @@ function EntityLabels(props: { entity: Entity }) {
|
||||
)}
|
||||
{entity.spec?.lifecycle && (
|
||||
<HeaderLabel
|
||||
label="Lifecycle"
|
||||
label={t('entityLabels.lifecycleLabel')}
|
||||
value={entity.spec.lifecycle?.toString()}
|
||||
/>
|
||||
)}
|
||||
@@ -234,6 +237,7 @@ export const EntityLayout = (props: EntityLayoutProps) => {
|
||||
const navigate = useNavigate();
|
||||
const catalogRoute = useRouteRef(rootRouteRef);
|
||||
const unregisterRedirectRoute = useRouteRef(unregisterRedirectRouteRef);
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
const cleanUpAfterRemoval = async () => {
|
||||
setConfirmationDialogOpen(false);
|
||||
@@ -286,7 +290,7 @@ export const EntityLayout = (props: EntityLayoutProps) => {
|
||||
{NotFoundComponent ? (
|
||||
NotFoundComponent
|
||||
) : (
|
||||
<WarningPanel title="Entity not found">
|
||||
<WarningPanel title={t('entityLabels.warningPanelTitle')}>
|
||||
There is no {kind} with the requested{' '}
|
||||
<Link to="https://backstage.io/docs/features/software-catalog/references">
|
||||
kind, namespace, and name
|
||||
|
||||
@@ -22,6 +22,8 @@ import { LinksGridList } from './LinksGridList';
|
||||
import { ColumnBreakpoints } from './types';
|
||||
import { IconComponent, useApp } from '@backstage/core-plugin-api';
|
||||
import { InfoCard, InfoCardVariants } from '@backstage/core-components';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
|
||||
/** @public */
|
||||
export interface EntityLinksCardProps {
|
||||
@@ -33,6 +35,7 @@ export const EntityLinksCard = (props: EntityLinksCardProps) => {
|
||||
const { cols = undefined, variant } = props;
|
||||
const { entity } = useEntity();
|
||||
const app = useApp();
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
const iconResolver = (key?: string): IconComponent =>
|
||||
key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon;
|
||||
@@ -40,7 +43,7 @@ export const EntityLinksCard = (props: EntityLinksCardProps) => {
|
||||
const links = entity?.metadata?.links;
|
||||
|
||||
return (
|
||||
<InfoCard title="Links" variant={variant}>
|
||||
<InfoCard title={t('entityLinksCard.title')} variant={variant}>
|
||||
{!links || links.length === 0 ? (
|
||||
<EntityLinksEmptyState />
|
||||
) : (
|
||||
|
||||
@@ -19,6 +19,8 @@ import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
import { CodeSnippet } from '@backstage/core-components';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
|
||||
const ENTITY_YAML = `metadata:
|
||||
name: example
|
||||
@@ -44,12 +46,12 @@ const useStyles = makeStyles(
|
||||
|
||||
export function EntityLinksEmptyState() {
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1">
|
||||
No links defined for this entity. You can add links to your entity YAML
|
||||
as shown in the highlighted example below:
|
||||
{t('entityLinksCard.emptyDescription')}
|
||||
</Typography>
|
||||
<div className={classes.code}>
|
||||
<CodeSnippet
|
||||
@@ -66,7 +68,7 @@ export function EntityLinksEmptyState() {
|
||||
target="_blank"
|
||||
href="https://backstage.io/docs/features/software-catalog/descriptor-format#links-optional"
|
||||
>
|
||||
Read more
|
||||
{t('entityLinksCard.readMoreButtonTitle')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,8 @@ import Button from '@material-ui/core/Button';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Illo } from './Illo';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
@@ -45,24 +47,24 @@ const useStyles = makeStyles(theme => ({
|
||||
|
||||
export function EntityNotFound() {
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
return (
|
||||
<Grid container spacing={0} className={classes.container}>
|
||||
<Illo />
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="h2" className={classes.title}>
|
||||
Entity was not found
|
||||
{t('entityNotFound.title')}
|
||||
</Typography>
|
||||
<Typography variant="body1" className={classes.body}>
|
||||
Want to help us build this? Check out our Getting Started
|
||||
documentation.
|
||||
{t('entityNotFound.description')}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href="https://backstage.io/docs"
|
||||
>
|
||||
DOCS
|
||||
{t('entityNotFound.docButtonTitle')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -23,6 +23,8 @@ import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import React, { useState } from 'react';
|
||||
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { assertError } from '@backstage/errors';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
interface DeleteEntityDialogProps {
|
||||
open: boolean;
|
||||
@@ -36,6 +38,7 @@ export function DeleteEntityDialog(props: DeleteEntityDialogProps) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
const onDelete = async () => {
|
||||
setBusy(true);
|
||||
@@ -54,7 +57,7 @@ export function DeleteEntityDialog(props: DeleteEntityDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle id="responsive-dialog-title">
|
||||
Are you sure you want to delete this entity?
|
||||
{t('deleteEntity.dialogTitle')}
|
||||
</DialogTitle>
|
||||
<DialogActions>
|
||||
<Button
|
||||
@@ -63,10 +66,10 @@ export function DeleteEntityDialog(props: DeleteEntityDialogProps) {
|
||||
disabled={busy}
|
||||
onClick={onDelete}
|
||||
>
|
||||
Delete
|
||||
{t('deleteEntity.deleteButtonTitle')}
|
||||
</Button>
|
||||
<Button onClick={onClose} color="primary">
|
||||
Cancel
|
||||
{t('deleteEntity.cancelButtonTitle')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -22,6 +22,8 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { DeleteEntityDialog } from './DeleteEntityDialog';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { rootRouteRef } from '../../routes';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/**
|
||||
* Returns true if the given entity has the orphan annotation given by the
|
||||
@@ -44,6 +46,7 @@ export function EntityOrphanWarning() {
|
||||
const catalogLink = useRouteRef(rootRouteRef);
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const { entity } = useEntity();
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
const cleanUpAfterRemoval = async () => {
|
||||
setConfirmationDialogOpen(false);
|
||||
@@ -53,8 +56,7 @@ export function EntityOrphanWarning() {
|
||||
return (
|
||||
<>
|
||||
<Alert severity="warning" onClick={() => setConfirmationDialogOpen(true)}>
|
||||
This entity is not referenced by any location and is therefore not
|
||||
receiving updates. Click here to delete.
|
||||
{t('deleteEntity.description')}
|
||||
</Alert>
|
||||
<DeleteEntityDialog
|
||||
open={confirmationDialogOpen}
|
||||
|
||||
+4
-1
@@ -31,6 +31,8 @@ import {
|
||||
import { useApi, ApiHolder } from '@backstage/core-plugin-api';
|
||||
import useAsync from 'react-use/esm/useAsync';
|
||||
import { SerializedError } from '@backstage/errors';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
const errorFilter = (i: EntityStatusItem) =>
|
||||
i.error &&
|
||||
@@ -95,6 +97,7 @@ export function EntityProcessingErrorsPanel() {
|
||||
const { loading, error, value } = useAsync(async () => {
|
||||
return getOwnAndAncestorsErrors(entityRef, catalogApi);
|
||||
}, [entityRef, catalogApi]);
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -115,7 +118,7 @@ export function EntityProcessingErrorsPanel() {
|
||||
{stringifyEntityRef(entity) !==
|
||||
stringifyEntityRef(ancestorError.entity) && (
|
||||
<Box p={1}>
|
||||
The error below originates from{' '}
|
||||
{t('entityProcessingErrorsDescription')}{' '}
|
||||
<EntityRefLink entityRef={ancestorError.entity} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -26,6 +26,8 @@ import useAsync from 'react-use/esm/useAsync';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import { ResponseErrorPanel } from '@backstage/core-components';
|
||||
import { useApi, ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
async function getRelationWarnings(entity: Entity, catalogApi: CatalogApi) {
|
||||
const entityRefRelations = entity.relations?.map(
|
||||
@@ -80,6 +82,7 @@ export function EntityRelationWarning() {
|
||||
const { loading, error, value } = useAsync(async () => {
|
||||
return getRelationWarnings(entity, catalogApi);
|
||||
}, [entity, catalogApi]);
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -95,10 +98,8 @@ export function EntityRelationWarning() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert severity="warning">
|
||||
This entity has relations to other entities, which can't be found in the
|
||||
catalog. <br />
|
||||
Entities not found are: {value.join(', ')}
|
||||
<Alert severity="warning" style={{ whiteSpace: 'pre-line' }}>
|
||||
{t('entityRelationWarningDescription')} {value.join(', ')}
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
componentEntityHelpLink,
|
||||
RelatedEntitiesCard,
|
||||
} from '../RelatedEntitiesCard';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @public */
|
||||
export interface HasComponentsCardProps {
|
||||
@@ -31,7 +33,8 @@ export interface HasComponentsCardProps {
|
||||
}
|
||||
|
||||
export function HasComponentsCard(props: HasComponentsCardProps) {
|
||||
const { variant = 'gridItem', title = 'Has components' } = props;
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
const { variant = 'gridItem', title = t('hasComponentsCard.title') } = props;
|
||||
return (
|
||||
<RelatedEntitiesCard
|
||||
variant={variant}
|
||||
@@ -39,7 +42,7 @@ export function HasComponentsCard(props: HasComponentsCardProps) {
|
||||
entityKind="Component"
|
||||
relationType={RELATION_HAS_PART}
|
||||
columns={componentEntityColumns}
|
||||
emptyMessage="No component is part of this system"
|
||||
emptyMessage={t('hasComponentsCard.emptyMessage')}
|
||||
emptyHelpLink={componentEntityHelpLink}
|
||||
asRenderableEntities={asComponentEntities}
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
resourceEntityColumns,
|
||||
resourceEntityHelpLink,
|
||||
} from '../RelatedEntitiesCard';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @public */
|
||||
export interface HasResourcesCardProps {
|
||||
@@ -31,7 +33,8 @@ export interface HasResourcesCardProps {
|
||||
}
|
||||
|
||||
export function HasResourcesCard(props: HasResourcesCardProps) {
|
||||
const { variant = 'gridItem', title = 'Has resources' } = props;
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
const { variant = 'gridItem', title = t('hasResourcesCard.title') } = props;
|
||||
return (
|
||||
<RelatedEntitiesCard
|
||||
variant={variant}
|
||||
@@ -40,7 +43,7 @@ export function HasResourcesCard(props: HasResourcesCardProps) {
|
||||
relationType={RELATION_HAS_PART}
|
||||
columns={resourceEntityColumns}
|
||||
asRenderableEntities={asResourceEntities}
|
||||
emptyMessage="No resource is part of this system"
|
||||
emptyMessage={t('hasResourcesCard.emptyMessage')}
|
||||
emptyHelpLink={resourceEntityHelpLink}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
componentEntityColumns,
|
||||
RelatedEntitiesCard,
|
||||
} from '../RelatedEntitiesCard';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @public */
|
||||
export interface HasSubcomponentsCardProps {
|
||||
@@ -31,10 +33,11 @@ export interface HasSubcomponentsCardProps {
|
||||
}
|
||||
|
||||
export function HasSubcomponentsCard(props: HasSubcomponentsCardProps) {
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
const {
|
||||
variant = 'gridItem',
|
||||
tableOptions = {},
|
||||
title = 'Has subcomponents',
|
||||
title = t('hasSubcomponentsCard.title'),
|
||||
} = props;
|
||||
return (
|
||||
<RelatedEntitiesCard
|
||||
@@ -44,7 +47,7 @@ export function HasSubcomponentsCard(props: HasSubcomponentsCardProps) {
|
||||
relationType={RELATION_HAS_PART}
|
||||
columns={componentEntityColumns}
|
||||
asRenderableEntities={asComponentEntities}
|
||||
emptyMessage="No subcomponent is part of this component"
|
||||
emptyMessage={t('hasSubcomponentsCard.emptyMessage')}
|
||||
emptyHelpLink="https://backstage.io/docs/features/software-catalog/descriptor-format#specsubcomponentof-optional"
|
||||
tableOptions={tableOptions}
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
systemEntityColumns,
|
||||
systemEntityHelpLink,
|
||||
} from '../RelatedEntitiesCard';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @public */
|
||||
export interface HasSystemsCardProps {
|
||||
@@ -31,7 +33,8 @@ export interface HasSystemsCardProps {
|
||||
}
|
||||
|
||||
export function HasSystemsCard(props: HasSystemsCardProps) {
|
||||
const { variant = 'gridItem', title = 'Has systems' } = props;
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
const { variant = 'gridItem', title = t('hasSystemsCard.title') } = props;
|
||||
return (
|
||||
<RelatedEntitiesCard
|
||||
variant={variant}
|
||||
@@ -40,7 +43,7 @@ export function HasSystemsCard(props: HasSystemsCardProps) {
|
||||
relationType={RELATION_HAS_PART}
|
||||
columns={systemEntityColumns}
|
||||
asRenderableEntities={asSystemEntities}
|
||||
emptyMessage="No system is part of this domain"
|
||||
emptyMessage={t('hasSystemsCard.emptyMessage')}
|
||||
emptyHelpLink={systemEntityHelpLink}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
TableColumn,
|
||||
TableOptions,
|
||||
} from '@backstage/core-components';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @public */
|
||||
export type RelatedEntitiesCardProps<T extends Entity> = {
|
||||
@@ -72,6 +74,7 @@ export function RelatedEntitiesCard<T extends Entity>(
|
||||
tableOptions = {},
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
const { entity } = useEntity();
|
||||
const { entities, loading, error } = useRelatedEntities(entity, {
|
||||
type: relationType,
|
||||
@@ -102,7 +105,9 @@ export function RelatedEntitiesCard<T extends Entity>(
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Typography variant="body1">{emptyMessage}</Typography>
|
||||
<Typography variant="body2">
|
||||
<Link to={emptyHelpLink}>Learn how to change this.</Link>
|
||||
<Link to={emptyHelpLink}>
|
||||
{t('relatedEntitiesCard.emptyHelpLinkTitle')}
|
||||
</Link>
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ import {
|
||||
} from '@backstage/core-components';
|
||||
|
||||
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { catalogTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @public */
|
||||
export type SystemDiagramCardClassKey =
|
||||
@@ -164,6 +166,7 @@ function RenderNode(props: DependencyGraphTypes.RenderNodeProps<any>) {
|
||||
export function SystemDiagramCard() {
|
||||
const { entity } = useEntity();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
const currentSystemName = entity.metadata.name;
|
||||
const currentSystemNode = stringifyEntityRef(entity);
|
||||
const systemNodes = new Array<{ id: string; kind: string; name: string }>();
|
||||
@@ -211,7 +214,7 @@ export function SystemDiagramCard() {
|
||||
systemEdges.push({
|
||||
from: currentSystemNode,
|
||||
to: stringifyEntityRef(foundDomain),
|
||||
label: 'part of',
|
||||
label: t('systemDiagramCard.edgeLabels.partOf'),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -235,7 +238,7 @@ export function SystemDiagramCard() {
|
||||
systemEdges.push({
|
||||
from: stringifyEntityRef(catalogItem),
|
||||
to: stringifyEntityRef(foundRelation),
|
||||
label: 'part of',
|
||||
label: t('systemDiagramCard.edgeLabels.partOf'),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -247,7 +250,7 @@ export function SystemDiagramCard() {
|
||||
systemEdges.push({
|
||||
from: stringifyEntityRef(catalogItem),
|
||||
to: stringifyEntityRef(foundRelation),
|
||||
label: 'provides',
|
||||
label: t('systemDiagramCard.edgeLabels.provides'),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -259,7 +262,7 @@ export function SystemDiagramCard() {
|
||||
systemEdges.push({
|
||||
from: stringifyEntityRef(catalogItem),
|
||||
to: stringifyEntityRef(foundRelation),
|
||||
label: 'depends on',
|
||||
label: t('systemDiagramCard.edgeLabels.dependsOn'),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -272,7 +275,7 @@ export function SystemDiagramCard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<InfoCard title="System Diagram">
|
||||
<InfoCard title={t('systemDiagramCard.title')}>
|
||||
<DependencyGraph
|
||||
nodes={systemNodes}
|
||||
edges={systemEdges}
|
||||
@@ -287,8 +290,8 @@ export function SystemDiagramCard() {
|
||||
variant="caption"
|
||||
style={{ display: 'block', textAlign: 'right' }}
|
||||
>
|
||||
<ZoomOutMap style={{ verticalAlign: 'bottom' }} /> Use pinch & zoom
|
||||
to move around the diagram.
|
||||
<ZoomOutMap style={{ verticalAlign: 'bottom' }} />
|
||||
{t('systemDiagramCard.description')}
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,141 @@ export const catalogTranslationRef = createTranslationRef({
|
||||
indexPage: {
|
||||
title: `{{orgName}} Catalog`,
|
||||
createButtonTitle: 'Create',
|
||||
supportButtonContent: 'All your software catalog entities',
|
||||
},
|
||||
aboutCard: {
|
||||
title: 'About',
|
||||
refreshButtonTitle: 'Schedule entity refresh',
|
||||
editButtonTitle: 'Edit Metadata',
|
||||
createSimilarButtonTitle: 'Create something similar',
|
||||
refreshScheduledMessage: 'Refresh scheduled',
|
||||
launchTemplate: 'Launch Template',
|
||||
viewTechdocs: 'View TechDocs',
|
||||
viewSource: 'View Source',
|
||||
descriptionField: {
|
||||
label: 'Description',
|
||||
value: 'No description',
|
||||
},
|
||||
ownerField: {
|
||||
label: 'Owner',
|
||||
value: 'No Owner',
|
||||
},
|
||||
domainField: {
|
||||
label: 'Domain',
|
||||
value: 'No Domain',
|
||||
},
|
||||
systemField: {
|
||||
label: 'System',
|
||||
value: 'No System',
|
||||
},
|
||||
parentComponentField: {
|
||||
label: 'Parent Component',
|
||||
value: 'No Parent Component',
|
||||
},
|
||||
typeField: {
|
||||
label: 'Type',
|
||||
},
|
||||
lifecycleField: {
|
||||
label: 'Lifecycle',
|
||||
},
|
||||
tagsField: {
|
||||
label: 'Tags',
|
||||
value: 'No Tags',
|
||||
},
|
||||
targetsField: {
|
||||
label: 'Targets',
|
||||
},
|
||||
},
|
||||
searchResultItem: {
|
||||
lifecycle: 'Lifecycle',
|
||||
Owner: 'Owner',
|
||||
},
|
||||
catalogTable: {
|
||||
warningPanelTitle: 'Could not fetch catalog entities.',
|
||||
viewActionTitle: 'View',
|
||||
editActionTitle: 'Edit',
|
||||
starActionTitle: 'Add to favorites',
|
||||
unStarActionTitle: 'Remove from favorites',
|
||||
},
|
||||
dependencyOfComponentsCard: {
|
||||
title: 'Dependency of components',
|
||||
emptyMessage: 'No component depends on this component',
|
||||
},
|
||||
dependsOnComponentsCard: {
|
||||
title: 'Depends on components',
|
||||
emptyMessage: 'No component is a dependency of this component',
|
||||
},
|
||||
dependsOnResourcesCard: {
|
||||
title: 'Depends on resources',
|
||||
emptyMessage: 'No resource is a dependency of this component',
|
||||
},
|
||||
entityContextMenu: {
|
||||
copiedMessage: 'Copied!',
|
||||
moreButtonTitle: 'More',
|
||||
inspectMenuTitle: 'Inspect entity',
|
||||
copyURLMenuTitle: 'Copy entity URL',
|
||||
unregisterMenuTitle: 'Unregister entity',
|
||||
},
|
||||
entityLabelsCard: {
|
||||
title: 'Labels',
|
||||
emptyDescription:
|
||||
'No labels defined for this entity. You can add labels to your entity YAML as shown in the highlighted example below:',
|
||||
readMoreButtonTitle: 'Read more',
|
||||
},
|
||||
entityLabels: {
|
||||
warningPanelTitle: 'Entity not found',
|
||||
ownerLabel: 'Owner',
|
||||
lifecycleLabel: 'Lifecycle',
|
||||
},
|
||||
entityLinksCard: {
|
||||
title: 'Links',
|
||||
emptyDescription:
|
||||
'No links defined for this entity. You can add links to your entity YAML as shown in the highlighted example below:',
|
||||
readMoreButtonTitle: 'Read more',
|
||||
},
|
||||
entityNotFound: {
|
||||
title: 'Entity was not found',
|
||||
description:
|
||||
'Want to help us build this? Check out our Getting Started documentation.',
|
||||
docButtonTitle: 'DOCS',
|
||||
},
|
||||
deleteEntity: {
|
||||
dialogTitle: 'Are you sure you want to delete this entity?',
|
||||
deleteButtonTitle: 'Delete',
|
||||
cancelButtonTitle: 'Cancel',
|
||||
description:
|
||||
'This entity is not referenced by any location and is therefore not receiving updates. Click here to delete.',
|
||||
},
|
||||
entityProcessingErrorsDescription: 'The error below originates from',
|
||||
entityRelationWarningDescription:
|
||||
"This entity has relations to other entities, which can't be found in the catalog.\n Entities not found are: ",
|
||||
hasComponentsCard: {
|
||||
title: 'Has components',
|
||||
emptyMessage: 'No component is part of this system',
|
||||
},
|
||||
hasResourcesCard: {
|
||||
title: 'Has resources',
|
||||
emptyMessage: 'No resource is part of this system',
|
||||
},
|
||||
hasSubcomponentsCard: {
|
||||
title: 'Has subcomponents',
|
||||
emptyMessage: 'No subcomponent is part of this component',
|
||||
},
|
||||
hasSystemsCard: {
|
||||
title: 'Has systems',
|
||||
emptyMessage: 'No system is part of this domain',
|
||||
},
|
||||
relatedEntitiesCard: {
|
||||
emptyHelpLinkTitle: 'Learn how to change this.',
|
||||
},
|
||||
systemDiagramCard: {
|
||||
title: 'System Diagram',
|
||||
description: 'Use pinch & zoo to move around the diagram.',
|
||||
edgeLabels: {
|
||||
partOf: 'part of',
|
||||
provides: 'provides',
|
||||
dependsOn: 'depends on',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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={[
|
||||
[
|
||||
|
||||
@@ -81,8 +81,6 @@ export const EntityPickerFieldExtension: FieldExtensionComponent_2<
|
||||
{
|
||||
defaultKind?: string | undefined;
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<
|
||||
string,
|
||||
@@ -101,6 +99,8 @@ export const EntityPickerFieldExtension: FieldExtensionComponent_2<
|
||||
}
|
||||
>[]
|
||||
| undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -110,8 +110,6 @@ export const EntityPickerFieldSchema: FieldSchema<
|
||||
{
|
||||
defaultKind?: string | undefined;
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<
|
||||
string,
|
||||
@@ -130,6 +128,8 @@ export const EntityPickerFieldSchema: FieldSchema<
|
||||
}
|
||||
>[]
|
||||
| undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -216,7 +216,6 @@ export const MultiEntityPickerFieldExtension: FieldExtensionComponent_2<
|
||||
{
|
||||
defaultKind?: string | undefined;
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<
|
||||
string,
|
||||
@@ -235,6 +234,7 @@ export const MultiEntityPickerFieldExtension: FieldExtensionComponent_2<
|
||||
}
|
||||
>[]
|
||||
| undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -269,8 +269,6 @@ export const OwnedEntityPickerFieldExtension: FieldExtensionComponent_2<
|
||||
{
|
||||
defaultKind?: string | undefined;
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<
|
||||
string,
|
||||
@@ -289,6 +287,8 @@ export const OwnedEntityPickerFieldExtension: FieldExtensionComponent_2<
|
||||
}
|
||||
>[]
|
||||
| undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -298,8 +298,6 @@ export const OwnedEntityPickerFieldSchema: FieldSchema<
|
||||
{
|
||||
defaultKind?: string | undefined;
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<
|
||||
string,
|
||||
@@ -318,6 +316,8 @@ export const OwnedEntityPickerFieldSchema: FieldSchema<
|
||||
}
|
||||
>[]
|
||||
| undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -330,8 +330,6 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent_2<
|
||||
string,
|
||||
{
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<
|
||||
string,
|
||||
@@ -350,6 +348,8 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent_2<
|
||||
}
|
||||
>[]
|
||||
| undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -358,8 +358,6 @@ export const OwnerPickerFieldSchema: FieldSchema<
|
||||
string,
|
||||
{
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
catalogFilter?:
|
||||
| Record<
|
||||
string,
|
||||
@@ -378,6 +376,8 @@ export const OwnerPickerFieldSchema: FieldSchema<
|
||||
}
|
||||
>[]
|
||||
| undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user