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/16] 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/16] /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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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 && (
); }; - -export default ComponentContextMenu; diff --git a/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx index 5b6e7dfe07..aaedf1bcba 100644 --- a/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx +++ b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React from 'react'; -import ComponentMetadataCard from './ComponentMetadataCard'; +import { ComponentMetadataCard } from './ComponentMetadataCard'; import { Component } from '../../data/component'; import { render } from '@testing-library/react'; diff --git a/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.tsx b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.tsx index 7059709992..d765b9cc0b 100644 --- a/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.tsx +++ b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.tsx @@ -13,18 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { InfoCard, Progress, StructuredMetadataTable } from '@backstage/core'; import React, { FC } from 'react'; import { Component } from '../../data/component'; -import { Progress, InfoCard, StructuredMetadataTable } from '@backstage/core'; -type ComponentMetadataCardProps = { +type Props = { loading: boolean; component: Component | undefined; }; -const ComponentMetadataCard: FC = ({ - loading, - component, -}) => { + +export const ComponentMetadataCard: FC = ({ loading, component }) => { if (loading) { return ( @@ -41,4 +39,3 @@ const ComponentMetadataCard: FC = ({ ); }; -export default ComponentMetadataCard; diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx index 98fa6b4408..e0d5c99e4d 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import ComponentPage from './ComponentPage'; +import { ComponentPage } from './ComponentPage'; import { render, wait } from '@testing-library/react'; import * as React from 'react'; import { wrapInTestApp } from '@backstage/test-utils'; diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx index 72aef7fe16..f487926183 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx @@ -13,27 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC, useEffect, useState } from 'react'; -import { useAsync } from 'react-use'; -import ComponentMetadataCard from '../ComponentMetadataCard/ComponentMetadataCard'; import { Content, - Header, - pageTheme, - Page, - useApi, ErrorApi, errorApiRef, + Header, HeaderTabs, + Page, + pageTheme, + useApi, } from '@backstage/core'; -import ComponentContextMenu from '../ComponentContextMenu/ComponentContextMenu'; -import ComponentRemovalDialog from '../ComponentRemovalDialog/ComponentRemovalDialog'; - import { SentryIssuesWidget } from '@backstage/plugin-sentry'; import { Grid } from '@material-ui/core'; +import React, { FC, useEffect, useState } from 'react'; +import { useAsync } from 'react-use'; import { catalogApiRef } from '../..'; -import { entityToComponent } from '../../data/utils'; import { Component } from '../../data/component'; +import { entityToComponent } from '../../data/utils'; +import { ComponentContextMenu } from '../ComponentContextMenu/ComponentContextMenu'; +import { ComponentMetadataCard } from '../ComponentMetadataCard/ComponentMetadataCard'; +import { ComponentRemovalDialog } from '../ComponentRemovalDialog/ComponentRemovalDialog'; const REDIRECT_DELAY = 1000; @@ -49,7 +48,7 @@ type ComponentPageProps = { }; }; -const ComponentPage: FC = ({ match, history }) => { +export const ComponentPage: FC = ({ match, history }) => { const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); const [removingPending, setRemovingPending] = useState(false); const showRemovalDialog = () => setConfirmationDialogOpen(true); @@ -151,4 +150,3 @@ const ComponentPage: FC = ({ match, history }) => { ); }; -export default ComponentPage; diff --git a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx index 1318edcca0..9ea4e64908 100644 --- a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx +++ b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx @@ -51,7 +51,7 @@ function useColocatedEntities(component: Component): AsyncState { }, [catalogApi, component]); } -const ComponentRemovalDialog: FC = ({ +export const ComponentRemovalDialog: FC = ({ onConfirm, onCancel, onClose, @@ -114,5 +114,3 @@ const ComponentRemovalDialog: FC = ({ ); }; - -export default ComponentRemovalDialog; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 1eee5efe07..6f9a580edd 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -15,9 +15,9 @@ */ import { createPlugin } from '@backstage/core'; -import CatalogPage from './components/CatalogPage'; -import ComponentPage from './components/ComponentPage/ComponentPage'; -import { rootRoute, entityRoute } from './routes'; +import { CatalogPage } from './components/CatalogPage/CatalogPage'; +import { ComponentPage } from './components/ComponentPage/ComponentPage'; +import { entityRoute, rootRoute } from './routes'; export const plugin = createPlugin({ id: 'catalog',