feat: catalog graph support i18n
Signed-off-by: mario ma <mario.ma.node@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-graph': patch
|
||||
---
|
||||
|
||||
Catalog graph plugin support i18n
|
||||
@@ -16,6 +16,35 @@ import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendPlugin } from '@backstage/frontend-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { TranslationRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const catalogGraphTranslationRef: TranslationRef<
|
||||
'catalog-graph',
|
||||
{
|
||||
readonly 'catalogGraphCard.title': 'Relations';
|
||||
readonly 'catalogGraphCard.deepLinkTitle': 'View graph';
|
||||
readonly 'catalogGraphPage.title': 'Catalog Graph';
|
||||
readonly 'catalogGraphPage.filterToggleButtonTitle': 'Filters';
|
||||
readonly 'catalogGraphPage.supportButtonDescription': 'Start tracking your component in by adding it to the software catalog.';
|
||||
readonly 'catalogGraphPage.simplifiedSwitchLabel': 'Simplified';
|
||||
readonly 'catalogGraphPage.mergeRelationsSwitchLabel': 'Merge relations';
|
||||
readonly 'catalogGraphPage.zoomOutDescription': 'Use pinch & zoom to move around the diagram. Click to change active node, shift click to navigate to entity.';
|
||||
readonly 'catalogGraphPage.curveFilter.title': 'Curve';
|
||||
readonly 'catalogGraphPage.curveFilter.curveStepBefore': 'Step Before';
|
||||
readonly 'catalogGraphPage.curveFilter.curveMonotoneX': 'Monotone X';
|
||||
readonly 'catalogGraphPage.directionFilter.title': 'Direction';
|
||||
readonly 'catalogGraphPage.directionFilter.leftToRight': 'Left to right';
|
||||
readonly 'catalogGraphPage.directionFilter.rightToLeft': 'Right to left';
|
||||
readonly 'catalogGraphPage.directionFilter.topToBottom': 'Top to bottom';
|
||||
readonly 'catalogGraphPage.directionFilter.bottomToTop': 'Bottom to top';
|
||||
readonly 'catalogGraphPage.maxDepthFilter.title': 'Max depth';
|
||||
readonly 'catalogGraphPage.maxDepthFilter.inputPlaceholder': '∞ Infinite';
|
||||
readonly 'catalogGraphPage.maxDepthFilter.clearButtonAriaLabel': 'clear max depth';
|
||||
readonly 'catalogGraphPage.selectedKindsFilter.title': 'Kinds';
|
||||
readonly 'catalogGraphPage.selectedRelationsFilter.title': 'Relations';
|
||||
}
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
const _default: FrontendPlugin<
|
||||
|
||||
@@ -96,3 +96,5 @@ export default createFrontendPlugin({
|
||||
},
|
||||
extensions: [CatalogGraphPage, CatalogGraphEntityCard],
|
||||
});
|
||||
|
||||
export { catalogGraphTranslationRef } from './translation';
|
||||
|
||||
@@ -34,6 +34,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { catalogGraphRouteRef } from '../../routes';
|
||||
import { CatalogGraphCard } from './CatalogGraphCard';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { translationApiRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
describe('<CatalogGraphCard/>', () => {
|
||||
let entity: Entity;
|
||||
@@ -52,7 +53,10 @@ describe('<CatalogGraphCard/>', () => {
|
||||
namespace: 'd',
|
||||
},
|
||||
};
|
||||
apis = TestApiRegistry.from([catalogApiRef, catalog]);
|
||||
apis = TestApiRegistry.from(
|
||||
[catalogApiRef, catalog],
|
||||
[translationApiRef, mockApis.translation()],
|
||||
);
|
||||
|
||||
wrapper = (
|
||||
<ApiProvider apis={apis}>
|
||||
@@ -213,7 +217,12 @@ describe('<CatalogGraphCard/>', () => {
|
||||
|
||||
const analyticsApi = mockApis.analytics();
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[analyticsApiRef, analyticsApi]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[analyticsApiRef, analyticsApi],
|
||||
[translationApiRef, mockApis.translation()],
|
||||
]}
|
||||
>
|
||||
{wrapper}
|
||||
</TestApiProvider>,
|
||||
{
|
||||
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
EntityRelationsGraph,
|
||||
EntityRelationsGraphProps,
|
||||
} from '../EntityRelationsGraph';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { catalogGraphTranslationRef } from '../../translation';
|
||||
|
||||
/** @public */
|
||||
export type CatalogGraphCardClassKey = 'card' | 'graph';
|
||||
@@ -66,6 +68,7 @@ export const CatalogGraphCard = (
|
||||
action?: ReactNode;
|
||||
},
|
||||
) => {
|
||||
const { t } = useTranslationRef(catalogGraphTranslationRef);
|
||||
const {
|
||||
variant = 'gridItem',
|
||||
relationPairs = ALL_RELATION_PAIRS,
|
||||
@@ -81,7 +84,7 @@ export const CatalogGraphCard = (
|
||||
action,
|
||||
rootEntityNames,
|
||||
onNodeClick,
|
||||
title = 'Relations',
|
||||
title = t('catalogGraphCard.title'),
|
||||
zoom = 'enable-on-click',
|
||||
} = props;
|
||||
|
||||
@@ -133,7 +136,7 @@ export const CatalogGraphCard = (
|
||||
variant={variant}
|
||||
noPadding
|
||||
deepLink={{
|
||||
title: 'View graph',
|
||||
title: t('catalogGraphCard.deepLinkTitle'),
|
||||
link: catalogGraphUrl,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -50,6 +50,8 @@ import { SelectedKindsFilter } from './SelectedKindsFilter';
|
||||
import { SelectedRelationsFilter } from './SelectedRelationsFilter';
|
||||
import { SwitchFilter } from './SwitchFilter';
|
||||
import { useCatalogGraphPage } from './useCatalogGraphPage';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { catalogGraphTranslationRef } from '../../translation';
|
||||
|
||||
/** @public */
|
||||
export type CatalogGraphPageClassKey =
|
||||
@@ -136,7 +138,7 @@ export const CatalogGraphPage = (
|
||||
initialState,
|
||||
entityFilter,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslationRef(catalogGraphTranslationRef);
|
||||
const navigate = useNavigate();
|
||||
const classes = useStyles();
|
||||
const catalogEntityRoute = useRouteRef(entityRouteRef);
|
||||
@@ -192,7 +194,7 @@ export const CatalogGraphPage = (
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header
|
||||
title="Catalog Graph"
|
||||
title={t('catalogGraphPage.title')}
|
||||
subtitle={rootEntityNames.map(e => humanizeEntityRef(e)).join(', ')}
|
||||
/>
|
||||
<Content stretch className={classes.content}>
|
||||
@@ -203,13 +205,12 @@ export const CatalogGraphPage = (
|
||||
selected={showFilters}
|
||||
onChange={() => toggleShowFilters()}
|
||||
>
|
||||
<FilterListIcon /> Filters
|
||||
<FilterListIcon /> {t('catalogGraphPage.filterToggleButtonTitle')}
|
||||
</ToggleButton>
|
||||
}
|
||||
>
|
||||
<SupportButton>
|
||||
Start tracking your component in by adding it to the software
|
||||
catalog.
|
||||
{t('catalogGraphPage.supportButtonDescription')}
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container alignItems="stretch" className={classes.container}>
|
||||
@@ -230,12 +231,12 @@ export const CatalogGraphPage = (
|
||||
<SwitchFilter
|
||||
value={unidirectional}
|
||||
onChange={setUnidirectional}
|
||||
label="Simplified"
|
||||
label={t('catalogGraphPage.simplifiedSwitchLabel')}
|
||||
/>
|
||||
<SwitchFilter
|
||||
value={mergeRelations}
|
||||
onChange={setMergeRelations}
|
||||
label="Merge Relations"
|
||||
label={t('catalogGraphPage.mergeRelationsSwitchLabel')}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
@@ -247,9 +248,8 @@ export const CatalogGraphPage = (
|
||||
display="block"
|
||||
className={classes.legend}
|
||||
>
|
||||
<ZoomOutMap className="icon" /> Use pinch & zoom to move
|
||||
around the diagram. Click to change active node, shift click to
|
||||
navigate to entity.
|
||||
<ZoomOutMap className="icon" />{' '}
|
||||
{t('catalogGraphPage.zoomOutDescription')}
|
||||
</Typography>
|
||||
<EntityRelationsGraph
|
||||
{...props}
|
||||
|
||||
@@ -14,21 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render, waitFor, screen, within } from '@testing-library/react';
|
||||
import { waitFor, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { CurveFilter } from './CurveFilter';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
describe('<CurveFilter/>', () => {
|
||||
test('should display current curve label', () => {
|
||||
test('should display current curve label', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(<CurveFilter value="curveMonotoneX" onChange={onChange} />);
|
||||
await renderInTestApp(
|
||||
<CurveFilter value="curveMonotoneX" onChange={onChange} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Monotone X')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should select an alternative curve factory', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(<CurveFilter value="curveStepBefore" onChange={onChange} />);
|
||||
await renderInTestApp(
|
||||
<CurveFilter value="curveStepBefore" onChange={onChange} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Step Before')).toBeInTheDocument();
|
||||
|
||||
|
||||
@@ -16,12 +16,10 @@
|
||||
import { Select, SelectedItems } from '@backstage/core-components';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import { useCallback } from 'react';
|
||||
import { catalogGraphTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
type Curve = 'curveStepBefore' | 'curveMonotoneX';
|
||||
const CURVE_DISPLAY_NAMES: Record<Curve, string> = {
|
||||
curveMonotoneX: 'Monotone X',
|
||||
curveStepBefore: 'Step Before',
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
value: Curve;
|
||||
@@ -31,6 +29,12 @@ export type Props = {
|
||||
const curves: Array<Curve> = ['curveMonotoneX', 'curveStepBefore'];
|
||||
|
||||
export const CurveFilter = ({ value, onChange }: Props) => {
|
||||
const { t } = useTranslationRef(catalogGraphTranslationRef);
|
||||
const CURVE_DISPLAY_NAMES: Record<Curve, string> = {
|
||||
curveMonotoneX: t('catalogGraphPage.curveFilter.curveMonotoneX'),
|
||||
curveStepBefore: t('catalogGraphPage.curveFilter.curveStepBefore'),
|
||||
};
|
||||
|
||||
const handleChange = useCallback(
|
||||
(v: SelectedItems) => onChange(v as Curve),
|
||||
[onChange],
|
||||
@@ -39,7 +43,7 @@ export const CurveFilter = ({ value, onChange }: Props) => {
|
||||
return (
|
||||
<Box pb={1} pt={1}>
|
||||
<Select
|
||||
label="Curve"
|
||||
label={t('catalogGraphPage.curveFilter.title')}
|
||||
selected={value}
|
||||
items={curves.map(v => ({
|
||||
label: CURVE_DISPLAY_NAMES[v],
|
||||
|
||||
@@ -14,14 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render, waitFor, screen, within } from '@testing-library/react';
|
||||
import { waitFor, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Direction } from '../EntityRelationsGraph';
|
||||
import { DirectionFilter } from './DirectionFilter';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
describe('<DirectionFilter/>', () => {
|
||||
test('should display current value', () => {
|
||||
render(
|
||||
test('should display current value', async () => {
|
||||
await renderInTestApp(
|
||||
<DirectionFilter value={Direction.LEFT_RIGHT} onChange={() => {}} />,
|
||||
);
|
||||
|
||||
@@ -30,7 +31,7 @@ describe('<DirectionFilter/>', () => {
|
||||
|
||||
test('should select direction', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<DirectionFilter value={Direction.RIGHT_LEFT} onChange={onChange} />,
|
||||
);
|
||||
|
||||
|
||||
@@ -17,13 +17,8 @@ import { Select, SelectedItems } from '@backstage/core-components';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import { useCallback } from 'react';
|
||||
import { Direction } from '../EntityRelationsGraph';
|
||||
|
||||
const DIRECTION_DISPLAY_NAMES = {
|
||||
[Direction.LEFT_RIGHT]: 'Left to right',
|
||||
[Direction.RIGHT_LEFT]: 'Right to left',
|
||||
[Direction.TOP_BOTTOM]: 'Top to bottom',
|
||||
[Direction.BOTTOM_TOP]: 'Bottom to top',
|
||||
};
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { catalogGraphTranslationRef } from '../../translation';
|
||||
|
||||
export type Props = {
|
||||
value: Direction;
|
||||
@@ -31,6 +26,13 @@ export type Props = {
|
||||
};
|
||||
|
||||
export const DirectionFilter = ({ value, onChange }: Props) => {
|
||||
const { t } = useTranslationRef(catalogGraphTranslationRef);
|
||||
const DIRECTION_DISPLAY_NAMES = {
|
||||
[Direction.LEFT_RIGHT]: t('catalogGraphPage.directionFilter.leftToRight'),
|
||||
[Direction.RIGHT_LEFT]: t('catalogGraphPage.directionFilter.rightToLeft'),
|
||||
[Direction.TOP_BOTTOM]: t('catalogGraphPage.directionFilter.topToBottom'),
|
||||
[Direction.BOTTOM_TOP]: t('catalogGraphPage.directionFilter.bottomToTop'),
|
||||
};
|
||||
const handleChange = useCallback(
|
||||
(v: SelectedItems) => onChange(v as Direction),
|
||||
[onChange],
|
||||
@@ -39,7 +41,7 @@ export const DirectionFilter = ({ value, onChange }: Props) => {
|
||||
return (
|
||||
<Box pb={1} pt={1}>
|
||||
<Select
|
||||
label="Direction"
|
||||
label={t('catalogGraphPage.directionFilter.title')}
|
||||
selected={value}
|
||||
items={Object.values(Direction).map(v => ({
|
||||
label: DIRECTION_DISPLAY_NAMES[v],
|
||||
|
||||
@@ -14,20 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import user from '@testing-library/user-event';
|
||||
import { MaxDepthFilter } from './MaxDepthFilter';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
describe('<MaxDepthFilter/>', () => {
|
||||
test('should display current value', () => {
|
||||
render(<MaxDepthFilter value={5} onChange={() => {}} />);
|
||||
test('should display current value', async () => {
|
||||
await renderInTestApp(<MaxDepthFilter value={5} onChange={() => {}} />);
|
||||
|
||||
expect(screen.getByLabelText('maxp')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('maxp')).toHaveValue(5);
|
||||
});
|
||||
|
||||
test('should display infinite if non finite', () => {
|
||||
render(
|
||||
test('should display infinite if non finite', async () => {
|
||||
await renderInTestApp(
|
||||
<MaxDepthFilter value={Number.POSITIVE_INFINITY} onChange={() => {}} />,
|
||||
);
|
||||
|
||||
@@ -37,7 +38,7 @@ describe('<MaxDepthFilter/>', () => {
|
||||
|
||||
test('should clear max depth', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(<MaxDepthFilter value={10} onChange={onChange} />);
|
||||
await renderInTestApp(<MaxDepthFilter value={10} onChange={onChange} />);
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByLabelText('clear max depth'));
|
||||
@@ -46,7 +47,7 @@ describe('<MaxDepthFilter/>', () => {
|
||||
|
||||
test('should set max depth to undefined if below one', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(<MaxDepthFilter value={1} onChange={onChange} />);
|
||||
await renderInTestApp(<MaxDepthFilter value={1} onChange={onChange} />);
|
||||
|
||||
await user.clear(screen.getByLabelText('maxp'));
|
||||
await user.type(screen.getByLabelText('maxp'), '0');
|
||||
@@ -56,7 +57,7 @@ describe('<MaxDepthFilter/>', () => {
|
||||
|
||||
test('should select direction', async () => {
|
||||
let value = 5;
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<MaxDepthFilter
|
||||
value={value}
|
||||
onChange={v => {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
@@ -22,6 +23,7 @@ import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import ClearIcon from '@material-ui/icons/Clear';
|
||||
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { catalogGraphTranslationRef } from '../../translation';
|
||||
|
||||
export type Props = {
|
||||
value: number;
|
||||
@@ -45,6 +47,7 @@ export const MaxDepthFilter = ({ value, onChange }: Props) => {
|
||||
const classes = useStyles();
|
||||
const onChangeRef = useRef(onChange);
|
||||
const [currentValue, setCurrentValue] = useState(value);
|
||||
const { t } = useTranslationRef(catalogGraphTranslationRef);
|
||||
|
||||
// Keep a fresh reference to the latest callback
|
||||
useEffect(() => {
|
||||
@@ -75,16 +78,20 @@ export const MaxDepthFilter = ({ value, onChange }: Props) => {
|
||||
return (
|
||||
<Box pb={1} pt={1}>
|
||||
<FormControl variant="outlined" className={classes.formControl}>
|
||||
<Typography variant="button">Max Depth</Typography>
|
||||
<Typography variant="button">
|
||||
{t('catalogGraphPage.maxDepthFilter.title')}
|
||||
</Typography>
|
||||
<OutlinedInput
|
||||
type="number"
|
||||
placeholder="∞ Infinite"
|
||||
placeholder={t('catalogGraphPage.maxDepthFilter.inputPlaceholder')}
|
||||
value={Number.isFinite(currentValue) ? String(currentValue) : ''}
|
||||
onChange={handleChange}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="clear max depth"
|
||||
aria-label={t(
|
||||
'catalogGraphPage.maxDepthFilter.clearButtonAriaLabel',
|
||||
)}
|
||||
onClick={reset}
|
||||
edge="end"
|
||||
>
|
||||
|
||||
@@ -15,13 +15,18 @@
|
||||
*/
|
||||
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
import { AlertApi, alertApiRef } from '@backstage/core-plugin-api';
|
||||
import { AlertApi, alertApiRef, errorApiRef } from '@backstage/core-plugin-api';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils';
|
||||
import {
|
||||
mockApis,
|
||||
renderWithEffects,
|
||||
TestApiRegistry,
|
||||
} from '@backstage/test-utils';
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { SelectedKindsFilter } from './SelectedKindsFilter';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { translationApiRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
const catalogApi = catalogApiMock.mock({
|
||||
getEntityFacets: jest.fn().mockResolvedValue({
|
||||
@@ -38,6 +43,8 @@ const catalogApi = catalogApiMock.mock({
|
||||
const apis = TestApiRegistry.from(
|
||||
[catalogApiRef, catalogApi],
|
||||
[alertApiRef, {} as AlertApi],
|
||||
[translationApiRef, mockApis.translation()],
|
||||
[errorApiRef, { post: jest.fn() }],
|
||||
);
|
||||
|
||||
describe('<SelectedKindsFilter/>', () => {
|
||||
|
||||
@@ -27,6 +27,8 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import useAsync from 'react-use/esm/useAsync';
|
||||
import { catalogGraphTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
/** @public */
|
||||
export type SelectedKindsFilterClassKey = 'formControl';
|
||||
@@ -49,6 +51,7 @@ export const SelectedKindsFilter = ({ value, onChange }: Props) => {
|
||||
const classes = useStyles();
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const { t } = useTranslationRef(catalogGraphTranslationRef);
|
||||
|
||||
const { error, value: kinds } = useAsync(async () => {
|
||||
return await catalogApi
|
||||
@@ -91,13 +94,15 @@ export const SelectedKindsFilter = ({ value, onChange }: Props) => {
|
||||
|
||||
return (
|
||||
<Box pb={1} pt={1}>
|
||||
<Typography variant="button">Kinds</Typography>
|
||||
<Typography variant="button">
|
||||
{t('catalogGraphPage.selectedKindsFilter.title')}
|
||||
</Typography>
|
||||
<Autocomplete
|
||||
className={classes.formControl}
|
||||
multiple
|
||||
limitTags={4}
|
||||
disableCloseOnSelect
|
||||
aria-label="Kinds"
|
||||
aria-label={t('catalogGraphPage.selectedKindsFilter.title')}
|
||||
options={normalizedKinds}
|
||||
value={value ?? normalizedKinds}
|
||||
getOptionLabel={k => kinds[normalizedKinds.indexOf(k)] ?? k}
|
||||
|
||||
+7
-6
@@ -19,14 +19,15 @@ import {
|
||||
RELATION_HAS_MEMBER,
|
||||
RELATION_OWNED_BY,
|
||||
} from '@backstage/catalog-model';
|
||||
import { render, waitFor, screen } from '@testing-library/react';
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ALL_RELATION_PAIRS } from '../EntityRelationsGraph';
|
||||
import { SelectedRelationsFilter } from './SelectedRelationsFilter';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
describe('<SelectedRelationsFilter/>', () => {
|
||||
test('should render current value', () => {
|
||||
render(
|
||||
test('should render current value', async () => {
|
||||
await renderInTestApp(
|
||||
<SelectedRelationsFilter
|
||||
relationPairs={ALL_RELATION_PAIRS}
|
||||
value={[RELATION_OWNED_BY, RELATION_CHILD_OF]}
|
||||
@@ -40,7 +41,7 @@ describe('<SelectedRelationsFilter/>', () => {
|
||||
|
||||
test('should select value', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<SelectedRelationsFilter
|
||||
relationPairs={ALL_RELATION_PAIRS}
|
||||
value={[RELATION_OWNED_BY, RELATION_CHILD_OF]}
|
||||
@@ -67,7 +68,7 @@ describe('<SelectedRelationsFilter/>', () => {
|
||||
|
||||
test('should return undefined if all values are selected', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<SelectedRelationsFilter
|
||||
relationPairs={ALL_RELATION_PAIRS}
|
||||
value={ALL_RELATION_PAIRS.flatMap(p => p).filter(
|
||||
@@ -92,7 +93,7 @@ describe('<SelectedRelationsFilter/>', () => {
|
||||
|
||||
test('should return all values when cleared', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<SelectedRelationsFilter
|
||||
relationPairs={ALL_RELATION_PAIRS}
|
||||
value={[]}
|
||||
|
||||
@@ -25,6 +25,8 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { RelationPairs } from '../EntityRelationsGraph';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { catalogGraphTranslationRef } from '../../translation';
|
||||
|
||||
/** @public */
|
||||
export type SelectedRelationsFilterClassKey = 'formControl';
|
||||
@@ -51,6 +53,7 @@ export const SelectedRelationsFilter = ({
|
||||
}: Props) => {
|
||||
const classes = useStyles();
|
||||
const relations = useMemo(() => relationPairs.flat(), [relationPairs]);
|
||||
const { t } = useTranslationRef(catalogGraphTranslationRef);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(_: unknown, v: string[]) => {
|
||||
@@ -65,13 +68,15 @@ export const SelectedRelationsFilter = ({
|
||||
|
||||
return (
|
||||
<Box pb={1} pt={1}>
|
||||
<Typography variant="button">Relations</Typography>
|
||||
<Typography variant="button">
|
||||
{t('catalogGraphPage.selectedRelationsFilter.title')}
|
||||
</Typography>
|
||||
<Autocomplete
|
||||
className={classes.formControl}
|
||||
multiple
|
||||
limitTags={4}
|
||||
disableCloseOnSelect
|
||||
aria-label="Relations"
|
||||
aria-label={t('catalogGraphPage.selectedRelationsFilter.title')}
|
||||
options={relations}
|
||||
value={value ?? relations}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2025 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/frontend-plugin-api';
|
||||
|
||||
/** @alpha */
|
||||
export const catalogGraphTranslationRef = createTranslationRef({
|
||||
id: 'catalog-graph',
|
||||
messages: {
|
||||
catalogGraphCard: {
|
||||
title: 'Relations',
|
||||
deepLinkTitle: 'View graph',
|
||||
},
|
||||
catalogGraphPage: {
|
||||
title: 'Catalog Graph',
|
||||
filterToggleButtonTitle: 'Filters',
|
||||
supportButtonDescription:
|
||||
'Start tracking your component in by adding it to the software catalog.',
|
||||
simplifiedSwitchLabel: 'Simplified',
|
||||
mergeRelationsSwitchLabel: 'Merge relations',
|
||||
zoomOutDescription:
|
||||
'Use pinch & zoom to move around the diagram. Click to change active node, shift click to navigate to entity.',
|
||||
curveFilter: {
|
||||
title: 'Curve',
|
||||
curveMonotoneX: 'Monotone X',
|
||||
curveStepBefore: 'Step Before',
|
||||
},
|
||||
directionFilter: {
|
||||
title: 'Direction',
|
||||
leftToRight: 'Left to right',
|
||||
rightToLeft: 'Right to left',
|
||||
topToBottom: 'Top to bottom',
|
||||
bottomToTop: 'Bottom to top',
|
||||
},
|
||||
maxDepthFilter: {
|
||||
title: 'Max depth',
|
||||
inputPlaceholder: '∞ Infinite',
|
||||
clearButtonAriaLabel: 'clear max depth',
|
||||
},
|
||||
selectedKindsFilter: {
|
||||
title: 'Kinds',
|
||||
},
|
||||
selectedRelationsFilter: {
|
||||
title: 'Relations',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user