Merge pull request #2659 from spotify/eide/user-settings-plugin

Plugin: user-settings
This commit is contained in:
Marcus Eide
2020-10-08 09:23:48 +02:00
committed by GitHub
50 changed files with 1173 additions and 435 deletions
+1
View File
@@ -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",
+2
View File
@@ -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={<RegisterComponentRouter catalogRouteRef={catalogRouteRef} />}
/>
<Route path="/settings" element={<SettingsRouter />} />
{...deprecatedAppRoutes}
</Routes>
);
+2 -3
View File
@@ -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 }) => (
/>
<SidebarSpace />
<SidebarDivider />
<SidebarUserSettings providerSettings={<DefaultProviderSettings />} />
<SidebarSettings />
</Sidebar>
{children}
</SidebarPage>
+1
View File
@@ -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';
@@ -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<SvgIconProps>;
};
/**
@@ -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<FeatureFlagsApi>({
id: 'core.featureflags',
description: 'Used to toggle functionality in features across Backstage',
+1 -1
View File
@@ -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';
+5 -5
View File
@@ -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<FeatureFlagName, FeatureFlagState> {
* 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<FeatureFlagsRegistryItem> {
static from(entries: FeatureFlagsRegistryItem[]) {
+2
View File
@@ -112,11 +112,13 @@ export type AppOptions = {
* title: 'Light Theme',
* variant: 'light',
* theme: lightTheme,
* icon: <LightIcon />,
* }, {
* id: 'dark',
* title: 'Dark Theme',
* variant: 'dark',
* theme: darkTheme,
* icon: <DarkIcon />,
* }]
* ```
*/
+4 -1
View File
@@ -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: <LightIcon />,
},
{
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
theme: darkTheme,
icon: <DarkIcon />,
},
];
const configLoader = options?.configLoader ?? defaultConfigLoader;
@@ -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') && (
<ProviderSettingsItem
title="Google"
apiRef={googleAuthApiRef}
icon={Star}
/>
)}
{providers.includes('microsoft') && (
<ProviderSettingsItem
title="Microsoft"
apiRef={microsoftAuthApiRef}
icon={Star}
/>
)}
{providers.includes('github') && (
<ProviderSettingsItem
title="Github"
apiRef={githubAuthApiRef}
icon={Star}
/>
)}
{providers.includes('gitlab') && (
<ProviderSettingsItem
title="Gitlab"
apiRef={gitlabAuthApiRef}
icon={Star}
/>
)}
{providers.includes('okta') && (
<ProviderSettingsItem
title="Okta"
apiRef={oktaAuthApiRef}
icon={Star}
/>
)}
{providers.includes('saml') && (
<ProviderSettingsItem
title="SAML"
apiRef={samlAuthApiRef}
icon={Star}
/>
)}
{providers.includes('oauth2') && (
<ProviderSettingsItem
title="YourOrg"
apiRef={oauth2ApiRef}
icon={Star}
/>
)}
</>
);
};
@@ -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 (
<Card className={classes.root}>
<CardHeader
avatar={<SignInAvatar size={48} />}
action={<UserSettingsMenu />}
title={displayName}
subheader={profile.email}
/>
<CardContent>
<AppSettingsList />
{providerSettings && (
<>
<Divider />
<AuthProvidersList providerSettings={providerSettings} />
</>
)}
{featureFlags.length > 0 && (
<>
<Divider />
<FeatureFlagsList featureFlags={featureFlags} />
</>
)}
</CardContent>
</Card>
);
};
@@ -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<HTMLButtonElement | undefined>(
undefined,
);
const handleOpen = (event?: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event?.currentTarget ?? undefined);
setOpen(true);
};
const handleClose = () => {
setAnchorEl(undefined);
setOpen(false);
};
useEffect(() => {
if (!sidebarOpen && open) setOpen(false);
}, [open, sidebarOpen]);
const SidebarAvatar = () => <SignInAvatar />;
return (
<>
<SidebarItem
text={displayName}
onClick={handleOpen}
icon={SidebarAvatar}
/>
<Popover
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'center',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<SettingsDialog providerSettings={providerSettings} />
</Popover>
</>
);
};
@@ -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 = () => (
<Sidebar>
{/* <SidebarLogo /> */}
<SidebarSearchField onSearch={handleSearch} />
<SidebarDivider />
<SidebarItem icon={HomeOutlinedIcon} to="#" text="Home" />
@@ -57,15 +52,5 @@ export const SampleSidebar = () => (
<SidebarDivider />
<SidebarIntro />
<SidebarSpace />
<SidebarDivider />
<SidebarUserSettings
providerSettings={
<ProviderSettingsItem
title="Github"
apiRef={githubAuthApiRef}
icon={Star}
/>
}
/>
</Sidebar>
);
@@ -31,5 +31,3 @@ export {
sidebarConfig,
} from './config';
export type { SidebarContextType } from './config';
export { DefaultProviderSettings } from './DefaultProviderSettings';
export * from './Settings';
@@ -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",
@@ -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 = () => (
<SidebarDivider />
<SidebarSpace />
<SidebarDivider />
<SidebarUserSettings providerSettings={<DefaultProviderSettings />} />
<SidebarSettings />
</Sidebar>
);
+1
View File
@@ -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',
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+65
View File
@@ -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, `<UserSettings />` is intended to be used within the [`<Sidebar>`](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';
<SidebarPage>
<Sidebar>
<SidebarSettings />
</Sidebar>
</SidebarPage>;
```
Add the page to the App routing:
```ts
import { Router as SettingsRouter } from '@backstage/plugin-user-settings';
const AppRoutes = () => (
<Routes>
<Route path="/settings" element={<SettingsRouter />} />
</Routes>
);
```
### 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 = () => (
<ListItem>
<ListItemText primary="example" />
<ListItemSecondaryAction>{someAction}</ListItemSecondaryAction>
</ListItem>
);
const AppRoutes = () => (
<Routes>
<Route
path="/settings"
element={<SettingsRouter providerSettings={<MyAuthProviders />} />}
/>
</Routes>
);
```
> **Note that the list of providers expects to be rendered within a MUI [`<List>`](https://material-ui.com/components/lists/)**
@@ -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 = () => (
<List dense subheader={<ListSubheader>App Settings</ListSubheader>}>
<SidebarThemeToggle />
<SidebarPinButton />
</List>
);
createDevApp().registerPlugin(plugin).render();
+49
View File
@@ -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"
]
}
@@ -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('<AuthProviders />', () => {
it('displays a provider and calls its sign-in handler on click', async () => {
const rendered = await renderWithEffects(
wrapInTestApp(
<ApiProvider apis={apiRegistry}>
<AuthProviders />
</ApiProvider>,
),
);
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);
});
});
@@ -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 ?? (
<DefaultProviderSettings configuredProviders={configuredProviders} />
);
if (!providerSettings && !configuredProviders?.length) {
return <EmptyProviders />;
}
return (
<InfoCard>
<List dense>{providers}</List>
</InfoCard>
);
};
@@ -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') && (
<ProviderSettingsItem
title="Google"
description={googleAuthApiRef.description}
apiRef={googleAuthApiRef}
icon={Star}
/>
)}
{configuredProviders.includes('microsoft') && (
<ProviderSettingsItem
title="Microsoft"
description={microsoftAuthApiRef.description}
apiRef={microsoftAuthApiRef}
icon={Star}
/>
)}
{configuredProviders.includes('github') && (
<ProviderSettingsItem
title="Github"
description={githubAuthApiRef.description}
apiRef={githubAuthApiRef}
icon={Star}
/>
)}
{configuredProviders.includes('gitlab') && (
<ProviderSettingsItem
title="Gitlab"
description={gitlabAuthApiRef.description}
apiRef={gitlabAuthApiRef}
icon={Star}
/>
)}
{configuredProviders.includes('okta') && (
<ProviderSettingsItem
title="Okta"
description={oktaAuthApiRef.description}
apiRef={oktaAuthApiRef}
icon={Star}
/>
)}
{configuredProviders.includes('oauth2') && (
<ProviderSettingsItem
title="YourOrg"
description={oauth2ApiRef.description}
apiRef={oauth2ApiRef}
icon={Star}
/>
)}
</>
);
@@ -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 = () => (
<EmptyState
missing="content"
title="No Authentication Providers"
description="You can add Authentication Providers to Backstage which allows you to use these providers to authenticate yourself."
action={
<>
<Typography variant="body1">
Open <code>app-config.yaml</code> and make the changes as highlighted
below:
</Typography>
<CodeSnippet
text={EXAMPLE}
language="yaml"
showLineNumbers
highlightedNumbers={[3, 4, 5, 6, 7, 8]}
customStyle={{ background: 'inherit', fontSize: '115%' }}
/>
<Button
variant="contained"
color="primary"
href="https://backstage.io/docs/auth/add-auth-provider"
>
Read More
</Button>
</>
}
/>
);
@@ -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<SessionApi>;
};
export const ProviderSettingsItem: FC<OAuthProviderSidebarProps> = ({
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<OAuthProviderSidebarProps> = ({
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={title} />
<ListItemSecondaryAction>
<ToggleButton
size="small"
value={title}
selected={signedIn}
onChange={() => (signedIn ? api.signOut() : api.signIn())}
>
<Tooltip
placement="top"
arrow
title={signedIn ? `Sign out from ${title}` : `Sign in to ${title}`}
>
<PowerButton />
<ListItemText
primary={title}
secondary={
<Tooltip placement="top" arrow title={description}>
<span>{description}</span>
</Tooltip>
</ToggleButton>
}
secondaryTypographyProps={{ noWrap: true, style: { width: '80%' } }}
/>
<ListItemSecondaryAction>
<Tooltip
placement="top"
arrow
title={signedIn ? `Sign out from ${title}` : `Sign in to ${title}`}
>
<ToggleButton
size="small"
value={title}
selected={signedIn}
onChange={() => (signedIn ? api.signOut() : api.signIn())}
>
<PowerButton color={signedIn ? 'primary' : undefined} />
</ToggleButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
@@ -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';
@@ -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 = () => (
<EmptyState
missing="content"
title="No Feature Flags"
description="Feature Flags makes it possible for plugins to register features in Backstage for users to opt into. You can use this to split out logic in your code for manual A/B testing, etc."
action={
<>
<Typography variant="body1">
An example how how to add a feature flags is highlighted below:
</Typography>
<CodeSnippet
text={EXAMPLE}
language="typescript"
showLineNumbers
highlightedNumbers={[6]}
customStyle={{ background: 'inherit', fontSize: '115%' }}
/>
<Button
variant="contained"
color="primary"
href="https://backstage.io/docs/api/utility-apis"
>
Read More
</Button>
</>
}
/>
);
@@ -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<FeatureFlagName, FeatureFlagState>,
);
const [state, setState] = useState<Record<FeatureFlagName, FeatureFlagState>>(
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 <EmptyFlags />;
}
return (
<InfoCard>
<List dense>
{featureFlags.map(featureFlag => {
const enabled = Boolean(state[featureFlag.name]);
return (
<FlagItem
key={featureFlag.name}
flag={featureFlag}
enabled={enabled}
toggleHandler={toggleFlag}
/>
);
})}
</List>
</InfoCard>
);
};
@@ -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 (
<ListItem>
<ListItemText
primary={featureFlag.name}
secondary={`Registered in ${featureFlag.pluginId} plugin`}
/>
<ListItemSecondaryAction>
export const FlagItem = ({ flag, enabled, toggleHandler }: Props) => (
<ListItem>
<ListItemText
primary={flag.name}
secondary={`Registered in ${flag.pluginId} plugin`}
/>
<ListItemSecondaryAction>
<Tooltip placement="top" arrow title={enabled ? 'Disable' : 'Enable'}>
<ToggleButton
size="small"
value="flag"
selected={enabled}
onChange={toggleFlag}
onChange={() => toggleHandler(flag.name)}
>
<Tooltip placement="top" arrow title={enabled ? 'Disable' : 'Enable'}>
<CheckIcon />
</Tooltip>
<CheckIcon color={enabled ? 'primary' : undefined} />
</ToggleButton>
</ListItemSecondaryAction>
</ListItem>
);
};
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
@@ -14,5 +14,4 @@
* limitations under the License.
*/
export { ProviderSettingsItem } from './ProviderSettingsItem';
export { SidebarUserSettings } from './UserSettings';
export { FeatureFlags } from './FeatureFlags';
@@ -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 = () => (
<Grid container spacing={3}>
<Grid item md={12}>
<Profile />
</Grid>
<Grid item md={12}>
<InfoCard>
<List dense>
<ThemeToggle />
<PinButton />
</List>
</InfoCard>
</Grid>
</Grid>
);
@@ -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('<PinButton />', () => {
it('toggles the pin sidebar button', async () => {
const mockToggleFn = jest.fn();
const rendered = await renderWithEffects(
wrapInTestApp(
<SidebarPinStateContext.Provider
value={{ isPinned: false, toggleSidebarPinState: mockToggleFn }}
>
<PinButton />
</SidebarPinStateContext.Provider>,
),
);
expect(rendered.getByText('Pin Sidebar')).toBeInTheDocument();
const pinButton = rendered.getByTitle('Pin Sidebar');
fireEvent.click(pinButton);
expect(mockToggleFn).toHaveBeenCalled();
});
});
@@ -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 ? <LockIcon color="primary" /> : <LockOpenIcon />;
export const PinButton = () => {
const { isPinned, toggleSidebarPinState } = useContext(
SidebarPinStateContext,
);
const PinIcon = () => (
<Tooltip
placement="top"
arrow
title={`${isPinned ? 'Unpin' : 'Pin'} Sidebar`}
>
{isPinned ? <LockIcon /> : <LockOpenIcon />}
</Tooltip>
);
return (
<ListItem>
<ListItemText
@@ -48,16 +43,22 @@ export const SidebarPinButton = () => {
secondary="Prevent the sidebar from collapsing"
/>
<ListItemSecondaryAction>
<ToggleButton
size="small"
value="pin"
selected={isPinned}
onChange={() => {
toggleSidebarPinState();
}}
<Tooltip
placement="top"
arrow
title={`${isPinned ? 'Unpin' : 'Pin'} Sidebar`}
>
<PinIcon />
</ToggleButton>
<ToggleButton
size="small"
value="pin"
selected={isPinned}
onChange={() => {
toggleSidebarPinState();
}}
>
<PinIcon isPinned={isPinned} />
</ToggleButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
@@ -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 (
<Grid container spacing={3}>
<Grid item md={12}>
<InfoCard title="Profile">
<Grid container spacing={6}>
<Grid item>
<SignInAvatar size={96} />
</Grid>
<Grid item xs={12} sm container>
<Grid item xs container direction="column" spacing={2}>
<Grid item xs>
<Typography variant="subtitle1" gutterBottom>
{displayName}
</Typography>
<Typography variant="body2" color="textSecondary">
{profile.email}
</Typography>
</Grid>
</Grid>
<Grid item>
<UserSettingsMenu />
</Grid>
</Grid>
</Grid>
</InfoCard>
</Grid>
</Grid>
);
};
@@ -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<BackstageTheme, { size: number }>({
const useStyles = makeStyles<BackstageTheme, { size: number }>(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 (
<Avatar src={profile.picture} className={classes.avatar}>
{displayName[0]}
</Avatar>
);
return <Avatar src={profile.picture} className={classes.avatar} />;
};
@@ -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('<ThemeToggle />', () => {
it('toggles the theme select button', async () => {
const themeApi = apiRegistry.get(appThemeApiRef);
const rendered = await renderWithEffects(
wrapInTestApp(
<ApiProvider apis={apiRegistry}>
<ThemeToggle />
</ApiProvider>,
),
);
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');
});
});
@@ -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,
})
) : (
<AutoIcon 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) => (
<Tooltip placement="top" arrow title={title}>
<ToggleButton value={value} {...props}>
{children}
</ToggleButton>
</Tooltip>
);
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: <DarkIcon />,
light: <LightIcon />,
};
const handleSetTheme = (
_event: React.MouseEvent<HTMLElement>,
@@ -64,22 +93,24 @@ export const SidebarThemeToggle = () => {
value={themeId ?? 'auto'}
onChange={handleSetTheme}
>
{themeIds.map(theme => (
<ToggleButton key={theme.id} value={theme.variant}>
<Tooltip
placement="top"
arrow
title={`Select ${theme.variant} theme`}
{themeIds.map(theme => {
const themeIcon = themeIds.find(t => t.id === theme.id)?.icon;
return (
<TooltipToggleButton
key={theme.id}
title={`Select ${theme.title}`}
value={theme.variant}
>
{themeIcons[theme.variant]}
</Tooltip>
<ThemeIcon id={theme.id} icon={themeIcon} activeId={themeId} />
</TooltipToggleButton>
);
})}
<Tooltip placement="top" arrow title="Select auto theme">
<ToggleButton value="auto">
<AutoIcon color={themeId === undefined ? 'primary' : undefined} />
</ToggleButton>
))}
<ToggleButton value="auto">
<Tooltip placement="top" arrow title="Select auto theme">
<AutoIcon />
</Tooltip>
</ToggleButton>
</Tooltip>
</ToggleButtonGroup>
</ListItemSecondaryAction>
</ListItem>
@@ -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('<UserSettingsMenu />', () => {
it('displays a menu button with a sign-out option', async () => {
const rendered = await renderWithEffects(
wrapInTestApp(<UserSettingsMenu />),
);
const menuButton = rendered.getByLabelText('more');
fireEvent.click(menuButton);
expect(rendered.getByText('Sign Out')).toBeInTheDocument();
});
});
@@ -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 (
<>
<IconButton onClick={handleOpen}>
<IconButton aria-label="more" onClick={handleOpen}>
<MoreVertIcon />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
@@ -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';
@@ -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 = () => <SignInAvatar />;
return (
<SidebarItem
text={displayName}
to={settingsRouteRef.path}
icon={SidebarAvatar}
/>
);
};
export const FeatureFlagsList = ({ featureFlags }: Props) => (
<List dense subheader={<ListSubheader>Feature Flags</ListSubheader>}>
{featureFlags.map(featureFlag => (
<FlagItem key={featureFlag.name} featureFlag={featureFlag} />
))}
</List>
);
@@ -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<number>(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 = [
<General />,
<AuthProviders providerSettings={providerSettings} />,
<FeatureFlags />,
];
return (
<Page theme={pageTheme.home}>
<Header title="Settings" />
<HeaderTabs tabs={tabs} onChange={onTabChange} />
<Content>{content[activeTab]}</Content>
</Page>
);
};
@@ -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) => (
<MemoryRouter initialEntries={['/']}>{storyFn()}</MemoryRouter>
),
],
};
export const SidebarItem = () => (
<Sidebar>
<SidebarSpace />
<SidebarDivider />
<Settings />
</Sidebar>
);
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 = () => (
<div style={{ border: '1px solid #ddd' }}>
<ApiProvider apis={apis}>
<SettingsPage />
</ApiProvider>
</div>
);
@@ -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);
+18
View File
@@ -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';
+22
View File
@@ -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();
});
});
@@ -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) => (
<List subheader={<ListSubheader>Available Auth Providers</ListSubheader>}>
{providerSettings}
</List>
);
export const plugin = createPlugin({
id: 'user-settings',
});
+16
View File
@@ -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';