From 47d5d8da4fabe4db1cf5f10eb6b1973c81bc8974 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 3 May 2020 14:26:59 +0200 Subject: [PATCH 01/13] packages/cli: add lib ES2019 to common tsconfig --- packages/cli/config/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/config/tsconfig.json b/packages/cli/config/tsconfig.json index 32f83edc9d..a05e809caa 100644 --- a/packages/cli/config/tsconfig.json +++ b/packages/cli/config/tsconfig.json @@ -10,6 +10,7 @@ "module": "ESNext", "resolveJsonModule": true, "esModuleInterop": true, + "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2019"], "types": ["node", "jest"] } } From 3b2aeae05a508f6253808ec118dafd2421362449 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 4 May 2020 13:34:08 +0900 Subject: [PATCH 02/13] Fix blinking animation bug for Status Running component (#706) * Refer to keyframe as a material-ul variable https://material-ui.com/guides/migration-v3/\#styles * Update background color of Status The background color should match with the theme's background color. --- packages/core/src/components/Status/Status.tsx | 2 +- packages/theme/src/themes.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/components/Status/Status.tsx b/packages/core/src/components/Status/Status.tsx index 0d02d86fb1..007cc4e9fd 100644 --- a/packages/core/src/components/Status/Status.tsx +++ b/packages/core/src/components/Status/Status.tsx @@ -47,7 +47,7 @@ const useStyles = makeStyles(theme => ({ 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': { diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 3dad461423..9e9e2945fa 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -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', From 031a4356f15cea2428055ac7f398b2c0d87c172a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Mon, 4 May 2020 11:09:30 +0200 Subject: [PATCH 03/13] [Storybook] Improve Status stories (#699) * [Storybook] Imrove Status stories * Remove duplicate classes --- .../src/components/Status/Status.stories.tsx | 96 ++++++++++++------- .../core/src/components/Status/Status.tsx | 6 +- .../TrendLine/TrendLine.stories.tsx | 10 +- 3 files changed, 70 insertions(+), 42 deletions(-) diff --git a/packages/core/src/components/Status/Status.stories.tsx b/packages/core/src/components/Status/Status.stories.tsx index 91ef53d7fe..0cdc38a10d 100644 --- a/packages/core/src/components/Status/Status.stories.tsx +++ b/packages/core/src/components/Status/Status.stories.tsx @@ -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 = () => ( - <> - Status OK - -); -export const statusWarning = () => ( - <> - Status Warning - -); -export const statusError = () => ( - <> - Status Error - -); -export const statusFailed = () => ( - <> - Status Failed - -); -export const statusPending = () => ( - <> - Status Pending - -); -export const statusRunning = () => ( - <> - Status Running - -); -export const statusNA = () => ( - <> - Status NA - +const data = [ + { + status: , + label: 'OK', + usage: 'Deployment successful', + }, + { + status: , + label: 'Warning', + usage: 'CPU utilization at 90%', + }, + { + status: , + label: 'Error', + usage: 'Service could not be created', + }, + { + status: , + label: 'Failed', + usage: 'Build for PR #34 failed', + }, + { + status: , + label: 'Pending', + usage: 'Job is waiting', + }, + { + status: , + label: 'Running', + usage: 'Job is running', + }, + { + status: , + 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 = () => ( +
+ + + +
); + +export const statusOK = () => ; +export const statusWarning = () => ; +export const statusError = () => ; +export const statusFailed = () => ; +export const statusPending = () => ; +export const statusRunning = () => ; +export const statusNA = () => ; diff --git a/packages/core/src/components/Status/Status.tsx b/packages/core/src/components/Status/Status.tsx index 007cc4e9fd..99109bfa21 100644 --- a/packages/core/src/components/Status/Status.tsx +++ b/packages/core/src/components/Status/Status.tsx @@ -34,6 +34,7 @@ const useStyles = makeStyles(theme => ({ backgroundColor: theme.palette.status.warning, }, error: { + // Use same for Failed status. width: '0', height: '0', borderLeft: '7px solid transparent', @@ -43,9 +44,6 @@ const useStyles = makeStyles(theme => ({ pending: { backgroundColor: theme.palette.status.pending, }, - failed: { - backgroundColor: 'rgba(245, 155, 35, 0.5)', - }, running: { animation: '$blink 0.8s step-start 0s infinite', backgroundColor: theme.palette.status.running, @@ -122,7 +120,7 @@ export const StatusFailed: FC<{}> = props => { const classes = useStyles(props); return ( diff --git a/packages/core/src/components/TrendLine/TrendLine.stories.tsx b/packages/core/src/components/TrendLine/TrendLine.stories.tsx index fab5388c81..5263385e1c 100644 --- a/packages/core/src/components/TrendLine/TrendLine.stories.tsx +++ b/packages/core/src/components/TrendLine/TrendLine.stories.tsx @@ -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 = () => ( - - - +
+ + + +
); export const TrendingMix = () => ( From 190f11c8f721bb78b2b2aa632f6453df753a8d8e Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Mon, 4 May 2020 11:10:35 +0200 Subject: [PATCH 04/13] Table component with filter and search (#600) * Initial table component with storybook example * Moved dependencies to package/core * Removed dependency to @backstage/core from base package.json * Clean up some examples and remove type from columns, replacing with property align instead * Removed more unused stuff and added toggle for filter input * Fixed some lint issues * Added example with material-table * Convert column array passed into MaterialTable Backstage component into MT column array. Allow multiple values in one cell * Removed subheader from MT * Nicer double value cell * Added custom sorting to example * Cleaned up stories a bit * Setting field instead of customSort * Improve styling. Still needs better theme integration * Using more colors from theme * Fixed typing issue * Removed react-table implementation and cleaned up material-table implementation * Fixed some type issues * Fixed lint issues --- packages/app/src/components/Root/Root.tsx | 2 +- packages/core/package.json | 1 + packages/core/src/api/app/AppBuilder.tsx | 2 +- .../core/src/components/Status/Status.tsx | 16 +- .../src/components/Table/SubvalueCell.tsx | 47 +++++ .../src/components/Table/Table.stories.tsx | 143 +++++++++++++ .../core/src/components/Table/Table.test.tsx | 50 +++++ packages/core/src/components/Table/Table.tsx | 192 ++++++++++++++++++ packages/core/src/components/Table/index.ts | 18 ++ .../core/src/layout/ErrorPage/ErrorPage.tsx | 2 +- .../core/src/layout/InfoCard/InfoCard.tsx | 8 +- packages/storybook/.storybook/main.js | 2 +- .../explore/src/components/ExploreCard.tsx | 2 +- .../src/components/ExplorePluginPage.tsx | 2 +- .../GraphiQLBrowser/GraphiQLBrowser.tsx | 2 +- .../src/inventory/AggregatorInventory.ts | 4 +- .../src/inventory/StaticInventory.ts | 2 +- plugins/inventory-backend/src/run.ts | 2 +- .../src/components/Audit/AuditRow.tsx | 19 +- .../components/AuditList/AuditListTable.tsx | 13 +- .../src/components/CreateAudit/index.tsx | 9 +- plugins/lighthouse/src/utils.ts | 8 +- .../src/pages/_document.tsx | 2 +- .../src/components/ScaffolderPage/index.tsx | 2 +- yarn.lock | 151 +++++++++++++- 25 files changed, 649 insertions(+), 52 deletions(-) create mode 100644 packages/core/src/components/Table/SubvalueCell.tsx create mode 100644 packages/core/src/components/Table/Table.stories.tsx create mode 100644 packages/core/src/components/Table/Table.test.tsx create mode 100644 packages/core/src/components/Table/Table.tsx create mode 100644 packages/core/src/components/Table/index.ts diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index a8c4cd3382..7ef3a5a22f 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -84,7 +84,7 @@ const Root: FC<{}> = ({ children }) => ( - + diff --git a/packages/core/package.json b/packages/core/package.json index 1d4372f15c..107cba0196 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/api/app/AppBuilder.tsx b/packages/core/src/api/app/AppBuilder.tsx index 0af21f5c8d..94d6df1b3d 100644 --- a/packages/core/src/api/app/AppBuilder.tsx +++ b/packages/core/src/api/app/AppBuilder.tsx @@ -118,7 +118,7 @@ export default class AppBuilder { {routes} ( + render={(props) => ( )} /> diff --git a/packages/core/src/components/Status/Status.tsx b/packages/core/src/components/Status/Status.tsx index 99109bfa21..a232aceb62 100644 --- a/packages/core/src/components/Status/Status.tsx +++ b/packages/core/src/components/Status/Status.tsx @@ -19,7 +19,7 @@ import { BackstageTheme } from '@backstage/theme'; import classNames from 'classnames'; import React, { FC } from 'react'; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme) => ({ status: { width: 12, height: 12, @@ -55,7 +55,7 @@ const useStyles = makeStyles(theme => ({ }, })); -export const StatusOK: FC<{}> = props => { +export const StatusOK: FC<{}> = (props) => { const classes = useStyles(props); return ( = props => { ); }; -export const StatusWarning: FC<{}> = props => { +export const StatusWarning: FC<{}> = (props) => { const classes = useStyles(props); return ( = props => { ); }; -export const StatusError: FC<{}> = props => { +export const StatusError: FC<{}> = (props) => { const classes = useStyles(props); return ( = props => { ); }; -export const StatusNA: FC<{}> = props => ( +export const StatusNA: FC<{}> = (props) => ( N/A ); -export const StatusPending: FC<{}> = props => { +export const StatusPending: FC<{}> = (props) => { const classes = useStyles(props); return ( = props => { ); }; -export const StatusRunning: FC<{}> = props => { +export const StatusRunning: FC<{}> = (props) => { const classes = useStyles(props); return ( = props => { ); }; -export const StatusFailed: FC<{}> = props => { +export const StatusFailed: FC<{}> = (props) => { const classes = useStyles(props); return ( (theme => ({ + value: { + marginBottom: '6px', + }, + subvalue: { + color: theme.palette.textSubtle, + fontWeight: 'normal', + }, +})); + +type SubvalueCellProps = { + value: React.ReactNode; + subvalue: React.ReactNode; +}; + +const SubvalueCell: FC = ({ value, subvalue }) => { + const classes = useSubvalueCellStyles(); + + return ( + <> +
{value}
+
{subvalue}
+ + ); +}; + +export default SubvalueCell; diff --git a/packages/core/src/components/Table/Table.stories.tsx b/packages/core/src/components/Table/Table.stories.tsx new file mode 100644 index 0000000000..b1a886ddc8 --- /dev/null +++ b/packages/core/src/components/Table/Table.stories.tsx @@ -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 ( + + ); +}; + +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 ( +
+ ); +}; + +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 => ( + + ), + }, + { + title: 'Column 2', + field: 'col2', + }, + { + title: 'Numeric value', + field: 'number', + type: 'numeric', + }, + { + title: 'A Date', + field: 'date', + type: 'date', + }, + ]; + + return ( +
+ ); +}; diff --git a/packages/core/src/components/Table/Table.test.tsx b/packages/core/src/components/Table/Table.test.tsx new file mode 100644 index 0000000000..f6801359eb --- /dev/null +++ b/packages/core/src/components/Table/Table.test.tsx @@ -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('
', () => { + it('renders without exploding', () => { + const rendered = render(wrapInTestApp(
)); + expect(rendered.getByText('second value, second row')).toBeInTheDocument(); + }); +}); diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx new file mode 100644 index 0000000000..a9a55ae763 --- /dev/null +++ b/packages/core/src/components/Table/Table.tsx @@ -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) => ( + + )), + Check: forwardRef((props, ref: React.Ref) => ( + + )), + Clear: forwardRef((props, ref: React.Ref) => ( + + )), + Delete: forwardRef((props, ref: React.Ref) => ( + + )), + DetailPanel: forwardRef((props, ref: React.Ref) => ( + + )), + Edit: forwardRef((props, ref: React.Ref) => ( + + )), + Export: forwardRef((props, ref: React.Ref) => ( + + )), + Filter: forwardRef((props, ref: React.Ref) => ( + + )), + FirstPage: forwardRef((props, ref: React.Ref) => ( + + )), + LastPage: forwardRef((props, ref: React.Ref) => ( + + )), + NextPage: forwardRef((props, ref: React.Ref) => ( + + )), + PreviousPage: forwardRef((props, ref: React.Ref) => ( + + )), + ResetSearch: forwardRef((props, ref: React.Ref) => ( + + )), + Search: forwardRef((props, ref: React.Ref) => ( + + )), + SortArrow: forwardRef((props, ref: React.Ref) => ( + + )), + ThirdStateCheck: forwardRef((props, ref: React.Ref) => ( + + )), + ViewColumn: forwardRef((props, ref: React.Ref) => ( + + )), +}; + +const useCellStyles = makeStyles(theme => ({ + root: { + color: theme.palette.grey[500], + padding: theme.spacing(0, 2, 0, 2.5), + height: '56px', + }, +})); + +const useHeaderStyles = makeStyles(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(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 = ({ columns, options, ...props }) => { + const cellClasses = useCellStyles(); + const headerClasses = useHeaderStyles(); + const toolbarClasses = useToolbarStyles(); + + const MTColumns = convertColumns(columns); + + const defaultOptions: Options = { + headerStyle: { + textTransform: 'uppercase', + }, + }; + + return ( + ( + + ), + Header: headerProps => ( + + ), + Toolbar: toolbarProps => ( + + ), + }} + options={{ ...defaultOptions, ...options }} + columns={MTColumns} + icons={tableIcons} + {...props} + /> + ); +}; + +export default Table; diff --git a/packages/core/src/components/Table/index.ts b/packages/core/src/components/Table/index.ts new file mode 100644 index 0000000000..e85aac3a01 --- /dev/null +++ b/packages/core/src/components/Table/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default, TableColumn } from './Table'; +export { default as SubvalueCell } from './SubvalueCell'; diff --git a/packages/core/src/layout/ErrorPage/ErrorPage.tsx b/packages/core/src/layout/ErrorPage/ErrorPage.tsx index ea593dbb49..49b79ef925 100644 --- a/packages/core/src/layout/ErrorPage/ErrorPage.tsx +++ b/packages/core/src/layout/ErrorPage/ErrorPage.tsx @@ -28,7 +28,7 @@ interface IErrorPageProps { }; } -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme) => ({ container: { padding: theme.spacing(8), }, diff --git a/packages/core/src/layout/InfoCard/InfoCard.tsx b/packages/core/src/layout/InfoCard/InfoCard.tsx index fe2bbfa1ce..7077167828 100644 --- a/packages/core/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core/src/layout/InfoCard/InfoCard.tsx @@ -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 = ({ 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']], diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js index 5a3eafa532..c187dc5f56 100644 --- a/packages/storybook/.storybook/main.js +++ b/packages/storybook/.storybook/main.js @@ -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 = { diff --git a/plugins/explore/src/components/ExploreCard.tsx b/plugins/explore/src/components/ExploreCard.tsx index 9c61e9bea8..af6eca61b7 100644 --- a/plugins/explore/src/components/ExploreCard.tsx +++ b/plugins/explore/src/components/ExploreCard.tsx @@ -28,7 +28,7 @@ import { } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme) => ({ card: { display: 'flex', flexDirection: 'column', diff --git a/plugins/explore/src/components/ExplorePluginPage.tsx b/plugins/explore/src/components/ExplorePluginPage.tsx index 7b5fc81a30..48a0f90c32 100644 --- a/plugins/explore/src/components/ExplorePluginPage.tsx +++ b/plugins/explore/src/components/ExplorePluginPage.tsx @@ -27,7 +27,7 @@ import { import ExploreCard, { CardData } from './ExploreCard'; import { BackstageTheme } from '@backstage/theme'; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme) => ({ container: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, 296px)', diff --git a/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.tsx b/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.tsx index c5a003a496..5885183b01 100644 --- a/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.tsx +++ b/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.tsx @@ -24,7 +24,7 @@ import { BackstageTheme } from '@backstage/theme'; const GraphiQL = React.lazy(() => import('graphiql')); -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme) => ({ root: { height: '100%', display: 'flex', diff --git a/plugins/inventory-backend/src/inventory/AggregatorInventory.ts b/plugins/inventory-backend/src/inventory/AggregatorInventory.ts index 3f1ca0f5de..babea8c7b9 100644 --- a/plugins/inventory-backend/src/inventory/AggregatorInventory.ts +++ b/plugins/inventory-backend/src/inventory/AggregatorInventory.ts @@ -20,13 +20,13 @@ export class AggregatorInventory implements Inventory { inventories: Inventory[] = []; list(): Promise> { - return Promise.all(this.inventories.map(i => i.list())).then(lists => + return Promise.all(this.inventories.map((i) => i.list())).then((lists) => lists.flat(), ); } item(id: string): Promise { - return this.list().then(items => items.find(i => i.id === id)); + return this.list().then((items) => items.find((i) => i.id === id)); } enlist(inventory: Inventory) { diff --git a/plugins/inventory-backend/src/inventory/StaticInventory.ts b/plugins/inventory-backend/src/inventory/StaticInventory.ts index cd4fa2b7d5..fdfc2afde7 100644 --- a/plugins/inventory-backend/src/inventory/StaticInventory.ts +++ b/plugins/inventory-backend/src/inventory/StaticInventory.ts @@ -24,6 +24,6 @@ export class StaticInventory implements Inventory { } item(id: string): Promise { - return this.list().then(items => items.find(i => i.id === id)); + return this.list().then((items) => items.find((i) => i.id === id)); } } diff --git a/plugins/inventory-backend/src/run.ts b/plugins/inventory-backend/src/run.ts index 979827c40f..9694a60c33 100644 --- a/plugins/inventory-backend/src/run.ts +++ b/plugins/inventory-backend/src/run.ts @@ -23,7 +23,7 @@ startServer({ ? Boolean(process.env.PLUGIN_CORS) : false, logger: getRootLogger(), -}).catch(err => { +}).catch((err) => { getRootLogger().error(err); process.exit(1); }); diff --git a/plugins/lighthouse/src/components/Audit/AuditRow.tsx b/plugins/lighthouse/src/components/Audit/AuditRow.tsx index c42e6a8538..71668138f4 100644 --- a/plugins/lighthouse/src/components/Audit/AuditRow.tsx +++ b/plugins/lighthouse/src/components/Audit/AuditRow.tsx @@ -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()} - - {formatTime(website.lastAudit.timeCreated)} - + {formatTime(website.lastAudit.timeCreated)} ); }; diff --git a/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx b/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx index 778eb71fb5..082433a550 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx @@ -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), }), {}, diff --git a/plugins/lighthouse/src/components/CreateAudit/index.tsx b/plugins/lighthouse/src/components/CreateAudit/index.tsx index 8ff0ce5ea8..19db33791b 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.tsx @@ -84,7 +84,14 @@ const CreateAudit: FC<{}> = () => { } finally { setSubmitting(false); } - }, [url, emulatedFormFactor, lighthouseApi, setSubmitting, errorApi, history]); + }, [ + url, + emulatedFormFactor, + lighthouseApi, + setSubmitting, + errorApi, + history, + ]); return ( diff --git a/plugins/lighthouse/src/utils.ts b/plugins/lighthouse/src/utils.ts index 1546521083..cc09b52f4f 100644 --- a/plugins/lighthouse/src/utils.ts +++ b/plugins/lighthouse/src/utils.ts @@ -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 = { }; export type SparklinesDataByCategory = Record; -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); -} \ No newline at end of file +} diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/_document.tsx b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/_document.tsx index 210de6412c..9b01a6a74c 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/_document.tsx +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/src/pages/_document.tsx @@ -20,7 +20,7 @@ export default class MyDocument extends Document { try { ctx.renderPage = () => originalRenderPage({ - enhanceApp: (App: any) => props => + enhanceApp: (App: any) => (props) => sheet.collectStyles(), }); diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx index e6adaca099..5cfeb122ce 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx @@ -33,7 +33,7 @@ const ScaffolderPage: React.FC<{}> = () => {
- {STATIC_DATA.map(item => { + {STATIC_DATA.map((item) => { return ( Date: Fri, 1 May 2020 22:31:54 +0200 Subject: [PATCH 05/13] [inventory] move out inventory instantiation + renaming --- packages/backend/src/index.ts | 23 +++++++++++++------ packages/backend/src/test/index.ts | 23 ------------------- plugins/inventory-backend/src/index.ts | 1 + .../src/inventory/AggregatorInventory.ts | 2 +- .../src/inventory/StaticInventory.ts | 2 +- plugins/inventory-backend/src/run.ts | 20 ++++++++-------- .../inventory-backend/src/service/router.ts | 14 +++-------- ...pplication.ts => standaloneApplication.ts} | 9 +++++--- .../{server.ts => standaloneServer.ts} | 20 +++++++++++++--- 9 files changed, 55 insertions(+), 59 deletions(-) delete mode 100644 packages/backend/src/test/index.ts rename plugins/inventory-backend/src/service/{application.ts => standaloneApplication.ts} (83%) rename plugins/inventory-backend/src/service/{server.ts => standaloneServer.ts} (70%) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index e9fc15f1c1..f8456a0e34 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -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()); diff --git a/packages/backend/src/test/index.ts b/packages/backend/src/test/index.ts deleted file mode 100644 index ce3f9f8c39..0000000000 --- a/packages/backend/src/test/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { Router } from 'express'; - -export const testRouter = Router(); - -testRouter.get('/', async (_, res) => { - res.status(200).send('hello'); -}); diff --git a/plugins/inventory-backend/src/index.ts b/plugins/inventory-backend/src/index.ts index 7612c392a2..52b4ed9a53 100644 --- a/plugins/inventory-backend/src/index.ts +++ b/plugins/inventory-backend/src/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ +export * from './inventory'; export * from './service/router'; diff --git a/plugins/inventory-backend/src/inventory/AggregatorInventory.ts b/plugins/inventory-backend/src/inventory/AggregatorInventory.ts index babea8c7b9..ef01be962f 100644 --- a/plugins/inventory-backend/src/inventory/AggregatorInventory.ts +++ b/plugins/inventory-backend/src/inventory/AggregatorInventory.ts @@ -19,7 +19,7 @@ import { Component, Inventory } from './types'; export class AggregatorInventory implements Inventory { inventories: Inventory[] = []; - list(): Promise> { + list(): Promise { return Promise.all(this.inventories.map((i) => i.list())).then((lists) => lists.flat(), ); diff --git a/plugins/inventory-backend/src/inventory/StaticInventory.ts b/plugins/inventory-backend/src/inventory/StaticInventory.ts index fdfc2afde7..95a4466392 100644 --- a/plugins/inventory-backend/src/inventory/StaticInventory.ts +++ b/plugins/inventory-backend/src/inventory/StaticInventory.ts @@ -19,7 +19,7 @@ import { Component, Inventory } from './types'; export class StaticInventory implements Inventory { constructor(private components: Component[]) {} - list(): Promise> { + list(): Promise { return Promise.resolve([...this.components]); } diff --git a/plugins/inventory-backend/src/run.ts b/plugins/inventory-backend/src/run.ts index 9694a60c33..0d5c3a5c12 100644 --- a/plugins/inventory-backend/src/run.ts +++ b/plugins/inventory-backend/src/run.ts @@ -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); }); diff --git a/plugins/inventory-backend/src/service/router.ts b/plugins/inventory-backend/src/service/router.ts index afa4a543b3..efaff00cb8 100644 --- a/plugins/inventory-backend/src/service/router.ts +++ b/plugins/inventory-backend/src/service/router.ts @@ -16,27 +16,19 @@ import express from 'express'; import { Logger } from 'winston'; -import { AggregatorInventory, StaticInventory } from '../inventory'; +import { Inventory } from '../inventory'; export interface RouterOptions { + inventory: Inventory; logger: Logger; } export async function createRouter( options: RouterOptions, ): Promise { + 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(); router .get('/', async (req, res) => { diff --git a/plugins/inventory-backend/src/service/application.ts b/plugins/inventory-backend/src/service/standaloneApplication.ts similarity index 83% rename from plugins/inventory-backend/src/service/application.ts rename to plugins/inventory-backend/src/service/standaloneApplication.ts index e5cf151050..8f0518bb8d 100644 --- a/plugins/inventory-backend/src/service/application.ts +++ b/plugins/inventory-backend/src/service/standaloneApplication.ts @@ -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 { + 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()); diff --git a/plugins/inventory-backend/src/service/server.ts b/plugins/inventory-backend/src/service/standaloneServer.ts similarity index 70% rename from plugins/inventory-backend/src/service/server.ts rename to plugins/inventory-backend/src/service/standaloneServer.ts index 9715a72fef..be339c5ecc 100644 --- a/plugins/inventory-backend/src/service/server.ts +++ b/plugins/inventory-backend/src/service/standaloneServer.ts @@ -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 { +export async function startStandaloneServer( + options: ServerOptions, +): Promise { 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, }); From 8c659179145529d1db69e1fcecfd6d3f958924a9 Mon Sep 17 00:00:00 2001 From: Niklas Ek Date: Mon, 4 May 2020 12:15:22 +0200 Subject: [PATCH 06/13] Update Discord link in design docs, fix #672 (#710) --- docs/design.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/design.md b/docs/design.md index 7fdec77adf..bcbf0bbdc5 100644 --- a/docs/design.md +++ b/docs/design.md @@ -9,11 +9,11 @@ Backstage Open Source is a newly launched endeavor, and we’re 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 we’re headed. We’ll 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 we’ve made and why we’ve 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 we’re headed. We’ll 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 we’ve made and why we’ve 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. We’ll also be creating issues that are focused on building up our core Backstage user experience. We’ll 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. We’ll also be creating issues that are focused on building up our core Backstage user experience. We’ll 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. We’ll 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. We’ll 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 you’d like to help build up our design system, you can also add components we’ve 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 that’s implementing Backstage? A designer who’s fascinated by the developer productivity problem space? A designer who’s curious about open source design? We’d love for you to contribute. Behind the scenes, we’re 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, we’ll make sure you’re equipped to chip in on this project. We’re excited to work with you! In the meantime, we’d love to hear from you on [Discord](https://discord.gg/PefUsZ). +Are you a designer at an organisation that’s implementing Backstage? A designer who’s fascinated by the developer productivity problem space? A designer who’s curious about open source design? We’d love for you to contribute. Behind the scenes, we’re 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, we’ll make sure you’re equipped to chip in on this project. We’re excited to work with you! In the meantime, we’d love to hear from you on [Discord](https://discord.gg/EBHEGzX). [Back to Docs](../README.md) From 48b4a86a97fe36c5bfd6e4f7c1ce1b849728a977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 4 May 2020 11:19:23 +0200 Subject: [PATCH 07/13] [backend] use common error types --- packages/backend-common/src/errors.ts | 107 ++++++++++++++++++ packages/backend-common/src/index.ts | 1 + .../src/middleware/errorHandler.test.ts | 30 ++++- .../src/middleware/errorHandler.ts | 29 ++++- plugins/inventory-backend/package.json | 1 + .../src/inventory/AggregatorInventory.ts | 22 +++- .../src/inventory/StaticInventory.ts | 13 ++- .../inventory-backend/src/inventory/types.ts | 4 +- .../inventory-backend/src/service/router.ts | 9 +- yarn.lock | 16 ++- 10 files changed, 208 insertions(+), 24 deletions(-) create mode 100644 packages/backend-common/src/errors.ts diff --git a/packages/backend-common/src/errors.ts b/packages/backend-common/src/errors.ts new file mode 100644 index 0000000000..c762df9a28 --- /dev/null +++ b/packages/backend-common/src/errors.ts @@ -0,0 +1,107 @@ +/* + * 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. + */ + +/** + * The request is malformed and cannot be processed. + */ +export class BadRequestError extends Error { + readonly cause?: Error; + + constructor(message?: string, cause?: Error) { + super(message); + Object.setPrototypeOf(this, BadRequestError.prototype); + Error.captureStackTrace(this, BadRequestError); + this.name = this.constructor.name; + this.cause = cause; + } +} + +/** + * The request requires authentication, which was not properly supplied. + */ +export class UnauthenticatedError extends Error { + readonly cause?: Error; + + constructor(message?: string, cause?: Error) { + super(message); + Object.setPrototypeOf(this, UnauthenticatedError.prototype); + Error.captureStackTrace(this, UnauthenticatedError); + this.name = this.constructor.name; + this.cause = cause; + } +} + +/** + * The authenticated caller is not permitted to perform this request. + */ +export class ForbiddenError extends Error { + readonly cause?: Error; + + constructor(message?: string, cause?: Error) { + super(message); + Object.setPrototypeOf(this, ForbiddenError.prototype); + Error.captureStackTrace(this, ForbiddenError); + this.name = this.constructor.name; + this.cause = cause; + } +} + +/** + * 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 Error { + readonly cause?: Error; + + constructor(message?: string, cause?: Error) { + super(message); + Object.setPrototypeOf(this, NotFoundError.prototype); + Error.captureStackTrace(this, NotFoundError); + this.name = this.constructor.name; + this.cause = cause; + } +} + +/** + * The request could not complete due to a conflict in the current state of the + * resource. + */ +export class ConflictError extends Error { + readonly cause?: Error; + + constructor(message?: string, cause?: Error) { + super(message); + Object.setPrototypeOf(this, ConflictError.prototype); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ConflictError); + } + this.name = this.constructor.name; + this.cause = cause; + } +} diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index 54b9f5c40f..b2c38ab506 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ +export * from './errors'; export * from './logging'; export * from './middleware'; diff --git a/packages/backend-common/src/middleware/errorHandler.test.ts b/packages/backend-common/src/middleware/errorHandler.test.ts index f90794b07b..8f4a8cfea4 100644 --- a/packages/backend-common/src/middleware/errorHandler.test.ts +++ b/packages/backend-common/src/middleware/errorHandler.test.ts @@ -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('/BadRequestError', () => { + throw new errors.BadRequestError(); + }); + app.use('/UnauthenticatedError', () => { + throw new errors.UnauthenticatedError(); + }); + app.use('/ForbiddenError', () => { + throw new errors.ForbiddenError(); + }); + 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('/BadRequestError')).status).toBe(400); + expect((await r.get('/UnauthenticatedError')).status).toBe(401); + expect((await r.get('/ForbiddenError')).status).toBe(403); + expect((await r.get('/NotFoundError')).status).toBe(404); + expect((await r.get('/ConflictError')).status).toBe(409); + }); }); diff --git a/packages/backend-common/src/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts index 655d93ad91..d14c32ce16 100644 --- a/packages/backend-common/src/middleware/errorHandler.ts +++ b/packages/backend-common/src/middleware/errorHandler.ts @@ -15,12 +15,12 @@ */ import { ErrorRequestHandler, NextFunction, Request, Response } from 'express'; +import * as errors from '../errors'; /** * 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 @@ -36,8 +36,12 @@ export function errorHandler(): ErrorRequestHandler { error: Error, _request: Request, response: Response, - _next: NextFunction, + next: NextFunction, ) => { + if (response.headersSent) { + next(error); + } + const status = getStatusCode(error); const message = error.message; response.status(status).send(message); @@ -45,8 +49,8 @@ export function errorHandler(): ErrorRequestHandler { } 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 +63,22 @@ function getStatusCode(error: Error): number { } } + // Handle well-known error types + switch (error.name) { + case errors.BadRequestError.name: + return 400; + case errors.UnauthenticatedError.name: + return 401; + case errors.ForbiddenError.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; } diff --git a/plugins/inventory-backend/package.json b/plugins/inventory-backend/package.json index 4b7fd0ecc7..86e3981147 100644 --- a/plugins/inventory-backend/package.json +++ b/plugins/inventory-backend/package.json @@ -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" diff --git a/plugins/inventory-backend/src/inventory/AggregatorInventory.ts b/plugins/inventory-backend/src/inventory/AggregatorInventory.ts index ef01be962f..b768b8eb23 100644 --- a/plugins/inventory-backend/src/inventory/AggregatorInventory.ts +++ b/plugins/inventory-backend/src/inventory/AggregatorInventory.ts @@ -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 { - return Promise.all(this.inventories.map((i) => i.list())).then((lists) => - lists.flat(), - ); + async list(): Promise { + const lists = await Promise.all(this.inventories.map((i) => i.list())); + return lists.flat(); } - item(id: string): Promise { - return this.list().then((items) => items.find((i) => i.id === id)); + item(id: string): Promise { + 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) { diff --git a/plugins/inventory-backend/src/inventory/StaticInventory.ts b/plugins/inventory-backend/src/inventory/StaticInventory.ts index 95a4466392..4569ae8ed2 100644 --- a/plugins/inventory-backend/src/inventory/StaticInventory.ts +++ b/plugins/inventory-backend/src/inventory/StaticInventory.ts @@ -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 { - return Promise.resolve([...this.components]); + async list(): Promise { + return this.components.slice(); } - item(id: string): Promise { - return this.list().then((items) => items.find((i) => i.id === id)); + async item(id: string): Promise { + const item = this.components.find((i) => i.id === id); + if (!item) { + throw new NotFoundError(`Found no component with ID ${id}`); + } + return item; } } diff --git a/plugins/inventory-backend/src/inventory/types.ts b/plugins/inventory-backend/src/inventory/types.ts index 3d30b11881..863dccb291 100644 --- a/plugins/inventory-backend/src/inventory/types.ts +++ b/plugins/inventory-backend/src/inventory/types.ts @@ -19,6 +19,6 @@ export type Component = { }; export type Inventory = { - list: () => Promise; - item: (id: string) => Promise; + list(): Promise; + item(id: string): Promise; }; diff --git a/plugins/inventory-backend/src/service/router.ts b/plugins/inventory-backend/src/service/router.ts index efaff00cb8..a05fac08be 100644 --- a/plugins/inventory-backend/src/service/router.ts +++ b/plugins/inventory-backend/src/service/router.ts @@ -16,6 +16,7 @@ import express from 'express'; import { Logger } from 'winston'; +import Router from 'express-promise-router'; import { Inventory } from '../inventory'; export interface RouterOptions { @@ -29,7 +30,7 @@ export async function createRouter( const inventory = options.inventory; const logger = options.logger.child({ plugin: 'inventory' }); - const router = express.Router(); + const router = Router(); router .get('/', async (req, res) => { const components = await inventory.list(); @@ -38,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(); diff --git a/yarn.lock b/yarn.lock index 686575eec3..85fb313077 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9208,6 +9208,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" @@ -13627,6 +13636,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" @@ -14152,7 +14166,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= From 1b4b98cd053770fac1f2906604ed171e2c700bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 4 May 2020 13:06:43 +0200 Subject: [PATCH 08/13] Show stack in dev --- .../src/middleware/errorHandler.ts | 18 ++++++++++++++++-- plugins/inventory-backend/package.json | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/backend-common/src/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts index d14c32ce16..eb1b0597a7 100644 --- a/packages/backend-common/src/middleware/errorHandler.ts +++ b/packages/backend-common/src/middleware/errorHandler.ts @@ -17,6 +17,15 @@ 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. * @@ -30,7 +39,12 @@ import * as errors from '../errors'; * * @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, @@ -43,7 +57,7 @@ export function errorHandler(): ErrorRequestHandler { } const status = getStatusCode(error); - const message = error.message; + const message = showStackTraces ? error.stack : error.message; response.status(status).send(message); }; } diff --git a/plugins/inventory-backend/package.json b/plugins/inventory-backend/package.json index 86e3981147..16d510a968 100644 --- a/plugins/inventory-backend/package.json +++ b/plugins/inventory-backend/package.json @@ -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", From e5529c249ba06b2341990b34ceac7d322e483915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 4 May 2020 15:54:07 +0200 Subject: [PATCH 09/13] Better errors --- packages/backend-common/src/errors.ts | 74 ++++++++++++--------------- 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/packages/backend-common/src/errors.ts b/packages/backend-common/src/errors.ts index c762df9a28..c3cf53453e 100644 --- a/packages/backend-common/src/errors.ts +++ b/packages/backend-common/src/errors.ts @@ -25,48 +25,52 @@ * use e.g. the http-errors library. */ +class CustomErrorBase extends Error { + readonly cause?: Error; + + constructor(constructor: Function, message?: string, cause?: Error) { + super(message); + Object.setPrototypeOf(this, constructor.prototype); + Error.captureStackTrace(this, constructor); + this.name = this.constructor.name; + this.cause = cause; + } + + toString() { + let result = super.toString(); + + if (this.cause) { + result += `; caused by ${this.cause.toString()}`; + } + + return result; + } +} + /** * The request is malformed and cannot be processed. */ -export class BadRequestError extends Error { - readonly cause?: Error; - +export class BadRequestError extends CustomErrorBase { constructor(message?: string, cause?: Error) { - super(message); - Object.setPrototypeOf(this, BadRequestError.prototype); - Error.captureStackTrace(this, BadRequestError); - this.name = this.constructor.name; - this.cause = cause; + super(BadRequestError, message, cause); } } /** * The request requires authentication, which was not properly supplied. */ -export class UnauthenticatedError extends Error { - readonly cause?: Error; - +export class UnauthenticatedError extends CustomErrorBase { constructor(message?: string, cause?: Error) { - super(message); - Object.setPrototypeOf(this, UnauthenticatedError.prototype); - Error.captureStackTrace(this, UnauthenticatedError); - this.name = this.constructor.name; - this.cause = cause; + super(UnauthenticatedError, message, cause); } } /** * The authenticated caller is not permitted to perform this request. */ -export class ForbiddenError extends Error { - readonly cause?: Error; - +export class ForbiddenError extends CustomErrorBase { constructor(message?: string, cause?: Error) { - super(message); - Object.setPrototypeOf(this, ForbiddenError.prototype); - Error.captureStackTrace(this, ForbiddenError); - this.name = this.constructor.name; - this.cause = cause; + super(ForbiddenError, message, cause); } } @@ -76,15 +80,9 @@ export class ForbiddenError extends Error { * 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 Error { - readonly cause?: Error; - +export class NotFoundError extends CustomErrorBase { constructor(message?: string, cause?: Error) { - super(message); - Object.setPrototypeOf(this, NotFoundError.prototype); - Error.captureStackTrace(this, NotFoundError); - this.name = this.constructor.name; - this.cause = cause; + super(NotFoundError, message, cause); } } @@ -92,16 +90,8 @@ export class NotFoundError extends Error { * The request could not complete due to a conflict in the current state of the * resource. */ -export class ConflictError extends Error { - readonly cause?: Error; - +export class ConflictError extends CustomErrorBase { constructor(message?: string, cause?: Error) { - super(message); - Object.setPrototypeOf(this, ConflictError.prototype); - if (Error.captureStackTrace) { - Error.captureStackTrace(this, ConflictError); - } - this.name = this.constructor.name; - this.cause = cause; + super(ConflictError, message, cause); } } From 129991fcfd4e2faf66ae1b3f334fcf39d0378bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 4 May 2020 16:34:17 +0200 Subject: [PATCH 10/13] Even denser errors --- packages/backend-common/src/errors.ts | 60 +++++++------------ .../src/middleware/errorHandler.test.ts | 12 ++-- .../src/middleware/errorHandler.ts | 4 +- packages/backend-common/tsconfig.json | 2 +- 4 files changed, 29 insertions(+), 49 deletions(-) diff --git a/packages/backend-common/src/errors.ts b/packages/backend-common/src/errors.ts index c3cf53453e..34a605a576 100644 --- a/packages/backend-common/src/errors.ts +++ b/packages/backend-common/src/errors.ts @@ -28,51 +28,39 @@ class CustomErrorBase extends Error { readonly cause?: Error; - constructor(constructor: Function, message?: string, cause?: Error) { - super(message); - Object.setPrototypeOf(this, constructor.prototype); - Error.captureStackTrace(this, constructor); - this.name = this.constructor.name; - this.cause = cause; - } - - toString() { - let result = super.toString(); - - if (this.cause) { - result += `; caused by ${this.cause.toString()}`; + constructor(message?: string, cause?: Error) { + let fullMessage = message; + if (cause) { + if (fullMessage) { + fullMessage += `; caused by ${cause}`; + } else { + fullMessage = `caused by ${cause}`; + } } - return result; + super(fullMessage); + + Error.captureStackTrace(this, this.constructor); + + this.name = this.constructor.name; + this.cause = cause; } } /** * The request is malformed and cannot be processed. */ -export class BadRequestError extends CustomErrorBase { - constructor(message?: string, cause?: Error) { - super(BadRequestError, message, cause); - } -} +export class BadRequestError extends CustomErrorBase {} /** * The request requires authentication, which was not properly supplied. */ -export class UnauthenticatedError extends CustomErrorBase { - constructor(message?: string, cause?: Error) { - super(UnauthenticatedError, message, cause); - } -} +export class AuthenticationError extends CustomErrorBase {} /** - * The authenticated caller is not permitted to perform this request. + * The authenticated caller is not allowed to perform this request. */ -export class ForbiddenError extends CustomErrorBase { - constructor(message?: string, cause?: Error) { - super(ForbiddenError, message, cause); - } -} +export class NotAllowedError extends CustomErrorBase {} /** * The requested resource could not be found. @@ -80,18 +68,10 @@ export class ForbiddenError extends CustomErrorBase { * 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 { - constructor(message?: string, cause?: Error) { - super(NotFoundError, message, cause); - } -} +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 { - constructor(message?: string, cause?: Error) { - super(ConflictError, message, cause); - } -} +export class ConflictError extends CustomErrorBase {} diff --git a/packages/backend-common/src/middleware/errorHandler.test.ts b/packages/backend-common/src/middleware/errorHandler.test.ts index 8f4a8cfea4..d282ccb99b 100644 --- a/packages/backend-common/src/middleware/errorHandler.test.ts +++ b/packages/backend-common/src/middleware/errorHandler.test.ts @@ -52,11 +52,11 @@ describe('errorHandler', () => { app.use('/BadRequestError', () => { throw new errors.BadRequestError(); }); - app.use('/UnauthenticatedError', () => { - throw new errors.UnauthenticatedError(); + app.use('/AuthenticationError', () => { + throw new errors.AuthenticationError(); }); - app.use('/ForbiddenError', () => { - throw new errors.ForbiddenError(); + app.use('/NotAllowedError', () => { + throw new errors.NotAllowedError(); }); app.use('/NotFoundError', () => { throw new errors.NotFoundError(); @@ -68,8 +68,8 @@ describe('errorHandler', () => { const r = request(app); expect((await r.get('/BadRequestError')).status).toBe(400); - expect((await r.get('/UnauthenticatedError')).status).toBe(401); - expect((await r.get('/ForbiddenError')).status).toBe(403); + 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); }); diff --git a/packages/backend-common/src/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts index eb1b0597a7..43782e4d7f 100644 --- a/packages/backend-common/src/middleware/errorHandler.ts +++ b/packages/backend-common/src/middleware/errorHandler.ts @@ -81,9 +81,9 @@ function getStatusCode(error: Error): number { switch (error.name) { case errors.BadRequestError.name: return 400; - case errors.UnauthenticatedError.name: + case errors.AuthenticationError.name: return 401; - case errors.ForbiddenError.name: + case errors.NotAllowedError.name: return 403; case errors.NotFoundError.name: return 404; diff --git a/packages/backend-common/tsconfig.json b/packages/backend-common/tsconfig.json index b463ac102f..6d7ca21afa 100644 --- a/packages/backend-common/tsconfig.json +++ b/packages/backend-common/tsconfig.json @@ -7,7 +7,7 @@ "sourceMap": true, "declaration": true, "strict": true, - "target": "es5", + "target": "ES2019", "module": "commonjs", "esModuleInterop": true, "types": ["node", "jest"] From 9724084186b684053b76d8ab7393960f22545dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 4 May 2020 16:42:40 +0200 Subject: [PATCH 11/13] It's InputError then --- packages/backend-common/src/errors.ts | 4 ++-- packages/backend-common/src/middleware/errorHandler.test.ts | 6 +++--- packages/backend-common/src/middleware/errorHandler.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend-common/src/errors.ts b/packages/backend-common/src/errors.ts index 34a605a576..b68dc8e3f0 100644 --- a/packages/backend-common/src/errors.ts +++ b/packages/backend-common/src/errors.ts @@ -48,9 +48,9 @@ class CustomErrorBase extends Error { } /** - * The request is malformed and cannot be processed. + * The given inputs are malformed and cannot be processed. */ -export class BadRequestError extends CustomErrorBase {} +export class InputError extends CustomErrorBase {} /** * The request requires authentication, which was not properly supplied. diff --git a/packages/backend-common/src/middleware/errorHandler.test.ts b/packages/backend-common/src/middleware/errorHandler.test.ts index d282ccb99b..022802dff8 100644 --- a/packages/backend-common/src/middleware/errorHandler.test.ts +++ b/packages/backend-common/src/middleware/errorHandler.test.ts @@ -49,8 +49,8 @@ describe('errorHandler', () => { it('handles well-known error classes', async () => { const app = express(); - app.use('/BadRequestError', () => { - throw new errors.BadRequestError(); + app.use('/InputError', () => { + throw new errors.InputError(); }); app.use('/AuthenticationError', () => { throw new errors.AuthenticationError(); @@ -67,7 +67,7 @@ describe('errorHandler', () => { app.use(errorHandler()); const r = request(app); - expect((await r.get('/BadRequestError')).status).toBe(400); + 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); diff --git a/packages/backend-common/src/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts index 43782e4d7f..14f6cfa8d7 100644 --- a/packages/backend-common/src/middleware/errorHandler.ts +++ b/packages/backend-common/src/middleware/errorHandler.ts @@ -79,7 +79,7 @@ function getStatusCode(error: Error): number { // Handle well-known error types switch (error.name) { - case errors.BadRequestError.name: + case errors.InputError.name: return 400; case errors.AuthenticationError.name: return 401; From 8354d4a104e29e5bb23db6e556028e4a0dfbbb2d Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Tue, 5 May 2020 02:32:47 +0900 Subject: [PATCH 12/13] Set the initial Storybook theme to light (#711) This fixes the inconsistency if user's OS/Browser theme is set to Dark mode. --- packages/storybook/.storybook/config.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/storybook/.storybook/config.js b/packages/storybook/.storybook/config.js index fab204fd9f..941b02718b 100644 --- a/packages/storybook/.storybook/config.js +++ b/packages/storybook/.storybook/config.js @@ -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) => ( {story()} )); + +addParameters({ + darkMode: { + // Set the initial theme + current: 'light', + }, +}); From 168db0fe9c5a7fb45b05e00fb19694fa4565d77a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Mon, 4 May 2020 19:37:03 +0200 Subject: [PATCH 13/13] Rename Scaffolder -> Create (#712) * Rename Scaffolder -> Create * Use Typography --- packages/app/src/components/Root/Root.tsx | 4 +-- .../src/components/ScaffolderPage/index.tsx | 26 ++++++++++++++++--- plugins/scaffolder/src/plugin.ts | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 7ef3a5a22f..6854ce015b 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -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,7 +83,7 @@ const Root: FC<{}> = ({ children }) => ( - + diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx index 5cfeb122ce..b05b86bd92 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx @@ -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 ( -
+
+ Create a new component {' '} + + } + subtitle="Create new software components using standard templates" + /> +
- {STATIC_DATA.map((item) => { + {STATIC_DATA.map((item, ix) => { return ( -

{item.description}

+ {item.description}
); })} diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index 3dfa5d0b89..31bdab2f84 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -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); }, });