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
+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/)**
+19
View File
@@ -0,0 +1,19 @@
/*
* 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 { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
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>
</>
}
/>
);
@@ -0,0 +1,99 @@
/*
* 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, useState } from 'react';
import {
ApiRef,
SessionApi,
useApi,
IconComponent,
SessionState,
} from '@backstage/core';
import {
ListItem,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
Tooltip,
} from '@material-ui/core';
import PowerButton from '@material-ui/icons/PowerSettingsNew';
import { ToggleButton } from '@material-ui/lab';
type Props = {
title: string;
description: string;
icon: IconComponent;
apiRef: ApiRef<SessionApi>;
};
export const ProviderSettingsItem = ({
title,
description,
icon: Icon,
apiRef,
}: Props) => {
const api = useApi(apiRef);
const [signedIn, setSignedIn] = useState(false);
useEffect(() => {
let didCancel = false;
const subscription = api
.sessionState$()
.subscribe((sessionState: SessionState) => {
if (!didCancel) {
setSignedIn(sessionState === SessionState.SignedIn);
}
});
return () => {
didCancel = true;
subscription.unsubscribe();
};
}, [api]);
return (
<ListItem>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText
primary={title}
secondary={
<Tooltip placement="top" arrow title={description}>
<span>{description}</span>
</Tooltip>
}
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>
);
};
@@ -0,0 +1,53 @@
/*
* 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 {
ListItem,
ListItemSecondaryAction,
ListItemText,
Tooltip,
} from '@material-ui/core';
import CheckIcon from '@material-ui/icons/CheckCircle';
import { ToggleButton } from '@material-ui/lab';
import { FeatureFlagsRegistryItem } from '@backstage/core';
type Props = {
flag: FeatureFlagsRegistryItem;
enabled: boolean;
toggleHandler: Function;
};
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={() => toggleHandler(flag.name)}
>
<CheckIcon color={enabled ? 'primary' : undefined} />
</ToggleButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
@@ -0,0 +1,17 @@
/*
* 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 { 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();
});
});
@@ -0,0 +1,65 @@
/*
* 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, { useContext } from 'react';
import {
ListItem,
ListItemSecondaryAction,
ListItemText,
Tooltip,
} from '@material-ui/core';
import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import { ToggleButton } from '@material-ui/lab';
import { SidebarPinStateContext } from '@backstage/core';
type PinIconProps = { isPinned: boolean };
const PinIcon = ({ isPinned }: PinIconProps) =>
isPinned ? <LockIcon color="primary" /> : <LockOpenIcon />;
export const PinButton = () => {
const { isPinned, toggleSidebarPinState } = useContext(
SidebarPinStateContext,
);
return (
<ListItem>
<ListItemText
primary="Pin Sidebar"
secondary="Prevent the sidebar from collapsing"
/>
<ListItemSecondaryAction>
<Tooltip
placement="top"
arrow
title={`${isPinned ? 'Unpin' : 'Pin'} Sidebar`}
>
<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>
);
};
@@ -0,0 +1,40 @@
/*
* 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 { BackstageTheme } from '@backstage/theme';
import { makeStyles, Avatar } from '@material-ui/core';
import { useUserProfile } from '../useUserProfileInfo';
import { sidebarConfig } from '@backstage/core';
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 } = useUserProfile();
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');
});
});
@@ -0,0 +1,118 @@
/*
* 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, { cloneElement } from 'react';
import { useObservable } from 'react-use';
import AutoIcon from '@material-ui/icons/BrightnessAuto';
import { appThemeApiRef, useApi } from '@backstage/core';
import ToggleButton from '@material-ui/lab/ToggleButton';
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
import {
ListItem,
ListItemText,
ListItemSecondaryAction,
Tooltip,
} from '@material-ui/core';
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$(),
appThemeApi.getActiveThemeId(),
);
const themeIds = appThemeApi.getInstalledThemes();
const handleSetTheme = (
_event: React.MouseEvent<HTMLElement>,
newThemeId: string | undefined,
) => {
if (themeIds.some(t => t.id === newThemeId)) {
appThemeApi.setActiveThemeId(newThemeId);
} else {
appThemeApi.setActiveThemeId(undefined);
}
};
return (
<ListItem>
<ListItemText primary="Theme" secondary="Change the theme mode" />
<ListItemSecondaryAction>
<ToggleButtonGroup
exclusive
size="small"
value={themeId ?? 'auto'}
onChange={handleSetTheme}
>
{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}
>
<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>
</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();
});
});
@@ -0,0 +1,55 @@
/*
* 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 { 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';
export const UserSettingsMenu = () => {
const identityApi = useApi(identityApiRef);
const [open, setOpen] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState<undefined | HTMLElement>(
undefined,
);
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
setOpen(true);
};
const handleClose = () => {
setAnchorEl(undefined);
setOpen(false);
};
return (
<>
<IconButton aria-label="more" onClick={handleOpen}>
<MoreVertIcon />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
<MenuItem onClick={() => identityApi.signOut()}>
<ListItemIcon>
<SignOutIcon />
</ListItemIcon>
Sign Out
</MenuItem>
</Menu>
</>
);
};
@@ -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';
@@ -0,0 +1,34 @@
/*
* 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 { SidebarItem } from '@backstage/core';
import { SignInAvatar } from './General';
import { useUserProfile } from './useUserProfileInfo';
import { settingsRouteRef } from '../plugin';
export const Settings = () => {
const { displayName } = useUserProfile();
const SidebarAvatar = () => <SignInAvatar />;
return (
<SidebarItem
text={displayName}
to={settingsRouteRef.path}
icon={SidebarAvatar}
/>
);
};
@@ -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>
);
@@ -0,0 +1,26 @@
/*
* 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 { useApi, identityApiRef } from '@backstage/core';
export const useUserProfile = () => {
const identityApi = useApi(identityApiRef);
const userId = identityApi.getUserId();
const profile = identityApi.getProfile();
const displayName = profile.displayName ?? userId;
return { profile, displayName };
};
+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();
});
});
+25
View File
@@ -0,0 +1,25 @@
/*
* 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 { createPlugin, createRouteRef } from '@backstage/core';
export const settingsRouteRef = createRouteRef({
path: '/settings',
title: 'Settings',
});
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';