Merge pull request #14396 from graynk/customize-user-settings-page
Make UserSettings page fully customizable
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-user-settings': minor
|
||||
---
|
||||
|
||||
Added the ability to fully customize settings page. Deprecated UserSettingsTab in favour of SettingsLayout.Route
|
||||
@@ -80,8 +80,8 @@ import {
|
||||
TextSize,
|
||||
} from '@backstage/plugin-techdocs-module-addons-contrib';
|
||||
import {
|
||||
SettingsLayout,
|
||||
UserSettingsPage,
|
||||
UserSettingsTab,
|
||||
} from '@backstage/plugin-user-settings';
|
||||
import { AdvancedSettings } from './components/advancedSettings';
|
||||
import AlarmIcon from '@material-ui/icons/Alarm';
|
||||
@@ -267,9 +267,9 @@ const routes = (
|
||||
element={<CostInsightsLabelDataflowInstructionsPage />}
|
||||
/>
|
||||
<Route path="/settings" element={<UserSettingsPage />}>
|
||||
<UserSettingsTab path="/advanced" title="Advanced">
|
||||
<SettingsLayout.Route path="/advanced" title="Advanced">
|
||||
<AdvancedSettings />
|
||||
</UserSettingsTab>
|
||||
</SettingsLayout.Route>
|
||||
</Route>
|
||||
<Route path="/azure-pull-requests" element={<AzurePullRequestsPage />} />
|
||||
<Route path="/apache-airflow" element={<ApacheAirflowPage />} />
|
||||
|
||||
@@ -16,7 +16,7 @@ for installation instructions.
|
||||
|
||||
Add the item to the Sidebar:
|
||||
|
||||
```ts
|
||||
```tsx
|
||||
import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
|
||||
|
||||
<SidebarPage>
|
||||
@@ -28,7 +28,7 @@ import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
|
||||
|
||||
Add the page to the App routing:
|
||||
|
||||
```ts
|
||||
```tsx
|
||||
import { UserSettingsPage } from '@backstage/plugin-user-settings';
|
||||
|
||||
const AppRoutes = () => (
|
||||
@@ -46,7 +46,7 @@ By default, the plugin provides a list of configured authentication providers fe
|
||||
|
||||
If you want to supply your own custom list of Authentication Providers, use the `providerSettings` prop:
|
||||
|
||||
```ts
|
||||
```tsx
|
||||
const MyAuthProviders = () => (
|
||||
<ListItem>
|
||||
<ListItemText primary="example" />
|
||||
@@ -71,20 +71,20 @@ const AppRoutes = () => (
|
||||
By default, the plugin renders 3 tabs of settings; GENERAL, AUTHENTICATION PROVIDERS, and FEATURE FLAGS.
|
||||
|
||||
If you want to add more options for your users,
|
||||
just pass the extra tabs using `UserSettingsTab` components as children of the `UserSettingsPage` route.
|
||||
just pass the extra tabs using `SettingsLayout.Route` components as children of the `UserSettingsPage` route.
|
||||
The path is in this case a child of the settings path,
|
||||
in the example below it would be `/settings/advanced` so that you can easily link to it.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
SettingsLayout,
|
||||
UserSettingsPage,
|
||||
UserSettingsTab,
|
||||
} from '@backstage/plugin-user-settings';
|
||||
|
||||
<Route path="/settings" element={<UserSettingsPage />}>
|
||||
<UserSettingsTab path="/advanced" title="Advanced">
|
||||
<SettingsLayout.Route path="/advanced" title="Advanced">
|
||||
<AdvancedSettings />
|
||||
</UserSettingsTab>
|
||||
</SettingsLayout.Route>
|
||||
</Route>;
|
||||
```
|
||||
|
||||
@@ -93,3 +93,40 @@ make sure you use a similar component structure as the other tabs.
|
||||
You can take a look at
|
||||
[the example extra tab](https://github.com/backstage/backstage/blob/master/packages/app/src/components/advancedSettings/AdvancedSettings.tsx)
|
||||
we have created in Backstage's demo app.
|
||||
|
||||
To change the layout altogether, create a custom page in `packages/app/src/components/user-settings/SettingsPage.tsx`:
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import {
|
||||
SettingsLayout,
|
||||
UserSettingsGeneral,
|
||||
} from '@backstage/plugin-user-settings';
|
||||
import { AdvancedSettings } from './advancedSettings';
|
||||
|
||||
export const settingsPage = (
|
||||
<SettingsLayout>
|
||||
<SettingsLayout.Route path="general" title="General">
|
||||
<UserSettingsGeneral />
|
||||
</SettingsLayout.Route>
|
||||
<SettingsLayout.Route path="advanced" title="Advanced">
|
||||
<AdvancedSettings />
|
||||
</SettingsLayout.Route>
|
||||
</SettingsLayout>
|
||||
);
|
||||
```
|
||||
|
||||
Now register the new settings page in `packages/app/src/App.tsx`:
|
||||
|
||||
```diff
|
||||
+ import {settingsPage} from './components/settings/settingsPage';
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
- <Route path="/settings" element={<UserSettingsPage />} />
|
||||
+ <Route path="/settings" element={<UserSettingsPage />}>
|
||||
+ {settingsPage}
|
||||
+ </Route>
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
@@ -18,10 +18,12 @@ import { Observable } from '@backstage/types';
|
||||
import { ProfileInfo } from '@backstage/core-plugin-api';
|
||||
import { ProfileInfoApi } from '@backstage/core-plugin-api';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { SessionApi } from '@backstage/core-plugin-api';
|
||||
import { StorageApi } from '@backstage/core-plugin-api';
|
||||
import { StorageValueSnapshot } from '@backstage/core-plugin-api';
|
||||
import { TabProps } from '@material-ui/core';
|
||||
|
||||
// @public (undocumented)
|
||||
export const DefaultProviderSettings: (props: {
|
||||
@@ -43,7 +45,33 @@ export const Router: (props: { providerSettings?: JSX.Element }) => JSX.Element;
|
||||
export const Settings: (props: { icon?: IconComponent }) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const USER_SETTINGS_TAB_KEY = 'user-settings.tab';
|
||||
export const SettingsLayout: {
|
||||
(props: SettingsLayoutProps): JSX.Element;
|
||||
Route: (props: SettingsLayoutRouteProps) => null;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type SettingsLayoutProps = {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
children?: React_2.ReactNode;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type SettingsLayoutRouteProps = {
|
||||
path: string;
|
||||
title: string;
|
||||
children: JSX.Element;
|
||||
tabProps?: TabProps<
|
||||
React_2.ElementType,
|
||||
{
|
||||
component?: React_2.ElementType;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const USER_SETTINGS_TAB_KEY = 'plugin.user-settings.settingsLayoutRoute';
|
||||
|
||||
// @public (undocumented)
|
||||
export const UserSettingsAppearanceCard: () => JSX.Element;
|
||||
@@ -116,10 +144,10 @@ export class UserSettingsStorage implements StorageApi {
|
||||
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
|
||||
}
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export const UserSettingsTab: (props: UserSettingsTabProps) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type UserSettingsTabProps = PropsWithChildren<{
|
||||
path: string;
|
||||
title: string;
|
||||
|
||||
+23
-9
@@ -16,39 +16,53 @@
|
||||
|
||||
import React from 'react';
|
||||
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { SettingsPage } from './SettingsPage';
|
||||
import { UserSettingsTab } from './UserSettingsTab';
|
||||
import { DefaultSettingsPage } from './DefaultSettingsPage';
|
||||
import { UserSettingsTab } from '../UserSettingsTab';
|
||||
import { useOutlet } from 'react-router';
|
||||
import { SettingsLayout } from '../SettingsLayout';
|
||||
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useOutlet: jest.fn().mockReturnValue(undefined),
|
||||
}));
|
||||
|
||||
import { useOutlet } from 'react-router';
|
||||
|
||||
describe('<SettingsPage />', () => {
|
||||
describe('<DefaultSettingsPage />', () => {
|
||||
beforeEach(() => {
|
||||
(useOutlet as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
it('should render the settings page with 3 tabs', async () => {
|
||||
const { container } = await renderWithEffects(
|
||||
wrapInTestApp(<SettingsPage />),
|
||||
wrapInTestApp(<DefaultSettingsPage />),
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] button');
|
||||
expect(tabs).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should render the settings page with 4 tabs when extra tabs are provided', async () => {
|
||||
it('should render the settings page with 4 tabs when extra tabs are provided via UserSettingsTab', async () => {
|
||||
const advancedTabRoute = (
|
||||
<UserSettingsTab path="/advanced" title="Advanced">
|
||||
<div>Advanced settings</div>
|
||||
</UserSettingsTab>
|
||||
);
|
||||
(useOutlet as jest.Mock).mockReturnValue(advancedTabRoute);
|
||||
const { container } = await renderWithEffects(
|
||||
wrapInTestApp(<SettingsPage />),
|
||||
wrapInTestApp(<DefaultSettingsPage tabs={[advancedTabRoute]} />),
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] button');
|
||||
expect(tabs).toHaveLength(4);
|
||||
expect(tabs[3].textContent).toEqual('Advanced');
|
||||
});
|
||||
|
||||
it('should render the settings page with 4 tabs when extra tabs are provided via SettingsLayout.Route', async () => {
|
||||
const advancedTabRoute = (
|
||||
<SettingsLayout.Route path="/advanced" title="Advanced">
|
||||
<div>Advanced settings</div>
|
||||
</SettingsLayout.Route>
|
||||
);
|
||||
const { container } = await renderWithEffects(
|
||||
wrapInTestApp(<DefaultSettingsPage tabs={[advancedTabRoute]} />),
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] button');
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2022 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 React from 'react';
|
||||
import { UserSettingsAuthProviders } from '../AuthProviders';
|
||||
import { UserSettingsFeatureFlags } from '../FeatureFlags';
|
||||
import { UserSettingsGeneral } from '../General';
|
||||
import { SettingsLayout, SettingsLayoutRouteProps } from '../SettingsLayout';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const DefaultSettingsPage = (props: {
|
||||
tabs?: React.ReactElement<SettingsLayoutRouteProps>[];
|
||||
providerSettings?: JSX.Element;
|
||||
}) => {
|
||||
const { providerSettings, tabs } = props;
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<SettingsLayout.Route path="general" title="General">
|
||||
<UserSettingsGeneral />
|
||||
</SettingsLayout.Route>
|
||||
<SettingsLayout.Route
|
||||
path="auth-providers"
|
||||
title="Authentication Providers"
|
||||
>
|
||||
<UserSettingsAuthProviders providerSettings={providerSettings} />
|
||||
</SettingsLayout.Route>
|
||||
<SettingsLayout.Route path="feature-flags" title="Feature Flags">
|
||||
<UserSettingsFeatureFlags />
|
||||
</SettingsLayout.Route>
|
||||
{tabs}
|
||||
</SettingsLayout>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { DefaultSettingsPage } from './DefaultSettingsPage';
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2022 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 React from 'react';
|
||||
import { TabProps } from '@material-ui/core';
|
||||
import {
|
||||
Header,
|
||||
Page,
|
||||
RoutedTabs,
|
||||
useSidebarPinState,
|
||||
} from '@backstage/core-components';
|
||||
import {
|
||||
attachComponentData,
|
||||
useElementFilter,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
/** @public */
|
||||
export type SettingsLayoutRouteProps = {
|
||||
path: string;
|
||||
title: string;
|
||||
children: JSX.Element;
|
||||
tabProps?: TabProps<React.ElementType, { component?: React.ElementType }>;
|
||||
};
|
||||
|
||||
export const LAYOUT_DATA_KEY = 'plugin.user-settings.settingsLayout';
|
||||
export const LAYOUT_ROUTE_DATA_KEY = 'plugin.user-settings.settingsLayoutRoute';
|
||||
|
||||
const Route: (props: SettingsLayoutRouteProps) => null = () => null;
|
||||
attachComponentData(Route, LAYOUT_ROUTE_DATA_KEY, true);
|
||||
|
||||
// This causes all mount points that are discovered within this route to use the path of the route itself
|
||||
attachComponentData(Route, 'core.gatherMountPoints', true);
|
||||
|
||||
/** @public */
|
||||
export type SettingsLayoutProps = {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const SettingsLayout = (props: SettingsLayoutProps) => {
|
||||
const { title, children } = props;
|
||||
const { isMobile } = useSidebarPinState();
|
||||
|
||||
const routes = useElementFilter(children, elements =>
|
||||
elements
|
||||
.selectByComponentData({
|
||||
key: LAYOUT_ROUTE_DATA_KEY,
|
||||
withStrictError:
|
||||
'Child of SettingsLayout must be an SettingsLayout.Route',
|
||||
})
|
||||
.getElements<SettingsLayoutRouteProps>()
|
||||
.map(child => child.props),
|
||||
);
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
{!isMobile && <Header title={title ?? 'Settings'} />}
|
||||
<RoutedTabs routes={routes} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
attachComponentData(SettingsLayout, LAYOUT_DATA_KEY, true);
|
||||
|
||||
SettingsLayout.Route = Route;
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type {
|
||||
SettingsLayoutProps,
|
||||
SettingsLayoutRouteProps,
|
||||
} from './SettingsLayout';
|
||||
export { SettingsLayout } from './SettingsLayout';
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 {
|
||||
Header,
|
||||
Page,
|
||||
TabbedLayout,
|
||||
useSidebarPinState,
|
||||
} from '@backstage/core-components';
|
||||
import React from 'react';
|
||||
import { useOutlet } from 'react-router';
|
||||
import { useElementFilter } from '@backstage/core-plugin-api';
|
||||
import { UserSettingsAuthProviders } from './AuthProviders';
|
||||
import { UserSettingsFeatureFlags } from './FeatureFlags';
|
||||
import { UserSettingsGeneral } from './General';
|
||||
import { USER_SETTINGS_TAB_KEY, UserSettingsTabProps } from './UserSettingsTab';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const SettingsPage = (props: { providerSettings?: JSX.Element }) => {
|
||||
const { providerSettings } = props;
|
||||
const { isMobile } = useSidebarPinState();
|
||||
const outlet = useOutlet();
|
||||
|
||||
const tabs = useElementFilter(outlet, elements =>
|
||||
elements
|
||||
.selectByComponentData({
|
||||
key: USER_SETTINGS_TAB_KEY,
|
||||
})
|
||||
.getElements<UserSettingsTabProps>(),
|
||||
);
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
{!isMobile && <Header title="Settings" />}
|
||||
<TabbedLayout>
|
||||
<TabbedLayout.Route path="general" title="General">
|
||||
<UserSettingsGeneral />
|
||||
</TabbedLayout.Route>
|
||||
<TabbedLayout.Route
|
||||
path="auth-providers"
|
||||
title="Authentication Providers"
|
||||
>
|
||||
<UserSettingsAuthProviders providerSettings={providerSettings} />
|
||||
</TabbedLayout.Route>
|
||||
<TabbedLayout.Route path="feature-flags" title="Feature Flags">
|
||||
<UserSettingsFeatureFlags />
|
||||
</TabbedLayout.Route>
|
||||
|
||||
{tabs.map((child, i) => (
|
||||
<TabbedLayout.Route key={i} {...child.props}>
|
||||
{child}
|
||||
</TabbedLayout.Route>
|
||||
))}
|
||||
</TabbedLayout>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2022 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 React from 'react';
|
||||
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { SettingsPage } from './SettingsPage';
|
||||
import { UserSettingsTab } from '../UserSettingsTab';
|
||||
import { useOutlet } from 'react-router';
|
||||
import { SettingsLayout } from '../SettingsLayout';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useOutlet: jest.fn().mockReturnValue(undefined),
|
||||
}));
|
||||
|
||||
describe('<SettingsPage />', () => {
|
||||
beforeEach(() => {
|
||||
(useOutlet as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
it('should render the default settings page with 3 tabs', async () => {
|
||||
const { container } = await renderWithEffects(
|
||||
wrapInTestApp(<SettingsPage />),
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] button');
|
||||
expect(tabs).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should render the default settings page with 4 tabs when extra tabs are provided via UserSettingsTab', async () => {
|
||||
const advancedTabRoute = (
|
||||
<UserSettingsTab path="/advanced" title="Advanced">
|
||||
<div>Advanced settings</div>
|
||||
</UserSettingsTab>
|
||||
);
|
||||
(useOutlet as jest.Mock).mockReturnValue(advancedTabRoute);
|
||||
const { container } = await renderWithEffects(
|
||||
wrapInTestApp(<SettingsPage />),
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] button');
|
||||
expect(tabs).toHaveLength(4);
|
||||
expect(tabs[3].textContent).toEqual('Advanced');
|
||||
});
|
||||
|
||||
it('should render the default settings page with 4 tabs when extra tabs are provided via SettingsLayout.Route', async () => {
|
||||
const advancedTabRoute = (
|
||||
<SettingsLayout.Route path="/advanced" title="Advanced">
|
||||
<div>Advanced settings</div>
|
||||
</SettingsLayout.Route>
|
||||
);
|
||||
(useOutlet as jest.Mock).mockReturnValue(advancedTabRoute);
|
||||
const { container } = await renderWithEffects(
|
||||
wrapInTestApp(<SettingsPage />),
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] button');
|
||||
expect(tabs).toHaveLength(4);
|
||||
expect(tabs[3].textContent).toEqual('Advanced');
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByText(/Advanced/i));
|
||||
const content = container.querySelectorAll('article');
|
||||
expect(content[0].textContent).toEqual('Advanced settings');
|
||||
});
|
||||
|
||||
it('should render the custom settings page when custom layout is provided', async () => {
|
||||
const customLayout = (
|
||||
<SettingsLayout>
|
||||
<SettingsLayout.Route path="general" title="General">
|
||||
<div>User settings</div>
|
||||
</SettingsLayout.Route>
|
||||
<SettingsLayout.Route path="advanced" title="Advanced">
|
||||
<div>Advanced settings</div>
|
||||
</SettingsLayout.Route>
|
||||
</SettingsLayout>
|
||||
);
|
||||
(useOutlet as jest.Mock).mockReturnValue(customLayout);
|
||||
const { container } = await renderWithEffects(
|
||||
wrapInTestApp(<SettingsPage />),
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] button');
|
||||
expect(tabs).toHaveLength(2);
|
||||
expect(tabs[0].textContent).toEqual('General');
|
||||
expect(tabs[1].textContent).toEqual('Advanced');
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByText(/Advanced/i));
|
||||
const content = container.querySelectorAll('article');
|
||||
expect(content[0].textContent).toEqual('Advanced settings');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2022 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 { useOutlet } from 'react-router';
|
||||
import React from 'react';
|
||||
import { DefaultSettingsPage } from '../DefaultSettingsPage';
|
||||
import { useElementFilter } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
SettingsLayoutProps,
|
||||
SettingsLayoutRouteProps,
|
||||
} from '../SettingsLayout';
|
||||
import {
|
||||
LAYOUT_DATA_KEY,
|
||||
LAYOUT_ROUTE_DATA_KEY,
|
||||
} from '../SettingsLayout/SettingsLayout';
|
||||
|
||||
/** @public */
|
||||
export const SettingsPage = (props: { providerSettings?: JSX.Element }) => {
|
||||
const { providerSettings } = props;
|
||||
const outlet = useOutlet();
|
||||
const layout = useElementFilter(outlet, elements =>
|
||||
elements
|
||||
.selectByComponentData({
|
||||
key: LAYOUT_DATA_KEY,
|
||||
})
|
||||
.getElements<SettingsLayoutProps>(),
|
||||
);
|
||||
const tabs = useElementFilter(outlet, elements =>
|
||||
elements
|
||||
.selectByComponentData({
|
||||
key: LAYOUT_ROUTE_DATA_KEY,
|
||||
})
|
||||
.getElements<SettingsLayoutRouteProps>(),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(layout.length !== 0 && layout) || (
|
||||
<DefaultSettingsPage tabs={tabs} providerSettings={providerSettings} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { SettingsPage } from './SettingsPage';
|
||||
@@ -15,11 +15,15 @@
|
||||
*/
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { attachComponentData } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
LAYOUT_ROUTE_DATA_KEY,
|
||||
SettingsLayout,
|
||||
} from '../SettingsLayout/SettingsLayout';
|
||||
|
||||
/** @public */
|
||||
export const USER_SETTINGS_TAB_KEY = 'user-settings.tab';
|
||||
/** @public @deprecated Use SettingsLayout.Route approach instead */
|
||||
export const USER_SETTINGS_TAB_KEY = LAYOUT_ROUTE_DATA_KEY;
|
||||
|
||||
/** @public */
|
||||
/** @public @deprecated Use SettingsLayoutRouteProps instead */
|
||||
export type UserSettingsTabProps = PropsWithChildren<{
|
||||
/**
|
||||
* The path to the tab in the settings route
|
||||
@@ -34,9 +38,12 @@ export type UserSettingsTabProps = PropsWithChildren<{
|
||||
* Renders a tab inside the settings page
|
||||
* @param props - Component props
|
||||
* @public
|
||||
* @deprecated Use SettingsLayout.Route instead
|
||||
*/
|
||||
export const UserSettingsTab = (props: UserSettingsTabProps) => {
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
export const UserSettingsTab = (props: UserSettingsTabProps) => (
|
||||
<SettingsLayout.Route path={props.path} title={props.title}>
|
||||
<>props.children</>
|
||||
</SettingsLayout.Route>
|
||||
);
|
||||
|
||||
attachComponentData(UserSettingsTab, USER_SETTINGS_TAB_KEY, 'UserSettingsTab');
|
||||
|
||||
@@ -21,3 +21,4 @@ export * from './General';
|
||||
export * from './FeatureFlags';
|
||||
export { useUserProfile } from './useUserProfileInfo';
|
||||
export * from './UserSettingsTab';
|
||||
export * from './SettingsLayout';
|
||||
|
||||
Reference in New Issue
Block a user