diff --git a/.changeset/three-rivers-remain.md b/.changeset/three-rivers-remain.md
new file mode 100644
index 0000000000..d6bfeba093
--- /dev/null
+++ b/.changeset/three-rivers-remain.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-user-settings': patch
+---
+
+Added the ability to render extra setting tabs
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index ce765bd312..9cbbcc86a9 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -69,7 +69,11 @@ import {
techdocsPlugin,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
-import { UserSettingsPage } from '@backstage/plugin-user-settings';
+import {
+ UserSettingsPage,
+ UserSettingsTab,
+} from '@backstage/plugin-user-settings';
+import { AdvancedSettings } from './components/advancedSettings';
import AlarmIcon from '@material-ui/icons/Alarm';
import React from 'react';
import { hot } from 'react-hot-loader/root';
@@ -83,7 +87,6 @@ import { defaultPreviewTemplate } from './components/scaffolder/defaultPreviewTe
import { searchPage } from './components/search/SearchPage';
import { providers } from './identityProviders';
import * as plugins from './plugins';
-
import { techDocsPage } from './components/techdocs/TechDocsPage';
import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow';
import { PermissionedRoute } from '@backstage/plugin-permission-react';
@@ -217,7 +220,11 @@ const routes = (
path="/cost-insights/labeling-jobs"
element={}
/>
- } />
+ }>
+
+
+
+
} />
} />
diff --git a/packages/app/src/components/advancedSettings/AdvancedSettings.tsx b/packages/app/src/components/advancedSettings/AdvancedSettings.tsx
new file mode 100644
index 0000000000..9a1e0fd162
--- /dev/null
+++ b/packages/app/src/components/advancedSettings/AdvancedSettings.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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 { InfoCard } from '@backstage/core-components';
+import {
+ List,
+ Grid,
+ ListItem,
+ ListItemText,
+ ListItemSecondaryAction,
+ Switch,
+} from '@material-ui/core';
+import useLocalStorage from 'react-use/lib/useLocalStorage';
+
+export function AdvancedSettings() {
+ const [value, setValue] = useLocalStorage<'on' | 'off'>(
+ 'advanced-option',
+ 'off',
+ );
+
+ const toggleValue = (ev: React.ChangeEvent) => {
+ setValue(ev.currentTarget.checked ? 'on' : 'off');
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/app/src/components/advancedSettings/index.ts b/packages/app/src/components/advancedSettings/index.ts
new file mode 100644
index 0000000000..9d5e946e99
--- /dev/null
+++ b/packages/app/src/components/advancedSettings/index.ts
@@ -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 * from './AdvancedSettings';
diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md
index db0b01681f..13cfe720c8 100644
--- a/plugins/user-settings/README.md
+++ b/plugins/user-settings/README.md
@@ -63,3 +63,31 @@ const AppRoutes = () => (
```
> **Note that the list of providers expects to be rendered within a MUI [``](https://material-ui.com/components/lists/)**
+
+**Tabs**
+
+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.
+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 {
+ UserSettingsPage,
+ UserSettingsTab,
+} from '@backstage/plugin-user-settings';
+
+}>
+
+
+
+;
+```
+
+To standardize the UI of all setting tabs,
+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.
diff --git a/plugins/user-settings/api-report.md b/plugins/user-settings/api-report.md
index 0d8f132f37..136b23bd71 100644
--- a/plugins/user-settings/api-report.md
+++ b/plugins/user-settings/api-report.md
@@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { ProfileInfo } from '@backstage/core-plugin-api';
+import { PropsWithChildren } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
import { SessionApi } from '@backstage/core-plugin-api';
@@ -43,6 +44,9 @@ export const Router: ({ providerSettings }: Props) => JSX.Element;
// @public (undocumented)
export const Settings: (props: SettingsProps) => JSX.Element;
+// @public (undocumented)
+export const USER_SETTINGS_TAB_KEY = 'user-settings.tab';
+
// Warning: (ae-missing-release-tag) "UserSettingsAppearanceCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -108,6 +112,15 @@ export const UserSettingsProfileCard: () => JSX.Element;
// @public (undocumented)
export const UserSettingsSignInAvatar: ({ size }: Props_5) => JSX.Element;
+// @public
+export const UserSettingsTab: (props: UserSettingsTabProps) => JSX.Element;
+
+// @public (undocumented)
+export type UserSettingsTabProps = PropsWithChildren<{
+ path: string;
+ title: string;
+}>;
+
// Warning: (ae-missing-release-tag) "UserSettingsThemeToggle" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json
index 96433eb9e2..e16ad1c75b 100644
--- a/plugins/user-settings/package.json
+++ b/plugins/user-settings/package.json
@@ -40,6 +40,7 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
+ "@types/react": "^16.13.1 || ^17.0.0",
"react-router": "6.0.0-beta.0",
"react-use": "^17.2.4"
},
diff --git a/plugins/user-settings/src/components/SettingsPage.test.tsx b/plugins/user-settings/src/components/SettingsPage.test.tsx
new file mode 100644
index 0000000000..01810bcc8e
--- /dev/null
+++ b/plugins/user-settings/src/components/SettingsPage.test.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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';
+
+jest.mock('react-router', () => ({
+ ...jest.requireActual('react-router'),
+ useOutlet: jest.fn().mockReturnValue(undefined),
+}));
+
+import { useOutlet } from 'react-router';
+
+describe('', () => {
+ beforeEach(() => {
+ (useOutlet as jest.Mock).mockReset();
+ });
+
+ it('should render the settings page with 3 tabs', async () => {
+ const { container } = await renderWithEffects(
+ wrapInTestApp(),
+ );
+
+ 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 () => {
+ const advancedTabRoute = (
+
+ Advanced settings
+
+ );
+ (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute);
+ const { container } = await renderWithEffects(
+ wrapInTestApp(),
+ );
+
+ const tabs = container.querySelectorAll('[class*=MuiTabs-root] button');
+ expect(tabs).toHaveLength(4);
+ expect(tabs[3].textContent).toEqual('Advanced');
+ });
+});
diff --git a/plugins/user-settings/src/components/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage.tsx
index 88b46fc372..8942235fda 100644
--- a/plugins/user-settings/src/components/SettingsPage.tsx
+++ b/plugins/user-settings/src/components/SettingsPage.tsx
@@ -21,9 +21,12 @@ import {
TabbedLayout,
} from '@backstage/core-components';
import React, { useContext } 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';
type Props = {
providerSettings?: JSX.Element;
@@ -31,6 +34,15 @@ type Props = {
export const SettingsPage = ({ providerSettings }: Props) => {
const { isMobile } = useContext(SidebarPinStateContext);
+ const outlet = useOutlet();
+
+ const tabs = useElementFilter(outlet, elements =>
+ elements
+ .selectByComponentData({
+ key: USER_SETTINGS_TAB_KEY,
+ })
+ .getElements(),
+ );
return (
@@ -48,6 +60,12 @@ export const SettingsPage = ({ providerSettings }: Props) => {
+
+ {tabs.map((child, i) => (
+
+ {child}
+
+ ))}
);
diff --git a/plugins/user-settings/src/components/UserSettingsTab/UserSettingsTab.tsx b/plugins/user-settings/src/components/UserSettingsTab/UserSettingsTab.tsx
new file mode 100644
index 0000000000..171c54ecf3
--- /dev/null
+++ b/plugins/user-settings/src/components/UserSettingsTab/UserSettingsTab.tsx
@@ -0,0 +1,42 @@
+/*
+ * 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 React, { PropsWithChildren } from 'react';
+import { attachComponentData } from '@backstage/core-plugin-api';
+
+/** @public */
+export const USER_SETTINGS_TAB_KEY = 'user-settings.tab';
+
+/** @public */
+export type UserSettingsTabProps = PropsWithChildren<{
+ /**
+ * The path to the tab in the settings route
+ * @example `/settings/advanced`
+ */
+ path: string;
+ /** The title of the tab. It will also reflect in the document title when the tab is active */
+ title: string;
+}>;
+
+/**
+ * Renders a tab inside the settings page
+ * @param props - Component props
+ * @public
+ */
+export const UserSettingsTab = (props: UserSettingsTabProps) => {
+ return <>{props.children}>;
+};
+
+attachComponentData(UserSettingsTab, USER_SETTINGS_TAB_KEY, 'UserSettingsTab');
diff --git a/plugins/user-settings/src/components/UserSettingsTab/index.ts b/plugins/user-settings/src/components/UserSettingsTab/index.ts
new file mode 100644
index 0000000000..ec7228b7a6
--- /dev/null
+++ b/plugins/user-settings/src/components/UserSettingsTab/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
+export * from './UserSettingsTab';
diff --git a/plugins/user-settings/src/components/index.ts b/plugins/user-settings/src/components/index.ts
index cb058f9c5e..90a7105876 100644
--- a/plugins/user-settings/src/components/index.ts
+++ b/plugins/user-settings/src/components/index.ts
@@ -19,3 +19,4 @@ export * from './AuthProviders';
export * from './General';
export * from './FeatureFlags';
export { useUserProfile } from './useUserProfileInfo';
+export * from './UserSettingsTab';