Merge branch 'master' of github.com:spotify/backstage into mob/prepare-from-catalog

* 'master' of github.com:spotify/backstage: (57 commits)
  fix: update readme with work-in-progress status
  fix: Techdocs to TechDocs
  fix: rename pulp-fiction to techdocs-core
  feat(techdocs): yarn create-plugin
  chore(backend-common): just tweaked the structure of useHotMemoize
  build(deps): bump @rollup/plugin-json from 4.0.3 to 4.1.0 (#1360)
  build(deps-dev): bump lerna from 3.22.0 to 3.22.1 (#1390)
  build(deps): bump graphql from 15.0.0 to 15.1.0 (#1391)
  core-api: switch IdentityApi id token access to async
  Adding name of signed in user to greeting message (#1387)
  Fix CircleCI plugin (#1384)
  packages/core: update to not use default exports for components
  auth-backend: clean up naming and types of TokenFactory time handling
  auth-backend: some review feedback of oidc bits
  auth-backend: remove logging from DatabaseKeyStore
  auth-backend: more docs for TokenFactory
  auth-backend: added tests for TokenFactory and fix keyDuration being ignored
  auth-backend: added tests for DatabaseKeyStore
  auth-backend: refactor identity to remove logic from storage layer
  auth-backend: document identity types
  ...
This commit is contained in:
blam
2020-06-22 14:30:34 +02:00
129 changed files with 2112 additions and 576 deletions
+3
View File
@@ -14,7 +14,9 @@
"@backstage/plugin-scaffolder": "^0.1.1-alpha.9",
"@backstage/plugin-sentry": "^0.1.1-alpha.9",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.9",
"@backstage/plugin-techdocs": "^0.1.1-alpha.9",
"@backstage/plugin-welcome": "^0.1.1-alpha.9",
"@backstage/test-utils": "^0.1.1-alpha.9",
"@backstage/theme": "^0.1.1-alpha.9",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
@@ -22,6 +24,7 @@
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-hot-loader": "^4.12.21",
"react-router": "6.0.0-alpha.5",
"react-router-dom": "6.0.0-alpha.5",
"react-use": "^14.2.0",
"zen-observable": "^0.8.15"
+14 -13
View File
@@ -15,23 +15,24 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import { renderWithEffects } from '@backstage/test-utils';
import App from './App';
describe('App', () => {
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
value: jest.fn(() => {
return {
matches: true,
addListener: jest.fn(),
removeListener: jest.fn(),
};
}),
it('should render', async () => {
Object.defineProperty(process.env, 'APP_CONFIG', {
configurable: true,
value: [
{
data: {
app: { title: 'Test' },
},
context: 'test',
},
],
});
});
it('should render', () => {
const rendered = render(<App />);
const rendered = await renderWithEffects(<App />);
expect(rendered.baseElement).toBeInTheDocument();
});
});
+1
View File
@@ -23,3 +23,4 @@ export { plugin as Circleci } from '@backstage/plugin-circleci';
export { plugin as RegisterComponent } from '@backstage/plugin-register-component';
export { plugin as Sentry } from '@backstage/plugin-sentry';
export { plugin as GitopsProfiles } from '@backstage/plugin-gitops-profiles';
export { plugin as TechDocs } from '@backstage/plugin-techdocs';
+69 -41
View File
@@ -14,10 +14,38 @@
* limitations under the License.
*/
// Find all active hot module APIs of all ancestors of a module, including the module itself
function findAllAncestors(_module: NodeModule): NodeModule[] {
const ancestors = new Array<NodeModule>();
const parentIds = new Set<string | number>();
function add(id: string | number, m: NodeModule) {
if (parentIds.has(id)) {
return;
}
parentIds.add(id);
ancestors.push(m);
for (const parentId of (m as any).parents) {
const parent = require.cache[parentId];
if (parent) {
add(parentId, parent);
}
}
}
add(_module.id, _module);
return ancestors;
}
/**
* This function allows devs to cleanup
* ongoing effects when module gets hot-reloaded
* useHotCleanup allows cleanup of ongoing effects when a module is
* hot-reloaded during development. The cleanup function will be called
* whenever the module itself or any of its parent modules is hot-reloaded.
*
* Useful for cleaning intervals, timers, requests etc
*
* @example
* ```ts
* const intervalId = setInterval(doStuff, 1000);
@@ -28,69 +56,69 @@
*/
export function useHotCleanup(_module: NodeModule, cancelEffect: () => void) {
if (_module.hot) {
_module.hot.addDisposeHandler(() => {
cancelEffect();
});
const ancestors = findAllAncestors(_module);
let cancelled = false;
const handler = () => {
if (!cancelled) {
cancelled = true;
cancelEffect();
}
};
for (const m of ancestors) {
m.hot?.addDisposeHandler(handler);
}
}
}
const CURRENT_HOT_MEMOIZE_INDEX_KEY = 'backstage.io/hmr-memoize-key';
/**
* This function allows devs to preserve
* some value between hot-reloads.
* Useful for stateful parts of the backend
* Memoizes a generated value across hot-module reloads. This is useful for
* stateful parts of the backend, e.g. to retain a database.
*
* @example
* ```ts
* const db = useHotMemoize(module, () => createDB(dbParams));
* ```
* @param _module Reference to the current module where you invoke the fn
* @param valueFactory Fn that returns the value you want to memoize
*
* @warning Don't use inside conditionals or loops,
* same rules as for hooks apply (https://reactjs.org/docs/hooks-rules.html)
*
* @param _module Reference to the current module where you invoke the fn
* @param valueFactory Fn that returns the value you want to memoize
*/
export function useHotMemoize<T>(
_module: NodeModule,
valueFactory: () => T,
): T {
const CURRENT_HOT_MEMOIZE_INDEX_KEY = 'backstage.io/hmr-memoize-key';
if (!_module.hot) {
// Just return value straight away
return valueFactory();
}
if (_module.hot && typeof _module.hot.data === 'undefined') {
// First run, init the module data
// When starting blank, reset the counter
if (!_module.hot.data?.[CURRENT_HOT_MEMOIZE_INDEX_KEY]) {
for (const ancestor of findAllAncestors(_module)) {
ancestor.hot?.addDisposeHandler(data => {
data[CURRENT_HOT_MEMOIZE_INDEX_KEY] = 1;
});
}
_module.hot.data = {
[CURRENT_HOT_MEMOIZE_INDEX_KEY]: 0,
..._module.hot.data,
[CURRENT_HOT_MEMOIZE_INDEX_KEY]: 1,
};
}
// Let's store data per module based on the order of the code invocation
const index = _module.hot.data[CURRENT_HOT_MEMOIZE_INDEX_KEY];
// Increasing the counter after each call
_module.hot.data[CURRENT_HOT_MEMOIZE_INDEX_KEY] += 1;
// Store data per module, based on the order of the code invocation
const index = _module.hot.data[CURRENT_HOT_MEMOIZE_INDEX_KEY]++;
const value = _module.hot.data[index] ?? valueFactory();
const prevValue = _module.hot.data[index];
const createDisposeHandler = (value: any) => (data: {
[key: number]: any;
[indexKey: string]: number;
}) => {
// Preserving the value through the HMR process
// Always add a handler that, upon a HMR event, reinstates the value.
_module.hot.addDisposeHandler(data => {
data[index] = value;
// Decreasing the counter after each handler
data[CURRENT_HOT_MEMOIZE_INDEX_KEY] =
// First hot update is still different, need to populate the data
typeof data[CURRENT_HOT_MEMOIZE_INDEX_KEY] === 'undefined'
? _module.hot!.data[CURRENT_HOT_MEMOIZE_INDEX_KEY] - 1
: data[CURRENT_HOT_MEMOIZE_INDEX_KEY] - 1;
};
});
if (prevValue) {
_module.hot!.addDisposeHandler(createDisposeHandler(prevValue));
return prevValue;
}
const newValue = valueFactory();
_module.hot.addDisposeHandler(createDisposeHandler(newValue));
return newValue;
return value;
}
+2 -1
View File
@@ -19,7 +19,8 @@ import { PluginEnvironment } from '../types';
export default async function createPlugin({
logger,
database,
config,
}: PluginEnvironment) {
return await createRouter({ logger, config });
return await createRouter({ logger, config, database });
}
+1 -2
View File
@@ -178,8 +178,7 @@ export function createBackendConfig(
context: paths.targetPath,
entry: [
'webpack/hot/poll?100',
paths.targetEntry,
...(paths.targetRunFile ? [paths.targetRunFile] : []),
paths.targetRunFile ? paths.targetRunFile : paths.targetEntry,
],
resolve: {
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
@@ -31,6 +31,9 @@
"lerna": "^3.20.2",
"prettier": "^1.19.1"
},
"resolutions": {
"**/esbuild": "0.5.3"
},
"prettier": "@spotify/prettier-config",
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
@@ -8,10 +8,12 @@
"@material-ui/lab": "4.0.0-alpha.45",
"@backstage/cli": "^{{version}}",
"@backstage/core": "^{{version}}",
"@backstage/test-utils": "^{{version}}",
"@backstage/theme": "^{{version}}",
"plugin-welcome": "0.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "6.0.0-alpha.5",
"react-router-dom": "6.0.0-alpha.5",
"react-use": "^14.2.0"
},
@@ -8,47 +8,38 @@
name="description"
content="Backstage is an open platform for building developer portals"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="apple-touch-icon" href="<%= publicPath %>/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link
rel="manifest"
href="%PUBLIC_URL%/manifest.json"
href="<%= publicPath %>/manifest.json"
crossorigin="use-credentials"
/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="<%= publicPath %>/favicon.ico" />
<link rel="shortcut icon" href="<%= publicPath %>/favicon.ico" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="%PUBLIC_URL%/apple-touch-icon.png"
href="<%= publicPath %>/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon-32x32.png"
href="<%= publicPath %>/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/favicon-16x16.png"
href="<%= publicPath %>/favicon-16x16.png"
/>
<link
rel="mask-icon"
href="%PUBLIC_URL%/safari-pinned-tab.svg"
href="<%= publicPath %>/safari-pinned-tab.svg"
color="#5bbad5"
/>
<style>
@@ -56,9 +47,9 @@
min-height: 100%;
}
</style>
<title>Backstage</title>
<title><%= app.title %></title>
</head>
<body style="margin: 0">
<body style="margin: 0;">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
@@ -1,10 +1,22 @@
import React from 'react';
import { render } from '@testing-library/react';
import { renderWithEffects } from '@backstage/test-utils';
import App from './App';
describe('App', () => {
it('should render', () => {
const rendered = render(<App />);
it('should render', async () => {
Object.defineProperty(process.env, 'APP_CONFIG', {
configurable: true,
value: [
{
data: {
app: { title: 'Test' },
},
context: 'test',
},
],
});
const rendered = await renderWithEffects(<App />);
expect(rendered.baseElement).toBeInTheDocument();
});
});
@@ -1,26 +1,7 @@
import { makeStyles } from '@material-ui/core';
import { createApp } from '@backstage/core';
import React, { FC } from 'react';
import * as plugins from './plugins';
const useStyles = makeStyles(theme => ({
'@global': {
html: {
height: '100%',
fontFamily: theme.typography.fontFamily,
},
body: {
height: '100%',
fontFamily: theme.typography.fontFamily,
'overscroll-behavior-y': 'none',
},
a: {
color: 'inherit',
textDecoration: 'none',
},
},
}));
const app = createApp({
plugins: Object.values(plugins),
});
@@ -29,15 +10,12 @@ const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
const AppRoutes = app.getRoutes();
const App: FC<{}> = () => {
useStyles();
return (
<AppProvider>
<AppRouter>
<AppRoutes />
</AppRouter>
</AppProvider>
);
};
const App: FC<{}> = () => (
<AppProvider>
<AppRouter>
<AppRoutes />
</AppRouter>
</AppProvider>
);
export default App;
@@ -35,7 +35,7 @@ export type IdentityApi = {
* The ID token will be undefined if the signed in user does not have a verified
* identity, such as a demo user or mocked user for e2e tests.
*/
getIdToken(): string | undefined;
getIdToken(): Promise<string | undefined>;
// TODO: getProfile(): Promise<Profile> - We want this to be async when added, but needs more work.
+23 -1
View File
@@ -157,25 +157,47 @@ export type ProfileInfoOptions = {
optional?: boolean;
};
/**
* This API provides access to profile information of the user from an auth provider.
*/
export type ProfileInfoApi = {
getProfile(options?: ProfileInfoOptions): Promise<ProfileInfo | undefined>;
};
/**
* Profile information of the user from an auth provider.
*/
export type ProfileInfo = {
provider: string;
/**
* Email ID.
*/
email: string;
/**
* Display name that can be presented to the user.
*/
name?: string;
/**
* URL to an avatar image of the user.
*/
picture?: string;
};
/**
* Session state values passed to subscribers of the SessionStateApi.
*/
export enum SessionState {
SignedIn = 'SignedIn',
SignedOut = 'SignedOut',
}
/**
* This API provides access to an sessionState$ observable which provides an update when the
* user performs a sign in or sign out from an auth provider.
*/
export type SessionStateApi = {
sessionState$(): Observable<SessionState>;
};
/**
* Provides authentication towards Google APIs and identities.
*
-2
View File
@@ -293,8 +293,6 @@ export class PrivateAppImpl implements BackstageApp {
if (!SignInPageComponent) {
this.identityApi.setSignInResult({
userId: 'guest',
idToken: undefined,
logout: async () => {},
});
return (
+4 -4
View File
@@ -24,7 +24,7 @@ import { SignInResult } from './types';
export class AppIdentity implements IdentityApi {
private hasIdentity = false;
private userId?: string;
private idToken?: string;
private idTokenFunc?: () => Promise<string>;
private logoutFunc?: () => Promise<void>;
getUserId(): string {
@@ -36,13 +36,13 @@ export class AppIdentity implements IdentityApi {
return this.userId!;
}
getIdToken(): string | undefined {
async getIdToken(): Promise<string | undefined> {
if (!this.hasIdentity) {
throw new Error(
'Tried to access IdentityApi idToken before app was loaded',
);
}
return this.idToken;
return this.idTokenFunc?.();
}
async logout(): Promise<void> {
@@ -64,7 +64,7 @@ export class AppIdentity implements IdentityApi {
}
this.hasIdentity = true;
this.userId = result.userId;
this.idToken = result.idToken;
this.idTokenFunc = result.getIdToken;
this.logoutFunc = result.logout;
}
}
+2 -2
View File
@@ -32,9 +32,9 @@ export type SignInResult = {
*/
userId: string;
/**
* ID token that will be returned by the IdentityApi
* Function used to retrieve an ID token for the signed in user.
*/
idToken?: string;
getIdToken?: () => Promise<string>;
/**
* Logout handler that will be called if the user requests a logout.
*/
+1 -1
View File
@@ -25,7 +25,7 @@ import privateExports, {
import { BrowserRouter, MemoryRouter } from 'react-router-dom';
import { ErrorPage } from '../layout/ErrorPage';
import Progress from '../components/Progress';
import { Progress } from '../components/Progress';
import { lightTheme, darkTheme } from '@backstage/theme';
import { AppConfig, JsonObject } from '@backstage/config';
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export * from './AlertDisplay';
export { AlertDisplay } from './AlertDisplay';
@@ -15,7 +15,7 @@
*/
import React from 'react';
import CodeSnippet from './CodeSnippet';
import { CodeSnippet } from './CodeSnippet';
import { InfoCard } from '../../layout/InfoCard';
export default {
@@ -18,7 +18,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import CodeSnippet from './CodeSnippet';
import { CodeSnippet } from './CodeSnippet';
const JAVASCRIPT = `const greeting = "Hello";
const world = "World";
@@ -31,7 +31,7 @@ const defaultProps = {
showLineNumbers: false,
};
const CodeSnippet: FC<Props> = props => {
export const CodeSnippet: FC<Props> = props => {
const { text, language, showLineNumbers } = {
...defaultProps,
...props,
@@ -57,5 +57,3 @@ CodeSnippet.propTypes = {
language: PropTypes.string.isRequired,
showLineNumbers: PropTypes.bool,
};
export default CodeSnippet;
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './CodeSnippet';
export { CodeSnippet } from './CodeSnippet';
@@ -15,7 +15,7 @@
*/
import React from 'react';
import CopyTextButton from '.';
import { CopyTextButton } from '.';
export default {
title: 'CopyTextButton',
@@ -17,7 +17,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import CopyTextButton from './CopyTextButton';
import { CopyTextButton } from './CopyTextButton';
import {
ApiRegistry,
errorApiRef,
@@ -56,7 +56,7 @@ const defaultProps = {
tooltipText: 'Text copied to clipboard',
};
const CopyTextButton: FC<Props> = props => {
export const CopyTextButton: FC<Props> = props => {
const { text, tooltipDelay, tooltipText } = {
...defaultProps,
...props,
@@ -110,5 +110,3 @@ CopyTextButton.propTypes = {
tooltipDelay: PropTypes.number,
tooltipText: PropTypes.string,
};
export default CopyTextButton;
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './CopyTextButton';
export { CopyTextButton } from './CopyTextButton';
@@ -15,7 +15,7 @@
*/
import React from 'react';
import DismissableBanner from './DismissableBanner';
import { DismissableBanner } from './DismissableBanner';
import { Link, Typography } from '@material-ui/core';
import {
ApiProvider,
@@ -17,7 +17,7 @@
import React from 'react';
import { fireEvent } from '@testing-library/react';
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
import DismissableBanner from './DismissableBanner';
import { DismissableBanner } from './DismissableBanner';
import {
ApiRegistry,
ApiProvider,
@@ -59,7 +59,7 @@ type Props = {
id: string;
};
const DismissableBanner: FC<Props> = ({ variant, message, id }) => {
export const DismissableBanner: FC<Props> = ({ variant, message, id }) => {
const classes = useStyles();
const storageApi = useApi(storageApiRef);
const notificationsStore = storageApi.forBucket('notifications');
@@ -111,5 +111,3 @@ const DismissableBanner: FC<Props> = ({ variant, message, id }) => {
</Snackbar>
);
};
export default DismissableBanner;
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './DismissableBanner';
export { DismissableBanner } from './DismissableBanner';
@@ -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 { FeatureCalloutCircular } from './FeatureCalloutCircular';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React from 'react';
import HorizontalScrollGrid from './HorizontalScrollGrid';
import { HorizontalScrollGrid } from './HorizontalScrollGrid';
const cardContentStyle = { height: 0, padding: 150, margin: 20 };
const containerStyle = { width: 800, height: 400, margin: 20 };
@@ -17,7 +17,7 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
import HorizontalScrollGrid from './HorizontalScrollGrid';
import { HorizontalScrollGrid } from './HorizontalScrollGrid';
import { Grid } from '@material-ui/core';
describe('<HorizontalScrollGrid />', () => {
@@ -181,7 +181,7 @@ function useSmoothScroll(
return setScrollTarget;
}
const HorizontalScrollGrid: FC<Props> = props => {
export const HorizontalScrollGrid: FC<Props> = props => {
const {
scrollStep = 100,
scrollSpeed = 50,
@@ -245,5 +245,3 @@ const HorizontalScrollGrid: FC<Props> = props => {
</div>
);
};
export default HorizontalScrollGrid;
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './HorizontalScrollGrid';
export { HorizontalScrollGrid } from './HorizontalScrollGrid';
@@ -15,7 +15,7 @@
*/
import React from 'react';
import Progress from '.';
import { Progress } from '.';
export default {
title: 'Progress',
@@ -17,7 +17,7 @@
import React, { FC, useState, useEffect } from 'react';
import { LinearProgress, LinearProgressProps } from '@material-ui/core';
const Progress: FC<LinearProgressProps> = props => {
export const Progress: FC<LinearProgressProps> = props => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
@@ -31,5 +31,3 @@ const Progress: FC<LinearProgressProps> = props => {
<div style={{ display: 'none' }} data-testid="progress" />
);
};
export default Progress;
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './Progress';
export { Progress } from './Progress';
@@ -17,7 +17,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import CircleProgress, { getProgressColor } from './CircleProgress';
import { CircleProgress, getProgressColor } from './CircleProgress';
describe('<CircleProgress />', () => {
it('renders without exploding', () => {
@@ -77,7 +77,7 @@ export function getProgressColor(
return palette.status.ok;
}
const CircleProgress: FC<Props> = props => {
export const CircleProgress: FC<Props> = props => {
const classes = useStyles(props);
const theme = useTheme<BackstageTheme>();
const { value, fractional, inverse, unit, max } = {
@@ -104,5 +104,3 @@ const CircleProgress: FC<Props> = props => {
</div>
);
};
export default CircleProgress;
@@ -15,7 +15,7 @@
*/
import React from 'react';
import HorizontalProgress from './HorizontalProgress';
import { HorizontalProgress } from './HorizontalProgress';
const containerStyle = { width: 300 };
@@ -28,7 +28,7 @@ type Props = {
value: number;
};
const HorizontalProgress: FC<Props> = ({ value }) => {
export const HorizontalProgress: FC<Props> = ({ value }) => {
const theme = useTheme<BackstageTheme>();
if (isNaN(value)) {
return null;
@@ -49,5 +49,3 @@ const HorizontalProgress: FC<Props> = ({ value }) => {
</Tooltip>
);
};
export default HorizontalProgress;
@@ -15,7 +15,7 @@
*/
import React from 'react';
import ProgressCard from './ProgressCard';
import { ProgressCard } from './ProgressCard';
import { Grid } from '@material-ui/core';
const linkInfo = { title: 'Go to XYZ Location', link: '#' };
@@ -18,7 +18,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import ProgressCard from './ProgressCard';
import { ProgressCard } from './ProgressCard';
const minProps = { title: 'Tingle upgrade', progress: 0.12 };
@@ -18,7 +18,7 @@ import React, { FC } from 'react';
import { makeStyles } from '@material-ui/core';
import { InfoCard } from '../../layout/InfoCard';
import { BottomLinkProps } from '../../layout/BottomLink';
import CircleProgress from './CircleProgress';
import { CircleProgress } from './CircleProgress';
type Props = {
title: string;
@@ -36,7 +36,7 @@ const useStyles = makeStyles({
},
});
const ProgressCard: FC<Props> = props => {
export const ProgressCard: FC<Props> = props => {
const classes = useStyles(props);
const { title, subheader, progress, deepLink, variant } = props;
@@ -53,5 +53,3 @@ const ProgressCard: FC<Props> = props => {
</div>
);
};
export default ProgressCard;
@@ -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.
*/
export { ProgressCard } from './ProgressCard';
export { CircleProgress } from './CircleProgress';
export { HorizontalProgress } from './HorizontalProgress';
@@ -16,8 +16,8 @@
import React from 'react';
import { render, fireEvent, within } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import Stepper from './SimpleStepper';
import Step from './SimpleStepperStep';
import { SimpleStepper as Stepper } from './SimpleStepper';
import { SimpleStepperStep as Step } from './SimpleStepperStep';
const getTextInSlide = (rendered: any, index: number) =>
within(rendered.getByTestId(`step${index}`)).getByText;
@@ -40,13 +40,17 @@ export interface StepperProps {
onStepChange?: (prevIndex: number, nextIndex: number) => void;
}
const Stepper: FC<StepperProps> = ({ children, elevated, onStepChange }) => {
export const SimpleStepper: FC<StepperProps> = ({
children,
elevated,
onStepChange,
}) => {
const [stepIndex, setStepIndex] = useState<number>(0);
const [stepHistory, setStepHistory] = useState<number[]>([0]);
const steps: React.ReactNode[] = [];
let endStep;
Children.forEach(children, (child) => {
Children.forEach(children, child => {
if (isValidElement(child)) {
if (child.props.end) {
endStep = child;
@@ -80,5 +84,3 @@ const Stepper: FC<StepperProps> = ({ children, elevated, onStepChange }) => {
</>
);
};
export default Stepper;
@@ -18,7 +18,7 @@ import { Button, makeStyles } from '@material-ui/core';
import { StepActions } from './SimpleStepperStep';
import { VerticalStepperContext } from './SimpleStepper';
const useStyles = makeStyles((theme) => ({
const useStyles = makeStyles(theme => ({
root: {
marginTop: theme.spacing(3),
'& button': {
@@ -71,7 +71,7 @@ export type SimpleStepperFooterProps = {
children?: ReactNode;
};
const SimpleStepperFooter: FC<SimpleStepperFooterProps> = ({
export const SimpleStepperFooter: FC<SimpleStepperFooterProps> = ({
actions = {},
children,
}) => {
@@ -145,5 +145,3 @@ const SimpleStepperFooter: FC<SimpleStepperFooterProps> = ({
</div>
);
};
export default SimpleStepperFooter;
@@ -21,9 +21,9 @@ import {
Typography,
makeStyles,
} from '@material-ui/core';
import SimpleStepperFooter from './SimpleStepperFooter';
import { SimpleStepperFooter } from './SimpleStepperFooter';
const useStyles = makeStyles((theme) => ({
const useStyles = makeStyles(theme => ({
end: {
padding: theme.spacing(3),
},
@@ -53,7 +53,7 @@ export type StepProps = {
actions?: StepActions;
};
const Step: FC<StepProps> = ({
export const SimpleStepperStep: FC<StepProps> = ({
title,
children,
end,
@@ -82,5 +82,3 @@ const Step: FC<StepProps> = ({
</MuiStep>
);
};
export default Step;
@@ -14,7 +14,5 @@
* limitations under the License.
*/
import SimpleStepper from './SimpleStepper';
import SimpleStepperStep from './SimpleStepperStep';
export { SimpleStepper, SimpleStepperStep };
export { SimpleStepper } from './SimpleStepper';
export { SimpleStepperStep } from './SimpleStepperStep';
@@ -23,7 +23,7 @@ import {
StatusRunning,
StatusWarning,
} from './Status';
import Table from '../Table';
import { Table } from '../Table';
import { InfoCard } from '../../layout/InfoCard';
export default {
@@ -16,7 +16,7 @@
import React, { FC } from 'react';
import { InfoCard } from '../../layout/InfoCard';
import { Grid } from '@material-ui/core';
import StructuredMetadataTable from '.';
import { StructuredMetadataTable } from './StructuredMetadataTable';
const cardContentStyle = { heightX: 200, width: 500 };
@@ -17,7 +17,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import StructuredMetadataTable from './StructuredMetadataTable';
import { StructuredMetadataTable } from './StructuredMetadataTable';
import { startCase } from 'lodash';
describe('<StructuredMetadataTable />', () => {
@@ -56,7 +56,7 @@ function renderMap(
nested?: boolean,
options?: any,
) {
const values = Object.keys(map).map((key) => {
const values = Object.keys(map).map(key => {
const value = toValue(map[key], true);
const fmtKey =
options && options.titleFormat
@@ -98,7 +98,7 @@ function toValue(
}
function mapToItems(info: { [key: string]: string }, options: any) {
return Object.keys(info).map((key) => (
return Object.keys(info).map(key => (
<TableItem key={key} title={key} value={info[key]} options={options} />
));
}
@@ -147,7 +147,8 @@ interface ComponentProps {
dense?: boolean;
options?: any;
}
export default class StructuredMetadataTable extends Component<ComponentProps> {
export class StructuredMetadataTable extends Component<ComponentProps> {
render() {
const { metadata, dense, options } = this.props;
const metadataItems = mapToItems(metadata, options || {});
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './StructuredMetadataTable';
export { StructuredMetadataTable } from './StructuredMetadataTable';
@@ -49,7 +49,7 @@ const useStyles = makeStyles(theme => ({
},
}));
const SupportButton: FC<Props> = ({
export const SupportButton: FC<Props> = ({
slackChannel = '#backstage',
email = [],
children,
@@ -155,5 +155,3 @@ const SupportButton: FC<Props> = ({
</Fragment>
);
};
export default SupportButton;
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './SupportButton';
export { SupportButton } from './SupportButton';
@@ -33,7 +33,7 @@ type SubvalueCellProps = {
subvalue: React.ReactNode;
};
const SubvalueCell: FC<SubvalueCellProps> = ({ value, subvalue }) => {
export const SubvalueCell: FC<SubvalueCellProps> = ({ value, subvalue }) => {
const classes = useSubvalueCellStyles();
return (
@@ -43,5 +43,3 @@ const SubvalueCell: FC<SubvalueCellProps> = ({ value, subvalue }) => {
</>
);
};
export default SubvalueCell;
@@ -15,7 +15,7 @@
*/
import React from 'react';
import Table, { SubvalueCell, TableColumn } from './';
import { Table, SubvalueCell, TableColumn } from './';
export default {
title: 'Table',
@@ -17,7 +17,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import Table from './';
import { Table } from './Table';
const minProps = {
columns: [
+1 -3
View File
@@ -162,7 +162,7 @@ export interface TableProps extends MaterialTableProps<{}> {
subtitle?: string;
}
const Table: FC<TableProps> = ({
export const Table: FC<TableProps> = ({
columns,
options,
title,
@@ -212,5 +212,3 @@ const Table: FC<TableProps> = ({
/>
);
};
export default Table;
+2 -2
View File
@@ -14,6 +14,6 @@
* limitations under the License.
*/
export { default } from './Table';
export { Table } from './Table';
export type { TableColumn } from './Table';
export { default as SubvalueCell } from './SubvalueCell';
export { SubvalueCell } from './SubvalueCell';
+1 -1
View File
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { Tabs as default } from './Tabs';
export { Tabs } from './Tabs';
@@ -15,8 +15,8 @@
*/
import React from 'react';
import TrendLine from '.';
import Table from '../Table';
import { Table } from '../Table';
import { TrendLine } from './TrendLine';
import { InfoCard } from '../../layout/InfoCard';
export default {
@@ -19,7 +19,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import TrendLine from '.';
import { TrendLine } from './TrendLine';
describe('TrendLine', () => {
describe('when no data is present', () => {
@@ -27,7 +27,7 @@ function color(data: number[], theme: BackstageTheme): string | undefined {
return theme.palette.status.error;
}
const Trendline: FC<SparklinesProps & { title?: string }> = props => {
export const TrendLine: FC<SparklinesProps & { title?: string }> = props => {
const theme = useTheme<BackstageTheme>();
if (!props.data) return null;
@@ -38,5 +38,3 @@ const Trendline: FC<SparklinesProps & { title?: string }> = props => {
</Sparklines>
);
};
export default Trendline;
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './TrendLine';
export { TrendLine } from './TrendLine';
@@ -15,7 +15,7 @@
*/
import React from 'react';
import WarningPanel from '.';
import { WarningPanel } from './WarningPanel';
import { Link, Button } from '@material-ui/core';
export default {
@@ -18,7 +18,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import WarningPanel from './WarningPanel';
import { WarningPanel } from './WarningPanel';
const minProps = { title: 'Mock title', message: 'Some more info' };
@@ -19,7 +19,7 @@ import { Typography, makeStyles } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
import ErrorOutline from '@material-ui/icons/ErrorOutline';
const useErrorOutlineStyles = makeStyles<BackstageTheme>((theme) => ({
const useErrorOutlineStyles = makeStyles<BackstageTheme>(theme => ({
root: {
marginRight: theme.spacing(1),
fill: theme.palette.warningText,
@@ -30,7 +30,7 @@ const ErrorOutlineStyled = () => {
return <ErrorOutline classes={classes} />;
};
const useStyles = makeStyles<BackstageTheme>((theme) => ({
const useStyles = makeStyles<BackstageTheme>(theme => ({
message: {
display: 'flex',
flexDirection: 'column',
@@ -62,7 +62,7 @@ type Props = {
title?: string;
};
const WarningPanel: FC<Props> = (props) => {
export const WarningPanel: FC<Props> = props => {
const classes = useStyles(props);
const { title, message, children } = props;
return (
@@ -82,5 +82,3 @@ const WarningPanel: FC<Props> = (props) => {
</div>
);
};
export default WarningPanel;
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { default } from './WarningPanel';
export { WarningPanel } from './WarningPanel';
+36
View File
@@ -0,0 +1,36 @@
/*
* 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 './AlertDisplay';
export * from './Button';
export * from './CodeSnippet';
export * from './CopyTextButton';
export * from './DismissableBanner';
export * from './FeatureDiscovery';
export * from './HorizontalScrollGrid';
export * from './Lifecycle';
export * from './Link';
export * from './OAuthRequestDialog';
export * from './Progress';
export * from './ProgressBars';
export * from './SimpleStepper';
export * from './Status';
export * from './StructuredMetadataTable';
export * from './SupportButton';
export * from './Table';
export * from './Tabs';
export * from './TrendLine';
export * from './WarningPanel';
+1 -24
View File
@@ -17,28 +17,5 @@
export * from '@backstage/core-api';
export * from './api-wrappers';
export * from './components';
export * from './layout';
export { default as CodeSnippet } from './components/CodeSnippet';
export { default as DismissableBanner } from './components/DismissableBanner';
export { AlertDisplay } from './components/AlertDisplay';
export { default as HorizontalScrollGrid } from './components/HorizontalScrollGrid';
export { default as ProgressCard } from './components/ProgressBars/ProgressCard';
export { default as CircleProgress } from './components/ProgressBars/CircleProgress';
export { default as HorizontalProgress } from './components/ProgressBars/HorizontalProgress';
export { default as CopyTextButton } from './components/CopyTextButton';
export { default as Progress } from './components/Progress';
export * from './components/SimpleStepper';
export { OAuthRequestDialog } from './components/OAuthRequestDialog';
export { Lifecycle } from './components/Lifecycle';
export { default as SupportButton } from './components/SupportButton';
export { default as Table, SubvalueCell } from './components/Table';
export type { TableColumn } from './components/Table/Table';
export { default as StructuredMetadataTable } from './components/StructuredMetadataTable';
export { default as TrendLine } from './components/TrendLine';
export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular';
export * from './components/Status';
export * from './components/Button';
export * from './components/Link';
export { default as WarningPanel } from './components/WarningPanel';
export { default as Tabs } from './components/Tabs';
@@ -22,7 +22,7 @@ import { ContentHeader } from '../ContentHeader/ContentHeader';
import { Grid } from '@material-ui/core';
import { SignInPageProps, useApi, configApiRef } from '@backstage/core-api';
import { useSignInProviders, SignInProviderId } from './providers';
import Progress from '../../components/Progress';
import { Progress } from '../../components/Progress';
export type Props = SignInPageProps & {
providers: SignInProviderId[];
@@ -28,7 +28,6 @@ import {
import isEmpty from 'lodash/isEmpty';
import { InfoCard } from '../InfoCard/InfoCard';
import { ProviderComponent, ProviderLoader, SignInProvider } from './types';
import { SignInResult } from '@backstage/core-api';
const ID_TOKEN_REGEX = /^[a-z0-9+/]+\.[a-z0-9+/]+\.[a-z0-9+/]+$/i;
@@ -43,12 +42,24 @@ const useFormStyles = makeStyles(theme => ({
},
}));
type Data = {
userId: string;
idToken?: string;
};
const Component: ProviderComponent = ({ onResult }) => {
const classes = useFormStyles();
const { register, handleSubmit, errors, formState } = useForm<SignInResult>({
const { register, handleSubmit, errors, formState } = useForm<Data>({
mode: 'onChange',
});
const handleResult = ({ userId, idToken }: Data) => {
onResult({
userId,
getIdToken: idToken ? async () => idToken : undefined,
});
};
return (
<Grid item>
<InfoCard title="Custom User">
@@ -58,7 +69,7 @@ const Component: ProviderComponent = ({ onResult }) => {
This selection will not be stored.
</Typography>
<form className={classes.form} onSubmit={handleSubmit(onResult)}>
<form className={classes.form} onSubmit={handleSubmit(handleResult)}>
<FormControl>
<TextField
name="userId"
@@ -35,12 +35,12 @@ const Component: ProviderComponent = ({ onResult }) => {
const handleLogin = async () => {
try {
const idToken = await googleAuthApi.getIdToken({ instantPopup: true });
await googleAuthApi.getIdToken({ instantPopup: true });
const profile = await googleAuthApi.getProfile();
onResult({
userId: parseUserId(profile!),
idToken,
getIdToken: () => googleAuthApi.getIdToken(),
logout: async () => {
await googleAuthApi.logout();
},
@@ -69,14 +69,11 @@ const Component: ProviderComponent = ({ onResult }) => {
const loader: ProviderLoader = async apis => {
const googleAuthApi = apis.get(googleAuthApiRef)!;
const [idToken, profile] = await Promise.all([
googleAuthApi.getIdToken({ optional: true }),
googleAuthApi.getProfile({ optional: true }),
]);
const profile = await googleAuthApi.getProfile({ optional: true });
return {
userId: parseUserId(profile!),
idToken,
getIdToken: () => googleAuthApi.getIdToken(),
logout: async () => {
await googleAuthApi.logout();
},
+1 -1
View File
@@ -2,7 +2,6 @@ import {
ApiRegistry,
alertApiRef,
errorApiRef,
identityApiRef,
oauthRequestApiRef,
OAuthRequestManager,
googleAuthApiRef,
@@ -12,6 +11,7 @@ import {
ErrorAlerter,
GoogleAuth,
GithubAuth,
identityApiRef,
} from '@backstage/core';
const builder = ApiRegistry.builder();