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';