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 (
<>
-
+