Merge branch 'master' of github.com:spotify/backstage into shmidt-i/location-update-results
This commit is contained in:
@@ -14,7 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { GoogleAuthProvider, THOUSAND_DAYS_MS } from './provider';
|
||||
import {
|
||||
GoogleAuthProvider,
|
||||
THOUSAND_DAYS_MS,
|
||||
TEN_MINUTES_MS,
|
||||
} from './provider';
|
||||
import passport from 'passport';
|
||||
import express from 'express';
|
||||
import * as utils from './../utils';
|
||||
@@ -52,11 +56,16 @@ describe('GoogleAuthProvider', () => {
|
||||
});
|
||||
|
||||
describe('start authentication handler', () => {
|
||||
const mockResponse = ({} as unknown) as express.Response;
|
||||
const mockResponse = ({
|
||||
send: jest.fn().mockReturnThis(),
|
||||
status: jest.fn().mockReturnThis(),
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
const mockNext: express.NextFunction = jest.fn();
|
||||
|
||||
it('should initiate authenticate request with provided scopes', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
query: {
|
||||
scope: 'a,b',
|
||||
},
|
||||
@@ -75,11 +84,36 @@ describe('GoogleAuthProvider', () => {
|
||||
scope: 'a,b',
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
state: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('should set a nonce cookie', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
query: {
|
||||
scope: 'a,b',
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const googleAuthProvider = new GoogleAuthProvider(
|
||||
googleAuthProviderConfig,
|
||||
);
|
||||
googleAuthProvider.start(mockRequest, mockResponse, mockNext);
|
||||
expect(mockResponse.cookie).toBeCalledTimes(1);
|
||||
expect(mockResponse.cookie).toBeCalledWith(
|
||||
'google-nonce',
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
maxAge: TEN_MINUTES_MS,
|
||||
path: `/auth/${googleAuthProviderConfig.provider}/handler`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if no scopes provided', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
query: {},
|
||||
} as unknown) as express.Request;
|
||||
|
||||
@@ -93,7 +127,9 @@ describe('GoogleAuthProvider', () => {
|
||||
});
|
||||
|
||||
describe('logout handler', () => {
|
||||
const mockRequest = ({} as unknown) as express.Request;
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
} as unknown) as express.Request;
|
||||
|
||||
it('should perform logout and respond with 200', () => {
|
||||
const mockResponse: any = ({
|
||||
@@ -122,7 +158,6 @@ describe('GoogleAuthProvider', () => {
|
||||
});
|
||||
|
||||
describe('redirect frame handler', () => {
|
||||
const mockRequest = ({} as unknown) as express.Request;
|
||||
const mockResponse: any = ({
|
||||
status: jest.fn().mockReturnThis(),
|
||||
send: jest.fn().mockReturnThis(),
|
||||
@@ -131,6 +166,14 @@ describe('GoogleAuthProvider', () => {
|
||||
const mockNext: express.NextFunction = jest.fn();
|
||||
|
||||
it('should call authenticate and post a response', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: { 'google-nonce': 'NONCE' },
|
||||
query: {
|
||||
state: 'NONCE',
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const spyPostMessage = jest
|
||||
.spyOn(utils, 'postMessageResponse')
|
||||
.mockImplementation(() => jest.fn());
|
||||
@@ -164,6 +207,14 @@ describe('GoogleAuthProvider', () => {
|
||||
});
|
||||
|
||||
it('should respond with a error message if no refresh token returned', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: { 'google-nonce': 'NONCE' },
|
||||
query: {
|
||||
state: 'NONCE',
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const spyPassport = jest
|
||||
.spyOn(passport, 'authenticate')
|
||||
.mockImplementation((_x, callbackFunc) => {
|
||||
@@ -190,6 +241,14 @@ describe('GoogleAuthProvider', () => {
|
||||
});
|
||||
|
||||
it('should respond with a error message if auth failed', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: { 'google-nonce': 'NONCE' },
|
||||
query: {
|
||||
state: 'NONCE',
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const spyPassport = jest
|
||||
.spyOn(passport, 'authenticate')
|
||||
.mockImplementation((_x, callbackFunc) => {
|
||||
@@ -214,6 +273,60 @@ describe('GoogleAuthProvider', () => {
|
||||
error: new Error('Google auth failed, Error: TokenError'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a error message if cookie nonce is missing', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: {},
|
||||
query: { state: 'NONCE' },
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const googleAuthProvider = new GoogleAuthProvider(
|
||||
googleAuthProviderConfig,
|
||||
);
|
||||
|
||||
googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext);
|
||||
expect(mockResponse.send).toBeCalledTimes(1);
|
||||
expect(mockResponse.send).toBeCalledWith('Missing nonce');
|
||||
expect(mockResponse.status).toBeCalledTimes(1);
|
||||
expect(mockResponse.status).toBeCalledWith(401);
|
||||
});
|
||||
|
||||
it('should respond with a error message if state nonce is missing', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: { 'google-nonce': 'NONCE' },
|
||||
query: {},
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const googleAuthProvider = new GoogleAuthProvider(
|
||||
googleAuthProviderConfig,
|
||||
);
|
||||
|
||||
googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext);
|
||||
expect(mockResponse.send).toBeCalledTimes(1);
|
||||
expect(mockResponse.send).toBeCalledWith('Missing nonce');
|
||||
expect(mockResponse.status).toBeCalledTimes(1);
|
||||
expect(mockResponse.status).toBeCalledWith(401);
|
||||
});
|
||||
|
||||
it('should respond with a error message if nonce mismatch', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: { 'google-nonce': 'NONCA' },
|
||||
query: { state: 'NONCEB' },
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const googleAuthProvider = new GoogleAuthProvider(
|
||||
googleAuthProviderConfig,
|
||||
);
|
||||
|
||||
googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext);
|
||||
expect(mockResponse.send).toBeCalledTimes(1);
|
||||
expect(mockResponse.send).toBeCalledWith('Invalid nonce');
|
||||
expect(mockResponse.status).toBeCalledTimes(1);
|
||||
expect(mockResponse.status).toBeCalledWith(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('strategy handler', () => {
|
||||
@@ -245,6 +358,7 @@ describe('GoogleAuthProvider', () => {
|
||||
it('should respond with a 401', () => {
|
||||
const mockRequest = ({
|
||||
cookies: jest.fn(),
|
||||
header: () => 'XMLHttpRequest',
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const googleAuthProvider = new GoogleAuthProvider(
|
||||
@@ -262,6 +376,7 @@ describe('GoogleAuthProvider', () => {
|
||||
|
||||
describe('refresh token cookie, no scope', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: { 'google-refresh-token': 'REFRESH_TOKEN' },
|
||||
query: {},
|
||||
} as unknown) as express.Request;
|
||||
@@ -349,6 +464,7 @@ describe('GoogleAuthProvider', () => {
|
||||
|
||||
describe('refresh token cookie and scope', () => {
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: { 'google-refresh-token': 'REFRESH_TOKEN' },
|
||||
query: {
|
||||
scope: 'a,b',
|
||||
@@ -387,6 +503,20 @@ describe('GoogleAuthProvider', () => {
|
||||
scope: 'a,b',
|
||||
});
|
||||
});
|
||||
|
||||
it('ensures x-requested-with header', () => {
|
||||
const mockHeaderRequest = ({
|
||||
header: () => 'TEST',
|
||||
} as unknown) as express.Request;
|
||||
|
||||
googleAuthProvider.refresh(mockHeaderRequest, mockResponse);
|
||||
expect(mockResponse.send).toBeCalledTimes(1);
|
||||
expect(mockResponse.send).toBeCalledWith(
|
||||
'Invalid X-Requested-With header',
|
||||
);
|
||||
expect(mockResponse.status).toBeCalledTimes(1);
|
||||
expect(mockResponse.status).toBeCalledWith(401);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import passport from 'passport';
|
||||
import express, { CookieOptions } from 'express';
|
||||
import crypto from 'crypto';
|
||||
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
|
||||
import refresh from 'passport-oauth2-refresh';
|
||||
import {
|
||||
@@ -23,10 +24,11 @@ import {
|
||||
AuthProviderRouteHandlers,
|
||||
AuthProviderConfig,
|
||||
} from './../types';
|
||||
import { postMessageResponse } from './../utils';
|
||||
import { postMessageResponse, ensuresXRequestedWith } from './../utils';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
|
||||
export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000;
|
||||
export const TEN_MINUTES_MS = 600 * 1000;
|
||||
export class GoogleAuthProvider
|
||||
implements AuthProvider, AuthProviderRouteHandlers {
|
||||
private readonly providerConfig: AuthProviderConfig;
|
||||
@@ -39,6 +41,19 @@ export class GoogleAuthProvider
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
) {
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
|
||||
const options: CookieOptions = {
|
||||
maxAge: TEN_MINUTES_MS,
|
||||
secure: false,
|
||||
sameSite: 'none',
|
||||
domain: 'localhost',
|
||||
path: `/auth/${this.providerConfig.provider}/handler`,
|
||||
httpOnly: true,
|
||||
};
|
||||
|
||||
res.cookie(`${this.providerConfig.provider}-nonce`, nonce, options);
|
||||
|
||||
const scope = req.query.scope?.toString() ?? '';
|
||||
if (!scope) {
|
||||
throw new InputError('missing scope parameter');
|
||||
@@ -47,6 +62,7 @@ export class GoogleAuthProvider
|
||||
scope,
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
state: nonce,
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
@@ -55,6 +71,17 @@ export class GoogleAuthProvider
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
) {
|
||||
const cookieNonce = req.cookies[`${this.providerConfig.provider}-nonce`];
|
||||
const stateNonce = req.query.state;
|
||||
|
||||
if (!cookieNonce || !stateNonce) {
|
||||
return res.status(401).send('Missing nonce');
|
||||
}
|
||||
|
||||
if (cookieNonce !== stateNonce) {
|
||||
return res.status(401).send('Invalid nonce');
|
||||
}
|
||||
|
||||
return passport.authenticate('google', (err, user) => {
|
||||
if (err) {
|
||||
return postMessageResponse(res, {
|
||||
@@ -95,7 +122,11 @@ export class GoogleAuthProvider
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
async logout(_req: express.Request, res: express.Response) {
|
||||
async logout(req: express.Request, res: express.Response) {
|
||||
if (!ensuresXRequestedWith(req)) {
|
||||
return res.status(401).send('Invalid X-Requested-With header');
|
||||
}
|
||||
|
||||
const options: CookieOptions = {
|
||||
maxAge: 0,
|
||||
secure: false,
|
||||
@@ -110,6 +141,10 @@ export class GoogleAuthProvider
|
||||
}
|
||||
|
||||
async refresh(req: express.Request, res: express.Response) {
|
||||
if (!ensuresXRequestedWith(req)) {
|
||||
return res.status(401).send('Invalid X-Requested-With header');
|
||||
}
|
||||
|
||||
const refreshToken =
|
||||
req.cookies[`${this.providerConfig.provider}-refresh-token`];
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ import { ProviderFactories } from './factories';
|
||||
|
||||
export const defaultRouter = (provider: AuthProviderRouteHandlers) => {
|
||||
const router = Router();
|
||||
router.get('/start', provider.start);
|
||||
router.get('/handler/frame', provider.frameHandler);
|
||||
router.get('/logout', provider.logout);
|
||||
router.get('/start', provider.start.bind(provider));
|
||||
router.get('/handler/frame', provider.frameHandler.bind(provider));
|
||||
router.get('/logout', provider.logout.bind(provider));
|
||||
if (provider.refresh) {
|
||||
router.get('/refresh', provider.refresh);
|
||||
router.get('/refresh', provider.refresh.bind(provider));
|
||||
}
|
||||
return router;
|
||||
};
|
||||
|
||||
@@ -39,3 +39,12 @@ export const postMessageResponse = (
|
||||
</html>
|
||||
`);
|
||||
};
|
||||
|
||||
export const ensuresXRequestedWith = (req: express.Request) => {
|
||||
const requiredHeader = req.header('X-Requested-With');
|
||||
|
||||
if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3003;
|
||||
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
|
||||
const logger = getRootLogger();
|
||||
|
||||
startStandaloneServer({ port, enableCors, logger }).catch((err) => {
|
||||
startStandaloneServer({ port, enableCors, logger }).catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import React, { FC } from 'react';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
DismissableBanner,
|
||||
Header,
|
||||
HomepageTimer,
|
||||
SupportButton,
|
||||
@@ -31,7 +32,7 @@ import {
|
||||
CatalogFilter,
|
||||
CatalogFilterItem,
|
||||
} from '../CatalogFilter/CatalogFilter';
|
||||
import { Button, makeStyles } from '@material-ui/core';
|
||||
import { Button, makeStyles, Typography, Link } from '@material-ui/core';
|
||||
import { filterGroups, defaultFilter } from '../../data/filters';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
@@ -65,6 +66,22 @@ const CatalogPage: FC<CatalogPageProps> = ({ componentFactory }) => {
|
||||
<HomepageTimer />
|
||||
</Header>
|
||||
<Content>
|
||||
<DismissableBanner
|
||||
variant="info"
|
||||
message={
|
||||
<Typography>
|
||||
<span role="img" aria-label="wave" style={{ fontSize: '125%' }}>
|
||||
👋🏼
|
||||
</span>{' '}
|
||||
Welcome to Backstage, we are happy to have you. Start by checking
|
||||
out our{' '}
|
||||
<Link href="/welcome" color="textSecondary">
|
||||
getting started
|
||||
</Link>{' '}
|
||||
page.
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<ContentHeader title="Services">
|
||||
<Button variant="contained" color="primary" href="/create">
|
||||
Create Service
|
||||
|
||||
@@ -83,8 +83,8 @@ const ComponentPage: FC<ComponentPageProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.home}>
|
||||
<Header title={catalogRequest?.value?.name || 'Catalog'}>
|
||||
<Page theme={pageTheme.service}>
|
||||
<Header title={catalogRequest?.value?.name || 'Catalog'} type="service">
|
||||
<ComponentContextMenu onUnregisterComponent={showRemovalDialog} />
|
||||
</Header>
|
||||
{confirmationDialogOpen && catalogRequest.value && (
|
||||
|
||||
@@ -20,7 +20,7 @@ const ARTIFICIAL_TIMEOUT = 800;
|
||||
let inMemoryStore = [...mock];
|
||||
export const MockComponentFactory: ComponentFactory = {
|
||||
getAllComponents(): Promise<Component[]> {
|
||||
return new Promise((resolve) =>
|
||||
return new Promise(resolve =>
|
||||
setTimeout(() => resolve(inMemoryStore), ARTIFICIAL_TIMEOUT),
|
||||
);
|
||||
},
|
||||
@@ -28,7 +28,7 @@ export const MockComponentFactory: ComponentFactory = {
|
||||
return new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
const mockComponent = inMemoryStore.find(
|
||||
(component) => component.name === name,
|
||||
component => component.name === name,
|
||||
);
|
||||
if (mockComponent) return resolve(mockComponent);
|
||||
return reject({ code: 'Component not found!' });
|
||||
@@ -36,10 +36,10 @@ export const MockComponentFactory: ComponentFactory = {
|
||||
);
|
||||
},
|
||||
removeComponentByName(name: string): Promise<boolean> {
|
||||
return new Promise((resolve) =>
|
||||
return new Promise(resolve =>
|
||||
setTimeout(() => {
|
||||
inMemoryStore = inMemoryStore.filter(
|
||||
(component) => component.name !== name,
|
||||
component => component.name !== name,
|
||||
);
|
||||
resolve(true);
|
||||
}, ARTIFICIAL_TIMEOUT),
|
||||
|
||||
@@ -22,7 +22,7 @@ import { withMockStore } from './data/with-mock-store';
|
||||
export const plugin = createPlugin({
|
||||
id: 'catalog',
|
||||
register({ router }) {
|
||||
router.registerRoute('/catalog', withMockStore(CatalogPage));
|
||||
router.registerRoute('/', withMockStore(CatalogPage));
|
||||
router.registerRoute('/catalog/:name/', withMockStore(ComponentPage));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ import SettingsIcon from '@material-ui/icons/Settings';
|
||||
import { useSettings } from '../../state';
|
||||
|
||||
export type Props = { title?: string };
|
||||
export const PluginHeader: FC<Props> = ({ title = 'Circle CI' }) => {
|
||||
export const PluginHeader: FC<Props> = ({ title = 'CircleCI' }) => {
|
||||
const [, { showSettings }] = useSettings();
|
||||
const location = useLocation();
|
||||
const notRoot = !location.pathname.match(/\/circleci\/?$/);
|
||||
|
||||
@@ -80,7 +80,7 @@ const Settings = () => {
|
||||
value={token}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
onChange={e => setToken(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
@@ -90,7 +90,7 @@ const Settings = () => {
|
||||
label="Owner"
|
||||
variant="outlined"
|
||||
value={owner}
|
||||
onChange={(e) => setOwner(e.target.value)}
|
||||
onChange={e => setOwner(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
@@ -100,7 +100,7 @@ const Settings = () => {
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={repo}
|
||||
onChange={(e) => setRepo(e.target.value)}
|
||||
onChange={e => setRepo(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
|
||||
@@ -35,7 +35,7 @@ const BuildName: FC<{ build?: BuildWithSteps }> = ({ build }) => (
|
||||
</IconLink>
|
||||
</Box>
|
||||
);
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
const useStyles = makeStyles(theme => ({
|
||||
neutral: {},
|
||||
failed: {
|
||||
position: 'relative',
|
||||
|
||||
@@ -50,8 +50,8 @@ export const ActionOutput: FC<{
|
||||
const [messages, setMessages] = useState([]);
|
||||
useEffect(() => {
|
||||
fetch(url)
|
||||
.then((res) => res.json())
|
||||
.then((actionOutput) => {
|
||||
.then(res => res.json())
|
||||
.then(actionOutput => {
|
||||
if (typeof actionOutput !== 'undefined') {
|
||||
setMessages(
|
||||
actionOutput.map(({ message }: { message: string }) => message),
|
||||
|
||||
@@ -26,7 +26,7 @@ export const useAsyncPolling = (
|
||||
|
||||
while (isPolling.current === true) {
|
||||
await pollingFn();
|
||||
await new Promise((resolve) => setTimeout(resolve, interval));
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useSettings() {
|
||||
if (
|
||||
stateFromStorage &&
|
||||
Object.keys(stateFromStorage).some(
|
||||
(k) => (settings as any)[k] !== stateFromStorage[k],
|
||||
k => (settings as any)[k] !== stateFromStorage[k],
|
||||
)
|
||||
)
|
||||
dispatch({
|
||||
|
||||
@@ -22,7 +22,7 @@ const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3003;
|
||||
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
|
||||
const logger = getRootLogger();
|
||||
|
||||
startStandaloneServer({ port, enableCors, logger }).catch((err) => {
|
||||
startStandaloneServer({ port, enableCors, logger }).catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('AuditView', () => {
|
||||
apis = ApiRegistry.from([
|
||||
[lighthouseApiRef, new LighthouseRestApi('https://lighthouse')],
|
||||
]);
|
||||
id = websiteResponse.audits.find((a) => a.status === 'COMPLETED')
|
||||
id = websiteResponse.audits.find(a => a.status === 'COMPLETED')
|
||||
?.id as string;
|
||||
useParams.mockReturnValue({ id });
|
||||
});
|
||||
@@ -101,7 +101,7 @@ describe('AuditView', () => {
|
||||
|
||||
await rendered.findByTestId('audit-sidebar');
|
||||
|
||||
websiteResponse.audits.forEach((a) => {
|
||||
websiteResponse.audits.forEach(a => {
|
||||
expect(
|
||||
rendered.queryByText(formatTime(a.timeCreated)),
|
||||
).toBeInTheDocument();
|
||||
@@ -119,14 +119,14 @@ describe('AuditView', () => {
|
||||
|
||||
await rendered.findByTestId('audit-sidebar');
|
||||
|
||||
const audit = websiteResponse.audits.find((a) => a.id === id) as Audit;
|
||||
const audit = websiteResponse.audits.find(a => a.id === id) as Audit;
|
||||
const auditElement = rendered.getByText(formatTime(audit.timeCreated));
|
||||
expect(auditElement.parentElement?.parentElement?.className).toContain(
|
||||
'selected',
|
||||
);
|
||||
|
||||
const notSelectedAudit = websiteResponse.audits.find(
|
||||
(a) => a.id !== id,
|
||||
a => a.id !== id,
|
||||
) as Audit;
|
||||
const notSelectedAuditElement = rendered.getByText(
|
||||
formatTime(notSelectedAudit.timeCreated),
|
||||
@@ -147,7 +147,7 @@ describe('AuditView', () => {
|
||||
|
||||
await rendered.findByTestId('audit-sidebar');
|
||||
|
||||
websiteResponse.audits.forEach((a) => {
|
||||
websiteResponse.audits.forEach(a => {
|
||||
expect(
|
||||
rendered.getByText(formatTime(a.timeCreated)).parentElement
|
||||
?.parentElement,
|
||||
@@ -186,7 +186,7 @@ describe('AuditView', () => {
|
||||
|
||||
describe.skip('when a loading audit is accessed', () => {
|
||||
it('shows a loading view', async () => {
|
||||
id = websiteResponse.audits.find((a) => a.status === 'RUNNING')
|
||||
id = websiteResponse.audits.find(a => a.status === 'RUNNING')
|
||||
?.id as string;
|
||||
useParams.mockReturnValueOnce({ id });
|
||||
|
||||
@@ -206,7 +206,7 @@ describe('AuditView', () => {
|
||||
|
||||
describe.skip('when a failed audit is accessed', () => {
|
||||
it('shows an error message', async () => {
|
||||
id = websiteResponse.audits.find((a) => a.status === 'FAILED')
|
||||
id = websiteResponse.audits.find(a => a.status === 'FAILED')
|
||||
?.id as string;
|
||||
useParams.mockReturnValueOnce({ id });
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ describe('CreateAudit', () => {
|
||||
fireEvent.click(rendered.getByText(/Create Audit/));
|
||||
|
||||
await wait(() => expect(rendered.getByLabelText(/URL/)).toBeEnabled());
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
|
||||
expect(errorApi.post).toHaveBeenCalledWith(expect.any(Error));
|
||||
});
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import { BackstageTheme } from '@backstage/theme';
|
||||
import { Progress } from '@backstage/core';
|
||||
import { ComponentIdValidators } from '../../util/validate';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>((theme) => ({
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
form: {
|
||||
alignItems: 'flex-start',
|
||||
display: 'flex',
|
||||
|
||||
@@ -40,6 +40,7 @@ const ScaffolderPage: React.FC<{}> = () => {
|
||||
return (
|
||||
<Page theme={pageTheme.home}>
|
||||
<Header
|
||||
pageTitleOverride="Create a new component"
|
||||
title={
|
||||
<>
|
||||
Create a new component <Lifecycle alpha shorthand />{' '}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# sentry-backend
|
||||
|
||||
Simple plugin forwarding requests to [Sentry](https://sentry.io) API.
|
||||
Simple plugin forwarding requests to [Sentry](https://sentry.io) API.
|
||||
|
||||
@@ -22,7 +22,7 @@ import { BackstageTheme } from '@backstage/theme';
|
||||
function stripText(text: string, maxLength: number) {
|
||||
return text.length > maxLength ? `${text.substr(0, maxLength)}...` : text;
|
||||
}
|
||||
const useStyles = makeStyles<BackstageTheme>((theme) => ({
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
root: {
|
||||
minWidth: 260,
|
||||
position: 'relative',
|
||||
|
||||
@@ -24,16 +24,16 @@ import { ErrorGraph } from '../ErrorGraph/ErrorGraph';
|
||||
const columns: TableColumn[] = [
|
||||
{
|
||||
title: 'Error',
|
||||
render: (data) => <ErrorCell sentryIssue={data as SentryIssue} />,
|
||||
render: data => <ErrorCell sentryIssue={data as SentryIssue} />,
|
||||
},
|
||||
{
|
||||
title: 'Graph',
|
||||
render: (data) => <ErrorGraph sentryIssue={data as SentryIssue} />,
|
||||
render: data => <ErrorGraph sentryIssue={data as SentryIssue} />,
|
||||
},
|
||||
{
|
||||
title: 'First seen',
|
||||
field: 'firstSeen',
|
||||
render: (data) => {
|
||||
render: data => {
|
||||
const { firstSeen } = data as SentryIssue;
|
||||
return format(firstSeen);
|
||||
},
|
||||
@@ -41,7 +41,7 @@ const columns: TableColumn[] = [
|
||||
{
|
||||
title: 'Last seen',
|
||||
field: 'lastSeen',
|
||||
render: (data) => {
|
||||
render: data => {
|
||||
const { lastSeen } = data as SentryIssue;
|
||||
return format(lastSeen);
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ function getMockIssues(number: number): SentryIssue[] {
|
||||
}
|
||||
export class MockSentryApi implements SentryApi {
|
||||
fetchIssues(): Promise<SentryIssue[]> {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(getMockIssues(14)), 800);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"lint": "backstage-cli lint",
|
||||
|
||||
@@ -105,14 +105,14 @@ export default class Radar extends React.Component {
|
||||
static adjustEntries(entries, activeEntry, quadrants, rings, radius) {
|
||||
let seed = 42;
|
||||
entries.forEach((entry, idx) => {
|
||||
const quadrant = quadrants.find((q) => {
|
||||
const quadrant = quadrants.find(q => {
|
||||
const match =
|
||||
typeof entry.quadrant === 'object'
|
||||
? entry.quadrant.id
|
||||
: entry.quadrant;
|
||||
return q.id === match;
|
||||
});
|
||||
const ring = rings.find((r) => {
|
||||
const ring = rings.find(r => {
|
||||
const match =
|
||||
typeof entry.ring === 'object' ? entry.ring.id : entry.ring;
|
||||
return r.id === match;
|
||||
@@ -190,7 +190,7 @@ export default class Radar extends React.Component {
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={(node) => {
|
||||
ref={node => {
|
||||
this.node = node;
|
||||
}}
|
||||
width={width}
|
||||
@@ -205,7 +205,7 @@ export default class Radar extends React.Component {
|
||||
quadrants={quadrants}
|
||||
rings={rings}
|
||||
activeEntry={activeEntry}
|
||||
onEntryMouseEnter={(entry) => this._setActiveEntry(entry)}
|
||||
onEntryMouseEnter={entry => this._setActiveEntry(entry)}
|
||||
onEntryMouseLeave={() => this._clearActiveEntry()}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@@ -49,16 +49,16 @@ class RadarBubble extends React.PureComponent {
|
||||
this._updatePosition();
|
||||
}
|
||||
|
||||
_setRect = (rect) => {
|
||||
_setRect = rect => {
|
||||
this.rect = rect;
|
||||
};
|
||||
_setNode = (node) => {
|
||||
_setNode = node => {
|
||||
this.node = node;
|
||||
};
|
||||
_setText = (text) => {
|
||||
_setText = text => {
|
||||
this.text = text;
|
||||
};
|
||||
_setPath = (path) => {
|
||||
_setPath = path => {
|
||||
this.path = path;
|
||||
};
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ class RadarGrid extends React.PureComponent {
|
||||
/>,
|
||||
];
|
||||
|
||||
const ringNodes = rings.map((r) => r.outerRadius).map(makeRingNode);
|
||||
const ringNodes = rings.map(r => r.outerRadius).map(makeRingNode);
|
||||
|
||||
return axisNodes.concat(ringNodes);
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ class RadarLegend extends React.PureComponent {
|
||||
<div className={classes.quadrant}>
|
||||
<h2 className={classes.quadrantHeading}>{quadrant.name}</h2>
|
||||
<div className={classes.rings}>
|
||||
{rings.map((ring) =>
|
||||
{rings.map(ring =>
|
||||
RadarLegend._renderRing(
|
||||
ring,
|
||||
RadarLegend._getSegment(segments, quadrant, ring),
|
||||
@@ -117,7 +117,7 @@ class RadarLegend extends React.PureComponent {
|
||||
<p>(empty)</p>
|
||||
) : (
|
||||
<ol className={classes.ringList}>
|
||||
{entries.map((entry) => {
|
||||
{entries.map(entry => {
|
||||
let node = <span className={classes.entry}>{entry.title}</span>;
|
||||
|
||||
if (entry.url) {
|
||||
@@ -175,7 +175,7 @@ class RadarLegend extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<g>
|
||||
{quadrants.map((quadrant) =>
|
||||
{quadrants.map(quadrant =>
|
||||
RadarLegend._renderQuadrant(
|
||||
segments,
|
||||
quadrant,
|
||||
|
||||
@@ -46,16 +46,16 @@ export default class RadarPlot extends React.PureComponent {
|
||||
rings={rings}
|
||||
entries={entries}
|
||||
onEntryMouseEnter={
|
||||
onEntryMouseEnter && ((entry) => onEntryMouseEnter(entry))
|
||||
onEntryMouseEnter && (entry => onEntryMouseEnter(entry))
|
||||
}
|
||||
onEntryMouseLeave={
|
||||
onEntryMouseLeave && ((entry) => onEntryMouseLeave(entry))
|
||||
onEntryMouseLeave && (entry => onEntryMouseLeave(entry))
|
||||
}
|
||||
/>
|
||||
<g transform={`translate(${width / 2}, ${height / 2})`}>
|
||||
<RadarGrid radius={radius} rings={rings} />
|
||||
<RadarFooter x={-0.5 * width} y={0.5 * height} />
|
||||
{entries.map((entry) => (
|
||||
{entries.map(entry => (
|
||||
<RadarEntry
|
||||
key={entry.id}
|
||||
x={entry.x}
|
||||
|
||||
@@ -20,7 +20,7 @@ import WelcomePage from './components/WelcomePage';
|
||||
export const plugin = createPlugin({
|
||||
id: 'welcome',
|
||||
register({ router, featureFlags }) {
|
||||
router.registerRoute('/', WelcomePage);
|
||||
router.registerRoute('/welcome', WelcomePage);
|
||||
|
||||
featureFlags.register('enable-welcome-box');
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user