diff --git a/packages/app/package.json b/packages/app/package.json index 20b9e24701..76e20bc0dc 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -28,6 +28,7 @@ "@backstage/plugin-tech-radar": "^0.1.1-alpha.24", "@backstage/plugin-techdocs": "^0.1.1-alpha.24", "@backstage/plugin-welcome": "^0.1.1-alpha.24", + "@backstage/plugin-user-settings": "^0.1.1-alpha.24", "@backstage/test-utils": "^0.1.1-alpha.24", "@backstage/theme": "^0.1.1-alpha.24", "@material-ui/core": "^4.11.0", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 667b14366f..81d08f58a9 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -33,6 +33,7 @@ import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql'; import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar'; import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse'; import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component'; +import { Router as SettingsRouter } from '@backstage/plugin-user-settings'; import { Route, Routes, Navigate } from 'react-router'; import { EntityPage } from './components/catalog/EntityPage'; @@ -81,6 +82,7 @@ const AppRoutes = () => ( path="/register-component" element={} /> + } /> {...deprecatedAppRoutes} ); diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 45ffca7801..e947e5c91d 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -35,11 +35,10 @@ import { SidebarDivider, SidebarSearchField, SidebarSpace, - SidebarUserSettings, - DefaultProviderSettings, } from '@backstage/core'; import { NavLink } from 'react-router-dom'; import { graphiQLRouteRef } from '@backstage/plugin-graphiql'; +import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; const useSidebarLogoStyles = makeStyles({ root: { @@ -103,7 +102,7 @@ const Root: FC<{}> = ({ children }) => ( /> - } /> + {children} diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 85c4977d67..57dc8a3443 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -36,3 +36,4 @@ export { plugin as GcpProjects } from '@backstage/plugin-gcp-projects'; export { plugin as Kubernetes } from '@backstage/plugin-kubernetes'; export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild'; export { plugin as CostInsights } from '@backstage/plugin-cost-insights'; +export { plugin as UserSettings } from '@backstage/plugin-user-settings'; diff --git a/packages/core-api/src/apis/definitions/AppThemeApi.ts b/packages/core-api/src/apis/definitions/AppThemeApi.ts index de70adabd9..3f580450f0 100644 --- a/packages/core-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/core-api/src/apis/definitions/AppThemeApi.ts @@ -17,6 +17,7 @@ import { createApiRef } from '../ApiRef'; import { BackstageTheme } from '@backstage/theme'; import { Observable } from '../../types'; +import { SvgIconProps } from '@material-ui/core'; /** * Describes a theme provided by the app. @@ -41,6 +42,11 @@ export type AppTheme = { * The specialized MaterialUI theme instance. */ theme: BackstageTheme; + + /** + * An Icon for the theme mode setting. + */ + icon?: React.ReactElement; }; /** diff --git a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts index b6aac75620..dd9657d7d8 100644 --- a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts @@ -15,11 +15,8 @@ */ import { createApiRef } from '../ApiRef'; -import { - UserFlags, - FeatureFlagsRegistry, - FeatureFlagsRegistryItem, -} from '../../app/FeatureFlags'; +import { UserFlags, FeatureFlagsRegistry } from '../../app/FeatureFlags'; +import { FeatureFlagName } from '../../plugin'; /** * The feature flags API is used to toggle functionality to users across plugins and Backstage. @@ -55,6 +52,11 @@ export interface FeatureFlagsApi { getRegisteredFlags(): FeatureFlagsRegistry; } +export interface FeatureFlagsRegistryItem { + pluginId: string; + name: FeatureFlagName; +} + export const featureFlagsApiRef = createApiRef({ id: 'core.featureflags', description: 'Used to toggle functionality in features across Backstage', diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 9dd2fb138f..c222c3c420 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -30,12 +30,12 @@ import { SignInPageProps, } from './types'; import { BackstagePlugin } from '../plugin'; -import { FeatureFlagsRegistryItem } from './FeatureFlags'; import { featureFlagsApiRef, AppThemeApi, ConfigApi, identityApiRef, + FeatureFlagsRegistryItem, } from '../apis/definitions'; import { AppThemeProvider } from './AppThemeProvider'; diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx index 3db2a18a02..7a69fa5456 100644 --- a/packages/core-api/src/app/FeatureFlags.tsx +++ b/packages/core-api/src/app/FeatureFlags.tsx @@ -15,7 +15,11 @@ */ import { FeatureFlagName } from '../plugin/types'; -import { FeatureFlagState, FeatureFlagsApi } from '../apis/definitions'; +import { + FeatureFlagState, + FeatureFlagsApi, + FeatureFlagsRegistryItem, +} from '../apis/definitions'; /** * Helper method for validating compatibility and flag name. @@ -129,10 +133,6 @@ export class UserFlags extends Map { * This acts as a holding data structure for feature flags * that plugins wish to register for use in Backstage. */ -export interface FeatureFlagsRegistryItem { - pluginId: string; - name: FeatureFlagName; -} export class FeatureFlagsRegistry extends Array { static from(entries: FeatureFlagsRegistryItem[]) { diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index 882ecd3d8b..8a1eb92e8e 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -112,11 +112,13 @@ export type AppOptions = { * title: 'Light Theme', * variant: 'light', * theme: lightTheme, + * icon: , * }, { * id: 'dark', * title: 'Dark Theme', * variant: 'dark', * theme: darkTheme, + * icon: , * }] * ``` */ diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx index 7fe77423e2..c7d0f5ce01 100644 --- a/packages/core/src/api-wrappers/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -22,7 +22,8 @@ import privateExports, { AppConfigLoader, } from '@backstage/core-api'; import { BrowserRouter, MemoryRouter } from 'react-router-dom'; - +import LightIcon from '@material-ui/icons/WbSunny'; +import DarkIcon from '@material-ui/icons/Brightness2'; import { ErrorPage } from '../layout/ErrorPage'; import { Progress } from '../components/Progress'; import { defaultApis } from './defaultApis'; @@ -110,12 +111,14 @@ export function createApp(options?: AppOptions) { title: 'Light Theme', variant: 'light', theme: lightTheme, + icon: , }, { id: 'dark', title: 'Dark Theme', variant: 'dark', theme: darkTheme, + icon: , }, ]; const configLoader = options?.configLoader ?? defaultConfigLoader; diff --git a/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx b/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx deleted file mode 100644 index 99c5225c83..0000000000 --- a/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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 { - configApiRef, - githubAuthApiRef, - gitlabAuthApiRef, - googleAuthApiRef, - oauth2ApiRef, - oktaAuthApiRef, - microsoftAuthApiRef, - samlAuthApiRef, - useApi, -} from '@backstage/core-api'; -import Star from '@material-ui/icons/Star'; -import React from 'react'; -import { ProviderSettingsItem } from './Settings'; - -export const DefaultProviderSettings = () => { - const configApi = useApi(configApiRef); - const providersConfig = configApi.getOptionalConfig('auth.providers'); - const providers = providersConfig?.keys() ?? []; - - return ( - <> - {providers.includes('google') && ( - - )} - {providers.includes('microsoft') && ( - - )} - {providers.includes('github') && ( - - )} - {providers.includes('gitlab') && ( - - )} - {providers.includes('okta') && ( - - )} - {providers.includes('saml') && ( - - )} - {providers.includes('oauth2') && ( - - )} - - ); -}; diff --git a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx deleted file mode 100644 index 71c4899862..0000000000 --- a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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 { - Card, - CardContent, - CardHeader, - makeStyles, - Divider, -} from '@material-ui/core'; -import { AppSettingsList } from './AppSettingsList'; -import { AuthProvidersList } from './AuthProviderList'; -import { FeatureFlagsList } from './FeatureFlagsList'; -import { SignInAvatar } from './SignInAvatar'; -import { UserSettingsMenu } from './UserSettingsMenu'; -import { useUserProfile } from './useUserProfileInfo'; -import { useApi, featureFlagsApiRef } from '@backstage/core-api'; - -const useStyles = makeStyles({ - root: { - minWidth: 400, - }, -}); - -type Props = { - providerSettings?: React.ReactNode; -}; - -export const SettingsDialog = ({ providerSettings }: Props) => { - const classes = useStyles(); - const { profile, displayName } = useUserProfile(); - const featureFlagsApi = useApi(featureFlagsApiRef); - const featureFlags = featureFlagsApi.getRegisteredFlags(); - - return ( - - } - action={} - title={displayName} - subheader={profile.email} - /> - - - {providerSettings && ( - <> - - - - )} - {featureFlags.length > 0 && ( - <> - - - - )} - - - ); -}; diff --git a/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx b/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx deleted file mode 100644 index 85dbefff9b..0000000000 --- a/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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, { useEffect, useContext } from 'react'; -import { Popover } from '@material-ui/core'; -import { SignInAvatar } from './SignInAvatar'; -import { SettingsDialog } from './SettingsDialog'; -import { SidebarItem } from '../Items'; -import { useUserProfile } from './useUserProfileInfo'; -import { SidebarContext } from '../config'; - -type Props = { - providerSettings?: React.ReactNode; -}; - -export const SidebarUserSettings = ({ providerSettings }: Props) => { - const { isOpen: sidebarOpen } = useContext(SidebarContext); - const { displayName } = useUserProfile(); - const [open, setOpen] = React.useState(false); - const [anchorEl, setAnchorEl] = React.useState( - undefined, - ); - - const handleOpen = (event?: React.MouseEvent) => { - setAnchorEl(event?.currentTarget ?? undefined); - setOpen(true); - }; - - const handleClose = () => { - setAnchorEl(undefined); - setOpen(false); - }; - - useEffect(() => { - if (!sidebarOpen && open) setOpen(false); - }, [open, sidebarOpen]); - - const SidebarAvatar = () => ; - - return ( - <> - - - - - - ); -}; diff --git a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx index 61975a728a..908fbba75c 100644 --- a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx +++ b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx @@ -22,14 +22,10 @@ import { SidebarDivider, SidebarSearchField, SidebarSpace, - SidebarUserSettings, - ProviderSettingsItem, } from '.'; import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined'; import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; -import Star from '@material-ui/icons/Star'; import { MemoryRouter } from 'react-router-dom'; -import { githubAuthApiRef } from '@backstage/core-api'; export default { title: 'Sidebar', @@ -48,7 +44,6 @@ const handleSearch = (input: string) => { export const SampleSidebar = () => ( - {/* */} @@ -57,15 +52,5 @@ export const SampleSidebar = () => ( - - - } - /> ); diff --git a/packages/core/src/layout/Sidebar/index.ts b/packages/core/src/layout/Sidebar/index.ts index a644c06e14..803306478d 100644 --- a/packages/core/src/layout/Sidebar/index.ts +++ b/packages/core/src/layout/Sidebar/index.ts @@ -31,5 +31,3 @@ export { sidebarConfig, } from './config'; export type { SidebarContextType } from './config'; -export { DefaultProviderSettings } from './DefaultProviderSettings'; -export * from './Settings'; diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs index a057826ba4..103e75f6e0 100644 --- a/packages/create-app/templates/default-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs @@ -19,6 +19,7 @@ "@backstage/plugin-lighthouse": "^{{version}}", "@backstage/plugin-tech-radar": "^{{version}}", "@backstage/plugin-github-actions": "^{{version}}", + "@backstage/plugin-user-settings": "^{{version}}", "@backstage/test-utils": "^{{version}}", "@backstage/theme": "^{{version}}", "history": "^5.0.0", diff --git a/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx b/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx index 9dd9ea64c3..901347645e 100644 --- a/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx @@ -9,6 +9,7 @@ import { Link, makeStyles } from '@material-ui/core'; import { NavLink } from 'react-router-dom'; import LogoFull from './LogoFull'; import LogoIcon from './LogoIcon'; +import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; import { Sidebar, @@ -17,8 +18,6 @@ import { sidebarConfig, SidebarContext, SidebarSpace, - SidebarUserSettings, - DefaultProviderSettings, } from '@backstage/core'; export const AppSidebar = () => ( @@ -37,7 +36,7 @@ export const AppSidebar = () => ( - } /> + ); diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js index c83bb221be..2c58144acc 100644 --- a/packages/storybook/.storybook/main.js +++ b/packages/storybook/.storybook/main.js @@ -5,6 +5,7 @@ module.exports = { stories: [ '../../core/src/layout/**/*.stories.tsx', '../../core/src/components/**/*.stories.tsx', + '../../../plugins/**/src/**/*.stories.tsx', ], addons: [ '@storybook/addon-actions', diff --git a/plugins/user-settings/.eslintrc.js b/plugins/user-settings/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/user-settings/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md new file mode 100644 index 0000000000..db0b01681f --- /dev/null +++ b/plugins/user-settings/README.md @@ -0,0 +1,65 @@ +# user-settings + +Welcome to the user-settings plugin! + +_This plugin was created through the Backstage CLI_ + +## About the plugin + +This plugin provides two components, `` is intended to be used within the [``](https://backstage.io/storybook/?path=/story/sidebar--sample-sidebar) and displays the signed-in users profile picture and name. + +The second component is a settings page where the user can control different settings across the App. + +## Usage + +Add the item to the Sidebar: + +```ts +import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; + + + + + +; +``` + +Add the page to the App routing: + +```ts +import { Router as SettingsRouter } from '@backstage/plugin-user-settings'; + +const AppRoutes = () => ( + + } /> + +); +``` + +### Props + +**Auth Providers** + +By default, the plugin provides a list of configured authentication providers fetched from `app-config.yaml` and displayed in the "Authentication Providers" tab. + +If you want to supply your own custom list of Authentication Providers, use the `providerSettings` prop: + +```ts +const MyAuthProviders = () => ( + + + {someAction} + +); + +const AppRoutes = () => ( + + } />} + /> + +); +``` + +> **Note that the list of providers expects to be rendered within a MUI [``](https://material-ui.com/components/lists/)** diff --git a/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx b/plugins/user-settings/dev/index.tsx similarity index 62% rename from packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx rename to plugins/user-settings/dev/index.tsx index 5dad178ccd..264d6f801f 100644 --- a/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx +++ b/plugins/user-settings/dev/index.tsx @@ -13,14 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { List, ListSubheader } from '@material-ui/core'; -import { SidebarThemeToggle } from './ThemeToggle'; -import { SidebarPinButton } from './PinButton'; +import { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; -export const AppSettingsList = () => ( - App Settings}> - - - -); +createDevApp().registerPlugin(plugin).render(); diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json new file mode 100644 index 0000000000..29f86c774b --- /dev/null +++ b/plugins/user-settings/package.json @@ -0,0 +1,49 @@ +{ + "name": "@backstage/plugin-user-settings", + "version": "0.1.1-alpha.24", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core": "^0.1.1-alpha.24", + "@backstage/theme": "^0.1.1-alpha.24", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router": "6.0.0-beta.0", + "react-use": "^15.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.24", + "@backstage/dev-utils": "^0.1.1-alpha.24", + "@backstage/test-utils": "^0.1.1-alpha.24", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^12.0.0", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/user-settings/src/components/AuthProviders/AuthProviders.test.tsx b/plugins/user-settings/src/components/AuthProviders/AuthProviders.test.tsx new file mode 100644 index 0000000000..4399b6bd3e --- /dev/null +++ b/plugins/user-settings/src/components/AuthProviders/AuthProviders.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + ApiProvider, + ApiRegistry, + configApiRef, + ConfigReader, + googleAuthApiRef, +} from '@backstage/core'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { fireEvent } from '@testing-library/react'; +import React from 'react'; +import { AuthProviders } from './AuthProviders'; + +const mockSignInHandler = jest.fn().mockReturnValue(''); +const mockGoogleAuth = { + sessionState$: () => ({ + subscribe: () => ({ + unsubscribe: () => null, + }), + }), + signIn: mockSignInHandler, +}; + +const createConfig = () => + ConfigReader.fromConfigs([ + { + context: '', + data: { + auth: { + providers: { + google: { development: {} }, + }, + }, + }, + }, + ]); + +const config = createConfig(); + +const apiRegistry = ApiRegistry.from([ + [configApiRef, config], + [googleAuthApiRef, mockGoogleAuth], +]); + +describe('', () => { + it('displays a provider and calls its sign-in handler on click', async () => { + const rendered = await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + expect(rendered.getByText('Google')).toBeInTheDocument(); + expect( + rendered.getByText(googleAuthApiRef.description), + ).toBeInTheDocument(); + + const button = rendered.getByTitle('Sign in to Google'); + fireEvent.click(button); + expect(mockSignInHandler).toHaveBeenCalledTimes(1); + }); +}); diff --git a/plugins/user-settings/src/components/AuthProviders/AuthProviders.tsx b/plugins/user-settings/src/components/AuthProviders/AuthProviders.tsx new file mode 100644 index 0000000000..563bf44150 --- /dev/null +++ b/plugins/user-settings/src/components/AuthProviders/AuthProviders.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { List } from '@material-ui/core'; +import { configApiRef, InfoCard, useApi } from '@backstage/core'; +import { EmptyProviders } from './EmptyProviders'; +import { DefaultProviderSettings } from './DefaultProviderSettings'; + +type Props = { + providerSettings?: JSX.Element; +}; + +export const AuthProviders = ({ providerSettings }: Props) => { + const configApi = useApi(configApiRef); + const providersConfig = configApi.getOptionalConfig('auth.providers'); + const configuredProviders = providersConfig?.keys() || []; + const providers = providerSettings ?? ( + + ); + + if (!providerSettings && !configuredProviders?.length) { + return ; + } + + return ( + + {providers} + + ); +}; diff --git a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx new file mode 100644 index 0000000000..a1dab1e7d4 --- /dev/null +++ b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx @@ -0,0 +1,83 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + githubAuthApiRef, + gitlabAuthApiRef, + googleAuthApiRef, + oauth2ApiRef, + oktaAuthApiRef, + microsoftAuthApiRef, +} from '@backstage/core'; +import Star from '@material-ui/icons/Star'; +import React from 'react'; +import { ProviderSettingsItem } from './ProviderSettingsItem'; + +type Props = { + configuredProviders: string[]; +}; + +export const DefaultProviderSettings = ({ configuredProviders }: Props) => ( + <> + {configuredProviders.includes('google') && ( + + )} + {configuredProviders.includes('microsoft') && ( + + )} + {configuredProviders.includes('github') && ( + + )} + {configuredProviders.includes('gitlab') && ( + + )} + {configuredProviders.includes('okta') && ( + + )} + {configuredProviders.includes('oauth2') && ( + + )} + +); diff --git a/plugins/user-settings/src/components/AuthProviders/EmptyProviders.tsx b/plugins/user-settings/src/components/AuthProviders/EmptyProviders.tsx new file mode 100644 index 0000000000..653f449e92 --- /dev/null +++ b/plugins/user-settings/src/components/AuthProviders/EmptyProviders.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { CodeSnippet, EmptyState } from '@backstage/core'; +import { Button, Typography } from '@material-ui/core'; + +const EXAMPLE = `auth: + providers: + google: + development: + clientId: + $env: AUTH_GOOGLE_CLIENT_ID + clientSecret: + $env: AUTH_GOOGLE_CLIENT_SECRET +`; + +export const EmptyProviders = () => ( + + + Open app-config.yaml and make the changes as highlighted + below: + + + + + } + /> +); diff --git a/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx b/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx similarity index 65% rename from packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx rename to plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx index 4db7ea0823..7213ee2b83 100644 --- a/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx +++ b/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx @@ -13,8 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import React, { FC, useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; +import { + ApiRef, + SessionApi, + useApi, + IconComponent, + SessionState, +} from '@backstage/core'; import { ListItem, ListItemIcon, @@ -24,25 +30,20 @@ import { } from '@material-ui/core'; import PowerButton from '@material-ui/icons/PowerSettingsNew'; import { ToggleButton } from '@material-ui/lab'; -import { - ApiRef, - SessionApi, - useApi, - IconComponent, - SessionState, -} from '@backstage/core-api'; -type OAuthProviderSidebarProps = { +type Props = { title: string; + description: string; icon: IconComponent; apiRef: ApiRef; }; -export const ProviderSettingsItem: FC = ({ +export const ProviderSettingsItem = ({ title, + description, icon: Icon, apiRef, -}) => { +}: Props) => { const api = useApi(apiRef); const [signedIn, setSignedIn] = useState(false); @@ -68,22 +69,30 @@ export const ProviderSettingsItem: FC = ({ - - - (signedIn ? api.signOut() : api.signIn())} - > - - + + {description} - + } + secondaryTypographyProps={{ noWrap: true, style: { width: '80%' } }} + /> + + + (signedIn ? api.signOut() : api.signIn())} + > + + + ); diff --git a/plugins/user-settings/src/components/AuthProviders/index.ts b/plugins/user-settings/src/components/AuthProviders/index.ts new file mode 100644 index 0000000000..2f6a2b5ab1 --- /dev/null +++ b/plugins/user-settings/src/components/AuthProviders/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { AuthProviders } from './AuthProviders'; +export { DefaultProviderSettings } from './DefaultProviderSettings'; diff --git a/plugins/user-settings/src/components/FeatureFlags/EmptyFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/EmptyFlags.tsx new file mode 100644 index 0000000000..5df4df4450 --- /dev/null +++ b/plugins/user-settings/src/components/FeatureFlags/EmptyFlags.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { CodeSnippet, EmptyState } from '@backstage/core'; +import { Button, Typography } from '@material-ui/core'; + +const EXAMPLE = `import { createPlugin } from '@backstage/core'; + +export default createPlugin({ + id: 'welcome', + register({ router, featureFlags }) { + featureFlags.register('enable-example-feature'); + }, +}); +`; + +export const EmptyFlags = () => ( + + + An example how how to add a feature flags is highlighted below: + + + + + } + /> +); diff --git a/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx new file mode 100644 index 0000000000..71f59c52b8 --- /dev/null +++ b/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx @@ -0,0 +1,82 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, { useCallback, useState } from 'react'; +import { + FeatureFlagName, + featureFlagsApiRef, + FeatureFlagsRegistryItem, + FeatureFlagState, + InfoCard, + useApi, +} from '@backstage/core'; +import { List } from '@material-ui/core'; +import { EmptyFlags } from './EmptyFlags'; +import { FlagItem } from './FeatureFlagsItem'; + +export const FeatureFlags = () => { + const featureFlagsApi = useApi(featureFlagsApiRef); + const featureFlags = featureFlagsApi.getRegisteredFlags(); + const initialFlagState = featureFlags.reduce( + (result, featureFlag: FeatureFlagsRegistryItem) => { + const state = featureFlagsApi.getFlags().get(featureFlag.name); + + result[featureFlag.name] = state; + return result; + }, + {} as Record, + ); + + const [state, setState] = useState>( + initialFlagState, + ); + + const toggleFlag = useCallback( + (flagName: FeatureFlagName) => { + const newState = featureFlagsApi.getFlags().toggle(flagName); + + setState(prevState => ({ + ...prevState, + [flagName]: newState, + })); + featureFlagsApi.getFlags().save(); + }, + [featureFlagsApi], + ); + + if (!featureFlags.length) { + return ; + } + + return ( + + + {featureFlags.map(featureFlag => { + const enabled = Boolean(state[featureFlag.name]); + + return ( + + ); + })} + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx b/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx similarity index 51% rename from packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx rename to plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx index 3e40ce69cc..92065f2ed6 100644 --- a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx @@ -15,11 +15,6 @@ */ import React from 'react'; -import { - FeatureFlagName, - useApi, - featureFlagsApiRef, -} from '@backstage/core-api'; import { ListItem, ListItemSecondaryAction, @@ -28,46 +23,31 @@ import { } from '@material-ui/core'; import CheckIcon from '@material-ui/icons/CheckCircle'; import { ToggleButton } from '@material-ui/lab'; - -export type Item = { - name: FeatureFlagName; - pluginId: string; -}; +import { FeatureFlagsRegistryItem } from '@backstage/core'; type Props = { - featureFlag: Item; + flag: FeatureFlagsRegistryItem; + enabled: boolean; + toggleHandler: Function; }; -export const FlagItem = ({ featureFlag }: Props) => { - const api = useApi(featureFlagsApiRef); - - const [enabled, setEnabled] = React.useState( - Boolean(api.getFlags().get(featureFlag.name)), - ); - - const toggleFlag = () => { - const newState = api.getFlags().toggle(featureFlag.name); - setEnabled(Boolean(newState)); - }; - - return ( - - - +export const FlagItem = ({ flag, enabled, toggleHandler }: Props) => ( + + + + toggleHandler(flag.name)} > - - - + - - - ); -}; + + + +); diff --git a/packages/core/src/layout/Sidebar/Settings/index.ts b/plugins/user-settings/src/components/FeatureFlags/index.ts similarity index 83% rename from packages/core/src/layout/Sidebar/Settings/index.ts rename to plugins/user-settings/src/components/FeatureFlags/index.ts index 15042dc85d..37c9fd1fd2 100644 --- a/packages/core/src/layout/Sidebar/Settings/index.ts +++ b/plugins/user-settings/src/components/FeatureFlags/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export { ProviderSettingsItem } from './ProviderSettingsItem'; -export { SidebarUserSettings } from './UserSettings'; +export { FeatureFlags } from './FeatureFlags'; diff --git a/plugins/user-settings/src/components/General/General.tsx b/plugins/user-settings/src/components/General/General.tsx new file mode 100644 index 0000000000..00ac17dfe1 --- /dev/null +++ b/plugins/user-settings/src/components/General/General.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { InfoCard } from '@backstage/core'; +import { Grid, List } from '@material-ui/core'; +import React from 'react'; +import { PinButton } from './PinButton'; +import { Profile } from './Profile'; +import { ThemeToggle } from './ThemeToggle'; + +export const General = () => ( + + + + + + + + + + + + + +); diff --git a/plugins/user-settings/src/components/General/PinButton.test.tsx b/plugins/user-settings/src/components/General/PinButton.test.tsx new file mode 100644 index 0000000000..76af8ce8d4 --- /dev/null +++ b/plugins/user-settings/src/components/General/PinButton.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { SidebarPinStateContext } from '@backstage/core'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { fireEvent } from '@testing-library/react'; +import React from 'react'; +import { PinButton } from './PinButton'; + +describe('', () => { + it('toggles the pin sidebar button', async () => { + const mockToggleFn = jest.fn(); + const rendered = await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + expect(rendered.getByText('Pin Sidebar')).toBeInTheDocument(); + + const pinButton = rendered.getByTitle('Pin Sidebar'); + fireEvent.click(pinButton); + expect(mockToggleFn).toHaveBeenCalled(); + }); +}); diff --git a/packages/core/src/layout/Sidebar/Settings/PinButton.tsx b/plugins/user-settings/src/components/General/PinButton.tsx similarity index 65% rename from packages/core/src/layout/Sidebar/Settings/PinButton.tsx rename to plugins/user-settings/src/components/General/PinButton.tsx index 2727313ede..e36fc365f8 100644 --- a/packages/core/src/layout/Sidebar/Settings/PinButton.tsx +++ b/plugins/user-settings/src/components/General/PinButton.tsx @@ -24,23 +24,18 @@ import { import LockIcon from '@material-ui/icons/Lock'; import LockOpenIcon from '@material-ui/icons/LockOpen'; import { ToggleButton } from '@material-ui/lab'; -import { SidebarPinStateContext } from '../Page'; +import { SidebarPinStateContext } from '@backstage/core'; -export const SidebarPinButton = () => { +type PinIconProps = { isPinned: boolean }; + +const PinIcon = ({ isPinned }: PinIconProps) => + isPinned ? : ; + +export const PinButton = () => { const { isPinned, toggleSidebarPinState } = useContext( SidebarPinStateContext, ); - const PinIcon = () => ( - - {isPinned ? : } - - ); - return ( { secondary="Prevent the sidebar from collapsing" /> - { - toggleSidebarPinState(); - }} + - - + { + toggleSidebarPinState(); + }} + > + + + ); diff --git a/plugins/user-settings/src/components/General/Profile.tsx b/plugins/user-settings/src/components/General/Profile.tsx new file mode 100644 index 0000000000..b0231d95b6 --- /dev/null +++ b/plugins/user-settings/src/components/General/Profile.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { InfoCard } from '@backstage/core'; +import { Grid, Typography } from '@material-ui/core'; +import React from 'react'; +import { SignInAvatar } from './SignInAvatar'; +import { UserSettingsMenu } from './UserSettingsMenu'; +import { useUserProfile } from '../useUserProfileInfo'; + +export const Profile = () => { + const { profile, displayName } = useUserProfile(); + + return ( + + + + + + + + + + + + {displayName} + + + {profile.email} + + + + + + + + + + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx b/plugins/user-settings/src/components/General/SignInAvatar.tsx similarity index 72% rename from packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx rename to plugins/user-settings/src/components/General/SignInAvatar.tsx index f0430edfcf..8a5bbd75bf 100644 --- a/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx +++ b/plugins/user-settings/src/components/General/SignInAvatar.tsx @@ -17,26 +17,24 @@ import React from 'react'; import { BackstageTheme } from '@backstage/theme'; import { makeStyles, Avatar } from '@material-ui/core'; -import { useUserProfile } from './useUserProfileInfo'; -import { sidebarConfig } from '../config'; +import { useUserProfile } from '../useUserProfileInfo'; +import { sidebarConfig } from '@backstage/core'; -const useStyles = makeStyles({ +const useStyles = makeStyles(theme => ({ avatar: { width: ({ size }) => size, height: ({ size }) => size, + fontSize: ({ size }) => size * 0.7, + border: `1px solid ${theme.palette.textSubtle}`, }, -}); +})); type Props = { size?: number }; export const SignInAvatar = ({ size }: Props) => { const { iconSize } = sidebarConfig; const classes = useStyles(size ? { size } : { size: iconSize }); - const { profile, displayName } = useUserProfile(); + const { profile } = useUserProfile(); - return ( - - {displayName[0]} - - ); + return ; }; diff --git a/plugins/user-settings/src/components/General/ThemeToggle.test.tsx b/plugins/user-settings/src/components/General/ThemeToggle.test.tsx new file mode 100644 index 0000000000..79d9210826 --- /dev/null +++ b/plugins/user-settings/src/components/General/ThemeToggle.test.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + ApiProvider, + ApiRegistry, + appThemeApiRef, + AppThemeSelector, +} from '@backstage/core'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { lightTheme } from '@backstage/theme'; +import { fireEvent } from '@testing-library/react'; +import React from 'react'; +import { ThemeToggle } from './ThemeToggle'; + +const mockTheme = { + id: 'light', + title: 'Mock Theme', + variant: 'light' as 'light', // wut? + theme: lightTheme, +}; + +const apiRegistry = ApiRegistry.from([ + [appThemeApiRef, AppThemeSelector.createWithStorage([mockTheme])], +]); + +describe('', () => { + it('toggles the theme select button', async () => { + const themeApi = apiRegistry.get(appThemeApiRef); + const rendered = await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + expect(rendered.getByText('Theme')).toBeInTheDocument(); + + const themeButton = rendered.getByTitle('Select Mock Theme'); + expect(themeApi?.getActiveThemeId()).toBe(undefined); + fireEvent.click(themeButton); + expect(themeApi?.getActiveThemeId()).toBe('light'); + }); +}); diff --git a/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx b/plugins/user-settings/src/components/General/ThemeToggle.tsx similarity index 53% rename from packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx rename to plugins/user-settings/src/components/General/ThemeToggle.tsx index a5e703089c..111bd838a8 100644 --- a/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx +++ b/plugins/user-settings/src/components/General/ThemeToggle.tsx @@ -14,12 +14,10 @@ * limitations under the License. */ -import React from 'react'; +import React, { cloneElement } from 'react'; import { useObservable } from 'react-use'; -import LightIcon from '@material-ui/icons/WbSunny'; -import DarkIcon from '@material-ui/icons/Brightness2'; import AutoIcon from '@material-ui/icons/BrightnessAuto'; -import { appThemeApiRef, useApi } from '@backstage/core-api'; +import { appThemeApiRef, useApi } from '@backstage/core'; import ToggleButton from '@material-ui/lab/ToggleButton'; import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'; import { @@ -29,7 +27,43 @@ import { Tooltip, } from '@material-ui/core'; -export const SidebarThemeToggle = () => { +type ThemeIconProps = { + id: string; + activeId: string | undefined; + icon: JSX.Element | undefined; +}; + +const ThemeIcon = ({ id, activeId, icon }: ThemeIconProps) => + icon ? ( + cloneElement(icon, { + color: activeId === id ? 'primary' : undefined, + }) + ) : ( + + ); + +type TooltipToggleButtonProps = { + children: JSX.Element; + title: string; + value: string; +}; + +// ToggleButtonGroup uses React.children.map instead of context +// so wrapping with Tooltip breaks ToggleButton functionality. +const TooltipToggleButton = ({ + children, + title, + value, + ...props +}: TooltipToggleButtonProps) => ( + + + {children} + + +); + +export const ThemeToggle = () => { const appThemeApi = useApi(appThemeApiRef); const themeId = useObservable( appThemeApi.activeThemeId$(), @@ -37,11 +71,6 @@ export const SidebarThemeToggle = () => { ); const themeIds = appThemeApi.getInstalledThemes(); - // TODO(marcuseide): can these be put on the theme itself? - const themeIcons = { - dark: , - light: , - }; const handleSetTheme = ( _event: React.MouseEvent, @@ -64,22 +93,24 @@ export const SidebarThemeToggle = () => { value={themeId ?? 'auto'} onChange={handleSetTheme} > - {themeIds.map(theme => ( - - { + const themeIcon = themeIds.find(t => t.id === theme.id)?.icon; + + return ( + - {themeIcons[theme.variant]} - + + + ); + })} + + + - ))} - - - - - + diff --git a/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx b/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx new file mode 100644 index 0000000000..c86d702e30 --- /dev/null +++ b/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { fireEvent } from '@testing-library/react'; +import React from 'react'; +import { UserSettingsMenu } from './UserSettingsMenu'; + +describe('', () => { + it('displays a menu button with a sign-out option', async () => { + const rendered = await renderWithEffects( + wrapInTestApp(), + ); + + const menuButton = rendered.getByLabelText('more'); + fireEvent.click(menuButton); + + expect(rendered.getByText('Sign Out')).toBeInTheDocument(); + }); +}); diff --git a/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx b/plugins/user-settings/src/components/General/UserSettingsMenu.tsx similarity index 93% rename from packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx rename to plugins/user-settings/src/components/General/UserSettingsMenu.tsx index 151ddb6e75..19c3ee4e2d 100644 --- a/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsMenu.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { identityApiRef, useApi } from '@backstage/core-api'; +import { identityApiRef, useApi } from '@backstage/core'; import { IconButton, ListItemIcon, Menu, MenuItem } from '@material-ui/core'; import SignOutIcon from '@material-ui/icons/MeetingRoom'; import MoreVertIcon from '@material-ui/icons/MoreVert'; @@ -39,7 +39,7 @@ export const UserSettingsMenu = () => { return ( <> - + diff --git a/plugins/user-settings/src/components/General/index.ts b/plugins/user-settings/src/components/General/index.ts new file mode 100644 index 0000000000..2015d345fe --- /dev/null +++ b/plugins/user-settings/src/components/General/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { General } from './General'; +export { SignInAvatar } from './SignInAvatar'; diff --git a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx b/plugins/user-settings/src/components/Settings.tsx similarity index 57% rename from packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx rename to plugins/user-settings/src/components/Settings.tsx index 5687446511..f754f1e7c3 100644 --- a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx +++ b/plugins/user-settings/src/components/Settings.tsx @@ -15,18 +15,20 @@ */ import React from 'react'; -import List from '@material-ui/core/List'; -import ListSubheader from '@material-ui/core/ListSubheader'; -import { FlagItem, Item } from './FeatureFlagsItem'; +import { SidebarItem } from '@backstage/core'; +import { SignInAvatar } from './General'; +import { useUserProfile } from './useUserProfileInfo'; +import { settingsRouteRef } from '../plugin'; -type Props = { - featureFlags: Item[]; +export const Settings = () => { + const { displayName } = useUserProfile(); + const SidebarAvatar = () => ; + + return ( + + ); }; - -export const FeatureFlagsList = ({ featureFlags }: Props) => ( - Feature Flags}> - {featureFlags.map(featureFlag => ( - - ))} - -); diff --git a/plugins/user-settings/src/components/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage.tsx new file mode 100644 index 0000000000..86096e9a1f --- /dev/null +++ b/plugins/user-settings/src/components/SettingsPage.tsx @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, { useState } from 'react'; +import { Content, Header, HeaderTabs, Page, pageTheme } from '@backstage/core'; +import { General } from './General'; +import { AuthProviders } from './AuthProviders'; +import { FeatureFlags } from './FeatureFlags'; + +type Props = { + providerSettings?: JSX.Element; +}; + +export const SettingsPage = ({ providerSettings }: Props) => { + const [activeTab, setActiveTab] = useState(0); + const onTabChange = (index: number) => { + setActiveTab(index); + }; + + const tabs = [ + { id: 'general', label: 'General' }, + { id: 'auth-providers', label: 'Authentication Providers' }, + { id: 'feature-flags', label: 'Feature Flags' }, + ]; + + const content = [ + , + , + , + ]; + + return ( + +
+ + {content[activeTab]} + + ); +}; diff --git a/plugins/user-settings/src/components/SidebarWithSettings.stories.tsx b/plugins/user-settings/src/components/SidebarWithSettings.stories.tsx new file mode 100644 index 0000000000..579f2de640 --- /dev/null +++ b/plugins/user-settings/src/components/SidebarWithSettings.stories.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + ApiProvider, + ApiRegistry, + appThemeApiRef, + AppThemeSelector, + configApiRef, + ConfigReader, + FeatureFlags, + featureFlagsApiRef, + Sidebar, + SidebarDivider, + SidebarSpace, +} from '@backstage/core'; +import { MemoryRouter } from 'react-router'; +import { Settings } from './Settings'; +import { SettingsPage } from './SettingsPage'; + +export default { + title: 'Settings', + component: Settings, + decorators: [ + (storyFn: () => JSX.Element) => ( + {storyFn()} + ), + ], +}; + +export const SidebarItem = () => ( + + + + + +); + +const createConfig = () => + ConfigReader.fromConfigs([ + { + context: '', + data: { + auth: { + providers: {}, + }, + }, + }, + ]); + +const config = createConfig(); + +const apis = ApiRegistry.from([ + [configApiRef, config], + [featureFlagsApiRef, new FeatureFlags()], + [appThemeApiRef, AppThemeSelector.createWithStorage([])], +]); + +export const TheSettingsPage = () => ( +
+ + + +
+); diff --git a/packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts b/plugins/user-settings/src/components/useUserProfileInfo.ts similarity index 93% rename from packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts rename to plugins/user-settings/src/components/useUserProfileInfo.ts index 60dae294a5..428719160d 100644 --- a/packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts +++ b/plugins/user-settings/src/components/useUserProfileInfo.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useApi, identityApiRef } from '@backstage/core-api'; +import { useApi, identityApiRef } from '@backstage/core'; export const useUserProfile = () => { const identityApi = useApi(identityApiRef); diff --git a/plugins/user-settings/src/index.ts b/plugins/user-settings/src/index.ts new file mode 100644 index 0000000000..113930902a --- /dev/null +++ b/plugins/user-settings/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { plugin } from './plugin'; +export { Settings } from './components/Settings'; +export { SettingsPage as Router } from './components/SettingsPage'; diff --git a/plugins/user-settings/src/plugin.test.ts b/plugins/user-settings/src/plugin.test.ts new file mode 100644 index 0000000000..810a2cda12 --- /dev/null +++ b/plugins/user-settings/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { plugin } from './plugin'; + +describe('user-settings', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx b/plugins/user-settings/src/plugin.ts similarity index 61% rename from packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx rename to plugins/user-settings/src/plugin.ts index 34cf6fb837..2f896c8197 100644 --- a/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx +++ b/plugins/user-settings/src/plugin.ts @@ -13,17 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { createPlugin, createRouteRef } from '@backstage/core'; -import React from 'react'; -import List from '@material-ui/core/List'; -import ListSubheader from '@material-ui/core/ListSubheader'; +export const settingsRouteRef = createRouteRef({ + path: '/settings', + title: 'Settings', +}); -type Props = { - providerSettings: React.ReactNode; -}; - -export const AuthProvidersList = ({ providerSettings }: Props) => ( - Available Auth Providers}> - {providerSettings} - -); +export const plugin = createPlugin({ + id: 'user-settings', +}); diff --git a/plugins/user-settings/src/setupTests.ts b/plugins/user-settings/src/setupTests.ts new file mode 100644 index 0000000000..0bfa67b49a --- /dev/null +++ b/plugins/user-settings/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 '@testing-library/jest-dom';