From 3293fe96a43c1778d99bcf2c7e7866e78d7099e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20DOREAU?= Date: Sat, 30 May 2020 03:43:40 +0200 Subject: [PATCH 01/38] feat(core): add Tabs component --- .../core/src/components/Tabs/Tab.test.tsx | 27 ++++ packages/core/src/components/Tabs/Tab.tsx | 65 ++++++++ packages/core/src/components/Tabs/TabBar.tsx | 54 +++++++ packages/core/src/components/Tabs/TabIcon.tsx | 72 +++++++++ .../core/src/components/Tabs/TabPanel.tsx | 42 +++++ .../core/src/components/Tabs/Tabs.stories.tsx | 47 ++++++ packages/core/src/components/Tabs/Tabs.tsx | 145 ++++++++++++++++++ packages/core/src/components/Tabs/index.ts | 17 ++ packages/core/src/components/Tabs/utils.ts | 42 +++++ packages/core/src/index.ts | 1 + packages/theme/src/themes.ts | 8 + packages/theme/src/types.ts | 4 + 12 files changed, 524 insertions(+) create mode 100644 packages/core/src/components/Tabs/Tab.test.tsx create mode 100644 packages/core/src/components/Tabs/Tab.tsx create mode 100644 packages/core/src/components/Tabs/TabBar.tsx create mode 100644 packages/core/src/components/Tabs/TabIcon.tsx create mode 100644 packages/core/src/components/Tabs/TabPanel.tsx create mode 100644 packages/core/src/components/Tabs/Tabs.stories.tsx create mode 100644 packages/core/src/components/Tabs/Tabs.tsx create mode 100644 packages/core/src/components/Tabs/index.ts create mode 100644 packages/core/src/components/Tabs/utils.ts diff --git a/packages/core/src/components/Tabs/Tab.test.tsx b/packages/core/src/components/Tabs/Tab.test.tsx new file mode 100644 index 0000000000..a841af776a --- /dev/null +++ b/packages/core/src/components/Tabs/Tab.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; +import Tab from './Tab'; + +describe('', () => { + it('renders without exploding', () => { + const rendered = render(wrapInTestApp()); + expect(rendered.getByText('test')).toBeInTheDocument(); + }); +}); diff --git a/packages/core/src/components/Tabs/Tab.tsx b/packages/core/src/components/Tabs/Tab.tsx new file mode 100644 index 0000000000..3726642d94 --- /dev/null +++ b/packages/core/src/components/Tabs/Tab.tsx @@ -0,0 +1,65 @@ +/* + * 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 { Tab, withStyles, Theme } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; + +const withStylesProps = (styles: any) => (Component: any) => (props: any) => { + const Comp = withStyles((theme: Theme) => styles(props, theme))(Component); + return ; +}; + +interface StyledTabProps { + label: string; + isFirstNav?: boolean; + isFirstIndex?: boolean; +} + +const tabMarginLeft = (isFirstNav: boolean, isFirstIndex: boolean) => { + if (isFirstIndex) { + if (isFirstNav) { + return '20px'; + } + return '0'; + } + return '40px'; +}; + +const tabStyles = (props: any, theme: BackstageTheme) => ({ + root: { + textTransform: 'none', + height: '64px', + fontWeight: theme.typography.fontWeightBold, + fontSize: theme.typography.pxToRem(13), + color: theme.palette.textSubtle, + marginLeft: tabMarginLeft(props.isFirstNav, props.isFirstIndex), + width: '130px', + minWidth: '130px', + '&:hover': { + outline: 'none', + backgroundColor: 'transparent', + color: theme.palette.textSubtle, + }, + }, +}); + +const StyledTab = withStylesProps(tabStyles)((props: StyledTabProps) => { + const { isFirstNav, isFirstIndex, ...rest } = props; + return ; +}); + +export default StyledTab; diff --git a/packages/core/src/components/Tabs/TabBar.tsx b/packages/core/src/components/Tabs/TabBar.tsx new file mode 100644 index 0000000000..510111dc37 --- /dev/null +++ b/packages/core/src/components/Tabs/TabBar.tsx @@ -0,0 +1,54 @@ +/* + * 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 { Tabs, makeStyles } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; + +interface StyledTabsProps { + value: number; + onChange: (event: React.ChangeEvent<{}>, newValue: number) => void; +} + +const useStyles = makeStyles(theme => ({ + indicator: { + display: 'flex', + justifyContent: 'center', + backgroundColor: theme.palette.tabbar.indicator, + height: '4px', + }, + flexContainer: { + alignItems: 'center', + }, + root: { + '&:last-child': { + marginLeft: 'auto', + }, + }, +})); + +const StyledTabs: FC = props => { + const classes = useStyles(props); + return ( + }} + /> + ); +}; + +export default StyledTabs; diff --git a/packages/core/src/components/Tabs/TabIcon.tsx b/packages/core/src/components/Tabs/TabIcon.tsx new file mode 100644 index 0000000000..fd58735427 --- /dev/null +++ b/packages/core/src/components/Tabs/TabIcon.tsx @@ -0,0 +1,72 @@ +/* + * 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 { IconButton, withStyles, Theme } from '@material-ui/core'; + +export const withStylesProps = (styles: any) => (Component: any) => ( + props: any, +) => { + const Comp = withStyles((theme: Theme) => styles(props, theme))(Component); + return ; +}; + +interface StyledIconProps { + ariaLabel: string; + children: any; + classes: any; + isNext?: boolean; + onClick: any; +} + +const iconStyles = (props: StyledIconProps) => ({ + root: { + color: '#6E6E6E', + overflow: 'visible', + fontSize: '1.5rem', + textAlign: 'center', + borderRadius: '50%', + backgroundColor: '#E6E6E6', + marginLeft: props.isNext ? 'auto' : '0', + marginRight: props.isNext ? '0' : '10px', + '&:hover': { + backgroundColor: '#E6E6E6', + opacity: '1', + }, + }, +}); + +const StyledIcon = withStylesProps(iconStyles)((props: StyledIconProps) => { + const { + classes: { root }, + ariaLabel, + onClick, + } = props; + return ( + + {props.children} + + ); +}); + +export default StyledIcon; diff --git a/packages/core/src/components/Tabs/TabPanel.tsx b/packages/core/src/components/Tabs/TabPanel.tsx new file mode 100644 index 0000000000..32536dc86d --- /dev/null +++ b/packages/core/src/components/Tabs/TabPanel.tsx @@ -0,0 +1,42 @@ +/* + * 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 Box from '@material-ui/core/Box'; + +export interface TabPanelProps { + children: any; + value: any; + index: number; +} + +const TabPanel: FC = props => { + const { children, value, index, ...other } = props; + + return ( + + ); +}; + +export default TabPanel; diff --git a/packages/core/src/components/Tabs/Tabs.stories.tsx b/packages/core/src/components/Tabs/Tabs.stories.tsx new file mode 100644 index 0000000000..5258e9a51a --- /dev/null +++ b/packages/core/src/components/Tabs/Tabs.stories.tsx @@ -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 from 'react'; +import Tabs from './Tabs'; + +export default { + title: 'Tabs', + component: Tabs, +}; + +const containerStyle = {}; + +export const Default = () => ( +
+ ({ + label: `ANOTHER TAB`, + content:
Content {index}
, + }))} + /> +
+); + +export const Expandable = () => ( +
+ ({ + label: `ANOTHER TAB`, + content:
Content {index}
, + }))} + /> +
+); diff --git a/packages/core/src/components/Tabs/Tabs.tsx b/packages/core/src/components/Tabs/Tabs.tsx new file mode 100644 index 0000000000..cb1f5d6138 --- /dev/null +++ b/packages/core/src/components/Tabs/Tabs.tsx @@ -0,0 +1,145 @@ +/* + * 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, useRef, useEffect, MutableRefObject } from 'react'; +import { BackstageTheme } from '@backstage/theme'; +import { AppBar } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; +import NavigateNextIcon from '@material-ui/icons/NavigateNext'; +import { chunkArray, useWindowWidth } from './utils'; + +/* Import Components */ + +import TabPanel from './TabPanel'; +import TabIcon from './TabIcon'; +import Tab from './Tab'; +import TabBar from './TabBar'; + +/* Props Types */ + +interface TabProps { + label: string; + content: any; +} + +export interface TabsProps { + tabs: TabProps[]; +} + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + root: { + flexGrow: 1, + width: '100%', + }, + styledTabs: { + backgroundColor: theme.palette.tabbar.background, + }, + appbar: { + boxShadow: 'none', + backgroundColor: theme.palette.tabbar.background, + paddingLeft: '10px', + paddingRight: '10px', + }, +})); + +const Tabs: FC = ({ tabs }) => { + const classes = useStyles(); + const [value, setValue] = React.useState(0); + const [navIndex, setNavIndex] = React.useState(0); + const [chunkedTabs, setChunkedTabs] = React.useState([[]] as TabProps[][]); + const wrapper = useRef() as MutableRefObject; + + const size = useWindowWidth(); + + const handleChange = (_: React.ChangeEvent<{}>, newValue: number) => { + setValue(newValue); + }; + + const navigateToPrevChunk = () => { + setValue(navIndex - 1 === 0 ? 0 : 1); + setNavIndex(navIndex - 1); + }; + + const navigateToNextChunk = () => { + setValue(1); + setNavIndex(navIndex + 1); + }; + + const hasNextNavIndex = () => navIndex + 1 < chunkedTabs.length; + + useEffect(() => { + // Each time the window is resized we calculate how many tabs wwe can render given the window width + const padding = 20; // The AppBar padding + + const numberOfTabIcons = navIndex === 0 ? 1 : 2; + const wrapperWidth = + wrapper.current.offsetWidth - padding - numberOfTabIcons * 30; + + const numberOfChunkedElement = Math.floor(wrapperWidth / 170); + setChunkedTabs( + chunkArray([...tabs], numberOfChunkedElement) as TabProps[][], + ); + }, [size]); + + return ( +
+ +
+ + {navIndex !== 0 && ( + + + + )} + {chunkedTabs[navIndex].map((tab, index) => ( + + ))} + {hasNextNavIndex() && ( + + + + )} + +
+
+ {chunkedTabs[navIndex].map((tab, index) => ( + + {tab.content} + + ))} +
+ ); +}; + +export default Tabs; diff --git a/packages/core/src/components/Tabs/index.ts b/packages/core/src/components/Tabs/index.ts new file mode 100644 index 0000000000..03995fe2ea --- /dev/null +++ b/packages/core/src/components/Tabs/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default } from './Tabs'; diff --git a/packages/core/src/components/Tabs/utils.ts b/packages/core/src/components/Tabs/utils.ts new file mode 100644 index 0000000000..d3fa0dbd3e --- /dev/null +++ b/packages/core/src/components/Tabs/utils.ts @@ -0,0 +1,42 @@ +/* + * 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 { useState, useEffect } from 'react'; + +export const chunkArray = (myArray: any[], chunkSize: number) => { + const results = []; + while (myArray.length) { + results.push(myArray.splice(0, chunkSize)); + } + return results; +}; + +export const useWindowWidth = () => { + const isClient = typeof window === 'object'; + const getWidth = () => (isClient ? window.innerWidth : undefined); + const [windowWidth, setWindowWidth] = useState(getWidth); + + useEffect((): any => { + if (!isClient) { + return false; + } + + const handleResize = () => setWindowWidth(getWidth()); + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + return windowWidth; +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2ceea470f4..cd53075c28 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -39,3 +39,4 @@ export { default as TrendLine } from './components/TrendLine'; export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular'; export * from './components/Status'; export { default as WarningPanel } from './components/WarningPanel'; +export { default as Tabs } from './components/Tabs'; diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index fe3c71a212..630e81c146 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -60,6 +60,10 @@ export const lightTheme = createTheme({ icon: '#BDBDBD', background: '#404040', }, + tabbar: { + indicator: '#9BF0E1', + background: '#FFFFFF', + }, }, }); @@ -106,5 +110,9 @@ export const darkTheme = createTheme({ icon: '#181818', background: '#BDBDBD', }, + tabbar: { + indicator: '#9BF0E1', + background: '#424242', + }, }, }); diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index c38d67d063..8d18caf4d1 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -44,6 +44,10 @@ type PaletteAdditions = { link: string; gold: string; sidebar: string; + tabbar: { + indicator: string; + background: string; + }; bursts: { fontColor: string; slackChannelText: string; From 63b1193cb819e9a9aa494d57a9ea2cf02db1cdd1 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Fri, 5 Jun 2020 12:04:07 +0200 Subject: [PATCH 02/38] /catalog/:namespace?/:kind/:name/ --- plugins/catalog/src/api/CatalogClient.ts | 15 +++++++++++++-- plugins/catalog/src/api/types.ts | 6 +++++- .../src/components/CatalogTable/CatalogTable.tsx | 5 ++++- .../ComponentPage/ComponentPage.test.tsx | 7 ++++--- .../components/ComponentPage/ComponentPage.tsx | 8 +++++--- plugins/catalog/src/routes.ts | 2 +- .../RegisterComponentPage.test.tsx | 2 +- .../RegisterComponentResultDialog.tsx | 2 ++ 8 files changed, 35 insertions(+), 12 deletions(-) diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 4ae4676703..c2f4497429 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -35,9 +35,20 @@ export class CatalogClient implements CatalogApi { const response = await fetch(`${this.apiOrigin}${this.basePath}/entities`); return await response.json(); } - async getEntityByName(name: string): Promise { + + async getEntity({ + name, + namespace, + kind, + }: { + name: string; + namespace?: string; + kind: string; + }): Promise { const response = await fetch( - `${this.apiOrigin}${this.basePath}/entities/by-name/Component/default/${name}`, + `${this.apiOrigin}${this.basePath}/entities/by-name/${kind}/${ + namespace ?? 'default' + }/${name}`, ); const entity = await response.json(); if (entity) return entity; diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 5bb6a6ca83..b45994b861 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -24,7 +24,11 @@ export const catalogApiRef = createApiRef({ export interface CatalogApi { getEntities(): Promise; - getEntityByName(name: string): Promise; + getEntity(params: { + name: string; + namespace?: string; + kind: string; + }): Promise; addLocation(type: string, target: string): Promise; getLocationByEntity(entity: Entity): Promise; } diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 7ff521ee67..094dfc5352 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -28,7 +28,10 @@ const columns: TableColumn[] = [ render: (componentData: any) => ( {componentData.name} diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx index f580a84d8a..d523b45622 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx @@ -20,11 +20,12 @@ import { wrapInTestApp } from '@backstage/test-utils'; import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; import { catalogApiRef, CatalogApi } from '../../api/types'; -const getTestProps = (componentName: string) => { +const getTestProps = () => { return { match: { params: { - name: componentName, + name: 'componentName', + kind: 'Component', }, }, history: { @@ -37,7 +38,7 @@ const errorApi = { post: () => {} }; describe('ComponentPage', () => { it('should redirect to component table page when name is not provided', async () => { - const props = getTestProps(''); + const props = getTestProps(); await render( wrapInTestApp( = ({ match, history }) => { const [removingPending, setRemovingPending] = useState(false); const showRemovalDialog = () => setConfirmationDialogOpen(true); const hideRemovalDialog = () => setConfirmationDialogOpen(false); - const componentName = match.params.name; + const { name, namespace, kind } = match.params; const errorApi = useApi(errorApiRef); const catalogApi = useApi(catalogApiRef); const catalogRequest = useAsync(() => - catalogApi.getEntityByName(match.params.name), + catalogApi.getEntity({ name, namespace, kind }), ); useEffect(() => { @@ -67,7 +69,7 @@ const ComponentPage: FC = ({ match, history }) => { } }, [catalogRequest.error, errorApi, history]); - if (componentName === '') { + if (name === '') { history.push('/catalog'); return null; } diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 6498d412b8..f5bb319a88 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -25,6 +25,6 @@ export const rootRoute = createRouteRef({ }); export const entityRoute = createRouteRef({ icon: NoIcon, - path: '/catalog/:name/', + path: '/catalog/:namespace?/:kind/:name/', title: 'Entity', }); diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx index ad47c3a41b..1d424858ad 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx @@ -28,7 +28,7 @@ const catalogApi: jest.Mocked = { /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ addLocation: jest.fn((_a, _b) => new Promise(() => {})), getEntities: jest.fn(), - getEntityByName: jest.fn(), + getEntity: jest.fn(), getLocationByEntity: jest.fn(), }; diff --git a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx index 4d158526a0..7c6e335f6e 100644 --- a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx +++ b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx @@ -69,10 +69,12 @@ export const RegisterComponentResultDialog: FC = ({ component={RouterLink} to={generatePath(entityRoute.path, { name: entity.metadata.name, + kind: entity.kind, })} > {generatePath(entityRoute.path, { name: entity.metadata.name, + kind: entity.kind, })} ), From 2ba33db74d8f76be9251ff06f3602ea8d33e920f Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Fri, 5 Jun 2020 12:22:07 +0200 Subject: [PATCH 03/38] Fix tests --- .../src/components/ComponentPage/ComponentPage.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx index d523b45622..510792a902 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx @@ -20,11 +20,11 @@ import { wrapInTestApp } from '@backstage/test-utils'; import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; import { catalogApiRef, CatalogApi } from '../../api/types'; -const getTestProps = () => { +const getTestProps = (name: string) => { return { match: { params: { - name: 'componentName', + name: name, kind: 'Component', }, }, @@ -38,7 +38,7 @@ const errorApi = { post: () => {} }; describe('ComponentPage', () => { it('should redirect to component table page when name is not provided', async () => { - const props = getTestProps(); + const props = getTestProps(''); await render( wrapInTestApp( { [ catalogApiRef, ({ - async getEntityByName() {}, + async getEntity() {}, } as unknown) as CatalogApi, ], ])} From f1dbf5a9fafaca408799ac5284434db0a403fc92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20DOREAU?= Date: Sat, 6 Jun 2020 17:56:39 +0200 Subject: [PATCH 04/38] refactor(core): update tabs --- .../core/src/components/Tabs/Tab.test.tsx | 4 +- packages/core/src/components/Tabs/Tab.tsx | 27 +++-- packages/core/src/components/Tabs/TabBar.tsx | 6 +- packages/core/src/components/Tabs/TabIcon.tsx | 34 ++---- .../core/src/components/Tabs/TabPanel.tsx | 9 +- .../core/src/components/Tabs/Tabs.stories.tsx | 26 ++++- packages/core/src/components/Tabs/Tabs.tsx | 101 +++++++++++------- packages/core/src/components/Tabs/index.ts | 2 +- packages/core/src/components/Tabs/utils.ts | 25 +---- packages/theme/src/themes.ts | 2 - packages/theme/src/types.ts | 1 - 11 files changed, 121 insertions(+), 116 deletions(-) diff --git a/packages/core/src/components/Tabs/Tab.test.tsx b/packages/core/src/components/Tabs/Tab.test.tsx index a841af776a..5b155ef536 100644 --- a/packages/core/src/components/Tabs/Tab.test.tsx +++ b/packages/core/src/components/Tabs/Tab.test.tsx @@ -17,11 +17,11 @@ import React from 'react'; import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; -import Tab from './Tab'; +import { StyledTab } from './Tab'; describe('', () => { it('renders without exploding', () => { - const rendered = render(wrapInTestApp()); + const rendered = render(wrapInTestApp()); expect(rendered.getByText('test')).toBeInTheDocument(); }); }); diff --git a/packages/core/src/components/Tabs/Tab.tsx b/packages/core/src/components/Tabs/Tab.tsx index 3726642d94..93243b2900 100644 --- a/packages/core/src/components/Tabs/Tab.tsx +++ b/packages/core/src/components/Tabs/Tab.tsx @@ -15,18 +15,15 @@ */ import React from 'react'; -import { Tab, withStyles, Theme } from '@material-ui/core'; +import { Tab, makeStyles } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; -const withStylesProps = (styles: any) => (Component: any) => (props: any) => { - const Comp = withStyles((theme: Theme) => styles(props, theme))(Component); - return ; -}; - interface StyledTabProps { - label: string; + label?: string; + icon?: any; // TODO: define type for material-ui icons isFirstNav?: boolean; isFirstIndex?: boolean; + value?: any; } const tabMarginLeft = (isFirstNav: boolean, isFirstIndex: boolean) => { @@ -39,14 +36,15 @@ const tabMarginLeft = (isFirstNav: boolean, isFirstIndex: boolean) => { return '40px'; }; -const tabStyles = (props: any, theme: BackstageTheme) => ({ +const useStyles = makeStyles(theme => ({ root: { textTransform: 'none', height: '64px', fontWeight: theme.typography.fontWeightBold, fontSize: theme.typography.pxToRem(13), color: theme.palette.textSubtle, - marginLeft: tabMarginLeft(props.isFirstNav, props.isFirstIndex), + marginLeft: props => + tabMarginLeft(props.isFirstNav as boolean, props.isFirstIndex as boolean), width: '130px', minWidth: '130px', '&:hover': { @@ -55,11 +53,10 @@ const tabStyles = (props: any, theme: BackstageTheme) => ({ color: theme.palette.textSubtle, }, }, -}); +})); -const StyledTab = withStylesProps(tabStyles)((props: StyledTabProps) => { +export const StyledTab = (props: StyledTabProps) => { + const classes = useStyles(props); const { isFirstNav, isFirstIndex, ...rest } = props; - return ; -}); - -export default StyledTab; + return ; +}; diff --git a/packages/core/src/components/Tabs/TabBar.tsx b/packages/core/src/components/Tabs/TabBar.tsx index 510111dc37..379d60668b 100644 --- a/packages/core/src/components/Tabs/TabBar.tsx +++ b/packages/core/src/components/Tabs/TabBar.tsx @@ -19,7 +19,7 @@ import { Tabs, makeStyles } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; interface StyledTabsProps { - value: number; + value: number | boolean; onChange: (event: React.ChangeEvent<{}>, newValue: number) => void; } @@ -40,7 +40,7 @@ const useStyles = makeStyles(theme => ({ }, })); -const StyledTabs: FC = props => { +export const StyledTabs: FC = props => { const classes = useStyles(props); return ( = props => { /> ); }; - -export default StyledTabs; diff --git a/packages/core/src/components/Tabs/TabIcon.tsx b/packages/core/src/components/Tabs/TabIcon.tsx index fd58735427..ffb2e12cbd 100644 --- a/packages/core/src/components/Tabs/TabIcon.tsx +++ b/packages/core/src/components/Tabs/TabIcon.tsx @@ -15,24 +15,17 @@ */ import React from 'react'; -import { IconButton, withStyles, Theme } from '@material-ui/core'; - -export const withStylesProps = (styles: any) => (Component: any) => ( - props: any, -) => { - const Comp = withStyles((theme: Theme) => styles(props, theme))(Component); - return ; -}; +import { IconButton, makeStyles } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; interface StyledIconProps { ariaLabel: string; children: any; - classes: any; isNext?: boolean; onClick: any; } -const iconStyles = (props: StyledIconProps) => ({ +const useStyles = makeStyles(() => ({ root: { color: '#6E6E6E', overflow: 'visible', @@ -40,25 +33,22 @@ const iconStyles = (props: StyledIconProps) => ({ textAlign: 'center', borderRadius: '50%', backgroundColor: '#E6E6E6', - marginLeft: props.isNext ? 'auto' : '0', - marginRight: props.isNext ? '0' : '10px', + marginLeft: props => (props.isNext ? 'auto' : '0'), + marginRight: props => (props.isNext ? '0' : '10px'), '&:hover': { backgroundColor: '#E6E6E6', opacity: '1', }, }, -}); +})); -const StyledIcon = withStylesProps(iconStyles)((props: StyledIconProps) => { - const { - classes: { root }, - ariaLabel, - onClick, - } = props; +export const StyledIcon = (props: StyledIconProps) => { + const classes = useStyles(props); + const { ariaLabel, onClick } = props; return ( { {props.children} ); -}); - -export default StyledIcon; +}; diff --git a/packages/core/src/components/Tabs/TabPanel.tsx b/packages/core/src/components/Tabs/TabPanel.tsx index 32536dc86d..ba8ca4bdee 100644 --- a/packages/core/src/components/Tabs/TabPanel.tsx +++ b/packages/core/src/components/Tabs/TabPanel.tsx @@ -19,18 +19,17 @@ import Box from '@material-ui/core/Box'; export interface TabPanelProps { children: any; - value: any; - index: number; + value?: any; + index?: number; } -const TabPanel: FC = props => { +export const TabPanel: FC = props => { const { children, value, index, ...other } = props; return ( ); }; - -export default TabPanel; diff --git a/packages/core/src/components/Tabs/Tabs.stories.tsx b/packages/core/src/components/Tabs/Tabs.stories.tsx index 5258e9a51a..930a825392 100644 --- a/packages/core/src/components/Tabs/Tabs.stories.tsx +++ b/packages/core/src/components/Tabs/Tabs.stories.tsx @@ -15,7 +15,8 @@ */ import React from 'react'; -import Tabs from './Tabs'; +import { Tabs } from './Tabs'; +import AccessAlarmIcon from '@material-ui/icons/AccessAlarm'; export default { title: 'Tabs', @@ -45,3 +46,26 @@ export const Expandable = () => ( /> ); + +export const Icons = () => ( +
+ ({ + icon: , + content:
Content {index}
, + }))} + /> +
+); + +export const IconsAndLabels = () => ( +
+ ({ + icon: , + label: `ANOTHER TAB`, + content:
Content {index}
, + }))} + /> +
+); diff --git a/packages/core/src/components/Tabs/Tabs.tsx b/packages/core/src/components/Tabs/Tabs.tsx index cb1f5d6138..e56cb93bfb 100644 --- a/packages/core/src/components/Tabs/Tabs.tsx +++ b/packages/core/src/components/Tabs/Tabs.tsx @@ -14,26 +14,34 @@ * limitations under the License. */ -import React, { FC, useRef, useEffect, MutableRefObject } from 'react'; +import React, { + FC, + useRef, + useEffect, + MutableRefObject, + useState, +} from 'react'; import { BackstageTheme } from '@backstage/theme'; import { AppBar } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; import NavigateNextIcon from '@material-ui/icons/NavigateNext'; -import { chunkArray, useWindowWidth } from './utils'; +import { chunkArray } from './utils'; +import { useWindowSize } from 'react-use'; /* Import Components */ -import TabPanel from './TabPanel'; -import TabIcon from './TabIcon'; -import Tab from './Tab'; -import TabBar from './TabBar'; +import { TabPanel } from './TabPanel'; +import { StyledIcon } from './TabIcon'; +import { StyledTab } from './Tab'; +import { StyledTabs } from './TabBar'; /* Props Types */ -interface TabProps { - label: string; +export interface TabProps { content: any; + label?: string; + icon?: any; // TODO: define type for material-ui icons } export interface TabsProps { @@ -46,36 +54,35 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({ width: '100%', }, styledTabs: { - backgroundColor: theme.palette.tabbar.background, + backgroundColor: theme.palette.background.paper, }, appbar: { boxShadow: 'none', - backgroundColor: theme.palette.tabbar.background, + backgroundColor: theme.palette.background.paper, paddingLeft: '10px', paddingRight: '10px', }, })); -const Tabs: FC = ({ tabs }) => { +export const Tabs: FC = ({ tabs }) => { const classes = useStyles(); - const [value, setValue] = React.useState(0); - const [navIndex, setNavIndex] = React.useState(0); - const [chunkedTabs, setChunkedTabs] = React.useState([[]] as TabProps[][]); + const [value, setValue] = useState([0, 0]); // [selectedChunckedNavIndex, selectedIndex] + const [navIndex, setNavIndex] = useState(0); + const [numberOfChunkedElement, setNumberOfChunkedElement] = useState(0); + const [chunkedTabs, setChunkedTabs] = useState([[]]); const wrapper = useRef() as MutableRefObject; - const size = useWindowWidth(); + const { width } = useWindowSize(); const handleChange = (_: React.ChangeEvent<{}>, newValue: number) => { - setValue(newValue); + setValue([navIndex, newValue]); }; const navigateToPrevChunk = () => { - setValue(navIndex - 1 === 0 ? 0 : 1); setNavIndex(navIndex - 1); }; const navigateToNextChunk = () => { - setValue(1); setNavIndex(navIndex + 1); }; @@ -88,58 +95,70 @@ const Tabs: FC = ({ tabs }) => { const numberOfTabIcons = navIndex === 0 ? 1 : 2; const wrapperWidth = wrapper.current.offsetWidth - padding - numberOfTabIcons * 30; + const flattenIndex = value[0] * numberOfChunkedElement + value[1]; + const newChunkedElementSize = Math.floor(wrapperWidth / 170); - const numberOfChunkedElement = Math.floor(wrapperWidth / 170); - setChunkedTabs( - chunkArray([...tabs], numberOfChunkedElement) as TabProps[][], - ); - }, [size]); + setNumberOfChunkedElement(newChunkedElementSize); + setChunkedTabs(chunkArray([...tabs], newChunkedElementSize)); + setValue([ + Math.floor(flattenIndex / newChunkedElementSize), + flattenIndex % newChunkedElementSize, + ]); + }, [width]); + + const currentIndex = navIndex === value[0] ? value[1] : false; return (
-
- +
+ {navIndex !== 0 && ( - - + )} {chunkedTabs[navIndex].map((tab, index) => ( - ))} {hasNextNavIndex() && ( - - + )} - +
- {chunkedTabs[navIndex].map((tab, index) => ( + {currentIndex !== false ? ( + chunkedTabs[navIndex].map((tab, index) => ( + + {tab.content} + + )) + ) : ( + // Render if the selected tab index is outside the current rendered chunked array - {tab.content} + {chunkedTabs[value[0]][value[1]].content} - ))} + )}
); }; - -export default Tabs; diff --git a/packages/core/src/components/Tabs/index.ts b/packages/core/src/components/Tabs/index.ts index 03995fe2ea..835ab5a8c3 100644 --- a/packages/core/src/components/Tabs/index.ts +++ b/packages/core/src/components/Tabs/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default } from './Tabs'; +export { Tabs as default } from './Tabs'; diff --git a/packages/core/src/components/Tabs/utils.ts b/packages/core/src/components/Tabs/utils.ts index d3fa0dbd3e..3e0ab6f2c3 100644 --- a/packages/core/src/components/Tabs/utils.ts +++ b/packages/core/src/components/Tabs/utils.ts @@ -13,30 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useState, useEffect } from 'react'; +import { TabProps } from './Tabs'; -export const chunkArray = (myArray: any[], chunkSize: number) => { +export const chunkArray = ( + myArray: TabProps[], + chunkSize: number, +): TabProps[][] => { const results = []; while (myArray.length) { results.push(myArray.splice(0, chunkSize)); } return results; }; - -export const useWindowWidth = () => { - const isClient = typeof window === 'object'; - const getWidth = () => (isClient ? window.innerWidth : undefined); - const [windowWidth, setWindowWidth] = useState(getWidth); - - useEffect((): any => { - if (!isClient) { - return false; - } - - const handleResize = () => setWindowWidth(getWidth()); - - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - return windowWidth; -}; diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 630e81c146..3071c0289e 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -62,7 +62,6 @@ export const lightTheme = createTheme({ }, tabbar: { indicator: '#9BF0E1', - background: '#FFFFFF', }, }, }); @@ -112,7 +111,6 @@ export const darkTheme = createTheme({ }, tabbar: { indicator: '#9BF0E1', - background: '#424242', }, }, }); diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index 8d18caf4d1..fdb9e09869 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -46,7 +46,6 @@ type PaletteAdditions = { sidebar: string; tabbar: { indicator: string; - background: string; }; bursts: { fontColor: string; From cec47d421b6cdc4241534b47cf052a25dd1c7c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20DOREAU?= Date: Sat, 6 Jun 2020 18:04:08 +0200 Subject: [PATCH 05/38] fix(core): lint error --- packages/core/src/components/Tabs/Tabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/components/Tabs/Tabs.tsx b/packages/core/src/components/Tabs/Tabs.tsx index e56cb93bfb..c52f0dea12 100644 --- a/packages/core/src/components/Tabs/Tabs.tsx +++ b/packages/core/src/components/Tabs/Tabs.tsx @@ -104,7 +104,7 @@ export const Tabs: FC = ({ tabs }) => { Math.floor(flattenIndex / newChunkedElementSize), flattenIndex % newChunkedElementSize, ]); - }, [width]); + }, [width, navIndex, numberOfChunkedElement, tabs, value]); const currentIndex = navIndex === value[0] ? value[1] : false; From 235007d2955f7d40e547c3333ba0c097eede8116 Mon Sep 17 00:00:00 2001 From: nikek Date: Mon, 8 Jun 2020 14:06:35 +0200 Subject: [PATCH 06/38] Collapsible sidebar item for auth providers --- packages/app/src/components/Root/Root.tsx | 4 +- packages/core/src/layout/Sidebar/Items.tsx | 1 - .../core/src/layout/Sidebar/UserSettings.tsx | 83 +++++++++++++++++++ packages/core/src/layout/Sidebar/index.ts | 1 + 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/layout/Sidebar/UserSettings.tsx diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 20eabbcac5..cdb8469ee2 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -31,7 +31,7 @@ import { SidebarDivider, SidebarSearchField, SidebarSpace, - SidebarUserBadge, + SidebarUserSettings, SidebarThemeToggle, } from '@backstage/core'; @@ -84,7 +84,7 @@ const Root: FC<{}> = ({ children }) => ( - + {children} diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index ddd71cc189..412800447a 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -148,7 +148,6 @@ export const SidebarItem: FC = ({ ); } - return ( (); // for scrolling down when collapse item opens + + const googleAuth = useApi(googleAuthApiRef); + const [profile, setProfile] = useState(); + + // TODO(soapraj): List all the providers supported by the app and let user log in from here + // TODO(soapraj): How to observe if the user is logged in + useEffect(() => { + googleAuth.getProfile({ optional: true }).then(googleProfile => { + setProfile(googleProfile); + }); + }, [googleAuth, open]); + + const handleClick = () => { + setOpen(!open); + setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300); + }; + + // Close the provider list when sidebar collapse + useEffect(() => { + if (!sidebarOpen && open) setOpen(false); + }, [sidebarOpen]); + + // Handle main auth info that is shown on the collapsible SidebarItem + let avatar; + let displayName; + if (profile) { + // const classes = useStyles(); + const email = profile.email; + const name = profile.name; + const imageUrl = profile.picture; + const avatarFallback = email.charAt(0).toUpperCase() + email.slice(1); + const emailTrimmed = email.split('@')[0]; + const displayEmail = + emailTrimmed.charAt(0).toUpperCase() + emailTrimmed.slice(1); + displayName = name ?? displayEmail; + avatar = imageUrl + ? () => + : () => {avatarFallback[0]}; + } + + return ( + <> + + + {open ? : } + + + + + profile ? googleAuth.logout() : googleAuth.getAccessToken() + } + > + + + + + + ); +} diff --git a/packages/core/src/layout/Sidebar/index.ts b/packages/core/src/layout/Sidebar/index.ts index baefc3d0e8..2bd961e197 100644 --- a/packages/core/src/layout/Sidebar/index.ts +++ b/packages/core/src/layout/Sidebar/index.ts @@ -33,3 +33,4 @@ export { } from './config'; export type { SidebarContextType } from './config'; export { SidebarThemeToggle } from './SidebarThemeToggle'; +export { SidebarUserSettings } from './UserSettings'; From eea242f14fa3702b0a152cbdff80846984a35da0 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Mon, 8 Jun 2020 16:45:43 +0200 Subject: [PATCH 07/38] List auth providers in UserSettings --- .../core/src/layout/Sidebar/UserSettings.tsx | 148 +++++++++++++++--- 1 file changed, 125 insertions(+), 23 deletions(-) diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index 26cffa0c35..f4f22dd151 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -1,32 +1,110 @@ +/* + * 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, { useState, useContext, useEffect, useRef } from 'react'; import Collapse from '@material-ui/core/Collapse'; import ExpandLess from '@material-ui/icons/ExpandLess'; import ExpandMore from '@material-ui/icons/ExpandMore'; import StarBorder from '@material-ui/icons/StarBorder'; +import Star from '@material-ui/icons/Star'; import { SidebarContext } from './config'; -import { SidebarItem, SidebarDivider } from './Items'; +import { SidebarItem } from './Items'; import AccountCircleIcon from '@material-ui/icons/AccountCircle'; import Divider from '@material-ui/core/Divider'; -import { useApi, googleAuthApiRef, ProfileInfo } from '@backstage/core-api'; -import { Avatar, IconButton } from '@material-ui/core'; +import { + useApi, + googleAuthApiRef, + githubAuthApiRef, + ProfileInfo, +} from '@backstage/core-api'; +import { Avatar, IconButton, makeStyles, Tooltip } from '@material-ui/core'; import PowerButton from '@material-ui/icons/PowerSettingsNew'; -import { SidebarThemeToggle } from './SidebarThemeToggle'; +// import { SidebarThemeToggle } from './SidebarThemeToggle'; + +type Provider = { + title: string; + api: any; + identity?: boolean; + isSignedIn: boolean; + icon: any; +}; + +const useProviders = () => { + const googleAuth = useApi(googleAuthApiRef); + const githubAuth = useApi(githubAuthApiRef); + const [providers, setProviders] = useState([ + { + title: 'Google', + api: googleAuth, + identity: true, + isSignedIn: false, + icon: Star, + }, + { + title: 'Github', + api: githubAuth, + isSignedIn: false, + icon: StarBorder, + }, + ]); + + Promise.all( + providers.map((provider: Provider) => + provider.identity + ? provider.api.getIdToken({ optional: true }) + : provider.api.getAccessToken('', { optional: true }), + ), + ).then(results => { + results.map((result, i) => { + providers[i].isSignedIn = !!result; + }); + + setProviders(providers); + }); + + return providers; +}; + +const useStyles = makeStyles({ + avatar: { + width: 24, + height: 24, + }, +}); export function SidebarUserSettings() { const { isOpen: sidebarOpen } = useContext(SidebarContext); const [open, setOpen] = React.useState(false); const ref = useRef(); // for scrolling down when collapse item opens - - const googleAuth = useApi(googleAuthApiRef); + const providers = useProviders(); const [profile, setProfile] = useState(); + const classes = useStyles(); // TODO(soapraj): List all the providers supported by the app and let user log in from here // TODO(soapraj): How to observe if the user is logged in useEffect(() => { - googleAuth.getProfile({ optional: true }).then(googleProfile => { - setProfile(googleProfile); - }); - }, [googleAuth, open]); + const identityProvider = providers.find( + (provider: Provider) => provider.identity, + ); + identityProvider?.api + .getProfile({ optional: true }) + .then((userProfile: ProfileInfo) => { + setProfile(userProfile); + }); + }, [providers, open]); const handleClick = () => { setOpen(!open); @@ -36,13 +114,12 @@ export function SidebarUserSettings() { // Close the provider list when sidebar collapse useEffect(() => { if (!sidebarOpen && open) setOpen(false); - }, [sidebarOpen]); + }, [open, sidebarOpen]); // Handle main auth info that is shown on the collapsible SidebarItem let avatar; let displayName; if (profile) { - // const classes = useStyles(); const email = profile.email; const name = profile.name; const imageUrl = profile.picture; @@ -52,8 +129,12 @@ export function SidebarUserSettings() { emailTrimmed.charAt(0).toUpperCase() + emailTrimmed.slice(1); displayName = name ?? displayEmail; avatar = imageUrl - ? () => - : () => {avatarFallback[0]}; + ? () => + : () => ( + + {avatarFallback[0]} + + ); } return ( @@ -65,18 +146,39 @@ export function SidebarUserSettings() { icon={avatar || AccountCircleIcon} disableSelected > - {open ? : } + {open ? : } - - - profile ? googleAuth.logout() : googleAuth.getAccessToken() - } + {providers.map((provider: Provider) => ( + - - - + + provider.isSignedIn + ? provider.api.logout() + : provider.api.getAccessToken() + } + > + + + + + + ))} ); From 8cd640394d86070518fa8e97a8bfbc89d882dfd7 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Mon, 8 Jun 2020 16:46:56 +0200 Subject: [PATCH 08/38] PinButton wip --- packages/app/src/components/Root/Root.tsx | 2 + packages/core/src/layout/Sidebar/Items.tsx | 8 +++- packages/core/src/layout/Sidebar/Page.tsx | 2 +- .../Sidebar/{UserBadge.tsx => PinButton.tsx} | 37 +++---------------- packages/core/src/layout/Sidebar/index.ts | 2 +- 5 files changed, 17 insertions(+), 34 deletions(-) rename packages/core/src/layout/Sidebar/{UserBadge.tsx => PinButton.tsx} (63%) diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index cdb8469ee2..9c70191687 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -33,6 +33,7 @@ import { SidebarSpace, SidebarUserSettings, SidebarThemeToggle, + SidebarPinButton, } from '@backstage/core'; const useSidebarLogoStyles = makeStyles({ @@ -85,6 +86,7 @@ const Root: FC<{}> = ({ children }) => ( + {children} diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index 412800447a..0a875c9035 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -59,6 +59,7 @@ const useStyles = makeStyles(theme => { fontWeight: 'bold', whiteSpace: 'nowrap', lineHeight: 1.0, + flex: '3 1 auto', }, iconContainer: { boxSizing: 'border-box', @@ -84,6 +85,11 @@ const useStyles = makeStyles(theme => { searchContainer: { width: drawerWidthOpen - iconContainerWidth, }, + secondaryAction: { + width: theme.spacing(6), + textAlign: 'center', + marginRight: theme.spacing(1), + }, selected: { '&$root': { borderLeft: `solid ${selectedIndicatorWidth}px #9BF0E1`, @@ -165,7 +171,7 @@ export const SidebarItem: FC = ({ {text} )} - {children} +
{children}
); }; diff --git a/packages/core/src/layout/Sidebar/Page.tsx b/packages/core/src/layout/Sidebar/Page.tsx index 2a4cf0fbb2..1e2537f167 100644 --- a/packages/core/src/layout/Sidebar/Page.tsx +++ b/packages/core/src/layout/Sidebar/Page.tsx @@ -57,7 +57,7 @@ export const SidebarPage: FC<{}> = props => { return ( diff --git a/packages/core/src/layout/Sidebar/UserBadge.tsx b/packages/core/src/layout/Sidebar/PinButton.tsx similarity index 63% rename from packages/core/src/layout/Sidebar/UserBadge.tsx rename to packages/core/src/layout/Sidebar/PinButton.tsx index d3e02d26cc..5d245dadf1 100644 --- a/packages/core/src/layout/Sidebar/UserBadge.tsx +++ b/packages/core/src/layout/Sidebar/PinButton.tsx @@ -14,16 +14,12 @@ * limitations under the License. */ -import React, { FC, useContext, useEffect, useState } from 'react'; +import React, { FC, useContext } from 'react'; import { makeStyles } from '@material-ui/core'; -import AccountCircleIcon from '@material-ui/icons/AccountCircle'; -import { SidebarContext } from './config'; -import { SidebarItem } from './Items'; -import { LoggedUserBadge } from './LoggedUserBadge'; import DoubleArrowIcon from '@material-ui/icons/DoubleArrow'; +import { SidebarContext } from './config'; import { BackstageTheme } from '@backstage/theme'; import { SidebarPinStateContext } from './Page'; -import { useApi, googleAuthApiRef, ProfileInfo } from '@backstage/core-api'; const ARROW_BUTTON_SIZE = 20; const useStyles = makeStyles(theme => { @@ -36,12 +32,12 @@ const useStyles = makeStyles(theme => { right: 0, width: ARROW_BUTTON_SIZE, height: ARROW_BUTTON_SIZE, - top: `calc(50% - ${ARROW_BUTTON_SIZE / 2}px)`, + top: `calc(-50% - ${ARROW_BUTTON_SIZE / 2}px)`, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '2px 0px 0px 2px', - background: theme.palette.pinSidebarButton.icon, + background: 'blue', color: theme.palette.pinSidebarButton.background, border: 'none', outline: 'none', @@ -53,37 +49,16 @@ const useStyles = makeStyles(theme => { }; }); -export const SidebarUserBadge: FC<{}> = () => { +export const SidebarPinButton: FC<{}> = () => { + console.log('hello'); const { isOpen } = useContext(SidebarContext); const { isPinned, toggleSidebarPinState } = useContext( SidebarPinStateContext, ); const classes = useStyles({ isPinned }); - const googleAuth = useApi(googleAuthApiRef); - const [profile, setProfile] = useState(); - - useEffect(() => { - // TODO(soapraj): How to observe if the user is logged in - // TODO(soapraj): List all the providers supported by the app and let user log in from here - googleAuth.getProfile({ optional: true }).then(googleProfile => { - setProfile(googleProfile); - }); - }, [googleAuth]); return (
- {profile ? ( - <> - - - ) : ( - - )} {isOpen && (
- {locations && ( - { - return { - ...entityToComponent(val), - location: findLocationForEntity(val, locations), - }; - })) || - [] - } - loading={loading} - error={error} - actions={actions} - /> - )} + { + return { + ...entityToComponent(val), + locationSpec: findLocationForEntityMeta(val.metadata), + }; + })) || + [] + } + loading={loading} + error={error} + actions={actions} + />
diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 029fad5474..ac7abe235a 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -20,9 +20,24 @@ import CatalogTable from './CatalogTable'; import { Component } from '../../data/component'; const components: Component[] = [ - { name: 'component1', kind: 'Component', description: 'Placeholder' }, - { name: 'component2', kind: 'Component', description: 'Placeholder' }, - { name: 'component3', kind: 'Component', description: 'Placeholder' }, + { + name: 'component1', + kind: 'Component', + metadata: { name: 'component1' }, + description: 'Placeholder', + }, + { + name: 'component2', + kind: 'Component', + metadata: { name: 'component2' }, + description: 'Placeholder', + }, + { + name: 'component3', + kind: 'Component', + metadata: { name: 'component3' }, + description: 'Placeholder', + }, ]; describe('CatalogTable component', () => { @@ -48,7 +63,7 @@ describe('CatalogTable component', () => { ), ); const errorMessage = await rendered.findByText( - 'Error encountered while fetching components.', + /Error encountered while fetching components./, ); expect(errorMessage).toBeInTheDocument(); }); diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 7ff521ee67..e918cc13aa 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Progress, Table, TableColumn } from '@backstage/core'; +import { Link } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; import React, { FC } from 'react'; +import { generatePath, Link as RouterLink } from 'react-router-dom'; import { Component } from '../../data/component'; -import { InfoCard, Progress, Table, TableColumn } from '@backstage/core'; -import { Typography, Link } from '@material-ui/core'; -import { Link as RouterLink, generatePath } from 'react-router-dom'; import { entityRoute } from '../../routes'; const columns: TableColumn[] = [ @@ -51,6 +52,7 @@ type CatalogTableProps = { error?: any; actions?: any; }; + const CatalogTable: FC = ({ components, loading, @@ -60,16 +62,16 @@ const CatalogTable: FC = ({ }) => { if (loading) { return ; - } - if (error) { + } else if (error) { return ( - - - Error encountered while fetching components. - - +
+ + Error encountered while fetching components. {error.toString()} + +
); } + return ( = ({ /> ); }; + export default CatalogTable; diff --git a/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.tsx b/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.tsx index 674d979f83..66c234e55e 100644 --- a/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.tsx +++ b/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.tsx @@ -13,22 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC, useEffect, useRef, useState } from 'react'; import { IconButton, ListItemIcon, - Menu, MenuItem, + MenuList, + Popover, Typography, } from '@material-ui/core'; import Cancel from '@material-ui/icons/Cancel'; import MoreVert from '@material-ui/icons/MoreVert'; import SwapHoriz from '@material-ui/icons/SwapHoriz'; +import React, { FC, useState } from 'react'; import { makeStyles } from '@material-ui/core/styles'; +// TODO(freben): It should probably instead be the case that Header sets the theme text color to white inside itself unconditionally instead const useStyles = makeStyles({ - menu: { - marginTop: 52, + button: { + color: 'white', }, }); @@ -39,52 +41,58 @@ type ComponentContextMenuProps = { const ComponentContextMenu: FC = ({ onUnregisterComponent, }) => { - const [menuOpen, setMenuOpen] = useState(false); - const menuAnchor = useRef(null); + const [anchorEl, setAnchorEl] = useState(); const classes = useStyles(); - useEffect(() => { - const globalCloseHandler = (event: any) => { - const menu = menuAnchor.current; - if (menu !== null && !menu.contains(event.target)) { - setMenuOpen(false); - } - }; + const onOpen = (event: React.SyntheticEvent) => { + setAnchorEl(event.currentTarget); + }; - window.addEventListener('click', globalCloseHandler); - return () => window.removeEventListener('click', globalCloseHandler); - }, [menuOpen]); + const onClose = () => { + setAnchorEl(undefined); + }; return ( -
+
setMenuOpen(!menuOpen)} + onClick={onOpen} data-testid="menu-button" + className={classes.button} > - - - - - - Unregister component - - - - - - More repository - - + + { + onClose(); + onUnregisterComponent(); + }} + > + + + + Unregister component + + + + + + Move repository + + +
); }; + export default ComponentContextMenu; diff --git a/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx index 62e3d52c29..5b6e7dfe07 100644 --- a/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx +++ b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx @@ -23,6 +23,7 @@ describe('ComponentMetadataCard component', () => { const testComponent: Component = { name: 'test', kind: 'Component', + metadata: { name: 'test' }, description: 'Placeholder', }; const rendered = await render( diff --git a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx index 7dde95c8f0..59c79cddc6 100644 --- a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx +++ b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC } from 'react'; + +import { Entity, LOCATION_ANNOTATION } from '@backstage/catalog-model'; +import { Progress, useApi } from '@backstage/core'; import { Button, Dialog, @@ -21,14 +23,16 @@ import { DialogContent, DialogContentText, DialogTitle, + Typography, useMediaQuery, useTheme, } from '@material-ui/core'; -import { Component } from '../../data/component'; +import Alert from '@material-ui/lab/Alert'; +import React, { FC } from 'react'; import { useAsync } from 'react-use'; -import { useApi } from '@backstage/core'; +import { AsyncState } from 'react-use/lib/useAsync'; import { catalogApiRef } from '../../api/types'; -import { Entity } from '@backstage/catalog-model'; +import { Component } from '../../data/component'; type ComponentRemovalDialogProps = { onConfirm: () => any; @@ -36,44 +40,81 @@ type ComponentRemovalDialogProps = { onClose: () => any; component: Component; }; + +function useColocatedEntities(component: Component): AsyncState { + const catalogApi = useApi(catalogApiRef); + return useAsync(async () => { + const myLocation = component.metadata.annotations?.[LOCATION_ANNOTATION]; + return myLocation + ? await catalogApi.getEntities({ [LOCATION_ANNOTATION]: myLocation }) + : []; + }, [catalogApi, component]); +} + const ComponentRemovalDialog: FC = ({ onConfirm, onCancel, onClose, component, }) => { - const catalogApi = useApi(catalogApiRef); - const { value } = useAsync(async () => { - let colocatedEntities: Array = []; - const locationId = component.location?.id; - if (locationId) { - colocatedEntities = await catalogApi.getEntitiesByLocationId(locationId); - } - return colocatedEntities; - }); + const { value: entities, loading, error } = useColocatedEntities(component); const theme = useTheme(); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - const infoMessage = `This action will unregister ${ - value ? value.map(e => e.metadata.name).join(', ') : '' - } from location with target ${component.location?.target}. To undo, - just re-register the component in Backstage.`; + return ( Are you sure you want to unregister this component? - {infoMessage} + {loading ? : null} + {error ? ( + + + {error.toString()} + + + ) : null} + {entities ? ( + <> + + This action will unregister the following entities: + + +
    + {entities.map(e => ( +
  • {e.metadata.name}
  • + ))} +
+
+ + That are located at the following location: + + +
    +
  • + {entities[0]?.metadata?.annotations?.[LOCATION_ANNOTATION]} +
  • +
+
+ + To undo, just re-register the component in Backstage. + + + ) : null}
- - +
); }; + export default ComponentRemovalDialog; diff --git a/plugins/catalog/src/data/component.ts b/plugins/catalog/src/data/component.ts index 73b349391b..86749c6faa 100644 --- a/plugins/catalog/src/data/component.ts +++ b/plugins/catalog/src/data/component.ts @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { EntityMeta } from '@backstage/catalog-model'; import { ReactNode } from 'react'; -import { Location } from '@backstage/catalog-model'; export type Component = { name: string; kind: string; + metadata: EntityMeta; description: ReactNode; - location?: Location; }; diff --git a/plugins/catalog/src/data/utils.tsx b/plugins/catalog/src/data/utils.tsx index 93d28f5ca2..3e8e0458d6 100644 --- a/plugins/catalog/src/data/utils.tsx +++ b/plugins/catalog/src/data/utils.tsx @@ -13,23 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { Component } from './component'; import { Entity, - Location, + LocationSpec, LOCATION_ANNOTATION, + EntityMeta, } from '@backstage/catalog-model'; -import Edit from '@material-ui/icons/Edit'; import IconButton from '@material-ui/core/IconButton'; import { styled } from '@material-ui/core/styles'; +import Edit from '@material-ui/icons/Edit'; +import React from 'react'; +import { Component } from './component'; const DescriptionWrapper = styled('span')({ display: 'flex', alignItems: 'center', }); -const createEditLink = (location: Location): string => { +const createEditLink = (location: LocationSpec): string => { switch (location.type) { case 'github': return location.target.replace('/blob/', '/edit/'); @@ -38,13 +39,12 @@ const createEditLink = (location: Location): string => { } }; -export function entityToComponent( - envelope: Entity, - location?: Location, -): Component { +export function entityToComponent(envelope: Entity): Component { + const location = findLocationForEntityMeta(envelope.metadata); return { name: envelope.metadata?.name ?? '', kind: envelope.kind ?? 'unknown', + metadata: envelope.metadata, description: ( {envelope.metadata?.annotations?.description ?? 'placeholder'} @@ -57,18 +57,28 @@ export function entityToComponent( ) : null} ), - location, }; } -export function findLocationForEntity( - entity: Entity, - locations: Location[], -): Location | undefined { - for (const loc of locations) { - if (loc.id === entity.metadata.annotations?.[LOCATION_ANNOTATION]) { - return loc; - } +export function findLocationForEntityMeta( + meta: EntityMeta, +): LocationSpec | undefined { + if (!meta) { + return undefined; } - return undefined; + + const annotation = meta.annotations?.[LOCATION_ANNOTATION]; + if (!annotation) { + return undefined; + } + + const separatorIndex = annotation.indexOf(':'); + if (separatorIndex === -1) { + return undefined; + } + + return { + type: annotation.substring(0, separatorIndex), + target: annotation.substring(separatorIndex + 1), + }; } diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx index ca463550d1..48130124b7 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx @@ -31,7 +31,6 @@ const catalogApi: jest.Mocked = { getEntityByName: jest.fn(), getLocationByEntity: jest.fn(), getLocationById: jest.fn(), - getEntitiesByLocationId: jest.fn(), }; const setup = () => ({ From e2dc24d237d7c50ee1af4975776ff3027e9e9ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 9 Jun 2020 10:26:37 +0200 Subject: [PATCH 12/38] Remove final DOM nesting warning --- .../ComponentRemovalDialog/ComponentRemovalDialog.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx index 59c79cddc6..1318edcca0 100644 --- a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx +++ b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx @@ -69,11 +69,9 @@ const ComponentRemovalDialog: FC = ({ {loading ? : null} {error ? ( - - - {error.toString()} - - + + {error.toString()} + ) : null} {entities ? ( <> From 00d33f93e42d5dc01068cf0c48568fd91c9f968e Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Tue, 9 Jun 2020 13:28:40 +0200 Subject: [PATCH 13/38] Enable Filtering of the Catalog Page (#1143) * feat(catalog/Filters): Add ability to render a React component as the count. Good for loading states and async data rendering for counts. * chore(Catalog/filters): Updating the table so that we use loading states of the table rather than the panel * chore(Catalog/filters): added some nice things for enabling the filtering with some nice count componeents * feat(Catalog/filters): Fetch the correct data and added in enum types to make some nice resolvers * chore(Catalog/filters): Use the new enum type here * chore(Catalog/filters): Removing the unused import to fix lintig * chore(Catalog/filters): Addressing some PR comments * feat(catalog/filters): Making WebStorage return the same instance for the same bucket for subscriptions * chore(core/Storage): fixing some issues with different instances of the storage and adding tests for it * chore(catalog/filters): fixing some tests and trying to remove some of the act warnings in the tests --- .../StorageApi/WebStorage.test.ts | 9 +++ .../implementations/StorageApi/WebStorage.ts | 8 +- .../DismissableBanner.stories.tsx | 2 +- .../DismissableBanner.test.tsx | 2 +- .../DismissableBanner/DismissableBanner.tsx | 2 +- plugins/catalog/package.json | 4 +- .../CatalogFilter/AllServicesCount.tsx | 32 ++++++++ .../CatalogFilter/CatalogFilter.test.tsx | 38 +++++++-- .../CatalogFilter/CatalogFilter.tsx | 13 +-- .../CatalogFilter/StarredCount.test.tsx | 33 ++++++++ .../components/CatalogFilter/StarredCount.tsx | 22 +++++ .../CatalogPage/CatalogPage.test.tsx | 53 +++++------- .../components/CatalogPage/CatalogPage.tsx | 14 +++- .../CatalogTable/CatalogTable.test.tsx | 10 --- .../components/CatalogTable/CatalogTable.tsx | 17 ++-- .../ComponentContextMenu.test.tsx | 2 +- .../ComponentPage/ComponentPage.test.tsx | 9 ++- plugins/catalog/src/data/filters.ts | 39 +++++++-- .../catalog/src/hooks/useStarredEntites.ts | 43 ++++++++++ .../src/hooks/useStarredEntities.test.tsx | 80 +++++++++++++++++++ plugins/catalog/src/types.ts | 6 ++ yarn.lock | 40 ++++++++++ 22 files changed, 400 insertions(+), 78 deletions(-) create mode 100644 plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx create mode 100644 plugins/catalog/src/components/CatalogFilter/StarredCount.test.tsx create mode 100644 plugins/catalog/src/components/CatalogFilter/StarredCount.tsx create mode 100644 plugins/catalog/src/hooks/useStarredEntites.ts create mode 100644 plugins/catalog/src/hooks/useStarredEntities.test.tsx diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts index a96813347e..971b023629 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -15,6 +15,7 @@ */ import { WebStorage } from './WebStorage'; import { CreateStorageApiOptions, StorageApi } from '../../definitions'; + describe('WebStorage Storage API', () => { const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; const createWebStorage = ( @@ -161,4 +162,12 @@ describe('WebStorage Storage API', () => { }), ); }); + + it('should return a singleton for the same namespace and same bucket', async () => { + const rootStorage = createWebStorage({ + namespace: '/Test/Mock/Thing/Thing ', + }); + + expect(rootStorage.forBucket('test')).toBe(rootStorage.forBucket('test')); + }); }); diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts index 8af8f760cf..b9b01cd20b 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -22,6 +22,8 @@ import { import { Observable } from '../../../types'; import ObservableImpl from 'zen-observable'; +const buckets = new Map(); + export class WebStorage implements StorageApi { constructor( private readonly namespace: string, @@ -46,7 +48,11 @@ export class WebStorage implements StorageApi { } forBucket(name: string): WebStorage { - return new WebStorage(`${this.namespace}/${name}`, this.errorApi); + const bucketPath = `${this.namespace}/${name}`; + if (!buckets.has(bucketPath)) { + buckets.set(bucketPath, new WebStorage(bucketPath, this.errorApi)); + } + return buckets.get(bucketPath)!; } async set(key: string, data: T): Promise { diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx b/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx index 37d982b0f4..78307bd5a3 100644 --- a/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx +++ b/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx @@ -25,7 +25,7 @@ import { storageApiRef, StorageApi, WebStorage, -} from '@backstage/core'; +} from '@backstage/core-api'; export default { title: 'DismissableBanner', diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.test.tsx b/packages/core/src/components/DismissableBanner/DismissableBanner.test.tsx index b31641eb8f..b3a33fc089 100644 --- a/packages/core/src/components/DismissableBanner/DismissableBanner.test.tsx +++ b/packages/core/src/components/DismissableBanner/DismissableBanner.test.tsx @@ -25,7 +25,7 @@ import { CreateStorageApiOptions, StorageApi, WebStorage, -} from '@backstage/core'; +} from '@backstage/core-api'; describe('', () => { let apis: ApiRegistry; diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core/src/components/DismissableBanner/DismissableBanner.tsx index 76c27827ec..e174ac8d55 100644 --- a/packages/core/src/components/DismissableBanner/DismissableBanner.tsx +++ b/packages/core/src/components/DismissableBanner/DismissableBanner.tsx @@ -15,7 +15,7 @@ */ import React, { FC, ReactNode, useState, useEffect } from 'react'; -import { useApi, storageApiRef } from '@backstage/core'; +import { useApi, storageApiRef } from '@backstage/core-api'; import { useObservable } from 'react-use'; import classNames from 'classnames'; import { makeStyles, Theme } from '@material-ui/core'; diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 28bfcb0af0..e19869c946 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -42,11 +42,13 @@ "@backstage/test-utils": "^0.1.1-alpha.6", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", + "@testing-library/react-hooks": "^3.3.0", "@testing-library/user-event": "^10.2.4", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", "@types/testing-library__jest-dom": "^5.0.4", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "react-test-renderer": "^16.13.1" }, "files": [ "dist/**/*.{js,d.ts}" diff --git a/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx b/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx new file mode 100644 index 0000000000..07879770b7 --- /dev/null +++ b/plugins/catalog/src/components/CatalogFilter/AllServicesCount.tsx @@ -0,0 +1,32 @@ +/* + * 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 { useApi } from '@backstage/core'; +import { catalogApiRef } from '../../api/types'; +import { useAsync } from 'react-use'; +import { CircularProgress, useTheme } from '@material-ui/core'; + +export const AllServicesCount: React.FC<{}> = () => { + const theme = useTheme(); + const catalogApi = useApi(catalogApiRef); + const { value, loading } = useAsync(() => catalogApi.getEntities()); + + if (loading) { + return ; + } + + return {value?.length ?? '-'}; +}; diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx index ec44f53777..6246726fab 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; import { CatalogFilter, CatalogFilterGroup } from './CatalogFilter'; +import { FilterGroupItem } from '../../types'; describe('Catalog Filter', () => { it('should render the different groups', async () => { @@ -40,11 +41,11 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: 'first', + id: FilterGroupItem.ALL, label: 'First Label', }, { - id: 'second', + id: FilterGroupItem.STARRED, label: 'Second Label', }, ], @@ -67,12 +68,12 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: 'first', + id: FilterGroupItem.ALL, label: 'First Label', count: 100, }, { - id: 'second', + id: FilterGroupItem.STARRED, label: 'Second Label', count: 400, }, @@ -96,12 +97,12 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: 'first', + id: FilterGroupItem.ALL, label: 'First Label', count: 100, }, { - id: 'second', + id: FilterGroupItem.STARRED, label: 'Second Label', count: 400, }, @@ -128,4 +129,29 @@ describe('Catalog Filter', () => { expect(onSelectedChangeHandler).toHaveBeenCalledWith(item); }); + + it('should render a component when a function is passed to the count component', async () => { + const mockGroups: CatalogFilterGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: FilterGroupItem.ALL, + label: 'First Label', + count: () => BACKSTAGE!, + }, + { + id: FilterGroupItem.STARRED, + label: 'Second Label', + count: 400, + }, + ], + }, + ]; + const { findByText } = render( + wrapInTestApp(), + ); + + expect(await findByText('BACKSTAGE!')).toBeInTheDocument(); + }); }); diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index eb45d8a80b..6541a4df8d 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -25,13 +25,12 @@ import { makeStyles, } from '@material-ui/core'; import type { IconComponent } from '@backstage/core'; - +import { FilterGroupItem } from '../../types'; export type CatalogFilterItem = { - id: string; + id: FilterGroupItem; label: string; icon?: IconComponent; - count?: number; - loading?: boolean; + count?: number | React.FC; }; export type CatalogFilterGroup = { @@ -105,7 +104,11 @@ export const CatalogFilter: React.FC = ({ {item.label} - {item.count} + {typeof item.count === 'function' ? ( + + ) : ( + item.count + )} ))} diff --git a/plugins/catalog/src/components/CatalogFilter/StarredCount.test.tsx b/plugins/catalog/src/components/CatalogFilter/StarredCount.test.tsx new file mode 100644 index 0000000000..57581e98f2 --- /dev/null +++ b/plugins/catalog/src/components/CatalogFilter/StarredCount.test.tsx @@ -0,0 +1,33 @@ +/* + * 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 { StarredCount } from './StarredCount'; +import * as Hooks from '../../hooks/useStarredEntites'; + +describe('Starred Count', () => { + it('should render the count returned from the hook', async () => { + jest.spyOn(Hooks, 'useStarredEntities').mockReturnValue({ + starredEntities: new Set(['id1', 'id2', 'id3', 'id4']), + }); + + const { findByText } = render(wrapInTestApp()); + + expect(await findByText('4')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog/src/components/CatalogFilter/StarredCount.tsx b/plugins/catalog/src/components/CatalogFilter/StarredCount.tsx new file mode 100644 index 0000000000..20e1be1a9a --- /dev/null +++ b/plugins/catalog/src/components/CatalogFilter/StarredCount.tsx @@ -0,0 +1,22 @@ +/* + * 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 { useStarredEntities } from '../../hooks/useStarredEntites'; + +export const StarredCount: React.FC<{}> = () => { + const { starredEntities } = useStarredEntities(); + return {starredEntities.size}; +}; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 238a5a4043..c2130d5110 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -18,47 +18,34 @@ import React from 'react'; import { render } from '@testing-library/react'; import CatalogPage from './CatalogPage'; import { - ApiProvider, ApiRegistry, - CreateStorageApiOptions, + ApiProvider, errorApiRef, storageApiRef, - StorageApi, WebStorage, } from '@backstage/core'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { wrapInTestApp, MockErrorApi } from '@backstage/test-utils'; import { catalogApiRef } from '../..'; import { CatalogApi } from '../../api/types'; import { Entity } from '@backstage/catalog-model'; -const errorApi = { post: () => {} }; -const catalogApi: Partial = { - getEntities: () => - Promise.resolve([ - { - metadata: { - name: 'Entity1', - }, - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - }, - ] as Entity[]), - getLocationByEntity: () => - Promise.resolve({ id: 'id', type: 'github', target: 'url' }), -}; - -const mockWebStorageErrorApi = { post: jest.fn(), error$: jest.fn() }; -const createWebStorage = ( - args?: Partial, -): StorageApi => { - return WebStorage.create({ - errorApi: mockWebStorageErrorApi, - ...args, - }); -}; -const storageApi = createWebStorage(); - describe('CatalogPage', () => { + const mockErrorApi = new MockErrorApi(); + const catalogApi: Partial = { + getEntities: () => + Promise.resolve([ + { + metadata: { + name: 'Entity1', + }, + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + }, + ] as Entity[]), + getLocationByEntity: () => + Promise.resolve({ id: 'id', type: 'github', target: 'url' }), + }; + // this test right now causes some red lines in the log output when running tests // related to some theme issues in mui-table // https://github.com/mbrn/material-table/issues/1293 @@ -67,9 +54,9 @@ describe('CatalogPage', () => { wrapInTestApp( diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 452254a3cf..859449c715 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -27,19 +27,22 @@ import { useApi, } from '@backstage/core'; import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; -import { Button, Link, makeStyles, Typography } from '@material-ui/core'; +import { Button, makeStyles, Typography, Link } from '@material-ui/core'; import GitHub from '@material-ui/icons/GitHub'; import React, { FC, useCallback, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { useAsync } from 'react-use'; import { catalogApiRef } from '../..'; import { Component } from '../../data/component'; -import { defaultFilter, filterGroups } from '../../data/filters'; +import { defaultFilter, filterGroups, dataResolvers } from '../../data/filters'; import { entityToComponent, findLocationForEntityMeta } from '../../data/utils'; import { CatalogFilter, CatalogFilterItem, } from '../CatalogFilter/CatalogFilter'; + +import { useStarredEntities } from '../../hooks/useStarredEntites'; + import CatalogTable from '../CatalogTable/CatalogTable'; const useStyles = makeStyles(theme => ({ @@ -57,15 +60,20 @@ const useStyles = makeStyles(theme => ({ const CatalogPage: FC<{}> = () => { const catalogApi = useApi(catalogApiRef); - const { value, error, loading } = useAsync(() => catalogApi.getEntities()); + const { starredEntities } = useStarredEntities(); const [selectedFilter, setSelectedFilter] = useState( defaultFilter, ); + const { value, error, loading } = useAsync( + () => dataResolvers[selectedFilter.id]({ catalogApi, starredEntities }), + [selectedFilter.id], + ); const onFilterSelected = useCallback( selected => setSelectedFilter(selected), [], ); + const styles = useStyles(); const actions = [ diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index ac7abe235a..41693188f1 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -41,16 +41,6 @@ const components: Component[] = [ ]; describe('CatalogTable component', () => { - it('should render loading when loading prop it set to true', async () => { - const rendered = render( - wrapInTestApp( - , - ), - ); - const progress = await rendered.findByTestId('progress'); - expect(progress).toBeInTheDocument(); - }); - it('should render error message when error is passed in props', async () => { const rendered = render( wrapInTestApp( diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index e918cc13aa..cd91838b06 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Progress, Table, TableColumn } from '@backstage/core'; +import { Table, TableColumn } from '@backstage/core'; import { Link } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React, { FC } from 'react'; -import { generatePath, Link as RouterLink } from 'react-router-dom'; +import { Link as RouterLink, generatePath } from 'react-router-dom'; import { Component } from '../../data/component'; + import { entityRoute } from '../../routes'; const columns: TableColumn[] = [ @@ -60,9 +61,7 @@ const CatalogTable: FC = ({ titlePreamble, actions, }) => { - if (loading) { - return ; - } else if (error) { + if (error) { return (
@@ -74,8 +73,14 @@ const CatalogTable: FC = ({ return (
{ it('should call onUnregisterComponent on button click', async () => { await act(async () => { const mockCallback = jest.fn(); - const menu = await render( + const menu = render( , ); const button = await menu.findByTestId('menu-button'); diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx index f580a84d8a..252f2d4e3f 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import ComponentPage from './ComponentPage'; -import { render } from '@testing-library/react'; +import { render, wait } from '@testing-library/react'; import * as React from 'react'; import { wrapInTestApp } from '@backstage/test-utils'; import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; @@ -38,7 +38,7 @@ const errorApi = { post: () => {} }; describe('ComponentPage', () => { it('should redirect to component table page when name is not provided', async () => { const props = getTestProps(''); - await render( + render( wrapInTestApp( { , ), ); - expect(props.history.push).toHaveBeenCalledWith('/catalog'); + + await wait(() => + expect(props.history.push).toHaveBeenCalledWith('/catalog'), + ); }); }); diff --git a/plugins/catalog/src/data/filters.ts b/plugins/catalog/src/data/filters.ts index 1b0a2a407c..1fcdfa27a2 100644 --- a/plugins/catalog/src/data/filters.ts +++ b/plugins/catalog/src/data/filters.ts @@ -19,21 +19,26 @@ import { } from '../components/CatalogFilter/CatalogFilter'; import SettingsIcon from '@material-ui/icons/Settings'; import StarIcon from '@material-ui/icons/Star'; +import { StarredCount } from '../components/CatalogFilter/StarredCount'; +import { AllServicesCount } from '../components/CatalogFilter/AllServicesCount'; +import { FilterGroupItem } from '../types'; +import { CatalogApi } from '../..'; +import { Entity } from '@backstage/catalog-model'; export const filterGroups: CatalogFilterGroup[] = [ { name: 'Personal', items: [ { - id: 'owned', + id: FilterGroupItem.OWNED, label: 'Owned', - count: 123, + count: 0, icon: SettingsIcon, }, { - id: 'starred', + id: FilterGroupItem.STARRED, label: 'Starred', - count: 10, + count: StarredCount, icon: StarIcon, }, ], @@ -43,12 +48,34 @@ export const filterGroups: CatalogFilterGroup[] = [ name: 'Company', items: [ { - id: 'all', + id: FilterGroupItem.ALL, label: 'All Services', - count: 123, + count: AllServicesCount, }, ], }, ]; +type ResolverFunction = ({ + catalogApi, + starredEntities, +}: { + catalogApi: CatalogApi; + starredEntities: Set; +}) => Promise; + +export const dataResolvers: Record = { + [FilterGroupItem.OWNED]: async () => [], + [FilterGroupItem.ALL]: async ({ catalogApi }) => { + return catalogApi.getEntities(); + }, + [FilterGroupItem.STARRED]: async ({ catalogApi, starredEntities }) => { + const allEntities = await catalogApi.getEntities(); + + return allEntities.filter(entity => + starredEntities.has(entity.metadata.name), + ); + }, +}; + export const defaultFilter: CatalogFilterItem = filterGroups[0].items[0]; diff --git a/plugins/catalog/src/hooks/useStarredEntites.ts b/plugins/catalog/src/hooks/useStarredEntites.ts new file mode 100644 index 0000000000..631991e9b0 --- /dev/null +++ b/plugins/catalog/src/hooks/useStarredEntites.ts @@ -0,0 +1,43 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { useApi, storageApiRef } from '@backstage/core'; +import { useObservable } from 'react-use'; + +export const useStarredEntities = () => { + const storageApi = useApi(storageApiRef); + const settingsStore = storageApi.forBucket('settings'); + const rawStarredItems = settingsStore.get('starredEntities') ?? []; + + const [starredEntities, setStarredEntities] = useState( + new Set(rawStarredItems), + ); + + const observedItems = useObservable( + settingsStore.observe$('starredEntities'), + ); + + useEffect(() => { + if (observedItems?.newValue) { + const currentValue = observedItems?.newValue ?? []; + setStarredEntities(new Set(currentValue)); + } + }, [observedItems?.newValue]); + + return { + starredEntities, + }; +}; diff --git a/plugins/catalog/src/hooks/useStarredEntities.test.tsx b/plugins/catalog/src/hooks/useStarredEntities.test.tsx new file mode 100644 index 0000000000..49890b7238 --- /dev/null +++ b/plugins/catalog/src/hooks/useStarredEntities.test.tsx @@ -0,0 +1,80 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useStarredEntities } from './useStarredEntites'; +import { + ApiProvider, + ApiRegistry, + storageApiRef, + WebStorage, + StorageApi, +} from '@backstage/core'; +import { MockErrorApi } from '@backstage/test-utils'; + +describe('useStarredEntities', () => { + let mockStorage: StorageApi | undefined; + + const wrapper: React.FC<{}> = ({ children }) => { + return ( + + {children} + + ); + }; + + beforeEach(() => { + mockStorage = new WebStorage('@backstage', new MockErrorApi()).forBucket( + Date.now().toString(), // TODO(blam): need something that changes every test run for now until the MockStorage is implemented + ); + }); + it('should return an empty set for when there is no items in storage', async () => { + const { result } = renderHook(() => useStarredEntities(), { wrapper }); + + expect(result.current.starredEntities.size).toBe(0); + }); + it('should return a set with the current items when there is items in storage', async () => { + const expectedIds = ['i', 'am', 'some', 'test', 'ids']; + const store = mockStorage?.forBucket('settings'); + await store?.set('starredEntities', expectedIds); + + const { result } = renderHook(() => useStarredEntities(), { wrapper }); + + for (const item of expectedIds) { + expect(result.current.starredEntities.has(item)).toBeTruthy(); + } + }); + it('should listen to changes when the storage is set elsewhere', async () => { + const store = mockStorage?.forBucket('settings'); + + const { result, waitForNextUpdate } = renderHook( + () => useStarredEntities(), + { wrapper }, + ); + + expect(result.current.starredEntities.size).toBe(0); + expect(result.current.starredEntities.has('something')).toBeFalsy(); + + // Make this happen after awaiting for the next update so we can + // catch when the hook re-renders with the latest data + setTimeout(() => store?.set('starredEntities', ['something']), 1); + + await waitForNextUpdate(); + + expect(result.current.starredEntities.size).toBe(1); + expect(result.current.starredEntities.has('something')).toBeTruthy(); + }); +}); diff --git a/plugins/catalog/src/types.ts b/plugins/catalog/src/types.ts index 0f91e1ed8e..42f156ecb7 100644 --- a/plugins/catalog/src/types.ts +++ b/plugins/catalog/src/types.ts @@ -157,3 +157,9 @@ export type KindParser = { envelope: DescriptorEnvelope, ): Promise; }; + +export enum FilterGroupItem { + ALL = 'ALL', + STARRED = 'STARRED', + OWNED = 'OWNED', +} diff --git a/yarn.lock b/yarn.lock index 074e666e55..2b7246b230 100644 --- a/yarn.lock +++ b/yarn.lock @@ -891,6 +891,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.5.4": + version "7.10.2" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839" + integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.3.3", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": version "7.8.6" resolved "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" @@ -3279,6 +3286,14 @@ lodash "^4.17.15" redent "^3.0.0" +"@testing-library/react-hooks@^3.3.0": + version "3.3.0" + resolved "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-3.3.0.tgz#dc217bfce8e7c34a99c811d73d23feef957b7c1d" + integrity sha512-rE9geI1+HJ6jqXkzzJ6abREbeud6bLF8OmF+Vyc7gBoPwZAEVBYjbC1up5nNoVfYBhO5HUwdD4u9mTehAUeiyw== + dependencies: + "@babel/runtime" "^7.5.4" + "@types/testing-library__react-hooks" "^3.0.0" + "@testing-library/react@^9.3.2": version "9.5.0" resolved "https://registry.npmjs.org/@testing-library/react/-/react-9.5.0.tgz#71531655a7890b61e77a1b39452fbedf0472ca5e" @@ -3910,6 +3925,13 @@ dependencies: "@types/react" "*" +"@types/react-test-renderer@*": + version "16.9.2" + resolved "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.2.tgz#e1c408831e8183e5ad748fdece02214a7c2ab6c5" + integrity sha512-4eJr1JFLIAlWhzDkBCkhrOIWOvOxcCAfQh+jiKg7l/nNZcCIL2MHl2dZhogIFKyHzedVWHaVP1Yydq/Ruu4agw== + dependencies: + "@types/react" "*" + "@types/react-textarea-autosize@^4.3.3": version "4.3.5" resolved "https://registry.npmjs.org/@types/react-textarea-autosize/-/react-textarea-autosize-4.3.5.tgz#6c4d2753fa1864c98c0b2b517f67bb1f6e4c46de" @@ -4067,6 +4089,14 @@ dependencies: "@types/jest" "*" +"@types/testing-library__react-hooks@^3.0.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.2.0.tgz#52f3a109bef06080e3b1e3ae7ea1c014ce859897" + integrity sha512-dE8iMTuR5lzB+MqnxlzORlXzXyCL0EKfzH0w/lau20OpkHD37EaWjZDz0iNG8b71iEtxT4XKGmSKAGVEqk46mw== + dependencies: + "@types/react" "*" + "@types/react-test-renderer" "*" + "@types/testing-library__react@^9.1.2": version "9.1.3" resolved "https://registry.npmjs.org/@types/testing-library__react/-/testing-library__react-9.1.3.tgz#35eca61cc6ea923543796f16034882a1603d7302" @@ -15617,6 +15647,16 @@ react-syntax-highlighter@^12.2.1: prismjs "^1.8.4" refractor "^2.4.1" +react-test-renderer@^16.13.1: + version "16.13.1" + resolved "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1" + integrity sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ== + dependencies: + object-assign "^4.1.1" + prop-types "^15.6.2" + react-is "^16.8.6" + scheduler "^0.19.1" + react-textarea-autosize@^7.1.0: version "7.1.2" resolved "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-7.1.2.tgz#70fdb333ef86bcca72717e25e623e90c336e2cda" From 40e95c06a8b546e21b933528e93b42f9267ac4c9 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 9 Jun 2020 14:21:30 +0200 Subject: [PATCH 14/38] Moved edit button in catalog to actions menu --- .../components/CatalogPage/CatalogPage.tsx | 25 +++++++++++++ .../catalog/src/data/{utils.tsx => utils.ts} | 36 ++----------------- 2 files changed, 28 insertions(+), 33 deletions(-) rename plugins/catalog/src/data/{utils.tsx => utils.ts} (57%) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 859449c715..3bd09af0bd 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -26,9 +26,11 @@ import { SupportButton, useApi, } from '@backstage/core'; +import { LocationSpec } from '@backstage/catalog-model'; import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; import { Button, makeStyles, Typography, Link } from '@material-ui/core'; import GitHub from '@material-ui/icons/GitHub'; +import Edit from '@material-ui/icons/Edit'; import React, { FC, useCallback, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { useAsync } from 'react-use'; @@ -89,6 +91,29 @@ const CatalogPage: FC<{}> = () => { hidden: location ? location?.type !== 'github' : true, }; }, + (rowData: Component) => { + const createEditLink = (location: LocationSpec): string => { + switch (location.type) { + case 'github': + return location.target.replace('/blob/', '/edit/'); + default: + return location.target; + } + }; + + const location = findLocationForEntityMeta(rowData.metadata); + + return { + icon: Edit, + tooltip: 'Edit', + iconProps: { size: 'small' }, + onClick: () => { + if (!location) return; + window.open(createEditLink(location), '_blank'); + }, + hidden: location ? location?.type !== 'github' : true, + }; + }, ]; // TODO: replace me with the proper tabs implemntation diff --git a/plugins/catalog/src/data/utils.tsx b/plugins/catalog/src/data/utils.ts similarity index 57% rename from plugins/catalog/src/data/utils.tsx rename to plugins/catalog/src/data/utils.ts index 3e8e0458d6..b731268c41 100644 --- a/plugins/catalog/src/data/utils.tsx +++ b/plugins/catalog/src/data/utils.ts @@ -19,44 +19,14 @@ import { LOCATION_ANNOTATION, EntityMeta, } from '@backstage/catalog-model'; -import IconButton from '@material-ui/core/IconButton'; -import { styled } from '@material-ui/core/styles'; -import Edit from '@material-ui/icons/Edit'; -import React from 'react'; import { Component } from './component'; -const DescriptionWrapper = styled('span')({ - display: 'flex', - alignItems: 'center', -}); - -const createEditLink = (location: LocationSpec): string => { - switch (location.type) { - case 'github': - return location.target.replace('/blob/', '/edit/'); - default: - return location.target; - } -}; - export function entityToComponent(envelope: Entity): Component { - const location = findLocationForEntityMeta(envelope.metadata); return { - name: envelope.metadata?.name ?? '', - kind: envelope.kind ?? 'unknown', + name: envelope.metadata.name, + kind: envelope.kind, metadata: envelope.metadata, - description: ( - - {envelope.metadata?.annotations?.description ?? 'placeholder'} - {location?.target ? ( - - - - - - ) : null} - - ), + description: envelope.metadata.annotations?.description ?? 'placeholder', }; } From c53bafc5f9d864a738ea2cb4a66454f03b10ef9c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Jun 2020 14:24:56 +0200 Subject: [PATCH 15/38] v0.1.1-alpha.7 --- lerna.json | 2 +- packages/app/package.json | 28 ++++++++++++------------- packages/backend-common/package.json | 4 ++-- packages/backend/package.json | 18 ++++++++-------- packages/catalog-model/package.json | 4 ++-- packages/cli/package.json | 6 +++--- packages/config-loader/package.json | 4 ++-- packages/config/package.json | 2 +- packages/core-api/package.json | 10 ++++----- packages/core/package.json | 12 +++++------ packages/dev-utils/package.json | 10 ++++----- packages/storybook/package.json | 4 ++-- packages/test-utils-core/package.json | 2 +- packages/test-utils/package.json | 10 ++++----- packages/theme/package.json | 4 ++-- plugins/auth-backend/package.json | 12 +++++------ plugins/catalog-backend/package.json | 8 +++---- plugins/catalog/package.json | 18 ++++++++-------- plugins/circleci/package.json | 10 ++++----- plugins/explore/package.json | 12 +++++------ plugins/graphiql/package.json | 12 +++++------ plugins/home-page/package.json | 10 ++++----- plugins/identity-backend/package.json | 6 +++--- plugins/lighthouse/package.json | 12 +++++------ plugins/register-component/package.json | 14 ++++++------- plugins/scaffolder-backend/package.json | 6 +++--- plugins/scaffolder/package.json | 10 ++++----- plugins/sentry-backend/package.json | 6 +++--- plugins/sentry/package.json | 10 ++++----- plugins/tech-radar/package.json | 12 +++++------ plugins/welcome/package.json | 10 ++++----- 31 files changed, 144 insertions(+), 144 deletions(-) diff --git a/lerna.json b/lerna.json index 5be143e0ef..2feea0137f 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "packages": ["packages/*", "plugins/*"], "npmClient": "yarn", "useWorkspaces": true, - "version": "0.1.1-alpha.6" + "version": "0.1.1-alpha.7" } diff --git a/packages/app/package.json b/packages/app/package.json index 15681d0548..6e314c3fe0 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,21 +1,21 @@ { "name": "example-app", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "private": true, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/plugin-catalog": "^0.1.1-alpha.6", - "@backstage/plugin-circleci": "^0.1.1-alpha.6", - "@backstage/plugin-explore": "^0.1.1-alpha.6", - "@backstage/plugin-home-page": "^0.1.1-alpha.6", - "@backstage/plugin-lighthouse": "^0.1.1-alpha.6", - "@backstage/plugin-register-component": "^0.1.1-alpha.6", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.6", - "@backstage/plugin-sentry": "^0.1.1-alpha.6", - "@backstage/plugin-tech-radar": "^0.1.1-alpha.6", - "@backstage/plugin-welcome": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/plugin-catalog": "^0.1.1-alpha.7", + "@backstage/plugin-circleci": "^0.1.1-alpha.7", + "@backstage/plugin-explore": "^0.1.1-alpha.7", + "@backstage/plugin-home-page": "^0.1.1-alpha.7", + "@backstage/plugin-lighthouse": "^0.1.1-alpha.7", + "@backstage/plugin-register-component": "^0.1.1-alpha.7", + "@backstage/plugin-scaffolder": "^0.1.1-alpha.7", + "@backstage/plugin-sentry": "^0.1.1-alpha.7", + "@backstage/plugin-tech-radar": "^0.1.1-alpha.7", + "@backstage/plugin-welcome": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "prop-types": "^15.7.2", diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 847f034550..94dd222c41 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist", "types": "src/index.ts", "private": false, @@ -32,7 +32,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", "@types/express": "^4.17.6", "@types/http-errors": "^1.6.3", "@types/morgan": "^1.9.0", diff --git a/packages/backend/package.json b/packages/backend/package.json index 51fe04e51d..65e2976489 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist", "types": "src/index.ts", "private": true, @@ -17,13 +17,13 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.6", - "@backstage/catalog-model": "^0.1.1-alpha.6", - "@backstage/plugin-auth-backend": "^0.1.1-alpha.6", - "@backstage/plugin-catalog-backend": "^0.1.1-alpha.6", - "@backstage/plugin-identity-backend": "^0.1.1-alpha.6", - "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.6", - "@backstage/plugin-sentry-backend": "^0.1.1-alpha.6", + "@backstage/backend-common": "^0.1.1-alpha.7", + "@backstage/catalog-model": "^0.1.1-alpha.7", + "@backstage/plugin-auth-backend": "^0.1.1-alpha.7", + "@backstage/plugin-catalog-backend": "^0.1.1-alpha.7", + "@backstage/plugin-identity-backend": "^0.1.1-alpha.7", + "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.7", + "@backstage/plugin-sentry-backend": "^0.1.1-alpha.7", "compression": "^1.7.4", "cors": "^2.8.5", "esm": "^3.2.25", @@ -34,7 +34,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", "@types/compression": "^1.7.0", "@types/cors": "^2.8.6", "@types/express": "^4.17.6", diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 2de2e70e8e..d2a63560e3 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", "main:src": "src/index.ts", @@ -26,7 +26,7 @@ "yup": "^0.28.5" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", "@types/jest": "^25.2.2", "@types/lodash": "^4.14.151", "@types/yup": "^0.28.2", diff --git a/packages/cli/package.json b/packages/cli/package.json index 30e56bf353..58b6c3fd84 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "private": false, "publishConfig": { "access": "public" @@ -29,8 +29,8 @@ "backstage-cli": "bin/backstage-cli" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.6", - "@backstage/config-loader": "^0.1.1-alpha.6", + "@backstage/config": "^0.1.1-alpha.7", + "@backstage/config-loader": "^0.1.1-alpha.7", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", "@lerna/project": "^3.18.0", diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 2641e988d6..7018dc40a8 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "private": false, "publishConfig": { "access": "public", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.6", + "@backstage/config": "^0.1.1-alpha.7", "fs-extra": "^9.0.0", "yaml": "^1.9.2" }, diff --git a/packages/config/package.json b/packages/config/package.json index 2ff5450a0b..c67264d4c9 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "private": false, "publishConfig": { "access": "public", diff --git a/packages/core-api/package.json b/packages/core-api/package.json index c91a96ab31..f08b03a77e 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-api", "description": "Internal Core API used by Backstage plugins and apps", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "private": false, "publishConfig": { "access": "public", @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/config": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@types/react": "^16.9", @@ -42,8 +42,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/test-utils-core": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/test-utils-core": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/packages/core/package.json b/packages/core/package.json index 6233f27065..8a476a58d5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "private": false, "publishConfig": { "access": "public", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.6", - "@backstage/core-api": "0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/config": "^0.1.1-alpha.7", + "@backstage/core-api": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -55,8 +55,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/test-utils": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/test-utils": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index eea0956a8b..01753db4d8 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "private": false, "publishConfig": { "access": "public", @@ -30,10 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/test-utils": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/test-utils": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^5.7.0", diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 883fb1452a..132bf29c72 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -1,6 +1,6 @@ { "name": "storybook", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "description": "Storybook build for core package", "private": true, "scripts": { @@ -14,7 +14,7 @@ ] }, "dependencies": { - "@backstage/theme": "^0.1.1-alpha.6" + "@backstage/theme": "^0.1.1-alpha.7" }, "devDependencies": { "@storybook/addon-actions": "^5.3.17", diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index b1ef87729d..f0b98add94 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils-core", "description": "Utilities to test Backstage core", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "private": false, "publishConfig": { "access": "public", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 2a9320ee63..f48fe65c30 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "private": false, "publishConfig": { "access": "public", @@ -30,10 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/core-api": "^0.1.1-alpha.6", - "@backstage/test-utils-core": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/core-api": "^0.1.1-alpha.7", + "@backstage/test-utils-core": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", diff --git a/packages/theme/package.json b/packages/theme/package.json index 501ef6473e..8b017d4cf9 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "private": false, "publishConfig": { "access": "public", @@ -32,7 +32,7 @@ "@material-ui/core": "^4.9.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6" + "@backstage/cli": "^0.1.1-alpha.7" }, "files": [ "dist/**/*.{js,d.ts}" diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index d662f3eb6c..f45a1cbf7a 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist", "types": "src/index.ts", "license": "Apache-2.0", @@ -15,8 +15,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.6", + "@backstage/backend-common": "^0.1.1-alpha.7", "@types/cookie-parser": "^1.4.2", + "@types/jwt-decode": "2.2.1", "@types/passport": "^1.0.3", "@types/passport-github2": "^1.2.4", "@types/passport-google-oauth20": "^2.0.3", @@ -28,18 +29,17 @@ "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", "helmet": "^3.22.0", + "jwt-decode": "2.2.0", "morgan": "^1.10.0", "passport": "^0.4.1", "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", "passport-saml": "^1.3.3", "winston": "^3.2.1", - "yn": "^4.0.0", - "jwt-decode": "2.2.0", - "@types/jwt-decode": "2.2.1" + "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", "@types/body-parser": "^1.19.0", "@types/passport-saml": "^1.1.2", "jest-fetch-mock": "^3.0.3", diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 253959c61b..7490d1221a 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist", "types": "src/index.ts", "license": "Apache-2.0", @@ -16,8 +16,8 @@ "mock-data": "./scripts/mock-data" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.6", - "@backstage/catalog-model": "^0.1.1-alpha.6", + "@backstage/backend-common": "^0.1.1-alpha.7", + "@backstage/catalog-model": "^0.1.1-alpha.7", "compression": "^1.7.4", "cors": "^2.8.5", "esm": "^3.2.25", @@ -37,7 +37,7 @@ "yup": "^0.28.5" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", "@types/lodash": "^4.14.151", "@types/node-fetch": "^2.5.7", "@types/supertest": "^2.0.8", diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index e19869c946..7b8c1ccc70 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,11 +22,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.6", - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.6", - "@backstage/plugin-sentry": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/catalog-model": "^0.1.1-alpha.7", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/plugin-scaffolder": "^0.1.1-alpha.7", + "@backstage/plugin-sentry": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -37,9 +37,9 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/dev-utils": "^0.1.1-alpha.6", - "@backstage/test-utils": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/dev-utils": "^0.1.1-alpha.7", + "@backstage/test-utils": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index d58ea6b406..c1570eaaad 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -31,8 +31,8 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -47,8 +47,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/dev-utils": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/dev-utils": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 400d212e69..15df6fae75 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,8 +22,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -33,9 +33,9 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/dev-utils": "^0.1.1-alpha.6", - "@backstage/test-utils": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/dev-utils": "^0.1.1-alpha.7", + "@backstage/test-utils": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index d4141aa367..1132ced6da 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "private": false, "publishConfig": { "access": "public", @@ -32,8 +32,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -44,9 +44,9 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/dev-utils": "^0.1.1-alpha.6", - "@backstage/test-utils": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/dev-utils": "^0.1.1-alpha.7", + "@backstage/test-utils": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/home-page/package.json b/plugins/home-page/package.json index 54144cefe3..cf4b077dfc 100644 --- a/plugins/home-page/package.json +++ b/plugins/home-page/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home-page", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,8 +22,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,8 +32,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/dev-utils": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/dev-utils": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/identity-backend/package.json b/plugins/identity-backend/package.json index 30512ee658..922ff8412d 100644 --- a/plugins/identity-backend/package.json +++ b/plugins/identity-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-identity-backend", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist", "types": "src/index.ts", "license": "Apache-2.0", @@ -15,7 +15,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.6", + "@backstage/backend-common": "^0.1.1-alpha.7", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", @@ -27,7 +27,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", "jest-fetch-mock": "^3.0.3", "tsc-watch": "^4.2.3" }, diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index fce604b609..eeb6bfa0bd 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,8 +22,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -34,9 +34,9 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/dev-utils": "^0.1.1-alpha.6", - "@backstage/test-utils": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/dev-utils": "^0.1.1-alpha.7", + "@backstage/test-utils": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 3f14d2f4cb..f1b28e99d2 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-register-component", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,10 +22,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/catalog-model": "^0.1.1-alpha.6", - "@backstage/plugin-catalog": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/catalog-model": "^0.1.1-alpha.7", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/plugin-catalog": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -37,8 +37,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/dev-utils": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/dev-utils": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 244294cd42..2fa5462fcc 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist", "types": "src/index.ts", "license": "Apache-2.0", @@ -14,7 +14,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.6", + "@backstage/backend-common": "^0.1.1-alpha.7", "compression": "^1.7.4", "cors": "^2.8.5", "dockerode": "^3.2.0", @@ -27,7 +27,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", "@types/fs-extra": "^9.0.1", "@types/supertest": "^2.0.8", "supertest": "^4.0.2" diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index b03fc39772..bc54e5f7c5 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,8 +22,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -33,8 +33,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/dev-utils": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/dev-utils": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/sentry-backend/package.json b/plugins/sentry-backend/package.json index 3d4abb8790..c9d7a585e4 100644 --- a/plugins/sentry-backend/package.json +++ b/plugins/sentry-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry-backend", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist", "types": "src/index.ts", "license": "Apache-2.0", @@ -15,7 +15,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.6", + "@backstage/backend-common": "^0.1.1-alpha.7", "axios": "^0.19.2", "compression": "^1.7.4", "cors": "^2.8.5", @@ -28,7 +28,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", "jest-fetch-mock": "^3.0.3", "tsc-watch": "^4.2.3" }, diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index f539afa886..cdf2025cb8 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,8 +22,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -34,8 +34,8 @@ "timeago.js": "^4.0.2" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/dev-utils": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/dev-utils": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 5327ce7cfb..a5befefe2f 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-radar", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,9 +22,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/test-utils-core": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/test-utils-core": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -37,8 +37,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/dev-utils": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/dev-utils": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 6debcc9c80..fd6aa917b4 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-welcome", - "version": "0.1.1-alpha.6", + "version": "0.1.1-alpha.7", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,8 +22,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.6", - "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/core": "^0.1.1-alpha.7", + "@backstage/theme": "^0.1.1-alpha.7", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -33,8 +33,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/dev-utils": "^0.1.1-alpha.6", + "@backstage/cli": "^0.1.1-alpha.7", + "@backstage/dev-utils": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", From 709cc4004d7585990013603b97b41ffbfaa10adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 8 Jun 2020 13:57:38 +0200 Subject: [PATCH 16/38] feat(backend-common): add common code for service shell --- packages/backend-common/package.json | 5 + packages/backend-common/src/index.ts | 1 + .../src/service/ServiceBuilderImpl.ts | 117 ++++++++++++++++++ .../src/service/createServiceBuilder.ts | 24 ++++ packages/backend-common/src/service/index.ts | 18 +++ packages/backend-common/src/service/types.ts | 65 ++++++++++ packages/backend/package.json | 5 - packages/backend/src/index.ts | 57 +++------ plugins/catalog-backend/package.json | 3 - .../src/service/standaloneApplication.ts | 71 ----------- .../src/service/standaloneServer.ts | 33 +++-- 11 files changed, 263 insertions(+), 136 deletions(-) create mode 100644 packages/backend-common/src/service/ServiceBuilderImpl.ts create mode 100644 packages/backend-common/src/service/createServiceBuilder.ts create mode 100644 packages/backend-common/src/service/index.ts create mode 100644 packages/backend-common/src/service/types.ts delete mode 100644 plugins/catalog-backend/src/service/standaloneApplication.ts diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 94dd222c41..54202975c3 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -27,12 +27,17 @@ "clean": "backstage-cli clean" }, "dependencies": { + "compression": "^1.7.4", + "cors": "^2.8.5", "express": "^4.17.1", + "helmet": "^3.22.0", "morgan": "^1.10.0", "winston": "^3.2.1" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.7", + "@types/compression": "^1.7.0", + "@types/cors": "^2.8.6", "@types/express": "^4.17.6", "@types/http-errors": "^1.6.3", "@types/morgan": "^1.9.0", diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index b2c38ab506..11689aafff 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -17,3 +17,4 @@ export * from './errors'; export * from './logging'; export * from './middleware'; +export * from './service'; diff --git a/packages/backend-common/src/service/ServiceBuilderImpl.ts b/packages/backend-common/src/service/ServiceBuilderImpl.ts new file mode 100644 index 0000000000..ac35d52112 --- /dev/null +++ b/packages/backend-common/src/service/ServiceBuilderImpl.ts @@ -0,0 +1,117 @@ +/* + * 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 compression from 'compression'; +import cors from 'cors'; +import express, { Router } from 'express'; +import helmet from 'helmet'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { getRootLogger } from '../logging'; +import { + errorHandler, + notFoundHandler, + requestLoggingHandler, +} from '../middleware'; +import { ServiceBuilder } from './types'; + +const DEFAULT_PORT = 7000; + +export class ServiceBuilderImpl implements ServiceBuilder { + private port: number | undefined; + private logger: Logger | undefined; + private corsOptions: cors.CorsOptions | undefined; + private routers: [string, Router][]; + + constructor() { + this.routers = []; + } + + setPort(port: number): ServiceBuilder { + this.port = port; + return this; + } + + setLogger(logger: Logger): ServiceBuilder { + this.logger = logger; + return this; + } + + enableCors(options: cors.CorsOptions): ServiceBuilder { + this.corsOptions = options; + return this; + } + + addRouter(root: string, router: Router): ServiceBuilder { + this.routers.push([root, router]); + return this; + } + + start(): Promise { + const app = express(); + const { port, logger, corsOptions } = this.getOptions(); + + app.use(helmet()); + if (corsOptions) { + app.use(cors(corsOptions)); + } + app.use(compression()); + app.use(express.json()); + app.use(requestLoggingHandler()); + for (const [root, route] of this.routers) { + app.use(root, route); + } + app.use(notFoundHandler()); + app.use(errorHandler()); + + return new Promise((resolve, reject) => { + app.on('error', e => { + logger.error(`Failed to start up on port ${port}, ${e}`); + reject(e); + }); + const server = app.listen(port, () => { + logger.info(`Listening on port ${port}`); + }); + resolve(server); + }); + } + + private getOptions(): { + port: number; + logger: Logger; + corsOptions?: cors.CorsOptions; + } { + let port: number; + if (this.port !== undefined) { + port = this.port; + } else { + port = parseInt(process.env.PORT ?? '', 10) || DEFAULT_PORT; + } + + let logger: Logger; + if (this.logger) { + logger = this.logger; + } else { + logger = getRootLogger(); + } + + return { + port, + logger, + corsOptions: this.corsOptions, + }; + } +} diff --git a/packages/backend-common/src/service/createServiceBuilder.ts b/packages/backend-common/src/service/createServiceBuilder.ts new file mode 100644 index 0000000000..ffd8901def --- /dev/null +++ b/packages/backend-common/src/service/createServiceBuilder.ts @@ -0,0 +1,24 @@ +/* + * 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 { ServiceBuilderImpl } from './ServiceBuilderImpl'; + +/** + * Creates a new service builder. + */ +export function createServiceBuilder() { + return new ServiceBuilderImpl(); +} diff --git a/packages/backend-common/src/service/index.ts b/packages/backend-common/src/service/index.ts new file mode 100644 index 0000000000..9bba9d6baf --- /dev/null +++ b/packages/backend-common/src/service/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 { createServiceBuilder } from './createServiceBuilder'; +export type { ServiceBuilder } from './types'; diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts new file mode 100644 index 0000000000..306f0d587e --- /dev/null +++ b/packages/backend-common/src/service/types.ts @@ -0,0 +1,65 @@ +/* + * 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 cors from 'cors'; +import { Router } from 'express'; +import { Server } from 'http'; +import { Logger } from 'winston'; + +export type ServiceBuilder = { + /** + * Sets the port to listen on. + * + * If no port is specified, the service will first look for an environment + * variable named PORT and use that if present, otherwise it picks a default + * port (7000). + * + * @param port The port to listen on + */ + setPort(port: number): ServiceBuilder; + + /** + * Sets the logger to use for service-specific logging. + * + * If no logger is given, the default root logger is used. + * + * @param logger A winston logger + */ + setLogger(logger: Logger): ServiceBuilder; + + /** + * Enables CORS handling using the given settings. + * + * If this method is not called, the resulting service will not have any + * built in CORS handling. + * + * @param options Standard CORS options + */ + enableCors(options: cors.CorsOptions): ServiceBuilder; + + /** + * Adds a router (similar to the express .use call) to the service. + * + * @param root The root URL to bind to (e.g. "/api/function1") + * @param router An express router + */ + addRouter(root: string, router: Router): ServiceBuilder; + + /** + * Starts the server using the given settings. + */ + start(): Promise; +}; diff --git a/packages/backend/package.json b/packages/backend/package.json index 65e2976489..4e2610380c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -24,19 +24,14 @@ "@backstage/plugin-identity-backend": "^0.1.1-alpha.7", "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.7", "@backstage/plugin-sentry-backend": "^0.1.1-alpha.7", - "compression": "^1.7.4", - "cors": "^2.8.5", "esm": "^3.2.25", "express": "^4.17.1", - "helmet": "^3.22.0", "knex": "^0.21.1", "sqlite3": "^4.2.0", "winston": "^3.2.1" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.7", - "@types/compression": "^1.7.0", - "@types/cors": "^2.8.6", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", "@types/helmet": "^0.0.47", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 5b1fc54330..0f0733a8ec 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -22,27 +22,15 @@ * Happy hacking! */ -import { - errorHandler, - getRootLogger, - notFoundHandler, - requestLoggingHandler, -} from '@backstage/backend-common'; -import compression from 'compression'; -import cors from 'cors'; -import express from 'express'; -import helmet from 'helmet'; +import { createServiceBuilder, getRootLogger } from '@backstage/backend-common'; import knex from 'knex'; +import auth from './plugins/auth'; import catalog from './plugins/catalog'; +import identity from './plugins/identity'; import scaffolder from './plugins/scaffolder'; import sentry from './plugins/sentry'; -import auth from './plugins/auth'; -import identity from './plugins/identity'; import { PluginEnvironment } from './types'; -const DEFAULT_PORT = 7000; -const PORT = parseInt(process.env.PORT ?? '', 10) || DEFAULT_PORT; - function createEnv(plugin: string): PluginEnvironment { const logger = getRootLogger().child({ type: 'plugin', plugin }); const database = knex({ @@ -57,30 +45,23 @@ function createEnv(plugin: string): PluginEnvironment { } async function main() { - const app = express(); - const corsOptions: cors.CorsOptions = { - origin: 'http://localhost:3000', - credentials: true, - }; + const service = createServiceBuilder() + .enableCors({ + origin: 'http://localhost:3000', + credentials: true, + }) + .addRouter('/catalog', await catalog(createEnv('catalog'))) + .addRouter('/scaffolder', await scaffolder(createEnv('scaffolder'))) + .addRouter( + '/sentry', + await sentry(getRootLogger().child({ type: 'plugin', plugin: 'sentry' })), + ) + .addRouter('/auth', await auth(createEnv('auth'))) + .addRouter('/identity', await identity(createEnv('identity'))); - app.use(helmet()); - app.use(cors(corsOptions)); - app.use(compression()); - app.use(express.json()); - app.use(requestLoggingHandler()); - app.use('/catalog', await catalog(createEnv('catalog'))); - app.use('/scaffolder', await scaffolder(createEnv('scaffolder'))); - app.use( - '/sentry', - await sentry(getRootLogger().child({ type: 'plugin', plugin: 'sentry' })), - ); - app.use('/auth', await auth(createEnv('auth'))); - app.use('/identity', await identity(createEnv('identity'))); - app.use(notFoundHandler()); - app.use(errorHandler()); - - app.listen(PORT, () => { - getRootLogger().info(`Listening on port ${PORT}`); + await service.start().catch(err => { + console.log(err); + process.exit(1); }); } diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 7490d1221a..761c042723 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -18,13 +18,10 @@ "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.7", "@backstage/catalog-model": "^0.1.1-alpha.7", - "compression": "^1.7.4", - "cors": "^2.8.5", "esm": "^3.2.25", "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", - "helmet": "^3.22.0", "knex": "^0.21.1", "lodash": "^4.17.15", "morgan": "^1.10.0", diff --git a/plugins/catalog-backend/src/service/standaloneApplication.ts b/plugins/catalog-backend/src/service/standaloneApplication.ts deleted file mode 100644 index e8c1a226f0..0000000000 --- a/plugins/catalog-backend/src/service/standaloneApplication.ts +++ /dev/null @@ -1,71 +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 { - errorHandler, - notFoundHandler, - requestLoggingHandler, -} from '@backstage/backend-common'; -import compression from 'compression'; -import cors from 'cors'; -import express from 'express'; -import helmet from 'helmet'; -import { Logger } from 'winston'; -import { EntitiesCatalog, LocationsCatalog } from '../catalog'; -import { HigherOrderOperation } from '../ingestion'; -import { createRouter } from './router'; - -export interface ApplicationOptions { - enableCors: boolean; - entitiesCatalog: EntitiesCatalog; - locationsCatalog?: LocationsCatalog; - higherOrderOperation?: HigherOrderOperation; - logger: Logger; -} - -export async function createStandaloneApplication( - options: ApplicationOptions, -): Promise { - const { - enableCors, - entitiesCatalog, - locationsCatalog, - higherOrderOperation, - logger, - } = options; - const app = express(); - - app.use(helmet()); - if (enableCors) { - app.use(cors()); - } - app.use(compression()); - app.use(express.json()); - app.use(requestLoggingHandler()); - app.use( - '/catalog', - await createRouter({ - entitiesCatalog, - locationsCatalog, - higherOrderOperation, - logger, - }), - ); - app.use(notFoundHandler()); - app.use(errorHandler()); - - return app; -} diff --git a/plugins/catalog-backend/src/service/standaloneServer.ts b/plugins/catalog-backend/src/service/standaloneServer.ts index 11fbd0b445..f19dfaf282 100644 --- a/plugins/catalog-backend/src/service/standaloneServer.ts +++ b/plugins/catalog-backend/src/service/standaloneServer.ts @@ -14,13 +14,15 @@ * limitations under the License. */ +import { createServiceBuilder } from '@backstage/backend-common'; import { Server } from 'http'; import { Logger } from 'winston'; +import { HigherOrderOperations } from '..'; import { DatabaseEntitiesCatalog } from '../catalog/DatabaseEntitiesCatalog'; import { DatabaseLocationsCatalog } from '../catalog/DatabaseLocationsCatalog'; import { DatabaseManager } from '../database/DatabaseManager'; -import { HigherOrderOperations, LocationReaders } from '../ingestion'; -import { createStandaloneApplication } from './standaloneApplication'; +import { createRouter } from './router'; +import { LocationReaders } from '../ingestion'; export interface ServerOptions { port: number; @@ -33,11 +35,11 @@ export async function startStandaloneServer( ): Promise { const logger = options.logger.child({ service: 'catalog-backend' }); + logger.debug('Creating application...'); const db = await DatabaseManager.createInMemoryDatabase(logger); - const entitiesCatalog = new DatabaseEntitiesCatalog(db); const locationsCatalog = new DatabaseLocationsCatalog(db); - const locationReader = new LocationReaders(options.logger); + const locationReader = new LocationReaders(); const higherOrderOperation = new HigherOrderOperations( entitiesCatalog, locationsCatalog, @@ -45,25 +47,18 @@ export async function startStandaloneServer( logger, ); - logger.debug('Creating application...'); - const app = await createStandaloneApplication({ - enableCors: options.enableCors, + logger.debug('Starting application server...'); + const router = await createRouter({ entitiesCatalog, locationsCatalog, higherOrderOperation, logger, }); - - logger.debug('Starting application server...'); - return await new Promise((resolve, reject) => { - const server = app.listen(options.port, (err?: Error) => { - if (err) { - reject(err); - return; - } - - logger.info(`Listening on port ${options.port}`); - resolve(server); - }); + const service = createServiceBuilder() + .enableCors({ origin: 'http://localhost:3000' }) + .addRouter('/catalog', router); + return await service.start().catch(err => { + logger.error(err); + process.exit(1); }); } From 90f253aaf89bf220993d082a20c8c5e9d078fc21 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 9 Jun 2020 15:20:38 +0200 Subject: [PATCH 17/38] feat(catalog/star): Ability to star items in the catalog table --- .../components/CatalogPage/CatalogPage.tsx | 16 ++++++++-- .../catalog/src/hooks/useStarredEntites.ts | 15 ++++++++- .../src/hooks/useStarredEntities.test.tsx | 32 ++++++++++++++++++- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 859449c715..d7c8825c9b 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -29,6 +29,8 @@ import { import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; import { Button, makeStyles, Typography, Link } from '@material-ui/core'; import GitHub from '@material-ui/icons/GitHub'; +import StarOutline from '@material-ui/icons/StarBorder'; +import Star from '@material-ui/icons/Star'; import React, { FC, useCallback, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { useAsync } from 'react-use'; @@ -60,13 +62,14 @@ const useStyles = makeStyles(theme => ({ const CatalogPage: FC<{}> = () => { const catalogApi = useApi(catalogApiRef); - const { starredEntities } = useStarredEntities(); + const { starredEntities, toggleStarredEntity } = useStarredEntities(); const [selectedFilter, setSelectedFilter] = useState( defaultFilter, ); + const { value, error, loading } = useAsync( () => dataResolvers[selectedFilter.id]({ catalogApi, starredEntities }), - [selectedFilter.id], + [selectedFilter.id, starredEntities.size], ); const onFilterSelected = useCallback( @@ -89,6 +92,15 @@ const CatalogPage: FC<{}> = () => { hidden: location ? location?.type !== 'github' : true, }; }, + (rowData: Component) => { + return { + icon: starredEntities.has(rowData.metadata.name) ? Star : StarOutline, + toolTip: `${ + starredEntities.has(rowData.metadata.name) ? 'Unstar' : 'Star' + } ${rowData.metadata.name}`, + onClick: () => toggleStarredEntity(rowData.metadata.name), + }; + }, ]; // TODO: replace me with the proper tabs implemntation diff --git a/plugins/catalog/src/hooks/useStarredEntites.ts b/plugins/catalog/src/hooks/useStarredEntites.ts index 631991e9b0..3d1f902583 100644 --- a/plugins/catalog/src/hooks/useStarredEntites.ts +++ b/plugins/catalog/src/hooks/useStarredEntites.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useApi, storageApiRef } from '@backstage/core'; import { useObservable } from 'react-use'; @@ -37,7 +37,20 @@ export const useStarredEntities = () => { } }, [observedItems?.newValue]); + const toggleStarredEntity = useCallback( + (entity: string) => { + if (starredEntities.has(entity)) { + starredEntities.delete(entity); + } else { + starredEntities.add(entity); + } + + settingsStore.set('starredEntities', Array.from(starredEntities)); + }, + [starredEntities, settingsStore], + ); return { starredEntities, + toggleStarredEntity, }; }; diff --git a/plugins/catalog/src/hooks/useStarredEntities.test.tsx b/plugins/catalog/src/hooks/useStarredEntities.test.tsx index 49890b7238..8127d71d50 100644 --- a/plugins/catalog/src/hooks/useStarredEntities.test.tsx +++ b/plugins/catalog/src/hooks/useStarredEntities.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react-hooks'; import { useStarredEntities } from './useStarredEntites'; import { ApiProvider, @@ -77,4 +77,34 @@ describe('useStarredEntities', () => { expect(result.current.starredEntities.size).toBe(1); expect(result.current.starredEntities.has('something')).toBeTruthy(); }); + + it('should write new entries to the local store when adding a togglging entity', async () => { + const store = mockStorage?.forBucket('settings'); + + await store?.set('starredEntities', ['something1']); + + const { result } = renderHook(() => useStarredEntities(), { wrapper }); + + act(() => { + result.current.toggleStarredEntity('something2'); + }); + + expect(result.current.starredEntities.has('something2')).toBeTruthy(); + expect(result.current.starredEntities.has('something1')).toBeTruthy(); + }); + + it('should remove an existing entity when toggling entries', async () => { + const store = mockStorage?.forBucket('settings'); + + await store?.set('starredEntities', ['something1', 'something2']); + + const { result } = renderHook(() => useStarredEntities(), { wrapper }); + + act(() => { + result.current.toggleStarredEntity('something2'); + }); + + expect(result.current.starredEntities.has('something2')).toBeFalsy(); + expect(result.current.starredEntities.has('something1')).toBeTruthy(); + }); }); From 8a3b1ec93e0d5a2184446f0d459a0923e28e966e Mon Sep 17 00:00:00 2001 From: nikek Date: Tue, 9 Jun 2020 15:32:34 +0200 Subject: [PATCH 18/38] make the sidebar pin button show up again --- packages/core/src/layout/Sidebar/Page.tsx | 2 +- packages/core/src/layout/Sidebar/PinButton.tsx | 8 ++++---- packages/theme/src/themes.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/src/layout/Sidebar/Page.tsx b/packages/core/src/layout/Sidebar/Page.tsx index 1e2537f167..2a4cf0fbb2 100644 --- a/packages/core/src/layout/Sidebar/Page.tsx +++ b/packages/core/src/layout/Sidebar/Page.tsx @@ -57,7 +57,7 @@ export const SidebarPage: FC<{}> = props => { return ( diff --git a/packages/core/src/layout/Sidebar/PinButton.tsx b/packages/core/src/layout/Sidebar/PinButton.tsx index 5d245dadf1..8c52eeb24d 100644 --- a/packages/core/src/layout/Sidebar/PinButton.tsx +++ b/packages/core/src/layout/Sidebar/PinButton.tsx @@ -26,19 +26,20 @@ const useStyles = makeStyles(theme => { return { root: { position: 'relative', + alignSelf: 'stretch', }, arrowButtonWrapper: { position: 'absolute', right: 0, width: ARROW_BUTTON_SIZE, height: ARROW_BUTTON_SIZE, - top: `calc(-50% - ${ARROW_BUTTON_SIZE / 2}px)`, + top: -(theme.spacing(6) + ARROW_BUTTON_SIZE) / 2, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '2px 0px 0px 2px', - background: 'blue', - color: theme.palette.pinSidebarButton.background, + background: theme.palette.pinSidebarButton.background, + color: theme.palette.pinSidebarButton.icon, border: 'none', outline: 'none', cursor: 'pointer', @@ -50,7 +51,6 @@ const useStyles = makeStyles(theme => { }); export const SidebarPinButton: FC<{}> = () => { - console.log('hello'); const { isOpen } = useContext(SidebarContext); const { isPinned, toggleSidebarPinState } = useContext( SidebarPinStateContext, diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index fe3c71a212..55aa5a2900 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -57,8 +57,8 @@ export const lightTheme = createTheme({ gold: yellow.A700, sidebar: '#171717', pinSidebarButton: { - icon: '#BDBDBD', - background: '#404040', + icon: '#181818', + background: '#BDBDBD', }, }, }); @@ -103,7 +103,7 @@ export const darkTheme = createTheme({ gold: yellow.A700, sidebar: '#424242', pinSidebarButton: { - icon: '#181818', + icon: '#404040', background: '#BDBDBD', }, }, From 9e8cab219a7c4656ca3defec4f99b38fc6568727 Mon Sep 17 00:00:00 2001 From: nikek Date: Tue, 9 Jun 2020 15:36:24 +0200 Subject: [PATCH 19/38] remove LoggedUserBadge --- .../src/layout/Sidebar/LoggedUserBadge.tsx | 298 ------------------ 1 file changed, 298 deletions(-) delete mode 100644 packages/core/src/layout/Sidebar/LoggedUserBadge.tsx diff --git a/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx b/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx deleted file mode 100644 index d0c3dc62c2..0000000000 --- a/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx +++ /dev/null @@ -1,298 +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 React, { FC, useState, useEffect } from 'react'; -import { makeStyles, Theme } from '@material-ui/core/styles'; -import { sidebarConfig } from './config'; -import { - Avatar, - ListItem, - ListItemAvatar, - ListItemText, - Popover, - List, - ListItemIcon, - ListItemSecondaryAction, - IconButton, - Tooltip, - Typography, -} from '@material-ui/core'; -import { blueGrey } from '@material-ui/core/colors'; -import { useSetState } from 'react-use'; -import { Skeleton } from '@material-ui/lab'; -import { useApi, googleAuthApiRef, ProfileInfo } from '@backstage/core-api'; -import LogoutIcon from '@material-ui/icons/PowerSettingsNew'; -import ControlPointIcon from '@material-ui/icons/ControlPoint'; -import AccountCircleIcon from '@material-ui/icons/AccountCircle'; - -const useStyles = makeStyles(theme => { - const { drawerWidthOpen, userBadgeDiameter } = sidebarConfig; - return { - root: { - width: drawerWidthOpen, - display: 'flex', - alignItems: 'center', - paddingLeft: 18, - paddingTop: 14, - paddingBottom: 14, - color: '#b5b5b5', - }, - avatar: { - width: userBadgeDiameter, - height: userBadgeDiameter, - marginRight: 8, - }, - purple: { - color: theme.palette.getContrastText(blueGrey[500]), - backgroundColor: blueGrey[500], - }, - listItemText: { - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }; -}); - -const SessionListItem: FC<{ - classes: any; - loading: boolean; - title: string; - icon: any; - user: any; - onSignIn: Function; - onSignOut: Function; -}> = ({ - classes, - loading, - title, - icon, - user, - onSignIn, - onSignOut, - ...props -}) => { - if (loading) { - return ( - - - - - } - secondary={} - /> - - - - - - - ); - } - - // TODO: Not functional yet to sign in from the sidebar - if (!user) { - return ( - - {icon} - - - - onSignIn()}> - - - - - - ); - } - - const { id, avatarUrl, avatarAlt } = user; - - return ( - - - - {avatarAlt && avatarAlt[0].toUpperCase()} - - - - {id} - - } - secondary={title} - /> - - - onSignOut()}> - - - - - - ); -}; - -const useGoogleLoginState = (open: boolean) => { - const googleAuth = useApi(googleAuthApiRef); - const [loading, setLoading] = useState(true); - const [profile, setProfile] = useState(); - - useEffect(() => { - let didCancel = false; - - if (open) { - googleAuth.getProfile().then(_profile => { - if (!didCancel) { - setProfile(_profile); - setLoading(false); - } - }); - } - - return () => { - didCancel = true; - }; - }, [open, googleAuth]); - - if (loading) { - return { loading: true }; - } - return { loading: false, isLoggedIn: !!profile, profile }; -}; - -type Props = { - email: string; - imageUrl?: string; - name?: string; - collapsedMode?: boolean; -}; - -export const LoggedUserBadge: FC = ({ - imageUrl, - name, - email, - collapsedMode = false, -}) => { - const [state, setState] = useSetState({ - open: false, - anchorEl: null, - }); - const googleAuth = useApi(googleAuthApiRef); - const googleLogin = useGoogleLoginState(state.open); - - const handleOpen = (event: { - preventDefault: () => void; - currentTarget: any; - }) => { - // This prevents ghost click. - event.preventDefault(); - setState({ - open: true, - anchorEl: event.currentTarget, - }); - }; - - const handleClose = () => { - setState({ - open: false, - }); - }; - - const handleGoogleSignIn = () => { - googleAuth.getIdToken(); - handleClose(); - }; - - const handleGoogleSignOut = () => { - googleAuth.logout(); - }; - - const classes = useStyles(); - const avatarFallback = email.charAt(0).toUpperCase() + email.slice(1); - const emailTrimmed = email.split('@')[0]; - const displayEmail = - emailTrimmed.charAt(0).toUpperCase() + emailTrimmed.slice(1); - const displayName = name ?? displayEmail; - - return ( - <> - - - - {imageUrl ? ( - - ) : ( - - {avatarFallback[0]} - - )} - - {!collapsedMode && ( - - {displayName} - - } - /> - )} - - - - - - - - - ); -}; From 2b39be42f0d7ca033380c995131e0927cfce024d Mon Sep 17 00:00:00 2001 From: nikek Date: Tue, 9 Jun 2020 18:17:19 +0200 Subject: [PATCH 20/38] Remove deleted UserBadge component from Sidebar story --- packages/core/src/layout/Sidebar/Sidebar.stories.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx index 1b9ba5bbab..a450a09d88 100644 --- a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx +++ b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx @@ -22,7 +22,6 @@ import { SidebarDivider, SidebarSearchField, SidebarSpace, - SidebarUserBadge, } from '.'; import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined'; import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; @@ -55,6 +54,5 @@ export const SampleSidebar = () => ( - ); From c8294b32857e18f0b525da6ecfd1e0251b2d4348 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Jun 2020 14:27:28 +0200 Subject: [PATCH 21/38] docs: added prettier config --- docs/prettier.config.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 docs/prettier.config.js diff --git a/docs/prettier.config.js b/docs/prettier.config.js new file mode 100644 index 0000000000..6fab539fb2 --- /dev/null +++ b/docs/prettier.config.js @@ -0,0 +1,21 @@ +/* + * 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. + */ + +// Use Object.assign since we haven't configured eslint for ... support in here +module.exports = Object.assign({}, require('@spotify/prettier-config'), { + proseWrap: 'always', + printWidth: 80, +}); From 250db6cc2a3f505aa1206a860e760ffc8bc3c625 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Jun 2020 15:55:44 +0200 Subject: [PATCH 22/38] docs: added plantuml generation script --- docs/generate-uml.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100755 docs/generate-uml.sh diff --git a/docs/generate-uml.sh b/docs/generate-uml.sh new file mode 100755 index 0000000000..55117838ea --- /dev/null +++ b/docs/generate-uml.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# This script uses plantuml to generate SVG images from inline plantuml descriptions. +# See ./auth/oauth.md for an example. +# +# You need to have plantuml installed (brew install plantuml, or some other method). +# +# Either call directly to generate diagrams for all markdown files in this directory, +# or add a --watch flag to rebuild SVGs on changes. + +cd "$( dirname "${BASH_SOURCE[0]}" )" + +if [[ "$1" == '--watch' ]]; then + npx --no-install nodemon --ext md --exec './generate-uml.sh' +fi + +grep '@startuml' -rl --include '*.md' . | while read -r file ; do + echo "Generating : $file" + plantuml -tsvg "$file" 2> >(grep -v "CoreText note:") +done From 4f3375073db11872897165356d3e2d96ef259e78 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Jun 2020 18:19:47 +0200 Subject: [PATCH 23/38] docs/auth: added overview, oauth description and glossary --- docs/auth/glossary.md | 26 ++++++ docs/auth/oauth-popup-flow.svg | 56 ++++++++++++ docs/auth/oauth.md | 151 +++++++++++++++++++++++++++++++++ docs/auth/overview.md | 44 ++++++++++ 4 files changed, 277 insertions(+) create mode 100644 docs/auth/glossary.md create mode 100644 docs/auth/oauth-popup-flow.svg create mode 100644 docs/auth/oauth.md create mode 100644 docs/auth/overview.md diff --git a/docs/auth/glossary.md b/docs/auth/glossary.md new file mode 100644 index 0000000000..6408587273 --- /dev/null +++ b/docs/auth/glossary.md @@ -0,0 +1,26 @@ +# Glossary + +- **Popup** - A separate browser window opened on top of the previous one. +- **OAuth** - More specifically OAuth 2.0, a standard protocol for + authorization. See [oauth.net/2/](https://oauth.net/2/). +- **OpenID Connect** - A layer on top of OAuth which standardises + authentication. See + [en.wikipedia.org/wiki/OpenID_Connect](https://en.wikipedia.org/wiki/OpenID_Connect). +- **JWT** - JSON Web Token, a popular JSON based token format that is commonly + encrypted and/or signed, see + [en.wikipedia.org/wiki/JSON_Web_Token](https://en.wikipedia.org/wiki/JSON_Web_Token) +- **Scope** - A string that describes a certain type of access that can be + granted to a user using OAuth. +- **Access token** - A token that gives access to perform actions on behalf of a + user. It will commonly have a short expiry time, and be limited to a set of + scopes. Part of the OAuth protocol. +- **ID token** - A JWT used to prove a user's identity, containing for example + the user's email. Part of OpenID Connect. +- **Offline access** - OAuth flow that results in both a refresh and access + token, where the refresh token has a long expiration or never expires, and can + be used to request more access tokens in the future. This lets the user go + "offline" with respect to the token issuer, but still be able to request more + tokens at a later time without further direct interaction for the user. +- **Code grant** - OAuth flow where the client receives an authorization code + that is passed to the backend to be exchanged for an access token and possibly + refresh token. diff --git a/docs/auth/oauth-popup-flow.svg b/docs/auth/oauth-popup-flow.svg new file mode 100644 index 0000000000..4d9e79787a --- /dev/null +++ b/docs/auth/oauth-popup-flow.svg @@ -0,0 +1,56 @@ +OAuth Consent and Refresh FlowBrowserBrowserPopup WindowPopup Windowauth-backend pluginauth-backend pluginConsent ScreenConsent ScreenOAuth ProviderOAuth ProviderComponents on page ask for anaccess token with greaterscope than the existing session.Open popupGET /auth/<provider>/start?scope=some%20scopesRedirect to consent screen withrandom nonce in OAuth state andshort-lived cookie with the same nonce.GET /consent_url?redirect_uri=<redirect_uri>?nonce=<n>where redirect_uri=<app-origin>/auth/<provider>/handler/frameUser consents toaccess the new scope.Redirect to given redirect URL, with authorization codeGET /auth/<provider>/handler/frame?code=<c>&nonce=<n>Request includes the previously set none cookieVerify that the nonce in the cookiematches the nonce in the OAuth stateAuthorization CodeClient IDClient SecretVerify and generate tokensAccess Token(ID Token)(Refresh Token)ScopeExpire TimeSmall HTML page with inlined response payloadStore Refresh Token in HTTP-only cookiepostMessage() with tokens and info or errorClose selfA later point when a refreshis needed. Either because ofa reload or an expiring session.GET /auth/<provider>/tokenRefresh Token cookie includedRefresh TokenClient IDClient SecretAccess Token(ID Token)ScopeExpire TimeTokens and info \ No newline at end of file diff --git a/docs/auth/oauth.md b/docs/auth/oauth.md new file mode 100644 index 0000000000..519cfe1d85 --- /dev/null +++ b/docs/auth/oauth.md @@ -0,0 +1,151 @@ +# OAuth and OpenID Connect + +This section describes how Backstage allows plugins to request OAuth Access +Tokens and OpenID Connect ID Tokens on behalf of the user, to be used for auth +to various third party APIs. + +## Summary + +There are occasions when the user wants to perform actions towards third party +services that require authorization via OAuth. Backstage provides standardized +[Utility APIs](../getting-started/utility-apis.md) such as the +[GoogleAuthApi](../../packages/core-api/src/apis/definitions/auth.ts) for that +use-case. Backstage also includes a set of implementations of these APIs that +integrate with the [auth-backend](../../plugins/auth-backend) plugin to provide +a popup-based OAuth flow. + +## Background + +Access control in OAuth is implemented in terms of scope, which is a list of +permissions given to the app. An OAuth service can issue Access Tokens that are +tied to a certain set of scopes, such as viewing profile information, reading +and/or writing user data in the service. The scope format and handling is +specific to each OAuth provider, and the set of available scopes are typically +found in the documentation describing the auth solution of the provider, for +example +[developers.google.com/identity/protocols/oauth2/scopes](https://developers.google.com/identity/protocols/oauth2/scopes). + +As a part of logging in with an OAuth provider, the user needs to consent to +both the login itself and the set of scopes that the app is requesting to use. +This is done by loading a page provided by the OAuth provider, where a user can +choose an account to log in with, and accept or reject the request. If the user +accepts the login request, a token is issued, and any holder of the token can +use it to make authenticated requests towards the third party service. + +## OAuth in @backstage/core-api and auth-backend + +The default OAuth implementation in Backstage is based on an OAuth server-side +offline access flow, which means that it uses the backend as a helper in order +to trade credentials. A benefit of this type of flow is that it does not require +the use of third party cookies, and is robust on a wide selection of browsers +and privacy browsing plugins, strict security settings, etc. + +The implementation also uses a popup-based flow, where auth requests are handled +in a new popup window that is opened by the app. By using a popup-based flow it +is possible to request authentication at any point in the app, without requiring +a redirect. Because of this there is no need to ask for all scopes upfront, or +interrupt the app with a redirect and forcing plugin authors to take care in +restoring state after a redirect has been make. All in all it makes it much +easier to make authenticated requests inside a plugin. + +## OAuth Flow + +The following describes the OAuth flow implemented by the +[auth-backend](../../plugins/auth-backend) and +[DefaultAuthConnector](../../packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts) +in `@backstage/core-api`. + +Component and APIs can request Access or ID Tokens from any available Auth +provider. If there already exists a cached fresh token that covers (at least) +the requested scopes, it will be returned immediately. If the OAuth provider +implements token refreshes, this check will also trigger a token refresh attempt +if no session is a available. + +If new scopes are requested, or the user is not yet logged in with that +provider, a dialog is shown informing the user that they need to log in with the +specified provider. If the user agrees to continue, a separate popup window is +opened that implements the entire consent flow. + +The popup window is pointed to the `/start` endpoint of the auth provider in the +`auth-backend` plugin, which then redirects to the OAuth consent screen of the +provider. The consent screen is controlled by the OAuth provider, and will do +things like prompting the user to log in with an account, and possibly reviewing +the set of requested scopes. If the login request is accepted, the popup window +will be redirected back to the `/handler/frame` endpoint of the auth backend. +The redirect URL will contain a short-term authorization code, which is picked +up by the backend and exchanged for long-term tokens via a call to the OAuth +provider. The Access and possibly ID Token is then handed back to the main +Backstage page via `postMessage`. If the OAuth provider implements offline +refresh, a refresh token will be stored in an HTTP-only cookie scoped to the +specific provider in the `auth-backend` plugin. + +To protect against certain attacks, the above flow also includes a simple nonce +check and a lightweight CSRF protection header. The nonce check is done to +protect against attacks where an attacker tricks a user to log in with an +account of the attacker's choosing in order to gather data. In the first part of +the flow where the popup is directed to the `/start` endpoint, a nonce is +generated and placed in both a cookie and the OAuth state. The nonces received +in the cookie and OAuth state in the redirect handler are then checked, and the +auth attempt will fail if they're not valid. The CSRF protection for the +`/refresh` and `/logout` endpoints is implemented by simply checking for the +presence of a `X-Requested-With` header. + +The target origin of the `postMessage` is also of importance to keep the flow +secure. It is configured to a single value for each auth provider and +environment. Without a single configured origin, any page could open a popup and +request an access token. + +### Sequence Diagram + +The following diagram visualizes the flow described in the previous section. + + + +![](oauth-popup-flow.svg) diff --git a/docs/auth/overview.md b/docs/auth/overview.md new file mode 100644 index 0000000000..2806b2dccf --- /dev/null +++ b/docs/auth/overview.md @@ -0,0 +1,44 @@ +# User Authentication and Authorization in Backstage + +## Summary + +The purpose of the Auth APIs in Backstage are to identify the user, and to +provide a way for plugins to request access to 3rd party services on behalf of +the user (OAuth). This documentation focuses on the implementation of that +solution and how to extend it. For documentation on how to consume the Auth APIs +in a plugin, see [TODO](#TODO). + +### Accessing Third Party Services + +The main pattern for talking to third party services in Backstage is +user-to-server requests, where short-lived OAuth Access Tokens are requested by +plugins to authenticate calls to external services. These calls can be made +either directly to the services or through a backend plugin or service. + +By relying on user-to-server calls we keep the coupling between the frontend and +backend low, and provide a much lower barrier for plugins to make use of third +party services. This is in comparison to for example a session-based system, +where access tokens are stored server-side. Such a solution would require a much +deeper coupling between the auth backend plugin, its session storage, and other +backend plugins or separate services. A goal of Backstage is to make it as easy +as possible to create new plugins, and an auth solution based on user-to-server +OAuth helps in that regard. + +The method with which frontend plugins request access to third party services is +through [Utility APIs](../getting-started/utility-apis.md) for each service +provider. For a full list of providers, see [TODO](#TODO). + +### Identity - TODO + +This documentation currently only covers the OAuth use-case, as identity +management is not settled yet and part of an +[upcoming milestone](https://github.com/spotify/backstage/milestone/12). + +## Further Reading + +More details are provided in dedicated sections of the documentation. + +- [OAuth](./oauth): Description of the generic OAuth flow implemented by the + [auth-backend](../../plugins/auth-backend). +- [Glossary](./glossary): Glossary of some common terms related to the auth + flows. From 2b5b3903f78a4ac4d189d181dd8a3ceb6c18b5d1 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 9 Jun 2020 18:32:41 +0200 Subject: [PATCH 24/38] Optional namespace and name as one part of URL --- .../components/CatalogTable/CatalogTable.tsx | 7 ++- .../ComponentPage/ComponentPage.test.tsx | 2 +- .../ComponentPage/ComponentPage.tsx | 6 +- plugins/catalog/src/data/component.ts | 1 + plugins/catalog/src/data/utils.ts | 1 + plugins/catalog/src/routes.ts | 2 +- .../RegisterComponentResultDialog.tsx | 63 ++++++++++--------- 7 files changed, 46 insertions(+), 36 deletions(-) diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index f04add7cc1..bea9caf273 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -31,7 +31,12 @@ const columns: TableColumn[] = [ diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx index ee5e7348b1..98fa6b4408 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx @@ -24,7 +24,7 @@ const getTestProps = (name: string) => { return { match: { params: { - name: name, + optionalNamespaceAndName: name, kind: 'Component', }, }, diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx index a410be2bf4..72aef7fe16 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx @@ -40,8 +40,7 @@ const REDIRECT_DELAY = 1000; type ComponentPageProps = { match: { params: { - name: string; - namespace?: string; + optionalNamespaceAndName: string; kind: string; }; }; @@ -55,7 +54,8 @@ const ComponentPage: FC = ({ match, history }) => { const [removingPending, setRemovingPending] = useState(false); const showRemovalDialog = () => setConfirmationDialogOpen(true); const hideRemovalDialog = () => setConfirmationDialogOpen(false); - const { name, namespace, kind } = match.params; + const { optionalNamespaceAndName, kind } = match.params; + const [name, namespace] = optionalNamespaceAndName.split(':').reverse(); const errorApi = useApi(errorApiRef); const catalogApi = useApi(catalogApiRef); diff --git a/plugins/catalog/src/data/component.ts b/plugins/catalog/src/data/component.ts index 86749c6faa..68be7588f0 100644 --- a/plugins/catalog/src/data/component.ts +++ b/plugins/catalog/src/data/component.ts @@ -18,6 +18,7 @@ import { ReactNode } from 'react'; export type Component = { name: string; + namespace?: string; kind: string; metadata: EntityMeta; description: ReactNode; diff --git a/plugins/catalog/src/data/utils.ts b/plugins/catalog/src/data/utils.ts index b731268c41..f84bfee73f 100644 --- a/plugins/catalog/src/data/utils.ts +++ b/plugins/catalog/src/data/utils.ts @@ -24,6 +24,7 @@ import { Component } from './component'; export function entityToComponent(envelope: Entity): Component { return { name: envelope.metadata.name, + namespace: envelope.metadata.namespace, kind: envelope.kind, metadata: envelope.metadata, description: envelope.metadata.annotations?.description ?? 'placeholder', diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index f5bb319a88..69c4e651b4 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -25,6 +25,6 @@ export const rootRoute = createRouteRef({ }); export const entityRoute = createRouteRef({ icon: NoIcon, - path: '/catalog/:namespace?/:kind/:name/', + path: '/catalog/:kind/:optionalNamespaceAndName/', title: 'Entity', }); diff --git a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx index 7c6e335f6e..68b06d1751 100644 --- a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx +++ b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx @@ -54,36 +54,39 @@ export const RegisterComponentResultDialog: FC = ({ The following components have been succefully created: - {entities.map((entity: any, index: number) => ( - - - - {generatePath(entityRoute.path, { - name: entity.metadata.name, - kind: entity.kind, - })} - - ), - }} - /> - - {index < entities.length - 1 && } - - ))} + {entities.map((entity: any, index: number) => { + const entityPath = generatePath(entityRoute.path, { + optionalNamespaceAndName: [ + entity.metadata.namespace, + entity.metadata.name, + ] + .filter(Boolean) + .join(':'), + kind: entity.kind, + }); + + return ( + + + + {entityPath} + + ), + }} + /> + + {index < entities.length - 1 && } + + ); + })} From a633dce818a9692bd2d9fd898bf589877e027063 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Jun 2020 19:50:41 +0200 Subject: [PATCH 25/38] docs: format with prettier (#1218) --- docs/FAQ.md | 283 ++++++++++++------ .../adr002-default-catalog-file-format.md | 145 ++++----- .../adr003-avoid-default-exports.md | 47 ++- .../adr004-module-export-structure.md | 44 ++- .../adr005-catalog-core-entities.md | 51 +++- docs/architecture-terminology.md | 15 +- docs/auth/add-auth-provider.md | 71 +++-- docs/create-an-app.md | 19 +- docs/design.md | 114 +++++-- docs/getting-started/Plugin development.md | 18 +- docs/getting-started/README.md | 4 +- docs/getting-started/app-custom-theme.md | 44 ++- .../contributing-to-storybook.md | 13 +- docs/getting-started/create-a-plugin.md | 21 +- .../development-environment.md | 30 +- docs/getting-started/structure-of-a-plugin.md | 33 +- docs/getting-started/utility-apis.md | 138 +++++---- docs/publishing.md | 17 +- docs/reference/createPlugin-feature-flags.md | 7 +- docs/reference/createPlugin-router.md | 6 +- 20 files changed, 756 insertions(+), 364 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 100e3e19de..5fb131528a 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -4,35 +4,67 @@ ### Can we call Backstage something different? So that it fits our company better? -Yes, Backstage is just a platform for building your own developer portal. We happen to call our internal version Backstage, as well, as a reference to our music roots. You can call your version whatever suits your team, company, or brand. +Yes, Backstage is just a platform for building your own developer portal. We +happen to call our internal version Backstage, as well, as a reference to our +music roots. You can call your version whatever suits your team, company, or +brand. ### Is Backstage a monitoring platform? -No, but it can be! Backstage is designed to be a developer portal for all your infrastructure tooling, services, and documentation. So, it's not a monitoring platform — but that doesn't mean you can't integrate a monitoring tool into Backstage by writing [a plugin](https://github.com/spotify/faq#what-is-a-plugin-in-backstage). +No, but it can be! Backstage is designed to be a developer portal for all your +infrastructure tooling, services, and documentation. So, it's not a monitoring +platform — but that doesn't mean you can't integrate a monitoring tool into +Backstage by writing +[a plugin](https://github.com/spotify/faq#what-is-a-plugin-in-backstage). ### How is Backstage licensed? -Backstage was released as free and open software by Spotify and is licensed under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). +Backstage was released as free and open software by Spotify and is licensed +under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). ### Why did we open source Backstage? -We hope to see Backstage become the infrastructure standard everywhere. When we saw how much Backstage improved developer experience and productivity internally, we wanted to share those gains. After all, if Backstage can create order in an engineering environment as open and diverse as ours, then we're pretty sure it can create order (and boost productivity) anywhere. To learn more, read our blog post, "[What the heck is Backstage anyway?](https://backstage.io/blog/2020/03/18/what-is-backstage)" +We hope to see Backstage become the infrastructure standard everywhere. When we +saw how much Backstage improved developer experience and productivity +internally, we wanted to share those gains. After all, if Backstage can create +order in an engineering environment as open and diverse as ours, then we're +pretty sure it can create order (and boost productivity) anywhere. To learn +more, read our blog post, +"[What the heck is Backstage anyway?](https://backstage.io/blog/2020/03/18/what-is-backstage)" ### Will Spotify's internal plugins be open sourced, too? -Yes, we've already started releasing open source versions of some of the plugins we use here, and we'll continue to do so. [Plugins](https://github.com/spotify/faq#what-is-a-plugin-in-backstage) are the building blocks of functionality in Backstage. We have over 120 plugins inside Spotify — many of those are specialized for our use, so will remain internal and proprietary to us. But we estimate that about a third of our existing plugins make good open source candidates. (And we'll probably end up writing some brand new ones, too.) -​ +Yes, we've already started releasing open source versions of some of the plugins +we use here, and we'll continue to do so. +[Plugins](https://github.com/spotify/faq#what-is-a-plugin-in-backstage) are the +building blocks of functionality in Backstage. We have over 120 plugins inside +Spotify — many of those are specialized for our use, so will remain internal and +proprietary to us. But we estimate that about a third of our existing plugins +make good open source candidates. (And we'll probably end up writing some brand +new ones, too.) ​ + ### What's the roadmap for Backstage? -​ -We envision three phases, which you can learn about in [our project roadmap](https://github.com/spotify/backstage#project-roadmap). Even though the open source version of Backstage is relatively new compared to our internal version, we have already begun work on various aspects of all three phases. Looking at the [milestones for active issues](https://github.com/spotify/backstage/milestones) will also give you a sense of our progress. + +​ We envision three phases, which you can learn about in +[our project roadmap](https://github.com/spotify/backstage#project-roadmap). +Even though the open source version of Backstage is relatively new compared to +our internal version, we have already begun work on various aspects of all three +phases. Looking at the +[milestones for active issues](https://github.com/spotify/backstage/milestones) +will also give you a sense of our progress. ### My company doesn't have thousands of developers or services. Is Backstage overkill? -Not at all! A core reason to adopt Backstage is to standardize how software is built at your company. It's easier to decide on those standards as a small company, and grows in importance as the company grows. Backstage sets a foundation, and an early investment in your infrastructure becomes even more valuable as you grow. +Not at all! A core reason to adopt Backstage is to standardize how software is +built at your company. It's easier to decide on those standards as a small +company, and grows in importance as the company grows. Backstage sets a +foundation, and an early investment in your infrastructure becomes even more +valuable as you grow. ### Our company has a strong design language system/brand that we want to incorporate. Does Backstage support this? -Yes! The Backstage UI is built using Material-UI. With the theming capabilities of Material-UI, you are able to adapt the interface to your brand guidelines. +Yes! The Backstage UI is built using Material-UI. With the theming capabilities +of Material-UI, you are able to adapt the interface to your brand guidelines. ## Technical FAQ: @@ -40,96 +72,155 @@ Yes! The Backstage UI is built using Material-UI. With the theming capabilities The short answer is that's what we've been using in Backstage internally. -The original decision was based on Google's Material Design being a thorough, well thought out and complete design system, with many mature and powerful libraries implemented in both the system itself and auxiliary components that we knew that we would like to use. +The original decision was based on Google's Material Design being a thorough, +well thought out and complete design system, with many mature and powerful +libraries implemented in both the system itself and auxiliary components that we +knew that we would like to use. + +It strikes a good balance between power, customizability, and ease of use. A +core focus of Backstage is to make plugin developers productive with as few +hurdles as possible. Material-UI lets plugin makers get going easily with both +well-known tech and a large flora of components. ​ -It strikes a good balance between power, customizability, and ease of use. A core focus of Backstage is to make plugin developers productive with as few hurdles as possible. Material-UI lets plugin makers get going easily with both well-known tech and a large flora of components. -​ ### What technology does Backstage use? -​ -The code base is a large-scale React application that uses TypeScript. For [Phase 2](https://github.com/spotify/backstage#project-roadmap), we plan to use Node.js and GraphQL. -​ -### What is the end-to-end user flow? The happy path story. -​ -There are three main user profiles for Backstage: the integrator, the contributor, and the software engineer. -​ -The **integrator** hosts the Backstage app and configures which plugins are available to use in the app. -​ -The **contributor** adds functionality to the app by writing plugins. -​ -The **software engineer** uses the app's functionality and interacts with its plugins. -​ -### What is a "plugin" in Backstage? -​ -Plugins are what provide the feature functionality in Backstage. They are used to integrate different systems into Backstage's frontend, so that the developer gets a consistent UX, no matter what tool or service is being accessed on the other side. -​ -Each plugin is treated as a self-contained web app and can include almost any type of content. Plugins all use a common set of platform APIs and reusable UI components. Plugins can fetch data either from the backend or an API exposed through the proxy. -​ -Learn more about [the different components](https://github.com/spotify/backstage#overview) that make up Backstage. -​ -### Do I have to write plugins in TypeScript? -​ -No, you can use JavaScript if you prefer. -​ -We want to keep the Backstage core APIs in TypeScript, but aren't forcing it on individual plugins. -​ -### How do I find out if a plugin already exists? -​ -Before you write a plugin, [search the plugin issues](https://github.com/spotify/backstage/issues?q=is%3Aissue+label%3Aplugin+) to see if it already exists or is in the works. If no one's thought of it yet, great! Open a new issue as [a plugin suggestion](https://github.com/spotify/backstage/issues/new/choose) and describe what your plugin will do. This will help coordinate our contributors' efforts and avoid duplicating existing functionality. -​ -In the future, we will create [a plugin gallery](https://github.com/spotify/backstage/issues/260) where people can browse and search for all available plugins. -​ -### Which plugin is used the most at Spotify? -​ -By far, our most-used plugin is our TechDocs plugin, which we use for creating technical documentation. Our philosophy at Spotify is to treat "docs like code", where you write documentation using the same workflow as you write your code. This makes it easier to create, find, and update documentation. We hope to release [the open source version](https://github.com/spotify/backstage/issues/687) in the future. (See also: "[Will Spotify's internal plugins be open sourced, too?](https://github.com/spotify/faq#will-spotifys-internal-plugins-be-open-sourced-too)" above) -​ -### Are you planning to have plugins baked into the repo? Or should they be developed in separate repos? -​ -Contributors can add open source plugins to the plugins directory in [this monorepo](https://github.com/spotify/backstage). Integrators can then configure which open source plugins are available to use in their instance of the app. Open source plugins are downloaded as npm packages published in the open source repository. -​ -While we encourage using the open source model, we know there are cases where contributors might want to experiment internally or keep their plugins closed source. Contributors writing closed source plugins should develop them in the plugins directory in their own Backstage repository. Integrators also configure closed source plugins locally from the monorepo. -​ -### Any plans for integrating with other repository managers, such as GitLab or Bitbucket? -​ -We chose GitHub because it is the tool that we are most familiar with, so that will naturally lead to integrations for GitHub being developed at an early stage. -​ -Hosting this project on GitHub does not exclude integrations with alternatives, such as GitLab or Bitbucket. We believe that in time there will be plugins that will provide functionality for these tools as well. Hopefully, contributed by the community! -​ -Also note, implementations of Backstage can be hosted wherever you feel suits your needs best. -​ -### Who maintains Backstage? -​ -Spotify will maintain the open source core, but we envision different parts of the project being maintained by various companies and contributors. We also envision a large, diverse ecosystem of open source plugins, which would be maintained by their original authors/contributors or by the community. -​ -When it comes to [deployment](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md), the system integrator (typically, the infrastructure team in your organization) maintains Backstage in your own environment. -​ -### Does Spotify provide a managed version of Backstage? -​ -No, this is not a service offering. We build the piece of software, and someone in your infrastructure team is responsible for [deploying](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md) and maintaining it. -​ -### How secure is Backstage? -​ -We take security seriously. When it comes to packages and code we scan our repositories periodically and update our packages to the latest versions. When it comes to deployment of Backstage within an organisation it depends on the deployment and security setup in your organisation. Reach out to us on [Discord](https://discord.gg/MUpMjP2) if you have specific queries. -Please report sensitive security issues via Spotify's [bug-bounty program](https://hackerone.com/spotify) rather than GitHub. -​ +​ The code base is a large-scale React application that uses TypeScript. For +[Phase 2](https://github.com/spotify/backstage#project-roadmap), we plan to use +Node.js and GraphQL. ​ + +### What is the end-to-end user flow? The happy path story. + +​ There are three main user profiles for Backstage: the integrator, the +contributor, and the software engineer. ​ The **integrator** hosts the Backstage +app and configures which plugins are available to use in the app. ​ The +**contributor** adds functionality to the app by writing plugins. ​ The +**software engineer** uses the app's functionality and interacts with its +plugins. ​ + +### What is a "plugin" in Backstage? + +​ Plugins are what provide the feature functionality in Backstage. They are used +to integrate different systems into Backstage's frontend, so that the developer +gets a consistent UX, no matter what tool or service is being accessed on the +other side. ​ Each plugin is treated as a self-contained web app and can include +almost any type of content. Plugins all use a common set of platform APIs and +reusable UI components. Plugins can fetch data either from the backend or an API +exposed through the proxy. ​ Learn more about +[the different components](https://github.com/spotify/backstage#overview) that +make up Backstage. ​ + +### Do I have to write plugins in TypeScript? + +​ No, you can use JavaScript if you prefer. ​ We want to keep the Backstage core +APIs in TypeScript, but aren't forcing it on individual plugins. ​ + +### How do I find out if a plugin already exists? + +​ Before you write a plugin, +[search the plugin issues](https://github.com/spotify/backstage/issues?q=is%3Aissue+label%3Aplugin+) +to see if it already exists or is in the works. If no one's thought of it yet, +great! Open a new issue as +[a plugin suggestion](https://github.com/spotify/backstage/issues/new/choose) +and describe what your plugin will do. This will help coordinate our +contributors' efforts and avoid duplicating existing functionality. ​ In the +future, we will create +[a plugin gallery](https://github.com/spotify/backstage/issues/260) where people +can browse and search for all available plugins. ​ + +### Which plugin is used the most at Spotify? + +​ By far, our most-used plugin is our TechDocs plugin, which we use for creating +technical documentation. Our philosophy at Spotify is to treat "docs like code", +where you write documentation using the same workflow as you write your code. +This makes it easier to create, find, and update documentation. We hope to +release +[the open source version](https://github.com/spotify/backstage/issues/687) in +the future. (See also: +"[Will Spotify's internal plugins be open sourced, too?](https://github.com/spotify/faq#will-spotifys-internal-plugins-be-open-sourced-too)" +above) ​ + +### Are you planning to have plugins baked into the repo? Or should they be developed in separate repos? + +​ Contributors can add open source plugins to the plugins directory in +[this monorepo](https://github.com/spotify/backstage). Integrators can then +configure which open source plugins are available to use in their instance of +the app. Open source plugins are downloaded as npm packages published in the +open source repository. ​ While we encourage using the open source model, we +know there are cases where contributors might want to experiment internally or +keep their plugins closed source. Contributors writing closed source plugins +should develop them in the plugins directory in their own Backstage repository. +Integrators also configure closed source plugins locally from the monorepo. ​ + +### Any plans for integrating with other repository managers, such as GitLab or Bitbucket? + +​ We chose GitHub because it is the tool that we are most familiar with, so that +will naturally lead to integrations for GitHub being developed at an early +stage. ​ Hosting this project on GitHub does not exclude integrations with +alternatives, such as GitLab or Bitbucket. We believe that in time there will be +plugins that will provide functionality for these tools as well. Hopefully, +contributed by the community! ​ Also note, implementations of Backstage can be +hosted wherever you feel suits your needs best. ​ + +### Who maintains Backstage? + +​ Spotify will maintain the open source core, but we envision different parts of +the project being maintained by various companies and contributors. We also +envision a large, diverse ecosystem of open source plugins, which would be +maintained by their original authors/contributors or by the community. ​ When it +comes to +[deployment](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md), +the system integrator (typically, the infrastructure team in your organization) +maintains Backstage in your own environment. ​ + +### Does Spotify provide a managed version of Backstage? + +​ No, this is not a service offering. We build the piece of software, and +someone in your infrastructure team is responsible for +[deploying](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md) and +maintaining it. ​ + +### How secure is Backstage? + +​ We take security seriously. When it comes to packages and code we scan our +repositories periodically and update our packages to the latest versions. When +it comes to deployment of Backstage within an organisation it depends on the +deployment and security setup in your organisation. Reach out to us on +[Discord](https://discord.gg/MUpMjP2) if you have specific queries. + +Please report sensitive security issues via Spotify's +[bug-bounty program](https://hackerone.com/spotify) rather than GitHub. ​ + ### Does Backstage collect any information that is shared with Spotify? -​ -No. Backstage does not collect any telemetry from any third party using the platform. Spotify, and the open source community, does have access to [GitHub Insights](https://github.com/features/insights), which contains information such as contributors, commits, traffic, and dependencies. -​ -Backstage is an open platform, but you are in control of your own data. You control who has access to any data you provide to your version of Backstage and who that data is shared with. -​ + +​ No. Backstage does not collect any telemetry from any third party using the +platform. Spotify, and the open source community, does have access to +[GitHub Insights](https://github.com/features/insights), which contains +information such as contributors, commits, traffic, and dependencies. ​ +Backstage is an open platform, but you are in control of your own data. You +control who has access to any data you provide to your version of Backstage and +who that data is shared with. ​ + ### Can Backstage be used to build something other than a developer portal? -​ -Yes. The core frontend framework could be used for building any large-scale web application where (1) multiple teams are building separate parts of the app, and (2) you want the overall experience to be consistent. -​ -That being said, in [Phase 2](https://github.com/spotify/backstage#project-roadmap) of the project we will add features that are needed for developer portals and systems for managing software ecosystems. Our ambition will be to keep Backstage modular. -​ + +​ Yes. The core frontend framework could be used for building any large-scale +web application where (1) multiple teams are building separate parts of the app, +and (2) you want the overall experience to be consistent. ​ That being said, in +[Phase 2](https://github.com/spotify/backstage#project-roadmap) of the project +we will add features that are needed for developer portals and systems for +managing software ecosystems. Our ambition will be to keep Backstage modular. ​ + ### How can I get involved? -​ -Jump right in! Come help us fix some of the [early bugs and first issues](https://github.com/spotify/backstage/labels/good%20first%20issue) or reach [a new milestone](https://github.com/spotify/backstage/milestones). Or write an open source plugin for Backstage, like this [Lighthouse plugin](https://github.com/spotify/backstage/tree/master/plugins/lighthouse). -​ -See all the ways you can [contribute here](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md). We'd love to have you as part of the community. -​ + +​ Jump right in! Come help us fix some of the +[early bugs and first issues](https://github.com/spotify/backstage/labels/good%20first%20issue) +or reach [a new milestone](https://github.com/spotify/backstage/milestones). Or +write an open source plugin for Backstage, like this +[Lighthouse plugin](https://github.com/spotify/backstage/tree/master/plugins/lighthouse). +​ See all the ways you can +[contribute here](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md). +We'd love to have you as part of the community. ​ + ### Can I join the Backstage team? -​ -If you're interested in being part of the Backstage team, reach out to [fossopportunities@spotify.com](mailto:fossopportunities@spotify.com) + +​ If you're interested in being part of the Backstage team, reach out to +[fossopportunities@spotify.com](mailto:fossopportunities@spotify.com) diff --git a/docs/architecture-decisions/adr002-default-catalog-file-format.md b/docs/architecture-decisions/adr002-default-catalog-file-format.md index 23db32cc9c..6cf649aedf 100644 --- a/docs/architecture-decisions/adr002-default-catalog-file-format.md +++ b/docs/architecture-decisions/adr002-default-catalog-file-format.md @@ -6,22 +6,22 @@ ## Background -Backstage comes with a software catalog functionality, that you can use to -track all your software components and more. It can be powered by data from -various sources, and one of them that is included with the package, is a -custom database backed catalog. It has the ability to keep itself updated -automatically based on the contents of little descriptor files in your -version control system of choice. Developers create these files and maintain -them side by side with their code, and the catalog system reacts accordingly. +Backstage comes with a software catalog functionality, that you can use to track +all your software components and more. It can be powered by data from various +sources, and one of them that is included with the package, is a custom database +backed catalog. It has the ability to keep itself updated automatically based on +the contents of little descriptor files in your version control system of +choice. Developers create these files and maintain them side by side with their +code, and the catalog system reacts accordingly. This ADR describes the default format of these descriptor files. ### Inspiration -Internally at Spotify, a home grown software catalog system is used heavily -and forms a core part of Backstage and other important pieces of the -infrastructure. The user experience, learnings and certain pieces of metadata -from that catalog are being carried over to the open source effort. +Internally at Spotify, a home grown software catalog system is used heavily and +forms a core part of Backstage and other important pieces of the infrastructure. +The user experience, learnings and certain pieces of metadata from that catalog +are being carried over to the open source effort. The file format described herein, also draws heavy inspiration from the [kubernetes object format](https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/). @@ -35,9 +35,9 @@ inside Backstage, or by push events from a CI/CD pipelines, or by webhook triggers from the version control system, etc. Each file describes one or more entities in accordance with the -[Backstage System Model](https://github.com/spotify/backstage/issues/390). -All of these entities have a common stucture and nomenclature, and they are -stored in the software catalog from which they then can be queried. +[Backstage System Model](https://github.com/spotify/backstage/issues/390). All +of these entities have a common stucture and nomenclature, and they are stored +in the software catalog from which they then can be queried. Entities have distinct names, and they may reference each other by those names. @@ -70,9 +70,9 @@ spec: ``` The root fields `apiVersion`, `kind`, `metadata`, and `spec` are part of the -_envelope_, defining the overall structure of all kinds of entity. Likewise, -the `name`, `namespace`, `labels`, and `annotations` metadata fields are of -special significance and have reserved purposes and distinct shapes. +_envelope_, defining the overall structure of all kinds of entity. Likewise, the +`name`, `namespace`, `labels`, and `annotations` metadata fields are of special +significance and have reserved purposes and distinct shapes. See below for details about these fields. @@ -88,30 +88,31 @@ first versions of the catalog will focus on the `Component` kind. The `apiVersion`is the version of specification format for that particular entity that this file is written against. The version is used for being able to -evolve the format, and the tuple of `apiVersion` and `kind` should be enough -for a parser to know how to interpret the contents of the rest of the document. +evolve the format, and the tuple of `apiVersion` and `kind` should be enough for +a parser to know how to interpret the contents of the rest of the document. Backstage specific entities have an `apiVersion` that is prefixed with -`backstage.io/`, to distinguish them from other types of object that share -the same type of structure. This may be relevant when co-hosting these +`backstage.io/`, to distinguish them from other types of object that share the +same type of structure. This may be relevant when co-hosting these specifications with e.g. kubernetes object manifests. -Early versions of the catalog will be using beta versions, e.g. `backstage.io/v1beta1`, -to signal that the format may still change. After that, we will be using -`backstage.io/v1` and up. +Early versions of the catalog will be using beta versions, e.g. +`backstage.io/v1beta1`, to signal that the format may still change. After that, +we will be using `backstage.io/v1` and up. ### `metadata` -A structure that contains metadata about the entity, i.e. things that aren't directly -part of the entity specification itself. See below for more details about this structure. +A structure that contains metadata about the entity, i.e. things that aren't +directly part of the entity specification itself. See below for more details +about this structure. ### `spec` The actual specification data that describes the entity. -The precise structure of the `spec` depends on the `apiVersion` and `kind` combination, -and some kinds may not even have a `spec` at all. See further down in this document for -the specification structure of specific kinds. +The precise structure of the `spec` depends on the `apiVersion` and `kind` +combination, and some kinds may not even have a `spec` at all. See further down +in this document for the specification structure of specific kinds. ## Metadata @@ -119,27 +120,31 @@ The `metadata` root field has the following nested structure. ### `name` -The name of the entity. This name is both meant for human eyes to recognize the entity, -and for machines and other components to reference the entity (e.g. in URLs or from -other entity specification files). +The name of the entity. This name is both meant for human eyes to recognize the +entity, and for machines and other components to reference the entity (e.g. in +URLs or from other entity specification files). -Names must be unique per kind, within a given namespace (if specified), at any point in -time. Names may be reused at a later time, after an entity is deleted from the registry. +Names must be unique per kind, within a given namespace (if specified), at any +point in time. Names may be reused at a later time, after an entity is deleted +from the registry. -Names are required to follow a certain format. Entities that do not follow those rules -will not be accepted for registration in the catalog. The ruleset is configurable to fit -your organization's needs, but the default behavior is as follows. +Names are required to follow a certain format. Entities that do not follow those +rules will not be accepted for registration in the catalog. The ruleset is +configurable to fit your organization's needs, but the default behavior is as +follows. - Strings of length at least 1, and at most 63 -- Must consist of sequences of `[a-z0-9A-Z]` possibly separated by one of `[-_.]` +- Must consist of sequences of `[a-z0-9A-Z]` possibly separated by one of + `[-_.]` Example: `visits-tracking-service`, `CircleciBuildsDs_avro_gcs` -In addition to this, names are passed through a normalization function and then compared -to the same normalized form of other entity names and made sure to not collide. This rule -of uniqueness exists to avoid situations where e.g. both `my-component` and `MyComponent` -are registered side by side, which leads to confusion and risk. The normalization function -is also configurable, but the default behavior is as follows. +In addition to this, names are passed through a normalization function and then +compared to the same normalized form of other entity names and made sure to not +collide. This rule of uniqueness exists to avoid situations where e.g. both +`my-component` and `MyComponent` are registered side by side, which leads to +confusion and risk. The normalization function is also configurable, but the +default behavior is as follows. - Strip out all characters outside of the set `[a-zA-Z0-9]` - Convert to lowercase @@ -148,55 +153,59 @@ Example: `CircleciBuildsDs_avro_gcs` -> `circlecibuildsdsavrogcs` ### `namespace` -The `name` of a namespace that the entity belongs to. This field is optional, and currently -has no special semantics apart from bounding the name uniqueness constraint if specified. -It is reserved for future use and may get broader semantic implication. +The `name` of a namespace that the entity belongs to. This field is optional, +and currently has no special semantics apart from bounding the name uniqueness +constraint if specified. It is reserved for future use and may get broader +semantic implication. Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities, i.e. not Backstage specific but the same as in Kubernetes. ### `description` -A human readable description of the entity, to be shown in Backstage. Should be kept short -and informative, suitable to give an overview of the entity's purpose at a glance. More -detailed explanations and documentation should be placed elsewhere. +A human readable description of the entity, to be shown in Backstage. Should be +kept short and informative, suitable to give an overview of the entity's purpose +at a glance. More detailed explanations and documentation should be placed +elsewhere. ### `labels` -Labels are optional key/value pairs of that are attached to the entity, and their use is -identical to [kubernetes object labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/). +Labels are optional key/value pairs of that are attached to the entity, and +their use is identical to +[kubernetes object labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/). -Their main purpose is for references to other entities, and for information that is -in one way or another classifying for the current entity. They are often used as values -in queries or filters. +Their main purpose is for references to other entities, and for information that +is in one way or another classifying for the current entity. They are often used +as values in queries or filters. Both the key and the value are strings, subject to the following restrictions. -Keys have an optional prefix followed by a slash, and then the name part which is required. -The prefix must be a valid lowercase domain name, at most 253 characters in total. The name -part must be sequences of `[a-zA-Z0-9]` separated by any of `[-_.]`, at most 63 characters -in total. +Keys have an optional prefix followed by a slash, and then the name part which +is required. The prefix must be a valid lowercase domain name, at most 253 +characters in total. The name part must be sequences of `[a-zA-Z0-9]` separated +by any of `[-_.]`, at most 63 characters in total. -The `backstage.io/` prefix is reserved for use by Backstage core components. Some keys such as -`system` also have predefined semantics. +The `backstage.io/` prefix is reserved for use by Backstage core components. +Some keys such as `system` also have predefined semantics. Values are strings that follow the same restrictions as `name` above. ### `annotations` An object with arbitrary non-identifying metadata attached to the entity, -identical in use to [kubernetes object annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/). +identical in use to +[kubernetes object annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/). -Their purpose is mainly, but not limited, to reference into external systems. This could -for example be a reference to the git ref the entity was ingested from, to monitoring -and logging systems, to pagerduty schedules, etc. +Their purpose is mainly, but not limited, to reference into external systems. +This could for example be a reference to the git ref the entity was ingested +from, to monitoring and logging systems, to pagerduty schedules, etc. Both the key and the value are strings, subject to the following restrictions. -Keys have an optional prefix followed by a slash, and then the name part which is required. -The prefix must be a valid lowercase domain name, at most 253 characters in total. The name -part must be sequences of `[a-zA-Z0-9]` separated by any of `[-_.]`, at most 63 characters -in total. +Keys have an optional prefix followed by a slash, and then the name part which +is required. The prefix must be a valid lowercase domain name, at most 253 +characters in total. The name part must be sequences of `[a-zA-Z0-9]` separated +by any of `[-_.]`, at most 63 characters in total. The `backstage.io/` prefix is reserved for use by Backstage core components. diff --git a/docs/architecture-decisions/adr003-avoid-default-exports.md b/docs/architecture-decisions/adr003-avoid-default-exports.md index 20a402e9c2..7becebaa4b 100644 --- a/docs/architecture-decisions/adr003-avoid-default-exports.md +++ b/docs/architecture-decisions/adr003-avoid-default-exports.md @@ -6,30 +6,50 @@ ## Context -When CommonJS was the primary authoring format, the best practice was to export only one thing from a module using the `module.exports = ...` format. This aligned with the [UNIX philosophy](https://en.wikipedia.org/wiki/Unix_philosophy) of "Do one thing well". The module would be consumed (`const localName = require('the-module');`) without having to know the internal structure. +When CommonJS was the primary authoring format, the best practice was to export +only one thing from a module using the `module.exports = ...` format. This +aligned with the +[UNIX philosophy](https://en.wikipedia.org/wiki/Unix_philosophy) of "Do one +thing well". The module would be consumed +(`const localName = require('the-module');`) without having to know the internal +structure. -Now, ESModules are the primary authoring format. They have numerous benefits, such as compile-time verification of exports, and standards-defined semantics. They have a similar mechanism known as "default exports", which allows for a consumer to `import localName from 'the-module';`. This is implicitly the same as `import { default as localName } from 'the-module';`. +Now, ESModules are the primary authoring format. They have numerous benefits, +such as compile-time verification of exports, and standards-defined semantics. +They have a similar mechanism known as "default exports", which allows for a +consumer to `import localName from 'the-module';`. This is implicitly the same +as `import { default as localName } from 'the-module';`. -However, there are numerous reasons to avoid default exports, as documented by others before: +However, there are numerous reasons to avoid default exports, as documented by +others before: - https://humanwhocodes.com/blog/2019/01/stop-using-default-exports-javascript-module/ A summary: -- They add indirection by encouraging a developer to create local names for modules, increasing cognitive load and slowing down code comprehension: `import TheListThing from 'not-a-list-thing';`. -- They thwart tools, such as IDEs, that can automatically rename and refactor code. -- They promote typos and mistakes, as the imported member is completely up to the consuming developer to define. -- They are ugly in CommonJS interop, as the default property must be manually specified by the consumer. This is often hidden by Babel's module interop. -- They break re-exports due to name conflicts, forcing the developer to manually name each. +- They add indirection by encouraging a developer to create local names for + modules, increasing cognitive load and slowing down code comprehension: + `import TheListThing from 'not-a-list-thing';`. +- They thwart tools, such as IDEs, that can automatically rename and refactor + code. +- They promote typos and mistakes, as the imported member is completely up to + the consuming developer to define. +- They are ugly in CommonJS interop, as the default property must be manually + specified by the consumer. This is often hidden by Babel's module interop. +- They break re-exports due to name conflicts, forcing the developer to manually + name each. -Using named exports helps prevent needing to rename symbols, which has myriad benefts. A few are: +Using named exports helps prevent needing to rename symbols, which has myriad +benefts. A few are: - IDE tools like "Find All References" and "Go To Definition" function - Manual codebase searching ("grep", etc) is easier with a unique symbol ## Decision -We will stop using default exports except when absolutely necessary (such as [`React.lazy`](https://reactjs.org/docs/code-splitting.html#reactlazy) modules). A workaround exists for those that would prefer to never use `default`: +We will stop using default exports except when absolutely necessary (such as +[`React.lazy`](https://reactjs.org/docs/code-splitting.html#reactlazy) modules). +A workaround exists for those that would prefer to never use `default`: ```ts const Component = React.lazy(() => @@ -39,6 +59,9 @@ const Component = React.lazy(() => ## Consequences -We will actively work to remove them from our codebases, being as explicit as possible. Have a connected component? `export const ConnectedComponent = connect(Component)`. +We will actively work to remove them from our codebases, being as explicit as +possible. Have a connected component? +`export const ConnectedComponent = connect(Component)`. -We will add tools, such as lint rules, to help migrate away from default exports. +We will add tools, such as lint rules, to help migrate away from default +exports. diff --git a/docs/architecture-decisions/adr004-module-export-structure.md b/docs/architecture-decisions/adr004-module-export-structure.md index 56c675f069..54132773c9 100644 --- a/docs/architecture-decisions/adr004-module-export-structure.md +++ b/docs/architecture-decisions/adr004-module-export-structure.md @@ -6,7 +6,8 @@ ## Context -With a growing number of exports of packages like `@backstage/core`, it is becoming more and more difficult to answer questions such as +With a growing number of exports of packages like `@backstage/core`, it is +becoming more and more difficult to answer questions such as > Is the export in this module also exported by the package? @@ -14,12 +15,19 @@ or > What is exported from this directory? -We currently do not use any pattern for how to structure exports. There is a mix of package-level re-exports deep into the directory tree, shallow re-exports for each directory, exports using `*` and explicit lists of each symbol, etc. -The mix and lack of predictability makes it difficult to reason about the boundaries of a module, and for example knowing whether is is safe to export a symbol in a given file. +We currently do not use any pattern for how to structure exports. There is a mix +of package-level re-exports deep into the directory tree, shallow re-exports for +each directory, exports using `*` and explicit lists of each symbol, etc. The +mix and lack of predictability makes it difficult to reason about the boundaries +of a module, and for example knowing whether is is safe to export a symbol in a +given file. ## Decision -We will make each exported symbol traceable through index files all the way down to the root of the package, `src/index.ts`. Each index file will only re-export from its own immediate directory children, and only index files will have re-exports. This gives a file tree similar to this: +We will make each exported symbol traceable through index files all the way down +to the root of the package, `src/index.ts`. Each index file will only re-export +from its own immediate directory children, and only index files will have +re-exports. This gives a file tree similar to this: ```text index.ts @@ -33,16 +41,25 @@ lib/index.ts /helper.ts ``` -To check whether for example `SubComponentY` is exported from the package, it should be possible to traverse the index files towards the root, starting at the adjacent one. If there is any index file that doesn't export the previous one, the symbol is not publicly exported. For example, if `components/ComponentX/index.ts` exports `SubComponentY`, but `components/index.ts` does not re-export `./ComponentX`, one should be certain that `SubComponentY` is not exported outside the package. This rule would be broken if for example the root `index.ts` re-exports `./components/ComponentX` +To check whether for example `SubComponentY` is exported from the package, it +should be possible to traverse the index files towards the root, starting at the +adjacent one. If there is any index file that doesn't export the previous one, +the symbol is not publicly exported. For example, if +`components/ComponentX/index.ts` exports `SubComponentY`, but +`components/index.ts` does not re-export `./ComponentX`, one should be certain +that `SubComponentY` is not exported outside the package. This rule would be +broken if for example the root `index.ts` re-exports `./components/ComponentX` -In addition, index files that are re-exporting other index files should always use wildcard form, that is: +In addition, index files that are re-exporting other index files should always +use wildcard form, that is: ```ts // in components/index.ts export * from './ComponentX'; ``` -Index files that are re-exporting symbols from non-index files should always enumerate all exports, that is: +Index files that are re-exporting symbols from non-index files should always +enumerate all exports, that is: ```ts // in components/ComponentX/index.ts @@ -50,14 +67,16 @@ export { ComponentX } from './ComponentX'; export type { ComponentXProps } from './ComponentX'; ``` -Internal cross-directory imports are allowed from non-index modules to index modules, for example: +Internal cross-directory imports are allowed from non-index modules to index +modules, for example: ```ts // in components/ComponentX/ComponentX.tsx import { UtilityX } from '../../lib/UtilityX'; ``` -Imports that bypass an index file are discouraged, but may sometimes be necessary, for example: +Imports that bypass an index file are discouraged, but may sometimes be +necessary, for example: ```ts // in components/ComponentX/ComponentX.tsx @@ -66,6 +85,9 @@ import { helperFunc } from '../../lib/UtilityX/helper'; ## Consequences -We will actively work to rework the export structure in our codebase, prioritizing the library packages such as `@backstage/core` and `@backstage/backend-common`. +We will actively work to rework the export structure in our codebase, +prioritizing the library packages such as `@backstage/core` and +`@backstage/backend-common`. -If possible, we will add tools, such as lint rules, to help enforce the export structure. +If possible, we will add tools, such as lint rules, to help enforce the export +structure. diff --git a/docs/architecture-decisions/adr005-catalog-core-entities.md b/docs/architecture-decisions/adr005-catalog-core-entities.md index ca2a66f6b9..578eb72b91 100644 --- a/docs/architecture-decisions/adr005-catalog-core-entities.md +++ b/docs/architecture-decisions/adr005-catalog-core-entities.md @@ -6,7 +6,8 @@ ## Context -We want to standardize on a few core entities that we are tracking in the Backstage catalog. This allows us to build specific plugins around them. +We want to standardize on a few core entities that we are tracking in the +Backstage catalog. This allows us to build specific plugins around them. ## Decision @@ -14,17 +15,26 @@ Backstage should eventually support the following core entities: - **Components** are individual pieces of software - **APIs** are the boundaries between different components -- **Resources** are physical or virtual infrastructure needed to operate a component +- **Resources** are physical or virtual infrastructure needed to operate a + component ![Catalog Core Entities](catalog-core-entities.png) -For now, we'll start by only implementing support for the Component entity in the Backstage catalog. This can later be extended to APIs, Resources and other potentially useful entities. +For now, we'll start by only implementing support for the Component entity in +the Backstage catalog. This can later be extended to APIs, Resources and other +potentially useful entities. ### Component -A component is a piece of software, for example a mobile application feature, web site, backend service or data pipeline (list not exhaustive). A component can be tracked in source control, or use some existing open source or commercial software. It can implement APIs for other components to consume. In turn it might depend on APIs implemented by other components, or resources that are attached to it at runtime. +A component is a piece of software, for example a mobile application feature, +web site, backend service or data pipeline (list not exhaustive). A component +can be tracked in source control, or use some existing open source or commercial +software. It can implement APIs for other components to consume. In turn it +might depend on APIs implemented by other components, or resources that are +attached to it at runtime. -Component entities are typically defined in YAML descriptor files next to the code of the component, and could look like this (actual schema will evolve): +Component entities are typically defined in YAML descriptor files next to the +code of the component, and could look like this (actual schema will evolve): ```yaml apiVersion: backstage.io/v1beta1 @@ -37,11 +47,20 @@ spec: ### API -APIs form an abstraction that allows large software ecosystems to scale. Thus, APIs are a first class citizen in the Backstage model and the primary way to discover existing functionality in the ecosystem. +APIs form an abstraction that allows large software ecosystems to scale. Thus, +APIs are a first class citizen in the Backstage model and the primary way to +discover existing functionality in the ecosystem. -APIs are implemented by components and make their boundaries explicit. They might be defined using an RPC IDL (e.g. in Protobuf, GraphQL or similar), a data schema (e.g. in Avro, TFRecord or similar), or as code interfaces (e.g. framework APIs in Swift, Kotlin, Java, C++, Typescript etc). In any case, APIs exposed by components need to be in a known machine-readable format so we can build further tooling and analysis on top. +APIs are implemented by components and make their boundaries explicit. They +might be defined using an RPC IDL (e.g. in Protobuf, GraphQL or similar), a data +schema (e.g. in Avro, TFRecord or similar), or as code interfaces (e.g. +framework APIs in Swift, Kotlin, Java, C++, Typescript etc). In any case, APIs +exposed by components need to be in a known machine-readable format so we can +build further tooling and analysis on top. -APIs are typically indexed from existing definitions in source control and thus wouldn't need their own descriptor files, but would be stored in the catalog somewhat like this (actual schema will evolve): +APIs are typically indexed from existing definitions in source control and thus +wouldn't need their own descriptor files, but would be stored in the catalog +somewhat like this (actual schema will evolve): ```yaml apiVersion: backstage.io/v1beta1 @@ -54,9 +73,11 @@ spec: service HelloService { rpc SayHello (HelloRequest) returns (HelloResponse); } + message HelloRequest { string greeting = 1; } + message HelloResponse { string reply = 1; } @@ -64,9 +85,16 @@ spec: ### Resource -Resources are the infrastructure your software needs to operate at runtime like Bigtable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together with components and APIs will allow us to visualize and create tooling around them in Backstage. +Resources are the infrastructure your software needs to operate at runtime like +Bigtable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together +with components and APIs will allow us to visualize and create tooling around +them in Backstage. -Resources are typically indexed from declarative definitions (e.g. Terraform, GCP Config Connector, AWS Cloud Formation) and/or inventories from cloud providers (e.g. GCP Asset Inventory) and thus wouldn't need their own descriptor files, but would be stored in the catalog somewhat like this (actual schema will evolve): +Resources are typically indexed from declarative definitions (e.g. Terraform, +GCP Config Connector, AWS Cloud Formation) and/or inventories from cloud +providers (e.g. GCP Asset Inventory) and thus wouldn't need their own descriptor +files, but would be stored in the catalog somewhat like this (actual schema will +evolve): ```yaml apiVersion: backstage.io/v1beta1 @@ -80,4 +108,5 @@ spec: ## Consequences -We will continue fleshing out support for the Component entity in the Backstage catalog. +We will continue fleshing out support for the Component entity in the Backstage +catalog. diff --git a/docs/architecture-terminology.md b/docs/architecture-terminology.md index 1570607896..836b2cfa99 100644 --- a/docs/architecture-terminology.md +++ b/docs/architecture-terminology.md @@ -1,9 +1,18 @@ # Architecture and Terminology -Backstage is constructed out of three parts. We separate Backstage in this way because we see three groups of contributors that work with Backstage in three different ways. +Backstage is constructed out of three parts. We separate Backstage in this way +because we see three groups of contributors that work with Backstage in three +different ways. - Core - Base functionality built by core devs in the open source project. -- App - The app is an instance of a Backstage app that is deployed and tweaked. The app ties together core functionality with additional plugins. The app is built and maintained by app developers, usually a productivity team within a company. -- Plugins - Additional functionality to make your Backstage app useful for your company. Plugins can be specific to a company or open sourced and reusable. At Spotify we have over 100 plugins built by over 50 different teams. It has been very powerful to get contributions from various infrastructure teams added into a single unified developer experience. +- App - The app is an instance of a Backstage app that is deployed and tweaked. + The app ties together core functionality with additional plugins. The app is + built and maintained by app developers, usually a productivity team within a + company. +- Plugins - Additional functionality to make your Backstage app useful for your + company. Plugins can be specific to a company or open sourced and reusable. At + Spotify we have over 100 plugins built by over 50 different teams. It has been + very powerful to get contributions from various infrastructure teams added + into a single unified developer experience. [Back to Docs](README.md) diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index 4bbbec88bb..33f1d335a0 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -2,17 +2,21 @@ ## Passport -We chose [Passport](http://www.passportjs.org/) as our authentication platform due to its comprehensive set of supported authentication [strategies](http://www.passportjs.org/packages/). +We chose [Passport](http://www.passportjs.org/) as our authentication platform +due to its comprehensive set of supported authentication +[strategies](http://www.passportjs.org/packages/). ## How to add a new strategy provider ### Quick guide -[1.](#installing-the-dependencies) Install the passport-js based provider package. +[1.](#installing-the-dependencies) Install the passport-js based provider +package. [2.](#create-implementation) Create a new folder structure for the provider. -[3.](#adding-an-oauth-based-provider) Implement the provider, extending the suitable framework if needed. +[3.](#adding-an-oauth-based-provider) Implement the provider, extending the +suitable framework if needed. [4.](#hook-it-up-to-the-backend) Add the provider to the backend. @@ -26,7 +30,8 @@ yarn add @types/passport-provider-a ### Create implementation -Make a new folder with the name of the provider following the below file structure: +Make a new folder with the name of the provider following the below file +structure: ```bash plugins/auth-backend/src/providers/providerA @@ -34,13 +39,16 @@ plugins/auth-backend/src/providers/providerA └── provider.ts ``` -**`plugins/auth-backend/src/providers/providerA/provider.ts`** defines the provider class which implements a handler for the chosen framework. +**`plugins/auth-backend/src/providers/providerA/provider.ts`** defines the +provider class which implements a handler for the chosen framework. #### Adding an OAuth based provider -If we're adding an `OAuth` based provider we would implement the [OAuthProviderHandlers](#OAuthProviderHandlers) interface. +If we're adding an `OAuth` based provider we would implement the +[OAuthProviderHandlers](#OAuthProviderHandlers) interface. -The provider class takes the provider's configuration as a class parameter. It also imports the `Strategy` from the passport package. +The provider class takes the provider's configuration as a class parameter. It +also imports the `Strategy` from the passport package. ```ts import { Strategy as ProviderAStrategy } from 'passport-provider-a'; @@ -64,9 +72,11 @@ export class ProviderAAuthProvider implements OAuthProviderHandlers { #### Adding an non-OAuth based provider -_**Note**: We have prioritized OAuth-based providers and non-OAuth providers should be considered experimental._ +_**Note**: We have prioritized OAuth-based providers and non-OAuth providers +should be considered experimental._ -An non-`OAuth` based provider could implement [AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead. +An non-`OAuth` based provider could implement +[AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead. ```ts export class ProviderAAuthProvider implements AuthProviderRouteHandlers { @@ -90,9 +100,12 @@ export class ProviderAAuthProvider implements AuthProviderRouteHandlers { #### Create method -Each provider exports a create method that creates the provider instance, optionally extending a supported authorization framework. This method exists to allow for flexibility if additional frameworks are supported in the future. +Each provider exports a create method that creates the provider instance, +optionally extending a supported authorization framework. This method exists to +allow for flexibility if additional frameworks are supported in the future. -Implementing OAuth by returning an instance of `OAuthProvider` based of the provider's class: +Implementing OAuth by returning an instance of `OAuthProvider` based of the +provider's class: ```ts export function createProviderAProvider(config: AuthProviderConfig) { @@ -102,7 +115,9 @@ export function createProviderAProvider(config: AuthProviderConfig) { } ``` -Not extending with OAuth, the main difference here is that the create method is returning a instance of the class without adding the OAuth authorization framework to it. +Not extending with OAuth, the main difference here is that the create method is +returning a instance of the class without adding the OAuth authorization +framework to it. ```ts export function createProviderAProvider(config: AuthProviderConfig) { @@ -112,14 +127,22 @@ export function createProviderAProvider(config: AuthProviderConfig) { #### Verify Callback -> Strategies require what is known as a verify callback. The purpose of a verify callback is to find the user that possesses a set of credentials. -> When Passport authenticates a request, it parses the credentials contained in the request. It then invokes the verify callback with those credentials as arguments [...]. If the credentials are valid, the verify callback invokes done to supply Passport with the user that authenticated. +> Strategies require what is known as a verify callback. The purpose of a verify +> callback is to find the user that possesses a set of credentials. When +> Passport authenticates a request, it parses the credentials contained in the +> request. It then invokes the verify callback with those credentials as +> arguments [...]. If the credentials are valid, the verify callback invokes +> done to supply Passport with the user that authenticated. > -> If the credentials are not valid (for example, if the password is incorrect), done should be invoked with false instead of a user to indicate an authentication failure. +> If the credentials are not valid (for example, if the password is incorrect), +> done should be invoked with false instead of a user to indicate an +> authentication failure. > > http://www.passportjs.org/docs/configure/ -**`plugins/auth-backend/src/providers/providerA/index.ts`** is simply re-exporting the create method to be used for hooking the provider up to the backend. +**`plugins/auth-backend/src/providers/providerA/index.ts`** is simply +re-exporting the create method to be used for hooking the provider up to the +backend. ```ts export { createProviderAProvider } from './provider'; @@ -127,7 +150,9 @@ export { createProviderAProvider } from './provider'; ### Hook it up to the backend -**`plugins/auth-backend/src/providers/config.ts`** The provider needs to be configured properly so you need to add it to the list of configured providers, all of which implement [AuthProviderConfig](#AuthProviderConfig): +**`plugins/auth-backend/src/providers/config.ts`** The provider needs to be +configured properly so you need to add it to the list of configured providers, +all of which implement [AuthProviderConfig](#AuthProviderConfig): ```ts export const providers = [ @@ -138,7 +163,10 @@ export const providers = [ }, ``` -**`plugins/auth-backend/src/providers/factories.ts`** When the `auth-backend` starts it sets up routing for all the available providers by calling `createAuthProviderRouter` on each provider. You need to import the create method from the provider and add it to the factory: +**`plugins/auth-backend/src/providers/factories.ts`** When the `auth-backend` +starts it sets up routing for all the available providers by calling +`createAuthProviderRouter` on each provider. You need to import the create +method from the provider and add it to the factory: ```ts import { createProviderAProvider } from './providerA'; @@ -157,11 +185,14 @@ router.post('/auth/providerA/logout'); router.get('/auth/providerA/refresh'); // if supported ``` -As you can see each endpoint is prefixed with both `/auth` and its provider name. +As you can see each endpoint is prefixed with both `/auth` and its provider +name. ### Test the new provider -You can `curl -i localhost:7000/auth/providerA/start` and which should provide a `302` redirect with a `Location` header. Paste the url from that header into a web browser and you should be able to trigger the authorization flow. +You can `curl -i localhost:7000/auth/providerA/start` and which should provide a +`302` redirect with a `Location` header. Paste the url from that header into a +web browser and you should be able to trigger the authorization flow. --- diff --git a/docs/create-an-app.md b/docs/create-an-app.md index 8c8050c2ff..b69ff1508c 100644 --- a/docs/create-an-app.md +++ b/docs/create-an-app.md @@ -1,12 +1,16 @@ # Backstage App -To get set up quickly with your own Backstage project you can create a Backstage App. +To get set up quickly with your own Backstage project you can create a Backstage +App. -A Backstage App is a monorepo setup with `lerna` that includes everything you need to run Backstage in your own environment. +A Backstage App is a monorepo setup with `lerna` that includes everything you +need to run Backstage in your own environment. ## Create an app -To create a Backstage app, you will need to have [NodeJS](https://nodejs.org/en/download/) Active LTS Release installed (currently v12). +To create a Backstage app, you will need to have +[NodeJS](https://nodejs.org/en/download/) Active LTS Release installed +(currently v12). With `npx`: @@ -14,13 +18,15 @@ With `npx`: npx @backstage/cli create-app ``` -This will create a new Backstage App inside the current folder. The name of the app-folder is the name that was provided when prompted. +This will create a new Backstage App inside the current folder. The name of the +app-folder is the name that was provided when prompted.

create app

-Inside that directory, it will generate all the files and folder structure needed for you to run your app. +Inside that directory, it will generate all the files and folder structure +needed for you to run your app. ### Folder structure @@ -69,4 +75,5 @@ cd my-backstage-app yarn start ``` -_When `yarn start` is ready it should open up a browser window displaying your app, if not you can navigate to `http://localhost:3000`._ +_When `yarn start` is ready it should open up a browser window displaying your +app, if not you can navigate to `http://localhost:3000`._ diff --git a/docs/design.md b/docs/design.md index bcbf0bbdc5..e3816bd2e5 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,70 +1,134 @@ ![header](designheader.png) -Much like Backstage Open Source, this is a *living* document! We'll keep this updated as we evolve our practices! +Much like Backstage Open Source, this is a _living_ document! We'll keep this +updated as we evolve our practices! ## 📚 Our Philosophy ### Iterative -Backstage Open Source is a newly launched endeavor, and we’re excited to scale up our design practices! With that said, we’ll be working closely with you, the community, and iterating and experimenting as we go to see what works best. As a continual work in progress, we aspire to release early and often. Not only that, we are committed to working with developers to create a seamless and easy handoff. If you’re curious to see how we grow and would like to play a role in that growth, check out the issues in this GitHub repo! +Backstage Open Source is a newly launched endeavor, and we’re excited to scale +up our design practices! With that said, we’ll be working closely with you, the +community, and iterating and experimenting as we go to see what works best. As a +continual work in progress, we aspire to release early and often. Not only that, +we are committed to working with developers to create a seamless and easy +handoff. If you’re curious to see how we grow and would like to play a role in +that growth, check out the issues in this GitHub repo! ### 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/EBHEGzX) (*#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/EBHEGzX). 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 -The chart below details how we work. ***Stay tuned***: We are currently in the process of securing a Figma workspace for Backstage Open Source, and we plan on referencing Figma documents to share specs and prototypes with the community. + +The chart below details how we work. **_Stay tuned_**: We are currently in the +process of securing a Figma workspace for Backstage Open Source, and we plan on +referencing Figma documents to share specs and prototypes with the community. ### Creating a New Design Component -| Step 1 | Step 2 | Step 3 | Step 4 | Step 5 | Step 6 | -|:---|:---|:---|:---|:---|:---| -| Platform design team submits an issue to **spotify/Backstage GitHub** with a potential component. | Backstage community offers feedback or approval on **spotify/Backstage GitHub**. | Platform design team adjusts accordingly (as they see fit) and update the Figma DLS document. | Designed component is added to **spotify/Backstage GitHub** as an issue. | External or internal Backstage open source contributors build the component. | External or internal contributors add the component to the **Backstage Storybook**. 🎉 | +| Step 1 | Step 2 | Step 3 | Step 4 | Step 5 | Step 6 | +| :------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- | :--------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | +| Platform design team submits an issue to **spotify/Backstage GitHub** with a potential component. | Backstage community offers feedback or approval on **spotify/Backstage GitHub**. | Platform design team adjusts accordingly (as they see fit) and update the Figma DLS document. | Designed component is added to **spotify/Backstage GitHub** as an issue. | External or internal Backstage open source contributors build the component. | External or internal contributors add the component to the **Backstage Storybook**. 🎉 | ### Building for Backstage -| Step 1 | Step 2 | Step 3 | Step 4 | -|:---|:---|:---|:---| -| External or internal contributors use Backstage and come up with an idea of an entity to build for Backstage. |External or internal contributors refer to the Backstage Open Source design system documentation in the Figma DLS document. | External or internal contributors leverage the components and tokens from the Backstage Storybook. | External or internal contributors build their Backstage entity. | -| Step 5 | Step 6 | Step 7 | Step 8 | -|:---|:---|:---|:---| -| External or internal contributors make a pull request for their entity on spotify/Backstage GitHub for review. | Platform designers and devs review the entity and submit feedback or approval on spotify/Backstage GitHub. | External or internal contributors make the changes, pull request is approved and the entity is merged. It’s live on Backstage! 🎉 | If the entity happens to be or include a UX component, it’s added to Backstage Storybook as well. | +| Step 1 | Step 2 | Step 3 | Step 4 | +| :------------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | +| External or internal contributors use Backstage and come up with an idea of an entity to build for Backstage. | External or internal contributors refer to the Backstage Open Source design system documentation in the Figma DLS document. | External or internal contributors leverage the components and tokens from the Backstage Storybook. | External or internal contributors build their Backstage entity. | +| Step 5 | Step 6 | Step 7 | Step 8 | +| :------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------ | +| External or internal contributors make a pull request for their entity on spotify/Backstage GitHub for review. | Platform designers and devs review the entity and submit feedback or approval on spotify/Backstage GitHub. | External or internal contributors make the changes, pull request is approved and the entity is merged. It’s live on Backstage! 🎉 | If the entity happens to be or include a UX component, it’s added to Backstage Storybook as well. | -The following diagram shows the relationship between the Backstage Design System and our foundation, which comprises of [Material UI](https://material-ui.com/) that is shaped by user experience and user interface decisions made by our Backstage Design Team. Also note, we encourage you to take the core experience we’ve crafted and add custom theming to better represent your organization! +The following diagram shows the relationship between the Backstage Design System +and our foundation, which comprises of [Material UI](https://material-ui.com/) +that is shaped by user experience and user interface decisions made by our +Backstage Design Team. Also note, we encourage you to take the core experience +we’ve crafted and add custom theming to better represent your organization! ![dls](DLS.png) - ## ✅ Our Priorities + ### Backstage Design System -This is the set of building blocks for Backstage contributors to leverage as they create rad plugins for Backstage! Why reinvent the wheel when you can use components that have already been vetted by our team and the Backstage community? In the spirit of crafting a cohesive and consistent user experience across all of Backstage, we strongly urge all plugin developers to utilize our Storybook as a reference. Our design system is new and evolving, and we’ll be building it up with your help! + +This is the set of building blocks for Backstage contributors to leverage as +they create rad plugins for Backstage! Why reinvent the wheel when you can use +components that have already been vetted by our team and the Backstage +community? In the spirit of crafting a cohesive and consistent user experience +across all of Backstage, we strongly urge all plugin developers to utilize our +Storybook as a reference. Our design system is new and evolving, and we’ll be +building it up with your help! + ### Core Backstage User Experience -This is the universal user experience that is shared amongst all Backstage users. From more concrete aspects like the plugins marketplace to more abstract ones like end-to-end workflows on Backstage, we’ll be working with the community to create a core user experience that best serves you and your organization. + +This is the universal user experience that is shared amongst all Backstage +users. From more concrete aspects like the plugins marketplace to more abstract +ones like end-to-end workflows on Backstage, we’ll be working with the community +to create a core user experience that best serves you and your organization. ## ⭐️ 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/EBHEGzX). We absolutely adore our external contributors and will send you virtual semlas for your contributions! + +### 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/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/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. + +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/EBHEGzX)** - all design questions should be directed to the *#design* channel. +**[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/EBHEGzX)** - all design questions should be +directed to the _#design_ channel. **Documentation** + - Patterns (stay tuned) - Figma files/libraries (stay tuned) ## 🔮 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/EBHEGzX). +### 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/EBHEGzX). [Back to Docs](../README.md) diff --git a/docs/getting-started/Plugin development.md b/docs/getting-started/Plugin development.md index 0f578151a2..41918b5de2 100644 --- a/docs/getting-started/Plugin development.md +++ b/docs/getting-started/Plugin development.md @@ -2,10 +2,10 @@ Backstage plugins provide features to a Backstage App. -Each plugin is treated as a self-contained web app and can include almost any type of content. -Plugins all use a common set of platform APIs and reusable UI components. -Plugins can fetch data from external sources using the regular browser APIs or by depending on -external modules to do the work. +Each plugin is treated as a self-contained web app and can include almost any +type of content. Plugins all use a common set of platform APIs and reusable UI +components. Plugins can fetch data from external sources using the regular +browser APIs or by depending on external modules to do the work.