Skip to content

Commit 57d32c5

Browse files
authored
[Manage] Use BUI page header (#7896)
* Parse and forward missing "kinds" settings from config - controlling what kinds to render tabs for Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com> * Move page header and tabs to BUI The tabs used to be MUI tabs _inside_ the page. Moving to BUI PluginHeader means a lot of refactoring was necessary, to move providers outside this so that the header has access to provider contexts. Support for the old frontend system is still there, so its ManagePage, ManagePageFilters and ManageTabs where moved into /src/components-ofs and the necessary refactoring of the NFS page was done in isolation. No API breakage or changes - both OFS and NFS work as before. Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com> * changeset Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com> --------- Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>
1 parent b010cd7 commit 57d32c5

56 files changed

Lines changed: 1864 additions & 525 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@backstage-community/plugin-manage-react': patch
3+
---
4+
5+
Use 'manage.kinds' config for deciding what kinds to use
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@backstage-community/plugin-manage': minor
3+
---
4+
5+
The page header is now replaced with the new BUI page header instead of MUI (only in NFS)

workspaces/manage/packages/app-next/src/extensions/manage.tsx

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
import { PropsWithChildren, useEffect, useState } from 'react';
1818

1919
import { Entity } from '@backstage/catalog-model';
20-
import { HeaderLabel } from '@backstage/core-components';
21-
import { Skeleton } from '@backstage/ui';
20+
import { Button, Skeleton, Tooltip, TooltipTrigger } from '@backstage/ui';
2221

2322
import {
2423
ManageEntityCardWidgetBlueprint,
@@ -32,10 +31,6 @@ import {
3231
import { ManageTechInsightsBlueprint } from '@backstage-community/plugin-manage-module-tech-insights';
3332

3433
import { useTheme } from '@material-ui/core';
35-
import FormGroup from '@mui/material/FormGroup';
36-
import FormControlLabel from '@mui/material/FormControlLabel';
37-
import Switch from '@mui/material/Switch';
38-
import Typography from '@mui/material/Typography';
3934

4035
function Foo({
4136
name,
@@ -230,21 +225,10 @@ const label = ManageHeaderLabelBlueprint.make({
230225
params: defineParams =>
231226
defineParams({
232227
loader: async () => (
233-
<HeaderLabel
234-
label="Example toggle"
235-
value={
236-
<FormGroup row>
237-
<FormControlLabel
238-
control={<Switch name="manage-page-combined" color="primary" />}
239-
label={
240-
<Typography sx={{ userSelect: 'none' }}>
241-
This does nothing
242-
</Typography>
243-
}
244-
/>
245-
</FormGroup>
246-
}
247-
/>
228+
<TooltipTrigger>
229+
<Tooltip>Custom header action example</Tooltip>
230+
<Button variant="tertiary">This does nothing</Button>
231+
</TooltipTrigger>
248232
),
249233
}),
250234
});

workspaces/manage/plugins/manage-react/report.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,8 @@ export interface ManageStaticConfig {
782782
// (undocumented)
783783
kindOrder: string[];
784784
// (undocumented)
785+
kinds: string[];
786+
// (undocumented)
785787
showCombined: boolean;
786788
// (undocumented)
787789
showOrganizationChart: boolean;

workspaces/manage/plugins/manage-react/src/components/OwnedProvider/OwnedProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ import { ErrorPanel } from '@backstage/core-components';
2828
import { Progress } from '../Progress';
2929
import { useKindOrder } from '../KindOrder';
3030
import { arrayify, joinKinds } from '../../utils';
31-
import { defaultKinds } from './types';
3231
import {
3332
type KindStarredType,
3433
KindStarred,
3534
} from '../CurrentKindProvider/types';
3635
import { Owners, manageApiRef } from '../../api';
36+
import { defaultKinds } from '../../config/types';
3737

3838
interface OwnedEntitiesProviderContext {
3939
kinds: string[];

workspaces/manage/plugins/manage-react/src/config/parseStaticConfig.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { ConfigApi } from '@backstage/frontend-plugin-api';
1818

19-
import { ManageStaticConfig } from './types';
19+
import { defaultKinds, ManageStaticConfig } from './types';
2020

2121
/**
2222
* This is an internal utility function
@@ -26,6 +26,7 @@ import { ManageStaticConfig } from './types';
2626
export function parseStaticConfig(
2727
config: ConfigApi | undefined,
2828
): ManageStaticConfig {
29+
const kinds = config?.getOptionalStringArray('manage.kinds') ?? defaultKinds;
2930
const title = config?.getOptionalString('manage.title');
3031
const subtitle = config?.getOptionalString('manage.subtitle');
3132
const themeId = config?.getOptionalString('manage.themeId');
@@ -55,7 +56,12 @@ export function parseStaticConfig(
5556
config?.getOptionalStringArray('manage.order.columns') ?? []
5657
).map(prefixColumnNodeId);
5758

59+
const lcKinds = Array.from(
60+
new Set(kinds.map(kind => kind.toLocaleLowerCase('en-US'))),
61+
);
62+
5863
return {
64+
kinds: lcKinds,
5965
title,
6066
subtitle,
6167
combined,

workspaces/manage/plugins/manage-react/src/config/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface ManageDynamicConfig {
2121

2222
/** @public */
2323
export interface ManageStaticConfig {
24+
kinds: string[];
2425
title: string | undefined;
2526
subtitle: string | undefined;
2627
themeId: string | undefined;
@@ -38,3 +39,13 @@ export interface ManageStaticConfig {
3839
widgetOrderContentBelow: string[];
3940
columnsOrder: string[];
4041
}
42+
43+
/** @internal */
44+
export const defaultKinds = [
45+
'System',
46+
'Component',
47+
'API',
48+
'Template',
49+
'Resource',
50+
'Domain',
51+
];

workspaces/manage/plugins/manage/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"@backstage/ui": "backstage:^",
6666
"@mui/icons-material": "^5.16.7",
6767
"@mui/material": "^5.15.16",
68+
"@remixicon/react": "^4.8.0",
6869
"lodash": "^4.17.21",
6970
"react-use": "^17.5.0"
7071
},
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright 2024 The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
18+
import {
19+
CatalogApi,
20+
catalogApiRef,
21+
StarredEntitiesApi,
22+
starredEntitiesApiRef,
23+
} from '@backstage/plugin-catalog-react';
24+
import { catalogPlugin } from '@backstage/plugin-catalog';
25+
import { Observable } from '@backstage/types';
26+
import { RELATION_OWNED_BY } from '@backstage/catalog-model';
27+
28+
import {
29+
ManageApi,
30+
manageApiRef,
31+
} from '@backstage-community/plugin-manage-react';
32+
33+
import { ManagePageImpl } from './ManagePageOld';
34+
import { ManageTabsImpl } from '../ManageTabs';
35+
36+
const starredEntities: StarredEntitiesApi = {
37+
toggleStarred: async () => {},
38+
starredEntitie$: () =>
39+
({
40+
subscribe: () => ({
41+
unsubscribe: () => {},
42+
closed: true,
43+
}),
44+
} as Observable<Set<string>>),
45+
};
46+
47+
const mockCatalogApi: CatalogApi = {
48+
getEntitiesByRefs: async () => {
49+
return { items: [] };
50+
},
51+
} satisfies Partial<CatalogApi> as any as CatalogApi;
52+
53+
describe('ManagePage', () => {
54+
it('should render an empty page if nothing owned', async () => {
55+
const mockApi: ManageApi = {
56+
getProviders: () => [],
57+
getOwnersAndEntities: async () => ({
58+
owners: { groups: [], ownerEntityRefs: [], user: undefined },
59+
ownedEntities: [],
60+
}),
61+
kindOrder: [],
62+
progressStyle: 'circular',
63+
};
64+
65+
const apis = [
66+
[manageApiRef, mockApi],
67+
[catalogApiRef, mockCatalogApi],
68+
[starredEntitiesApiRef, starredEntities],
69+
] as const;
70+
71+
const { getByText } = await renderInTestApp(
72+
<TestApiProvider apis={apis}>
73+
<ManagePageImpl subtitle="Things you own">
74+
<ManageTabsImpl />
75+
</ManagePageImpl>
76+
</TestApiProvider>,
77+
{
78+
mountedRoutes: {
79+
'/catalog': catalogPlugin.routes.catalogIndex,
80+
'/catalog/:kind/:namespace/:name': catalogPlugin.routes.catalogEntity,
81+
},
82+
},
83+
);
84+
85+
expect(getByText('Manage')?.tagName).toBe('H1');
86+
expect(getByText('Things you own')).toBeDefined();
87+
expect(
88+
getByText("You and your team(s) don't own any entities"),
89+
).toBeDefined();
90+
});
91+
92+
it('should render a table of owned entities', async () => {
93+
const mockApi: ManageApi = {
94+
getProviders: () => [],
95+
getOwnersAndEntities: async () => ({
96+
owners: {
97+
groups: [],
98+
ownerEntityRefs: ['user:default/guest'],
99+
user: undefined,
100+
},
101+
ownedEntities: [
102+
{
103+
apiVersion: 'backstage.io/v1alpha1',
104+
kind: 'Component',
105+
metadata: {
106+
name: 'foo',
107+
title: 'The Foo',
108+
},
109+
relations: [
110+
{
111+
type: RELATION_OWNED_BY,
112+
targetRef: 'user:default/guest',
113+
},
114+
],
115+
},
116+
],
117+
}),
118+
kindOrder: [],
119+
progressStyle: 'circular',
120+
};
121+
122+
const apis = [
123+
[manageApiRef, mockApi],
124+
[catalogApiRef, mockCatalogApi],
125+
[starredEntitiesApiRef, starredEntities],
126+
] as const;
127+
128+
const { getByText } = await renderInTestApp(
129+
<TestApiProvider apis={apis}>
130+
<ManagePageImpl subtitle="Things you own">
131+
<ManageTabsImpl />
132+
</ManagePageImpl>
133+
</TestApiProvider>,
134+
{
135+
mountedRoutes: {
136+
'/catalog': catalogPlugin.routes.catalogIndex,
137+
'/catalog/:kind/:namespace/:name': catalogPlugin.routes.catalogEntity,
138+
},
139+
},
140+
);
141+
142+
expect(getByText('Manage')?.tagName).toBe('H1');
143+
expect(getByText('Things you own')).toBeDefined();
144+
expect(getByText('The Foo')).toBeDefined();
145+
});
146+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2025 The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { ComponentType, ReactNode, PropsWithChildren } from 'react';
18+
19+
import { Content } from '@backstage/core-components';
20+
import { KindOrderProvider } from '@backstage-community/plugin-manage-react';
21+
22+
import { useManagePageCombined } from '../ManagePageFilters';
23+
24+
export interface ManagePageInnerProps {
25+
combined: boolean | undefined;
26+
headerComponent: ReactNode;
27+
providers: (
28+
| ComponentType<{ children?: ReactNode | undefined }>
29+
| {
30+
provider: ComponentType<{ children?: ReactNode | undefined }>;
31+
props: Record<string, unknown>;
32+
}
33+
)[];
34+
}
35+
36+
export function ManagePageInner(
37+
props: PropsWithChildren<ManagePageInnerProps>,
38+
) {
39+
const { combined, headerComponent, providers, children } = props;
40+
41+
// Initialize the state, set default value
42+
useManagePageCombined(combined);
43+
44+
return (
45+
<KindOrderProvider>
46+
{providers.reduce(
47+
(prev, Provider) =>
48+
'provider' in Provider ? (
49+
<Provider.provider {...Provider.props} children={prev} />
50+
) : (
51+
<Provider children={prev} />
52+
),
53+
<>
54+
{headerComponent}
55+
<Content noPadding>{children}</Content>
56+
</>,
57+
)}
58+
</KindOrderProvider>
59+
);
60+
}

0 commit comments

Comments
 (0)