Merge branch 'mob/create-vcs-step' of github.com:spotify/backstage into shmidt-i/scaffolder-flow-frontend

This commit is contained in:
Ivan Shmidt
2020-07-01 21:50:57 +02:00
69 changed files with 3505 additions and 234 deletions
-7
View File
@@ -76,13 +76,6 @@
"pathRewrite": {
"^/circleci/api/": "/"
}
},
"/catalog/api": {
"target": "http://localhost:7000",
"changeOrigin": true,
"pathRewrite": {
"^/catalog/api/": "/catalog/"
}
}
}
}
+1
View File
@@ -26,6 +26,7 @@ describe('App', () => {
{
data: {
app: { title: 'Test' },
backend: { baseUrl: 'http://localhost:7000' },
},
context: 'test',
},
+18 -8
View File
@@ -27,11 +27,13 @@ import {
GoogleAuth,
GithubAuth,
OktaAuth,
GitlabAuth,
oauthRequestApiRef,
OAuthRequestManager,
googleAuthApiRef,
githubAuthApiRef,
oktaAuthApiRef,
gitlabAuthApiRef,
storageApiRef,
WebStorage,
} from '@backstage/core';
@@ -51,15 +53,14 @@ import {
graphQlBrowseApiRef,
GraphQLEndpoints,
} from '@backstage/plugin-graphiql';
import {
scaffolderApiRef,
ScaffolderApi,
} from '@backstage/plugin-scaffolder/src/api';
import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
export const apis = (config: ConfigApi) => {
// eslint-disable-next-line no-console
console.log(`Creating APIs for ${config.getString('app.title')}`);
const backendUrl = config.getString('backend.baseUrl');
const builder = ApiRegistry.builder();
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
@@ -82,7 +83,7 @@ export const apis = (config: ConfigApi) => {
builder.add(
googleAuthApiRef,
GoogleAuth.create({
apiOrigin: 'http://localhost:7000',
apiOrigin: backendUrl,
basePath: '/auth/',
oauthRequestApi,
}),
@@ -91,7 +92,7 @@ export const apis = (config: ConfigApi) => {
const githubAuthApi = builder.add(
githubAuthApiRef,
GithubAuth.create({
apiOrigin: 'http://localhost:7000',
apiOrigin: backendUrl,
basePath: '/auth/',
oauthRequestApi,
}),
@@ -100,6 +101,15 @@ export const apis = (config: ConfigApi) => {
builder.add(
oktaAuthApiRef,
OktaAuth.create({
apiOrigin: backendUrl,
basePath: '/auth/',
oauthRequestApi,
}),
);
builder.add(
gitlabAuthApiRef,
GitlabAuth.create({
apiOrigin: 'http://localhost:7000',
basePath: '/auth/',
oauthRequestApi,
@@ -117,7 +127,7 @@ export const apis = (config: ConfigApi) => {
builder.add(
catalogApiRef,
new CatalogClient({
apiOrigin: 'http://localhost:7000',
apiOrigin: backendUrl,
basePath: '/catalog',
}),
);
@@ -125,7 +135,7 @@ export const apis = (config: ConfigApi) => {
builder.add(
scaffolderApiRef,
new ScaffolderApi({
apiOrigin: 'http://localhost:7000',
apiOrigin: backendUrl,
basePath: '/scaffolder/v1',
}),
);
+18 -5
View File
@@ -242,11 +242,24 @@ export const githubAuthApiRef = createApiRef<
*/
export const oktaAuthApiRef = createApiRef<
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionStateApi
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionStateApi
>({
id: 'core.auth.okta',
description: 'Provides authentication towards Okta APIs',
});
});
/**
* Provides authentication towards Gitlab APIs.
*
* See https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token
* for a full list of supported scopes.
*/
export const gitlabAuthApiRef = createApiRef<
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi
>({
id: 'core.auth.gitlab',
description: 'Provides authentication towards Gitlab APIs',
});
@@ -27,7 +27,10 @@ import {
} from '../../../definitions/auth';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager';
import {
AuthSessionStore,
StaticAuthSessionManager,
} from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
type CreateOptions = {
@@ -91,7 +94,13 @@ class GithubAuth implements OAuthApi, SessionStateApi {
sessionScopes: (session: GithubSession) => session.providerInfo.scopes,
});
return new GithubAuth(sessionManager);
const authSessionStore = new AuthSessionStore<GithubSession>({
manager: sessionManager,
storageKey: 'githubSession',
sessionScopes: (session: GithubSession) => session.providerInfo.scopes,
});
return new GithubAuth(authSessionStore);
}
sessionState$(): Observable<SessionState> {
@@ -0,0 +1,46 @@
/*
* 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 GitlabAuth from './GitlabAuth';
describe('GitlabAuth', () => {
it('should get access token', async () => {
const getSession = jest
.fn()
.mockResolvedValue({ providerInfo: { accessToken: 'access-token' } });
const gitlabAuth = new GitlabAuth({ getSession } as any);
expect(await gitlabAuth.getAccessToken()).toBe('access-token');
expect(getSession).toBeCalledTimes(1);
});
it('should normalize scope', () => {
const tests = [
{
arguments: ['read_user api write_repository'],
expect: new Set(['read_user', 'api', 'write_repository']),
},
{
arguments: ['read_repository sudo'],
expect: new Set(['read_repository', 'sudo']),
},
];
for (const test of tests) {
expect(GitlabAuth.normalizeScope(...test.arguments)).toEqual(test.expect);
}
});
});
@@ -0,0 +1,135 @@
/*
* 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 GitlabIcon from '@material-ui/icons/AcUnit';
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import { GitlabSession } from './types';
import {
OAuthApi,
SessionStateApi,
SessionState,
ProfileInfo,
BackstageIdentity,
AuthRequestOptions,
} from '../../../definitions/auth';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
type CreateOptions = {
apiOrigin: string;
basePath: string;
oauthRequestApi: OAuthRequestApi;
environment?: string;
provider?: AuthProvider & { id: string };
};
export type GitlabAuthResponse = {
providerInfo: {
accessToken: string;
scope: string;
expiresInSeconds: number;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
const DEFAULT_PROVIDER = {
id: 'gitlab',
title: 'Gitlab',
icon: GitlabIcon,
};
class GitlabAuth implements OAuthApi, SessionStateApi {
static create({
apiOrigin,
basePath,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
apiOrigin,
basePath,
environment,
provider,
oauthRequestApi,
sessionTransform(res: GitlabAuthResponse): GitlabSession {
return {
...res,
providerInfo: {
accessToken: res.providerInfo.accessToken,
scopes: GitlabAuth.normalizeScope(res.providerInfo.scope),
expiresAt: new Date(
Date.now() + res.providerInfo.expiresInSeconds * 1000,
),
},
};
},
});
const sessionManager = new StaticAuthSessionManager({
connector,
defaultScopes: new Set(['read_user']),
sessionScopes: (session: GitlabSession) => session.providerInfo.scopes,
});
return new GitlabAuth(sessionManager);
}
sessionState$(): Observable<SessionState> {
return this.sessionManager.sessionState$();
}
constructor(private readonly sessionManager: SessionManager<GitlabSession>) {}
async getAccessToken(scope?: string, options?: AuthRequestOptions) {
const session = await this.sessionManager.getSession({
...options,
scopes: GitlabAuth.normalizeScope(scope),
});
return session?.providerInfo.accessToken ?? '';
}
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
const session = await this.sessionManager.getSession(options);
return session?.backstageIdentity;
}
async getProfile(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.profile;
}
async logout() {
await this.sessionManager.removeSession();
}
static normalizeScope(scope?: string): Set<string> {
if (!scope) {
return new Set();
}
const scopeList = Array.isArray(scope) ? scope : scope.split(' ');
return new Set(scopeList);
}
}
export default GitlabAuth;
@@ -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 * from './types';
export { default as GitlabAuth } from './GitlabAuth';
@@ -0,0 +1,27 @@
/*
* 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 { ProfileInfo, BackstageIdentity } from '../../../definitions';
export type GitlabSession = {
providerInfo: {
accessToken: string;
scopes: Set<string>;
expiresAt: Date;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
@@ -16,4 +16,5 @@
export * from './google';
export * from './github';
export * from './gitlab';
export * from './okta';
@@ -36,6 +36,8 @@ type Options<T> = {
/**
* AuthSessionStore decorates another SessionManager with a functionality
* to store the session in local storage.
*
* Session is serialized to JSON with special support for following types: Set.
*/
export class AuthSessionStore<T> implements SessionManager<T> {
private readonly manager: SessionManager<T>;
@@ -90,7 +92,12 @@ export class AuthSessionStore<T> implements SessionManager<T> {
try {
const sessionJson = localStorage.getItem(this.storageKey);
if (sessionJson) {
const session = JSON.parse(sessionJson);
const session = JSON.parse(sessionJson, (_key, value) => {
if (value?.__type === 'Set') {
return new Set(value.__value);
}
return value;
});
return session;
}
@@ -105,7 +112,18 @@ export class AuthSessionStore<T> implements SessionManager<T> {
if (session === undefined) {
localStorage.removeItem(this.storageKey);
} else {
localStorage.setItem(this.storageKey, JSON.stringify(session));
localStorage.setItem(
this.storageKey,
JSON.stringify(session, (_key, value) => {
if (value instanceof Set) {
return {
__type: 'Set',
__value: Array.from(value),
};
}
return value;
}),
);
}
}
}
@@ -16,4 +16,5 @@
export { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager';
export { StaticAuthSessionManager } from './StaticAuthSessionManager';
export { AuthSessionStore } from './AuthSessionStore';
export * from './types';
@@ -185,3 +185,38 @@ export const SubvalueTable = () => {
</div>
);
};
export const DenseTable = () => {
const columns: TableColumn[] = [
{
title: 'Column 1',
field: 'col1',
highlight: true,
},
{
title: 'Column 2',
field: 'col2',
},
{
title: 'Numeric value',
field: 'number',
type: 'numeric',
},
{
title: 'A Date',
field: 'date',
type: 'date',
},
];
return (
<div style={containerStyle}>
<Table
options={{ paging: false, padding: 'dense' }}
data={testData10}
columns={columns}
title="Backstage Table"
/>
</div>
);
};
@@ -35,7 +35,6 @@ import ViewColumn from '@material-ui/icons/ViewColumn';
import MTable, {
Column,
MaterialTableProps,
MTableCell,
MTableHeader,
MTableToolbar,
Options,
@@ -96,14 +95,6 @@ const tableIcons = {
)),
};
const useCellStyles = makeStyles<BackstageTheme>(theme => ({
root: {
color: theme.palette.grey[500],
padding: theme.spacing(0, 2, 0, 2.5),
height: '56px',
},
}));
const useHeaderStyles = makeStyles<BackstageTheme>(theme => ({
header: {
padding: theme.spacing(1, 2, 1, 2.5),
@@ -169,7 +160,6 @@ export function Table<T extends object = {}>({
subtitle,
...props
}: TableProps<T>) {
const cellClasses = useCellStyles();
const headerClasses = useHeaderStyles();
const toolbarClasses = useToolbarStyles();
const theme = useTheme<BackstageTheme>();
@@ -185,9 +175,6 @@ export function Table<T extends object = {}>({
return (
<MTable<T>
components={{
Cell: cellProps => (
<MTableCell className={cellClasses.root} {...cellProps} />
),
Header: headerProps => (
<MTableHeader classes={headerClasses} {...headerProps} />
),
@@ -0,0 +1,67 @@
/*
* 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 { ItemCard } from '.';
import { Grid } from '@material-ui/core';
export default {
title: 'Item Card',
component: ItemCard,
};
export const Default = () => (
<Grid container spacing={4}>
<Grid item xs={6} sm={4} md={2}>
<ItemCard
title="Item Card"
description="This is the description of an Item Card"
label="Button"
type="Pretitle"
onClick={() => {}}
/>
</Grid>
<Grid item xs={6} sm={4} md={2}>
<ItemCard
title="Item Card"
description="This is the description of an Item Card"
label="Button"
type="Pretitle"
onClick={() => {}}
/>
</Grid>
</Grid>
);
export const Tags = () => (
<Grid container spacing={4}>
<Grid item xs={6} sm={4} md={2}>
<ItemCard
title="Item Card"
description="This is a Item Card"
tags={['one tag', 'two tag']}
label="Button"
/>
</Grid>
<Grid item xs={6} sm={4} md={2}>
<ItemCard
title="Item Card"
description="This is a Item Card"
tags={['one tag', 'two tag']}
label="Button"
/>
</Grid>
</Grid>
);
@@ -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, { FC } from 'react';
import { Button, Card, Chip, Typography, makeStyles } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
header: {
color: theme.palette.common.white,
padding: theme.spacing(2, 2, 6),
backgroundImage:
'linear-gradient(-137deg, rgb(25, 230, 140) 0%, rgb(29, 127, 110) 100%)',
},
content: {
padding: theme.spacing(2),
},
description: {
height: 175,
overflow: 'hidden',
textOverflow: 'ellipsis',
},
footer: {
display: 'flex',
flexDirection: 'row-reverse',
},
}));
type ItemCardProps = {
description: string;
tags?: string[];
title: string;
type?: string;
label: string;
onClick?: () => void;
};
export const ItemCard: FC<ItemCardProps> = ({
description,
tags,
title,
type,
label,
onClick,
}) => {
const classes = useStyles();
return (
<Card>
<div className={classes.header}>
{type ?? <Typography variant="subtitle2">{type}</Typography>}
<Typography variant="h6">{title}</Typography>
</div>
<div className={classes.content}>
{tags?.map(tag => (
<Chip label={tag} key={tag} />
))}
<Typography variant="body2" paragraph className={classes.description}>
{description}
</Typography>
<div className={classes.footer}>
<Button onClick={onClick} color="primary">
{label}
</Button>
</div>
</div>
</Card>
);
};
@@ -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 { ItemCard } from './ItemCard';
@@ -22,6 +22,7 @@ import { SidebarContext } from './config';
import {
googleAuthApiRef,
githubAuthApiRef,
gitlabAuthApiRef,
identityApiRef,
oktaAuthApiRef,
useApi,
@@ -57,6 +58,11 @@ export function SidebarUserSettings() {
apiRef={githubAuthApiRef}
icon={Star}
/>
<OAuthProviderSettings
title="Gitlab"
apiRef={gitlabAuthApiRef}
icon={Star}
/>
<OIDCProviderSettings
title="Okta"
apiRef={oktaAuthApiRef}
+1
View File
@@ -26,3 +26,4 @@ export * from './Sidebar';
export * from './SignInPage';
export * from './TabbedCard';
export * from './HeaderTabs';
export * from './ItemCard';
@@ -28,6 +28,8 @@ import {
googleAuthApiRef,
GithubAuth,
githubAuthApiRef,
GitlabAuth,
gitlabAuthApiRef,
} from '@backstage/core';
// TODO(rugvip): We should likely figure out how to reuse all of these between apps
@@ -75,3 +77,14 @@ export const githubAuthApiFactory = createApiFactory({
oauthRequestApi,
}),
});
export const gitlabAuthApiFactory = createApiFactory({
implements: gitlabAuthApiRef,
deps: { oauthRequestApi: oauthRequestApiRef },
factory: ({ oauthRequestApi }) =>
GitlabAuth.create({
apiOrigin: 'http://localhost:7000',
basePath: '/auth/',
oauthRequestApi,
}),
});
+11
View File
@@ -6,12 +6,14 @@ import {
OAuthRequestManager,
googleAuthApiRef,
githubAuthApiRef,
gitlabAuthApiRef,
oktaAuthApiRef,
AlertApiForwarder,
ErrorApiForwarder,
ErrorAlerter,
GoogleAuth,
GithubAuth,
GitlabAuth,
OktaAuth,
identityApiRef,
} from '@backstage/core';
@@ -52,6 +54,15 @@ builder.add(
}),
);
builder.add(
gitlabAuthApiRef,
GitlabAuth.create({
apiOrigin: 'http://localhost:7000',
basePath: '/auth/',
oauthRequestApi,
}),
);
builder.add(
oktaAuthApiRef,
OktaAuth.create({
+4 -1
View File
@@ -118,9 +118,12 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
verticalAlign: 'middle',
lineHeight: '1',
margin: 0,
padding: '8px',
padding: theme.spacing(3, 2, 3, 2.5),
borderBottom: 0,
},
sizeSmall: {
padding: theme.spacing(1, 2, 1, 2.5),
},
head: {
wordBreak: 'break-word',
overflow: 'hidden',