Merge branch 'master' of github.com:spotify/backstage into blam/scaffolder

* 'master' of github.com:spotify/backstage:
  Rename Scaffolder -> Create (#712)
  Set the initial Storybook theme to light (#711)
  It's InputError then
  Even denser errors
  Better errors
  Show stack in dev
  [backend] use common error types
  Update Discord link in design docs, fix #672 (#710)
  [inventory] move out inventory instantiation + renaming
  Table component with filter and search (#600)
  [Storybook] Improve Status stories (#699)
  Fix blinking animation bug for Status Running component (#706)
  packages/cli: add lib ES2019 to common tsconfig
This commit is contained in:
blam
2020-05-04 19:40:18 +02:00
44 changed files with 991 additions and 176 deletions
+6 -6
View File
@@ -9,11 +9,11 @@ Backstage Open Source is a newly launched endeavor, and were excited to scale
### Collaborative
The Backstage Design Team is small but mighty, and we truly cherish the amazing opportunity we have to work with the Backstage Open Source community! Have an idea? A component request? Feel free to communicate with us via [Discord](https://discord.gg/PefUsZ) (*#design* channel). Collaboration trumps individual speed, and we want to work with you to make Backstage work for all of our users.
The Backstage Design Team is small but mighty, and we truly cherish the amazing opportunity we have to work with the Backstage Open Source community! Have an idea? A component request? Feel free to communicate with us via [Discord](https://discord.gg/EBHEGzX) (*#design* channel). Collaboration trumps individual speed, and we want to work with you to make Backstage work for all of our users.
### Transparent
There are a lot of exciting things coming up and we want to keep you in the loop! Keep an eye on our Milestones in GitHub to see where were headed. Well also be posting updates in the *#design* channel on [Discord](https://discord.gg/PefUsZ). Not only that, we want to keep you informed on the decisions weve made and why weve made them.
There are a lot of exciting things coming up and we want to keep you in the loop! Keep an eye on our Milestones in GitHub to see where were headed. Well also be posting updates in the *#design* channel on [Discord](https://discord.gg/EBHEGzX). Not only that, we want to keep you informed on the decisions weve made and why weve made them.
## 🛠 Our Practice
@@ -48,15 +48,15 @@ This is the universal user experience that is shared amongst all Backstage users
## ⭐️ How to Contribute
### Pick up an issue!
In the beginning, most of our issues will be centered around creating universal components for our Backstage Design System and adding them to our Storybook so plugin developers can reference them. Well also be creating issues that are focused on building up our core Backstage user experience. Well be labeling our issues in GitHub with design and/or storybook - so feel free to browse and tackle the tasks that interest you. If you have any questions regarding an issue, you can ask them in the comments section of the issue or on [Discord](https://discord.gg/PefUsZ). We absolutely adore our external contributors and will send you virtual semlas for your contributions!
In the beginning, most of our issues will be centered around creating universal components for our Backstage Design System and adding them to our Storybook so plugin developers can reference them. Well also be creating issues that are focused on building up our core Backstage user experience. Well be labeling our issues in GitHub with design and/or storybook - so feel free to browse and tackle the tasks that interest you. If you have any questions regarding an issue, you can ask them in the comments section of the issue or on [Discord](https://discord.gg/EBHEGzX). We absolutely adore our external contributors and will send you virtual semlas for your contributions!
### Request a component.
Create an issue (label it design and assign it to katz95) or send us a message on [Discord](https://discord.gg/PefUsZ) (*#design* channel) with details of what the component is and its relevant use cases. Your request will be reviewed by our design team and you should hear back from us within 1-2 business days. Well get back to you and let you know whether your requested component will get picked up by our team as something to be added to our design system.
Create an issue (label it design and assign it to katz95) or send us a message on [Discord](https://discord.gg/EBHEGzX) (*#design* channel) with details of what the component is and its relevant use cases. Your request will be reviewed by our design team and you should hear back from us within 1-2 business days. Well get back to you and let you know whether your requested component will get picked up by our team as something to be added to our design system.
## ✏️ Resources
**[Storybook](http://storybook.backstage.io/)** - where you can view our components. If youd like to help build up our design system, you can also add components weve designed to the Storybook as well.
**[Discord](https://discord.gg/PefUsZ)** - all design questions should be directed to the *#design* channel.
**[Discord](https://discord.gg/EBHEGzX)** - all design questions should be directed to the *#design* channel.
**Documentation**
- Patterns (stay tuned)
@@ -64,7 +64,7 @@ Create an issue (label it design and assign it to katz95) or send us a message o
## 🔮 Future
### Contributions from designers
Are you a designer at an organisation thats implementing Backstage? A designer whos fascinated by the developer productivity problem space? A designer whos curious about open source design? Wed love for you to contribute. Behind the scenes, were setting up a few foundational elements to make sure that contributing to Backstage as a designer is easy. From styling guidelines to UX principles to Figma documents, well make sure youre equipped to chip in on this project. Were excited to work with you! In the meantime, wed love to hear from you on [Discord](https://discord.gg/PefUsZ).
Are you a designer at an organisation thats implementing Backstage? A designer whos fascinated by the developer productivity problem space? A designer whos curious about open source design? Wed love for you to contribute. Behind the scenes, were setting up a few foundational elements to make sure that contributing to Backstage as a designer is easy. From styling guidelines to UX principles to Figma documents, well make sure youre equipped to chip in on this project. Were excited to work with you! In the meantime, wed love to hear from you on [Discord](https://discord.gg/EBHEGzX).
[Back to Docs](../README.md)
+3 -3
View File
@@ -20,7 +20,7 @@ import { Link, makeStyles, Typography } from '@material-ui/core';
import HomeIcon from '@material-ui/icons/Home';
import ExploreIcon from '@material-ui/icons/Explore';
import AccountCircle from '@material-ui/icons/AccountCircle';
import CreateComponentIcon from '@material-ui/icons/Create';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import AccountTreeIcon from '@material-ui/icons/AccountTree';
import {
Sidebar,
@@ -83,8 +83,8 @@ const Root: FC<{}> = ({ children }) => (
<SidebarDivider />
<SidebarItem icon={HomeIcon} to="/" text="Home" />
<SidebarItem icon={ExploreIcon} to="/explore" text="Explore" />
<SidebarItem icon={CreateComponentIcon} to="/scaffolder" text="Create" />
<SidebarDivider />
<SidebarItem icon={CreateComponentIcon} to="/create" text="Create..." />
<SidebarDivider />
<SidebarItem icon={AccountTreeIcon} to="/inventory" text="Inventory" />
<SidebarItem icon={AccountCircle} to="/login" text="Login" />
<SidebarDivider />
+77
View File
@@ -0,0 +1,77 @@
/*
* 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.
*/
/*
* A set of common business logic errors.
*
* The error handler middleware understands these and will translate them to
* well formed HTTP responses.
*
* While these are intentionally analogous to HTTP errors, they are not
* intended to be thrown by the request handling layer. In those places, please
* use e.g. the http-errors library.
*/
class CustomErrorBase extends Error {
readonly cause?: Error;
constructor(message?: string, cause?: Error) {
let fullMessage = message;
if (cause) {
if (fullMessage) {
fullMessage += `; caused by ${cause}`;
} else {
fullMessage = `caused by ${cause}`;
}
}
super(fullMessage);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.cause = cause;
}
}
/**
* The given inputs are malformed and cannot be processed.
*/
export class InputError extends CustomErrorBase {}
/**
* The request requires authentication, which was not properly supplied.
*/
export class AuthenticationError extends CustomErrorBase {}
/**
* The authenticated caller is not allowed to perform this request.
*/
export class NotAllowedError extends CustomErrorBase {}
/**
* The requested resource could not be found.
*
* Note that this error usually is used to indicate that an entity with a given
* ID does not exist, rather than signalling that an entire route is missing.
*/
export class NotFoundError extends CustomErrorBase {}
/**
* The request could not complete due to a conflict in the current state of the
* resource.
*/
export class ConflictError extends CustomErrorBase {}
+1
View File
@@ -14,5 +14,6 @@
* limitations under the License.
*/
export * from './errors';
export * from './logging';
export * from './middleware';
@@ -17,6 +17,7 @@
import express from 'express';
import createError from 'http-errors';
import request from 'supertest';
import * as errors from '../errors';
import { errorHandler } from './errorHandler';
describe('errorHandler', () => {
@@ -33,7 +34,7 @@ describe('errorHandler', () => {
expect(response.text).toBe('some message');
});
it('takes code from StatusCodeError', async () => {
it('takes code from http-errors library errors', async () => {
const app = express();
app.use('/breaks', () => {
throw createError(432, 'Some Message');
@@ -45,4 +46,31 @@ describe('errorHandler', () => {
expect(response.status).toBe(432);
expect(response.text).toContain('Some Message');
});
it('handles well-known error classes', async () => {
const app = express();
app.use('/InputError', () => {
throw new errors.InputError();
});
app.use('/AuthenticationError', () => {
throw new errors.AuthenticationError();
});
app.use('/NotAllowedError', () => {
throw new errors.NotAllowedError();
});
app.use('/NotFoundError', () => {
throw new errors.NotFoundError();
});
app.use('/ConflictError', () => {
throw new errors.ConflictError();
});
app.use(errorHandler());
const r = request(app);
expect((await r.get('/InputError')).status).toBe(400);
expect((await r.get('/AuthenticationError')).status).toBe(401);
expect((await r.get('/NotAllowedError')).status).toBe(403);
expect((await r.get('/NotFoundError')).status).toBe(404);
expect((await r.get('/ConflictError')).status).toBe(409);
});
});
@@ -15,12 +15,21 @@
*/
import { ErrorRequestHandler, NextFunction, Request, Response } from 'express';
import * as errors from '../errors';
export type ErrorHandlerOptions = {
/**
* Whether error response bodies should show error stack traces or not.
*
* If not specified, by default shows stack traces only in development mode.
*/
showStackTraces?: boolean;
};
/**
* Express middleware to handle errors during request processing.
*
* This is commonly the second to last middleware in the chain (before the
* notFoundHandler).
* This is commonly the very last middleware in the chain.
*
* Its primary purpose is not to do translation of business logic exceptions,
* but rather to be a gobal catch-all for uncaught "fatal" errors that are
@@ -30,23 +39,32 @@ import { ErrorRequestHandler, NextFunction, Request, Response } from 'express';
*
* @returns An Express error request handler
*/
export function errorHandler(): ErrorRequestHandler {
export function errorHandler(
options: ErrorHandlerOptions = {},
): ErrorRequestHandler {
const showStackTraces =
options.showStackTraces ?? process.env.NODE_ENV === 'development';
/* eslint-disable @typescript-eslint/no-unused-vars */
return (
error: Error,
_request: Request,
response: Response,
_next: NextFunction,
next: NextFunction,
) => {
if (response.headersSent) {
next(error);
}
const status = getStatusCode(error);
const message = error.message;
const message = showStackTraces ? error.stack : error.message;
response.status(status).send(message);
};
}
function getStatusCode(error: Error): number {
// Look for common http library status codes
const knownStatusCodeFields = ['statusCode', 'status'];
for (const field of knownStatusCodeFields) {
const statusCode = (error as any)[field];
if (
@@ -59,5 +77,22 @@ function getStatusCode(error: Error): number {
}
}
// Handle well-known error types
switch (error.name) {
case errors.InputError.name:
return 400;
case errors.AuthenticationError.name:
return 401;
case errors.NotAllowedError.name:
return 403;
case errors.NotFoundError.name:
return 404;
case errors.ConflictError.name:
return 409;
default:
break;
}
// Fall back to internal server error
return 500;
}
+1 -1
View File
@@ -7,7 +7,7 @@
"sourceMap": true,
"declaration": true,
"strict": true,
"target": "es5",
"target": "ES2019",
"module": "commonjs",
"esModuleInterop": true,
"types": ["node", "jest"]
+16 -7
View File
@@ -28,21 +28,31 @@ import {
notFoundHandler,
requestLoggingHandler,
} from '@backstage/backend-common';
import { createRouter as inventoryRouter } from '@backstage/plugin-inventory-backend';
import {
AggregatorInventory,
createRouter as inventoryRouter,
StaticInventory,
} from '@backstage/plugin-inventory-backend';
import { createScaffolder } from '@backstage/plugin-scaffolder-backend';
import compression from 'compression';
import cors from 'cors';
import express from 'express';
import helmet from 'helmet';
import { testRouter } from './test';
const DEFAULT_PORT = 7000;
const PORT = parseInt(process.env.PORT ?? '', 10) || DEFAULT_PORT;
const logger = getRootLogger().child({ type: 'plugin' });
async function main() {
const inventory = await inventoryRouter({ logger });
const scaffolder = createScaffolder();
const inventory = new AggregatorInventory();
inventory.enlist(
new StaticInventory([
{ id: 'component1' },
{ id: 'component2' },
{ id: 'component3' },
{ id: 'component4' },
]),
);
const app = express();
@@ -51,9 +61,8 @@ async function main() {
app.use(compression());
app.use(express.json());
app.use(requestLoggingHandler());
app.use('/test', testRouter);
app.use('/inventory', inventory);
app.use('/scaffolder', scaffolder);
app.use('/inventory', await inventoryRouter({ inventory, logger }));
app.use('/scaffolder', createScaffolder());
app.use(notFoundHandler());
app.use(errorHandler());
+1
View File
@@ -10,6 +10,7 @@
"module": "ESNext",
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2019"],
"types": ["node", "jest"]
}
}
+1
View File
@@ -45,6 +45,7 @@
"classnames": "^2.2.6",
"clsx": "^1.1.0",
"lodash": "^4.17.15",
"material-table": "^1.57.2",
"prop-types": "^15.7.2",
"rc-progress": "^2.5.2",
"react": "^16.12.0",
+1 -1
View File
@@ -118,7 +118,7 @@ export default class AppBuilder {
<Switch>
{routes}
<Route
render={props => (
render={(props) => (
<ErrorPage {...props} status="404" statusMessage="PAGE NOT FOUND" />
)}
/>
@@ -24,44 +24,72 @@ import {
StatusRunning,
StatusWarning,
} from './Status';
import SortableTable from 'components/SortableTable';
import InfoCard from 'layout/InfoCard';
export default {
title: 'Status',
component: StatusOK,
};
export const statusOK = () => (
<>
<StatusOK /> Status OK
</>
);
export const statusWarning = () => (
<>
<StatusWarning /> Status Warning
</>
);
export const statusError = () => (
<>
<StatusError /> Status Error
</>
);
export const statusFailed = () => (
<>
<StatusFailed /> Status Failed
</>
);
export const statusPending = () => (
<>
<StatusPending /> Status Pending
</>
);
export const statusRunning = () => (
<>
<StatusRunning /> Status Running
</>
);
export const statusNA = () => (
<>
<StatusNA /> Status NA
</>
const data = [
{
status: <StatusOK />,
label: 'OK',
usage: 'Deployment successful',
},
{
status: <StatusWarning />,
label: 'Warning',
usage: 'CPU utilization at 90%',
},
{
status: <StatusError />,
label: 'Error',
usage: 'Service could not be created',
},
{
status: <StatusFailed />,
label: 'Failed',
usage: 'Build for PR #34 failed',
},
{
status: <StatusPending />,
label: 'Pending',
usage: 'Job is waiting',
},
{
status: <StatusRunning />,
label: 'Running',
usage: 'Job is running',
},
{
status: <StatusNA />,
label: 'N/A',
usage: 'Not sure what to do',
},
];
const columns = [
{ id: 'status', label: 'Status' },
{ id: 'label', label: 'Label' },
{ id: 'usage', label: 'Example usage' },
];
const containerStyle = { width: 600 };
export const Default = () => (
<div style={containerStyle}>
<InfoCard title="Available status types">
<SortableTable data={data} columns={columns} />
</InfoCard>
</div>
);
export const statusOK = () => <StatusOK />;
export const statusWarning = () => <StatusWarning />;
export const statusError = () => <StatusError />;
export const statusFailed = () => <StatusFailed />;
export const statusPending = () => <StatusPending />;
export const statusRunning = () => <StatusRunning />;
export const statusNA = () => <StatusNA />;
+11 -13
View File
@@ -19,7 +19,7 @@ import { BackstageTheme } from '@backstage/theme';
import classNames from 'classnames';
import React, { FC } from 'react';
const useStyles = makeStyles<BackstageTheme>(theme => ({
const useStyles = makeStyles<BackstageTheme>((theme) => ({
status: {
width: 12,
height: 12,
@@ -34,6 +34,7 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
backgroundColor: theme.palette.status.warning,
},
error: {
// Use same for Failed status.
width: '0',
height: '0',
borderLeft: '7px solid transparent',
@@ -43,11 +44,8 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
pending: {
backgroundColor: theme.palette.status.pending,
},
failed: {
backgroundColor: 'rgba(245, 155, 35, 0.5)',
},
running: {
animation: 'blink 0.8s step-start 0s infinite',
animation: '$blink 0.8s step-start 0s infinite',
backgroundColor: theme.palette.status.running,
},
'@keyframes blink': {
@@ -57,7 +55,7 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
},
}));
export const StatusOK: FC<{}> = props => {
export const StatusOK: FC<{}> = (props) => {
const classes = useStyles(props);
return (
<span
@@ -68,7 +66,7 @@ export const StatusOK: FC<{}> = props => {
);
};
export const StatusWarning: FC<{}> = props => {
export const StatusWarning: FC<{}> = (props) => {
const classes = useStyles(props);
return (
<span
@@ -79,7 +77,7 @@ export const StatusWarning: FC<{}> = props => {
);
};
export const StatusError: FC<{}> = props => {
export const StatusError: FC<{}> = (props) => {
const classes = useStyles(props);
return (
<span
@@ -90,13 +88,13 @@ export const StatusError: FC<{}> = props => {
);
};
export const StatusNA: FC<{}> = props => (
export const StatusNA: FC<{}> = (props) => (
<span aria-label="Status N/A" {...props}>
N/A
</span>
);
export const StatusPending: FC<{}> = props => {
export const StatusPending: FC<{}> = (props) => {
const classes = useStyles(props);
return (
<span
@@ -107,7 +105,7 @@ export const StatusPending: FC<{}> = props => {
);
};
export const StatusRunning: FC<{}> = props => {
export const StatusRunning: FC<{}> = (props) => {
const classes = useStyles(props);
return (
<span
@@ -118,11 +116,11 @@ export const StatusRunning: FC<{}> = props => {
);
};
export const StatusFailed: FC<{}> = props => {
export const StatusFailed: FC<{}> = (props) => {
const classes = useStyles(props);
return (
<span
className={classNames(classes.status, classes.failed)}
className={classNames(classes.status, classes.error)}
aria-label="Status failed"
{...props}
/>
@@ -0,0 +1,47 @@
/*
* 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 { BackstageTheme } from '@backstage/theme';
import { makeStyles } from '@material-ui/core';
const useSubvalueCellStyles = makeStyles<BackstageTheme>(theme => ({
value: {
marginBottom: '6px',
},
subvalue: {
color: theme.palette.textSubtle,
fontWeight: 'normal',
},
}));
type SubvalueCellProps = {
value: React.ReactNode;
subvalue: React.ReactNode;
};
const SubvalueCell: FC<SubvalueCellProps> = ({ value, subvalue }) => {
const classes = useSubvalueCellStyles();
return (
<>
<div className={classes.value}>{value}</div>
<div className={classes.subvalue}>{subvalue}</div>
</>
);
};
export default SubvalueCell;
@@ -0,0 +1,143 @@
/*
* 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 Table, { SubvalueCell, TableColumn } from './';
export default {
title: 'Table',
component: Table,
};
const generateTestData: (number: number) => Array<{}> = (rows = 20) => {
const data: Array<{}> = [];
while (data.length <= rows) {
data.push({
col1: `Some value ${data.length}`,
col2: `More data ${data.length}`,
subvalue: `Subvalue ${data.length}`,
number: Math.floor(Math.random() * 1000),
date: new Date(Math.random() * 10000000000000),
});
}
return data;
};
const testData100 = generateTestData(100);
export const DefaultTable = () => {
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 (
<Table
options={{ paging: false }}
data={testData100}
columns={columns}
title="Backstage Table"
/>
);
};
export const HiddenSearchTable = () => {
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 (
<Table
options={{ paging: false, search: false }}
data={testData100}
columns={columns}
/>
);
};
export const SubvalueTable = () => {
const columns: TableColumn[] = [
{
title: 'Column 1',
customFilterAndSearch: (
query,
row: any, // Only needed if you want subvalue searchable
) =>
`${row.col1} ${row.subvalue}`
.toUpperCase()
.includes(query.toUpperCase()),
field: 'col1',
highlight: true,
render: (row: any): React.ReactNode => (
<SubvalueCell value={row.col1} subvalue={row.subvalue} />
),
},
{
title: 'Column 2',
field: 'col2',
},
{
title: 'Numeric value',
field: 'number',
type: 'numeric',
},
{
title: 'A Date',
field: 'date',
type: 'date',
},
];
return (
<Table options={{ paging: false }} data={testData100} columns={columns} />
);
};
@@ -0,0 +1,50 @@
/*
* 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 { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import Table from './';
const minProps = {
columns: [
{
title: 'Column 1',
field: 'col1',
},
{
title: 'Column 2',
field: 'col2',
},
],
data: [
{
col1: 'first value, first row',
col2: 'second value, first row',
},
{
col1: 'first value, second row',
col2: 'second value, second row',
},
],
};
describe('<Table />', () => {
it('renders without exploding', () => {
const rendered = render(wrapInTestApp(<Table {...minProps} />));
expect(rendered.getByText('second value, second row')).toBeInTheDocument();
});
});
@@ -0,0 +1,192 @@
/*
* 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, forwardRef } from 'react';
import MTable, {
MTableCell,
MTableHeader,
MTableToolbar,
MaterialTableProps,
Options,
Column,
} from 'material-table';
import { BackstageTheme } from '@backstage/theme';
import { makeStyles } from '@material-ui/core';
// Material-table is not using the standard icons available in in material-ui. https://github.com/mbrn/material-table/issues/51
import {
AddBox,
ArrowUpward,
Check,
ChevronLeft,
ChevronRight,
Clear,
DeleteOutline,
Edit,
FilterList,
FirstPage,
LastPage,
Remove,
SaveAlt,
Search,
ViewColumn,
} from '@material-ui/icons';
const tableIcons = {
Add: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<AddBox {...props} ref={ref} />
)),
Check: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<Check {...props} ref={ref} />
)),
Clear: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<Clear {...props} ref={ref} />
)),
Delete: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<DeleteOutline {...props} ref={ref} />
)),
DetailPanel: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<ChevronRight {...props} ref={ref} />
)),
Edit: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<Edit {...props} ref={ref} />
)),
Export: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<SaveAlt {...props} ref={ref} />
)),
Filter: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<FilterList {...props} ref={ref} />
)),
FirstPage: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<FirstPage {...props} ref={ref} />
)),
LastPage: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<LastPage {...props} ref={ref} />
)),
NextPage: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<ChevronRight {...props} ref={ref} />
)),
PreviousPage: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<ChevronLeft {...props} ref={ref} />
)),
ResetSearch: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<Clear {...props} ref={ref} />
)),
Search: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<Search {...props} ref={ref} />
)),
SortArrow: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<ArrowUpward {...props} ref={ref} />
)),
ThirdStateCheck: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<Remove {...props} ref={ref} />
)),
ViewColumn: forwardRef((props, ref: React.Ref<SVGSVGElement>) => (
<ViewColumn {...props} ref={ref} />
)),
};
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),
borderTop: `1px solid ${theme.palette.grey.A100}`,
borderBottom: `1px solid ${theme.palette.grey.A100}`,
color: theme.palette.textSubtle,
fontWeight: 'bold',
position: 'static',
},
}));
const useToolbarStyles = makeStyles<BackstageTheme>(theme => ({
root: {
padding: theme.spacing(3, 0, 2.5, 2.5),
},
title: {
'& > h6': {
fontWeight: 'bold',
},
},
}));
const convertColumns = (columns: TableColumn[]): TableColumn[] => {
return columns.map(column => {
const headerStyle: React.CSSProperties = {};
const cellStyle: React.CSSProperties = {};
if (column.highlight) {
headerStyle.color = '#000000';
cellStyle.fontWeight = 'bold';
}
return {
...column,
headerStyle,
cellStyle,
};
});
};
export interface TableColumn extends Column<{}> {
highlight?: boolean;
}
export interface TableProps extends MaterialTableProps<{}> {
columns: TableColumn[];
}
const Table: FC<TableProps> = ({ columns, options, ...props }) => {
const cellClasses = useCellStyles();
const headerClasses = useHeaderStyles();
const toolbarClasses = useToolbarStyles();
const MTColumns = convertColumns(columns);
const defaultOptions: Options = {
headerStyle: {
textTransform: 'uppercase',
},
};
return (
<MTable
components={{
Cell: cellProps => (
<MTableCell className={cellClasses.root} {...cellProps} />
),
Header: headerProps => (
<MTableHeader classes={headerClasses} {...headerProps} />
),
Toolbar: toolbarProps => (
<MTableToolbar classes={toolbarClasses} {...toolbarProps} />
),
}}
options={{ ...defaultOptions, ...options }}
columns={MTColumns}
icons={tableIcons}
{...props}
/>
);
};
export default Table;
@@ -14,10 +14,5 @@
* limitations under the License.
*/
import { Router } from 'express';
export const testRouter = Router();
testRouter.get('/', async (_, res) => {
res.status(200).send('hello');
});
export { default, TableColumn } from './Table';
export { default as SubvalueCell } from './SubvalueCell';
@@ -24,7 +24,7 @@ export default {
component: TrendLine,
};
const containerStyle = { width: 600 };
const containerStyle = { width: 700 };
const data = [
{
@@ -66,9 +66,11 @@ const columns = [
];
export const Default = () => (
<InfoCard style={containerStyle} title="Trends over time">
<SortableTable data={data} columns={columns} />
</InfoCard>
<div style={containerStyle}>
<InfoCard title="Trends over time">
<SortableTable data={data} columns={columns} />
</InfoCard>
</div>
);
export const TrendingMix = () => (
@@ -28,7 +28,7 @@ interface IErrorPageProps {
};
}
const useStyles = makeStyles<BackstageTheme>(theme => ({
const useStyles = makeStyles<BackstageTheme>((theme) => ({
container: {
padding: theme.spacing(8),
},
@@ -27,18 +27,18 @@ import {
import ErrorBoundary from 'layout/ErrorBoundary/ErrorBoundary';
import BottomLink, { Props as BottomLinkProps } from '../BottomLink';
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
header: {
padding: theme.spacing(2, 2, 2, 2.5),
},
}));
const BoldHeader = withStyles(theme => ({
const BoldHeader = withStyles((theme) => ({
title: { fontWeight: 700 },
subheader: { paddingTop: theme.spacing(1) },
}))(CardHeader);
const CardActionsTopRight = withStyles(theme => ({
const CardActionsTopRight = withStyles((theme) => ({
root: {
display: 'inline-block',
padding: theme.spacing(8, 8, 0, 0),
@@ -154,7 +154,7 @@ const InfoCard: FC<Props> = ({
if (variant) {
const variants = variant.split(/[\s]+/g);
variants.forEach(name => {
variants.forEach((name) => {
calculatedStyle = {
...calculatedStyle,
...VARIANT_STYLES.card[name as keyof typeof VARIANT_STYLES['card']],
+9 -2
View File
@@ -1,14 +1,21 @@
import React from 'react';
import { addDecorator } from '@storybook/react';
import { addDecorator, addParameters } from '@storybook/react';
import { lightTheme, darkTheme } from '@backstage/theme';
import { CssBaseline, ThemeProvider } from '@material-ui/core';
import { useDarkMode } from 'storybook-dark-mode';
import { Content } from '@backstage/core';
addDecorator(story => (
addDecorator((story) => (
<ThemeProvider theme={useDarkMode() ? darkTheme : lightTheme}>
<CssBaseline>
<Content>{story()}</Content>
</CssBaseline>
</ThemeProvider>
));
addParameters({
darkMode: {
// Set the initial theme
current: 'light',
},
});
+1 -1
View File
@@ -11,7 +11,7 @@ module.exports = {
'@storybook/addon-storysource',
'storybook-dark-mode/register',
],
webpackFinal: async config => {
webpackFinal: async (config) => {
const coreSrc = path.resolve(__dirname, '../../core/src');
config.resolve.alias = {
+2 -2
View File
@@ -28,7 +28,7 @@ export const lightTheme = createTheme({
error: '#CA001B',
running: '#BEBEBE',
pending: '#5BC0DE',
background: '#FEFEFE',
background: '#F8F8F8',
},
bursts: {
fontColor: '#FEFEFE',
@@ -68,7 +68,7 @@ export const darkTheme = createTheme({
error: '#CA001B',
running: '#BEBEBE',
pending: '#5BC0DE',
background: '#FEFEFE',
background: '#282828',
},
bursts: {
fontColor: '#FEFEFE',
@@ -28,7 +28,7 @@ import {
} from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
const useStyles = makeStyles<BackstageTheme>(theme => ({
const useStyles = makeStyles<BackstageTheme>((theme) => ({
card: {
display: 'flex',
flexDirection: 'column',
@@ -27,7 +27,7 @@ import {
import ExploreCard, { CardData } from './ExploreCard';
import { BackstageTheme } from '@backstage/theme';
const useStyles = makeStyles<BackstageTheme>(theme => ({
const useStyles = makeStyles<BackstageTheme>((theme) => ({
container: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, 296px)',
@@ -24,7 +24,7 @@ import { BackstageTheme } from '@backstage/theme';
const GraphiQL = React.lazy(() => import('graphiql'));
const useStyles = makeStyles<BackstageTheme>(theme => ({
const useStyles = makeStyles<BackstageTheme>((theme) => ({
root: {
height: '100%',
display: 'flex',
+2 -1
View File
@@ -5,7 +5,7 @@
"license": "Apache-2.0",
"private": true,
"scripts": {
"start": "tsc-watch --onFirstSuccess \"nodemon dist/run.js\"",
"start": "tsc-watch --onFirstSuccess \"cross-env NODE_ENV=development nodemon dist/run.js\"",
"build": "tsc",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
@@ -21,6 +21,7 @@
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"helmet": "^3.22.0",
"morgan": "^1.10.0",
"winston": "^3.2.1"
+1
View File
@@ -14,4 +14,5 @@
* limitations under the License.
*/
export * from './inventory';
export * from './service/router';
@@ -14,19 +14,29 @@
* limitations under the License.
*/
import { NotFoundError } from '@backstage/backend-common';
import { Component, Inventory } from './types';
export class AggregatorInventory implements Inventory {
inventories: Inventory[] = [];
list(): Promise<Array<Component>> {
return Promise.all(this.inventories.map(i => i.list())).then(lists =>
lists.flat(),
);
async list(): Promise<Component[]> {
const lists = await Promise.all(this.inventories.map((i) => i.list()));
return lists.flat();
}
item(id: string): Promise<Component | undefined> {
return this.list().then(items => items.find(i => i.id === id));
item(id: string): Promise<Component> {
return new Promise((resolve, reject) => {
const promises = this.inventories.map((i) =>
i.item(id).then(resolve, () => {
// For now, just swallow errors in individual inventories;
// should handle partial errors better
}),
);
Promise.all(promises).then(() =>
reject(new NotFoundError(`Found no component with ID ${id}`)),
);
});
}
enlist(inventory: Inventory) {
@@ -14,16 +14,21 @@
* limitations under the License.
*/
import { NotFoundError } from '@backstage/backend-common';
import { Component, Inventory } from './types';
export class StaticInventory implements Inventory {
constructor(private components: Component[]) {}
list(): Promise<Array<Component>> {
return Promise.resolve([...this.components]);
async list(): Promise<Component[]> {
return this.components.slice();
}
item(id: string): Promise<Component | undefined> {
return this.list().then(items => items.find(i => i.id === id));
async item(id: string): Promise<Component> {
const item = this.components.find((i) => i.id === id);
if (!item) {
throw new NotFoundError(`Found no component with ID ${id}`);
}
return item;
}
}
@@ -19,6 +19,6 @@ export type Component = {
};
export type Inventory = {
list: () => Promise<Component[]>;
item: (id: string) => Promise<Component | undefined>;
list(): Promise<Component[]>;
item(id: string): Promise<Component>;
};
+10 -10
View File
@@ -15,20 +15,20 @@
*/
import { getRootLogger } from '@backstage/backend-common';
import { startServer } from './service/server';
import { startStandaloneServer } from './service/standaloneServer';
startServer({
port: process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3003,
enableCors: process.env.PLUGIN_CORS
? Boolean(process.env.PLUGIN_CORS)
: false,
logger: getRootLogger(),
}).catch(err => {
getRootLogger().error(err);
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3003;
const enableCors = process.env.PLUGIN_CORS
? Boolean(process.env.PLUGIN_CORS)
: false;
const logger = getRootLogger();
startStandaloneServer({ port, enableCors, logger }).catch((err) => {
logger.error(err);
process.exit(1);
});
process.on('SIGINT', () => {
getRootLogger().info('CTRL+C pressed; exiting.');
logger.info('CTRL+C pressed; exiting.');
process.exit(0);
});
@@ -16,28 +16,21 @@
import express from 'express';
import { Logger } from 'winston';
import { AggregatorInventory, StaticInventory } from '../inventory';
import Router from 'express-promise-router';
import { Inventory } from '../inventory';
export interface RouterOptions {
inventory: Inventory;
logger: Logger;
}
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const inventory = options.inventory;
const logger = options.logger.child({ plugin: 'inventory' });
const inventory = new AggregatorInventory();
inventory.enlist(
new StaticInventory([
{ id: 'component1' },
{ id: 'component2' },
{ id: 'component3' },
{ id: 'component4' },
]),
);
const router = express.Router();
const router = Router();
router
.get('/', async (req, res) => {
const components = await inventory.list();
@@ -46,11 +39,7 @@ export async function createRouter(
.get('/:id', async (req, res) => {
const { id } = req.params;
const component = await inventory.item(id);
if (component) {
res.status(200).send(component);
} else {
res.status(404).send();
}
res.status(200).send(component);
});
const app = express();
@@ -24,26 +24,29 @@ import cors from 'cors';
import express from 'express';
import helmet from 'helmet';
import { Logger } from 'winston';
import { Inventory } from '../inventory';
import { createRouter } from './router';
export interface ApplicationOptions {
enableCors: boolean;
inventory: Inventory;
logger: Logger;
}
export async function createApplication(
export async function createStandaloneApplication(
options: ApplicationOptions,
): Promise<express.Application> {
const { enableCors, inventory, logger } = options;
const app = express();
app.use(helmet());
if (options.enableCors) {
if (enableCors) {
app.use(cors());
}
app.use(compression());
app.use(express.json());
app.use(requestLoggingHandler());
app.use('/', await createRouter({ logger: options.logger }));
app.use('/', await createRouter({ inventory, logger }));
app.use(notFoundHandler());
app.use(errorHandler());
@@ -16,7 +16,8 @@
import { Server } from 'http';
import { Logger } from 'winston';
import { createApplication } from './application';
import { AggregatorInventory, StaticInventory } from '../inventory';
import { createStandaloneApplication } from './standaloneApplication';
export interface ServerOptions {
port: number;
@@ -24,12 +25,25 @@ export interface ServerOptions {
logger: Logger;
}
export async function startServer(options: ServerOptions): Promise<Server> {
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'inventory-backend' });
const inventory = new AggregatorInventory();
inventory.enlist(
new StaticInventory([
{ id: 'component1' },
{ id: 'component2' },
{ id: 'component3' },
{ id: 'component4' },
]),
);
logger.debug('Creating application...');
const app = await createApplication({
const app = await createStandaloneApplication({
enableCors: options.enableCors,
inventory,
logger,
});
@@ -14,17 +14,16 @@
* limitations under the License.
*/
import React, { FC } from 'react';
import {
Link,
TableCell,
TableRow,
} from '@material-ui/core';
import { Link, TableCell, TableRow } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { TrendLine } from '@backstage/core';
import { Website } from '../../api';
import {
Website,
} from '../../api';
import { formatTime, CATEGORIES, CATEGORY_LABELS, SparklinesDataByCategory } from '../../utils';
formatTime,
CATEGORIES,
CATEGORY_LABELS,
SparklinesDataByCategory,
} from '../../utils';
import AuditStatusIcon from '../AuditStatusIcon';
const useStyles = makeStyles(theme => ({
@@ -76,9 +75,7 @@ export const AuditRow: FC<{
{website.lastAudit.status.toLowerCase()}
</span>
</TableCell>
<TableCell>
{formatTime(website.lastAudit.timeCreated)}
</TableCell>
<TableCell>{formatTime(website.lastAudit.timeCreated)}</TableCell>
</TableRow>
);
};
@@ -23,11 +23,14 @@ import {
TableRow,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { Website } from '../../api';
import {
Website,
} from '../../api';
import { CATEGORIES, CATEGORY_LABELS, SparklinesDataByCategory, buildSparklinesDataForItem } from '../../utils';
import Audit from '../Audit'
CATEGORIES,
CATEGORY_LABELS,
SparklinesDataByCategory,
buildSparklinesDataForItem,
} from '../../utils';
import Audit from '../Audit';
const useStyles = makeStyles(theme => ({
table: {
@@ -51,7 +54,7 @@ export const AuditListTable: FC<{ items: Website[] }> = ({ items }) => {
() =>
items.reduce(
(res, item) => ({
...res,
...res,
[item.url]: buildSparklinesDataForItem(item),
}),
{},
@@ -84,7 +84,14 @@ const CreateAudit: FC<{}> = () => {
} finally {
setSubmitting(false);
}
}, [url, emulatedFormFactor, lighthouseApi, setSubmitting, errorApi, history]);
}, [
url,
emulatedFormFactor,
lighthouseApi,
setSubmitting,
errorApi,
history,
]);
return (
<Page theme={pageTheme.tool}>
+5 -3
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { useLocation } from 'react-router-dom';
import {Website, Audit, LighthouseCategoryId, AuditCompleted} from './api'
import { Website, Audit, LighthouseCategoryId, AuditCompleted } from './api';
export function useQuery(): URLSearchParams {
return new URLSearchParams(useLocation().search);
}
@@ -45,7 +45,9 @@ export const CATEGORY_LABELS: Record<LighthouseCategoryId, string> = {
};
export type SparklinesDataByCategory = Record<LighthouseCategoryId, number[]>;
export function buildSparklinesDataForItem(item: Website): SparklinesDataByCategory {
export function buildSparklinesDataForItem(
item: Website,
): SparklinesDataByCategory {
return item.audits
.filter(
(audit: Audit): audit is AuditCompleted => audit.status === 'COMPLETED',
@@ -63,4 +65,4 @@ export function buildSparklinesDataForItem(item: Website): SparklinesDataByCateg
return scores;
}, {} as SparklinesDataByCategory);
}
}
@@ -20,7 +20,7 @@ export default class MyDocument extends Document {
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App: any) => props =>
enhanceApp: (App: any) => (props) =>
sheet.collectStyles(<App {...props} />),
});
@@ -15,7 +15,16 @@
*/
import React from 'react';
import { Content, InfoCard, Header, Page, pageTheme } from '@backstage/core';
import {
AlphaLabel,
Content,
ContentHeader,
InfoCard,
Header,
Page,
pageTheme,
} from '@backstage/core';
import { Typography } from '@material-ui/core';
// TODO(blam): Connect to backend
const STATIC_DATA = [
@@ -30,16 +39,25 @@ const STATIC_DATA = [
const ScaffolderPage: React.FC<{}> = () => {
return (
<Page theme={pageTheme.home}>
<Header title="Create a new component" subtitle="All your stuff" />
<Header
title={
<>
Create a new component <AlphaLabel isShorthand />{' '}
</>
}
subtitle="Create new software components using standard templates"
/>
<Content>
<ContentHeader title="Available templates" />
<div style={{ display: 'flex' }}>
{STATIC_DATA.map(item => {
{STATIC_DATA.map((item, ix) => {
return (
<InfoCard
title={item.name}
deepLink={{ title: 'Create', link: '#' }}
key={ix}
>
<p>{item.description}</p>
<Typography paragraph>{item.description}</Typography>
</InfoCard>
);
})}
+1 -1
View File
@@ -20,6 +20,6 @@ import ScaffolderPage from './components/ScaffolderPage';
export const plugin = createPlugin({
id: 'scaffolder',
register({ router }) {
router.registerRoute('/scaffolder', ScaffolderPage);
router.registerRoute('/create', ScaffolderPage);
},
});
+159 -8
View File
@@ -926,6 +926,14 @@
"@babel/helper-plugin-utils" "^7.8.3"
"@babel/plugin-transform-typescript" "^7.9.0"
"@babel/runtime-corejs2@^7.4.4":
version "7.9.2"
resolved "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.9.2.tgz#f11d074ff99b9b4319b5ecf0501f12202bf2bf4d"
integrity sha512-ayjSOxuK2GaSDJFCtLgHnYjuMyIpViNujWrZo8GUpN60/n7juzJKK5yOo6RFVb0zdU9ACJFK+MsZrUnj3OmXMw==
dependencies:
core-js "^2.6.5"
regenerator-runtime "^0.13.4"
"@babel/runtime-corejs3@^7.7.4", "@babel/runtime-corejs3@^7.8.3":
version "7.9.2"
resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.9.2.tgz#26fe4aa77e9f1ecef9b776559bbb8e84d34284b7"
@@ -941,7 +949,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.9.2"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==
@@ -1178,6 +1186,18 @@
debug "^3.1.0"
lodash.once "^4.1.1"
"@date-io/core@1.x", "@date-io/core@^1.3.13":
version "1.3.13"
resolved "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa"
integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==
"@date-io/date-fns@^1.1.0":
version "1.3.13"
resolved "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.3.13.tgz#7798844041640ab393f7e21a7769a65d672f4735"
integrity sha512-yXxGzcRUPcogiMj58wVgFjc9qUYrCnnU9eLcyNbsQCmae4jPuZCDoIBR21j8ZURsM7GRtU62VOw5yNd4dDHunA==
dependencies:
"@date-io/core" "^1.3.13"
"@emotion/cache@^10.0.27":
version "10.0.29"
resolved "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
@@ -2468,6 +2488,18 @@
prop-types "^15.7.2"
react-is "^16.8.0"
"@material-ui/pickers@^3.2.2":
version "3.2.10"
resolved "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.10.tgz#19df024895876eb0ec7cd239bbaea595f703f0ae"
integrity sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w==
dependencies:
"@babel/runtime" "^7.6.0"
"@date-io/core" "1.x"
"@types/styled-jsx" "^2.2.8"
clsx "^1.0.2"
react-transition-group "^4.0.0"
rifm "^0.7.0"
"@material-ui/styles@^4.9.6":
version "4.9.6"
resolved "https://registry.npmjs.org/@material-ui/styles/-/styles-4.9.6.tgz#924a30bf7c9b91af9c8f19c12c8573b8a4ecd085"
@@ -4387,6 +4419,13 @@
resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
"@types/styled-jsx@^2.2.8":
version "2.2.8"
resolved "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234"
integrity sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg==
dependencies:
"@types/react" "*"
"@types/superagent@*":
version "4.1.7"
resolved "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.7.tgz#a7d92d98c490ee0f802a127fdf149b9a114f77a5"
@@ -7232,7 +7271,7 @@ core-js-pure@^3.0.0, core-js-pure@^3.0.1:
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
core-js@^2.4.0, core-js@^2.5.0:
core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.5:
version "2.6.11"
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
@@ -7415,6 +7454,13 @@ css-blank-pseudo@^0.1.4:
dependencies:
postcss "^7.0.5"
css-box-model@^1.1.2:
version "1.2.0"
resolved "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.0.tgz#3a26377b4162b3200d2ede4b064ec5b6a75186d0"
integrity sha512-lri0br+jSNV0kkkiGEp9y9y3Njq2PmpqbeGWRFQJuZteZzY9iC9GZhQ8Y4WpPwM/2YocjHePxy14igJY7YKzkA==
dependencies:
tiny-invariant "^1.0.6"
css-color-names@0.0.4, css-color-names@^0.0.4:
version "0.0.4"
resolved "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
@@ -7851,11 +7897,21 @@ date-fns@^1.27.2:
resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
date-fns@^2.0.0-alpha.27:
version "2.12.0"
resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.12.0.tgz#01754c8a2f3368fc1119cf4625c3dad8c1845ee6"
integrity sha512-qJgn99xxKnFgB1qL4jpxU7Q2t0LOn1p8KMIveef3UZD7kqjT3tpFNNdXJelEHhE+rUgffriXriw/sOSU+cS1Hw==
dateformat@^3.0.0:
version "3.0.3"
resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
debounce@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131"
integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -9188,6 +9244,15 @@ expect@^25.1.0:
jest-message-util "^25.1.0"
jest-regex-util "^25.1.0"
express-promise-router@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/express-promise-router/-/express-promise-router-3.0.3.tgz#5e6d22a5a3f013d71833172fe8d7ab780c3f6b70"
integrity sha1-Xm0ipaPwE9cYMxcv6NereAw/a3A=
dependencies:
is-promise "^2.1.0"
lodash.flattendeep "^4.0.0"
methods "^1.0.0"
express@^4.17.0, express@^4.17.1:
version "4.17.1"
resolved "https://registry.npmjs.org/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@@ -9294,7 +9359,7 @@ extsprintf@^1.2.0:
resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
fast-deep-equal@^2.0.1:
fast-deep-equal@2.0.1, fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
@@ -9466,6 +9531,11 @@ file-uri-to-path@1.0.0:
resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
filefy@0.1.10:
version "0.1.10"
resolved "https://registry.npmjs.org/filefy/-/filefy-0.1.10.tgz#174677c8e2fa5bc39a3af0ed6fb492f16b8fbf42"
integrity sha512-VgoRVOOY1WkTpWH+KBy8zcU1G7uQTVsXqhWEgzryB9A5hg2aqCyZ6aQ/5PSzlqM5+6cnVrX6oYV0XqD3HZSnmQ==
filesize@3.6.1:
version "3.6.1"
resolved "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
@@ -13607,6 +13677,11 @@ lodash.escaperegexp@^4.1.2:
resolved "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=
lodash.flattendeep@^4.0.0:
version "4.4.0"
resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
@@ -13956,6 +14031,22 @@ marked@^0.8.0:
resolved "https://registry.npmjs.org/marked/-/marked-0.8.1.tgz#a233f39572fab15ede53a3c3be8a139bff86d2dd"
integrity sha512-tZfJS8uE0zpo7xpTffwFwYRfW9AzNcdo04Qcjs+C9+oCy8MSRD2reD5iDVtYx8mtLaqsGughw/YLlcwNxAHA1g==
material-table@^1.57.2:
version "1.57.2"
resolved "https://registry.npmjs.org/material-table/-/material-table-1.57.2.tgz#8854cb4d623b294138c83a0c7ff9f2900267c81f"
integrity sha512-hiJdRTrqu8pyYwSxzmcG1TnR4KWG2gtXrFB3XL9h4ij3A68EOJmlss6VH/LXh3NLlUce1TteK6W7fGa7YcnKGg==
dependencies:
"@date-io/date-fns" "^1.1.0"
"@material-ui/pickers" "^3.2.2"
classnames "^2.2.6"
date-fns "^2.0.0-alpha.27"
debounce "^1.2.0"
fast-deep-equal "2.0.1"
filefy "0.1.10"
prop-types "^15.6.2"
react-beautiful-dnd "11.0.3"
react-double-scrollbar "0.0.15"
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@@ -14013,6 +14104,11 @@ mem@^4.0.0:
mimic-fn "^2.0.0"
p-is-promise "^2.0.0"
memoize-one@^5.0.4:
version "5.1.1"
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
memoizerific@^1.11.3:
version "1.11.3"
resolved "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a"
@@ -14111,7 +14207,7 @@ merge@^1.2.1:
resolved "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
methods@^1.1.1, methods@^1.1.2, methods@~1.1.2:
methods@^1.0.0, methods@^1.1.1, methods@^1.1.2, methods@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
@@ -17019,6 +17115,11 @@ qw@~1.0.1:
resolved "https://registry.npmjs.org/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4"
integrity sha1-77/cdA+a0FQwRCassYNBLMi5ltQ=
raf-schd@^4.0.0:
version "4.0.2"
resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
raf@^3.4.1:
version "3.4.1"
resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
@@ -17109,6 +17210,20 @@ react-app-polyfill@^1.0.6:
regenerator-runtime "^0.13.3"
whatwg-fetch "^3.0.0"
react-beautiful-dnd@11.0.3:
version "11.0.3"
resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-11.0.3.tgz#5678bb3e725d8b56cb7cf57f56e952105fc4f2af"
integrity sha512-2FX2SnOlKMmfn90xUHCav7cxRWXwY7FeRa6TzdxWeX7DdP5JTvVQcsWgiOkdbJSj+J+1q1nA9QO4/HQ52D0DAA==
dependencies:
"@babel/runtime-corejs2" "^7.4.4"
css-box-model "^1.1.2"
memoize-one "^5.0.4"
raf-schd "^4.0.0"
react-redux "^7.0.3"
redux "^4.0.1"
tiny-invariant "^1.0.4"
use-memo-one "^1.1.0"
react-clientside-effect@^1.2.2:
version "1.2.2"
resolved "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz#6212fb0e07b204e714581dd51992603d1accc837"
@@ -17201,6 +17316,11 @@ react-dom@16.13.1, react-dom@^16.12.0, react-dom@^16.13.1, react-dom@^16.8.3:
prop-types "^15.6.2"
scheduler "^0.19.1"
react-double-scrollbar@0.0.15:
version "0.0.15"
resolved "https://registry.npmjs.org/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz#e915ab8cb3b959877075f49436debfdb04288fe4"
integrity sha1-6RWrjLO5WYdwdfSUNt6/2wQoj+Q=
react-draggable@^4.0.3:
version "4.2.0"
resolved "https://registry.npmjs.org/react-draggable/-/react-draggable-4.2.0.tgz#40cc5209082ca7d613104bf6daf31372cc0e1114"
@@ -17268,7 +17388,7 @@ react-inspector@^4.0.0:
is-dom "^1.0.9"
prop-types "^15.6.1"
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -17313,6 +17433,17 @@ react-popper@^1.3.6:
typed-styles "^0.0.7"
warning "^4.0.2"
react-redux@^7.0.3:
version "7.2.0"
resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"
integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==
dependencies:
"@babel/runtime" "^7.5.5"
hoist-non-react-statics "^3.3.0"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^16.9.0"
react-router-dom@5.1.2, react-router-dom@^5.1.2:
version "5.1.2"
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18"
@@ -17445,7 +17576,7 @@ react-textarea-autosize@^7.1.0:
"@babel/runtime" "^7.1.2"
prop-types "^15.6.0"
react-transition-group@^4.3.0:
react-transition-group@^4.0.0, react-transition-group@^4.3.0:
version "4.3.0"
resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.3.0.tgz#fea832e386cf8796c58b61874a3319704f5ce683"
integrity sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw==
@@ -17749,6 +17880,14 @@ redeyed@~2.1.0:
dependencies:
esprima "~4.0.0"
redux@^4.0.1:
version "4.0.5"
resolved "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
referrer-policy@1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz#b99cfb8b57090dc454895ef897a4cc35ef67a98e"
@@ -18178,6 +18317,13 @@ rgba-regex@^1.0.0:
resolved "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
rifm@^0.7.0:
version "0.7.0"
resolved "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be"
integrity sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==
dependencies:
"@babel/runtime" "^7.3.1"
right-pad@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0"
@@ -19671,7 +19817,7 @@ svgo@^1.0.0, svgo@^1.2.2:
unquote "~1.1.1"
util.promisify "~1.0.0"
symbol-observable@^1.1.0:
symbol-observable@^1.1.0, symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
@@ -19982,7 +20128,7 @@ tiny-emitter@^2.0.0:
resolved "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
tiny-invariant@^1.0.2:
tiny-invariant@^1.0.2, tiny-invariant@^1.0.4, tiny-invariant@^1.0.6:
version "1.1.0"
resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
@@ -20631,6 +20777,11 @@ use-callback-ref@^1.2.1:
resolved "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.1.tgz#898759ccb9e14be6c7a860abafa3ffbd826c89bb"
integrity sha512-C3nvxh0ZpaOxs9RCnWwAJ+7bJPwQI8LHF71LzbQ3BvzH5XkdtlkMadqElGevg5bYBDFip4sAnD4m06zAKebg1w==
use-memo-one@^1.1.0:
version "1.1.1"
resolved "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c"
integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ==
use-sidecar@^1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.0.2.tgz#e72f582a75842f7de4ef8becd6235a4720ad8af6"