diff --git a/packages/core/src/data/localStorage.test.ts b/packages/core/src/data/localStorage.test.ts new file mode 100644 index 0000000000..0cfcac73c5 --- /dev/null +++ b/packages/core/src/data/localStorage.test.ts @@ -0,0 +1,25 @@ +/* + * 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 { LocalStorage } from './localStorage'; + +describe('local storage access object', () => { + it('should save and read sidebar pin state', () => { + LocalStorage.setSidebarPinState(false); + expect(LocalStorage.getSidebarPinState()).toBe(false); + LocalStorage.setSidebarPinState(true); + expect(LocalStorage.getSidebarPinState()).toBe(true); + }); +}); diff --git a/packages/core/src/data/localStorage.ts b/packages/core/src/data/localStorage.ts new file mode 100644 index 0000000000..665c97423a --- /dev/null +++ b/packages/core/src/data/localStorage.ts @@ -0,0 +1,40 @@ +/* + * 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. + */ + +enum LocalStorageKeys { + SIDEBAR_PIN_STATE = 'sidebarPinState', +} + +export const LocalStorage = { + getSidebarPinState(): boolean { + let value; + try { + value = JSON.parse( + window.localStorage.getItem(LocalStorageKeys.SIDEBAR_PIN_STATE) || + 'false', + ); + } catch { + return false; + } + return !!value; + }, + setSidebarPinState(state: boolean) { + return window.localStorage.setItem( + LocalStorageKeys.SIDEBAR_PIN_STATE, + JSON.stringify(state), + ); + }, +}; diff --git a/packages/core/src/hooks/useSidebarPinState.ts b/packages/core/src/hooks/useSidebarPinState.ts new file mode 100644 index 0000000000..bbe548c699 --- /dev/null +++ b/packages/core/src/hooks/useSidebarPinState.ts @@ -0,0 +1,28 @@ +/* + * 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 { useContext } from 'react'; +import { SidebarPinStateContext } from '../layout/Sidebar'; + +export function useSidebarPinState() { + const { isPinned, toggleSidebarPinState } = useContext( + SidebarPinStateContext, + ); + + return { + isPinned, + toggleSidebarPinState, + }; +} diff --git a/packages/core/src/layout/Sidebar/Bar.tsx b/packages/core/src/layout/Sidebar/Bar.tsx index 8fd89c313b..c5a8ae5669 100644 --- a/packages/core/src/layout/Sidebar/Bar.tsx +++ b/packages/core/src/layout/Sidebar/Bar.tsx @@ -19,8 +19,9 @@ import clsx from 'clsx'; import React, { FC, useRef, useState } from 'react'; import { sidebarConfig, SidebarContext } from './config'; import { BackstageTheme } from '@backstage/theme'; +import { useSidebarPinState } from '../../hooks/useSidebarPinState'; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(theme => ({ root: { zIndex: 1000, position: 'relative', @@ -75,8 +76,12 @@ export const Sidebar: FC = ({ const classes = useStyles(); const [state, setState] = useState(State.Closed); const hoverTimerRef = useRef(); + const { isPinned } = useSidebarPinState(); const handleOpen = () => { + if (isPinned) { + return; + } if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = undefined; @@ -92,6 +97,9 @@ export const Sidebar: FC = ({ }; const handleClose = () => { + if (isPinned) { + return; + } if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = undefined; @@ -115,11 +123,15 @@ export const Sidebar: FC = ({ onBlur={handleClose} data-testid="sidebar-root" > - +
{children} diff --git a/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx b/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx new file mode 100644 index 0000000000..f9bd4ed2ab --- /dev/null +++ b/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx @@ -0,0 +1,61 @@ +/* + * 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 { makeStyles, Theme } from '@material-ui/core/styles'; +import { sidebarConfig } from './config'; +import { Avatar, Typography } from '@material-ui/core'; + +const useStyles = makeStyles(() => { + const { drawerWidthOpen, userBadgeDiameter } = sidebarConfig; + return { + root: { + width: drawerWidthOpen, + display: 'flex', + alignItems: 'center', + color: '#b5b5b5', + paddingLeft: 18, + paddingTop: 14, + paddingBottom: 14, + }, + avatar: { + width: userBadgeDiameter, + height: userBadgeDiameter, + marginRight: 8, + }, + }; +}); + +type Props = { + imageUrl: string; + name: string; + hideName?: boolean; +}; + +export const LoggedUserBadge: FC = ({ + imageUrl, + name, + hideName = false, +}) => { + const classes = useStyles(); + + return ( +
+ + {!hideName && {name}} +
+ ); +}; diff --git a/packages/core/src/layout/Sidebar/Page.tsx b/packages/core/src/layout/Sidebar/Page.tsx index 1c02537ebe..79df2852d9 100644 --- a/packages/core/src/layout/Sidebar/Page.tsx +++ b/packages/core/src/layout/Sidebar/Page.tsx @@ -15,18 +15,53 @@ */ import { makeStyles } from '@material-ui/core'; -import React, { FC } from 'react'; +import React, { createContext, FC, useEffect, useState } from 'react'; import { sidebarConfig } from './config'; +import { BackstageTheme } from '@backstage/theme'; +import { LocalStorage } from '../../data/localStorage'; -const useStyles = makeStyles({ +const useStyles = makeStyles({ root: { width: '100%', minHeight: '100%', - paddingLeft: sidebarConfig.drawerWidthClosed, + transition: 'padding-left 0.1s ease-out', + paddingLeft: ({ isPinned }) => + isPinned + ? sidebarConfig.drawerWidthOpen + : sidebarConfig.drawerWidthClosed, }, }); -export const SidebarPage: FC<{}> = ({ children }) => { - const classes = useStyles(); - return
{children}
; +export type SidebarPinStateContextType = { + isPinned: boolean; + toggleSidebarPinState: () => any; +}; + +export const SidebarPinStateContext = createContext( + { + isPinned: false, + toggleSidebarPinState: () => {}, + }, +); + +export const SidebarPage: FC<{}> = (props) => { + const [isPinned, setIsPinned] = useState(LocalStorage.getSidebarPinState()); + + useEffect(() => { + LocalStorage.setSidebarPinState(isPinned); + }, [isPinned]); + + const toggleSidebarPinState = () => setIsPinned(!isPinned); + + const classes = useStyles({ isPinned }); + return ( + +
{props.children}
+
+ ); }; diff --git a/packages/core/src/layout/Sidebar/UserBadge.tsx b/packages/core/src/layout/Sidebar/UserBadge.tsx index 1614558ab9..b030ed2996 100644 --- a/packages/core/src/layout/Sidebar/UserBadge.tsx +++ b/packages/core/src/layout/Sidebar/UserBadge.tsx @@ -15,58 +15,73 @@ */ import React, { FC, useContext } from 'react'; -import { Avatar, makeStyles, Theme, Typography } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core'; import People from '@material-ui/icons/People'; -import { sidebarConfig, SidebarContext } from './config'; +import { SidebarContext } from './config'; import { SidebarItem } from './Items'; +import { LoggedUserBadge } from './LoggedUserBadge'; +import DoubleArrowIcon from '@material-ui/icons/DoubleArrow'; +import { BackstageTheme } from '@backstage/theme'; +import { SidebarPinStateContext } from './Page'; -const useStyles = makeStyles(() => { - const { drawerWidthOpen, userBadgeDiameter } = sidebarConfig; +const ARROW_BUTTON_SIZE = 20; +const useStyles = makeStyles(theme => { return { root: { - width: drawerWidthOpen, + position: 'relative', + }, + arrowButtonWrapper: { + position: 'absolute', + right: 0, + width: ARROW_BUTTON_SIZE, + height: ARROW_BUTTON_SIZE, + top: `calc(50% - ${ARROW_BUTTON_SIZE / 2}px)`, display: 'flex', alignItems: 'center', - color: '#b5b5b5', - paddingLeft: 18, - paddingTop: 14, - paddingBottom: 14, + justifyContent: 'center', + borderRadius: '2px 0px 0px 2px', + background: theme.palette.pinSidebarButton.icon, + color: theme.palette.pinSidebarButton.background, + border: 'none', + outline: 'none', + cursor: 'pointer', }, - avatar: { - width: userBadgeDiameter, - height: userBadgeDiameter, - marginRight: 8, + arrowButtonIcon: { + transform: ({ isPinned }) => (isPinned ? 'rotate(180deg)' : 'none'), }, }; }); -type Props = { - imageUrl: string; - name: string; - hideName?: boolean; -}; - -export const UserBadge: FC = ({ imageUrl, name, hideName = false }) => { - const classes = useStyles(); +export const SidebarUserBadge: FC<{}> = () => { + const { isOpen } = useContext(SidebarContext); + const { isPinned, toggleSidebarPinState } = useContext( + SidebarPinStateContext, + ); + const classes = useStyles({ isPinned }); + const isUserLoggedIn = false; return (
- - {!hideName && {name}} + {isUserLoggedIn ? ( + + ) : ( + + )} + {isOpen && ( + + )}
); }; - -export const SidebarUserBadge: FC = () => { - const { isOpen } = useContext(SidebarContext); - const isUserLoggedIn = false; - return isUserLoggedIn ? ( - - ) : ( - - ); -}; diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index b251ebfb6d..882a7b8a87 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -110,7 +110,6 @@ class DevAppBuilder { // Create a sidebar that exposes the touchpoints of a plugin private setupSidebar(plugins: BackstagePlugin[]): JSX.Element { const sidebarItems = new Array(); - for (const plugin of plugins) { for (const output of plugin.output()) { switch (output.type) { diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index ff86cca330..fe3c71a212 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -56,6 +56,10 @@ export const lightTheme = createTheme({ link: '#0A6EBE', gold: yellow.A700, sidebar: '#171717', + pinSidebarButton: { + icon: '#BDBDBD', + background: '#404040', + }, }, }); @@ -98,5 +102,9 @@ export const darkTheme = createTheme({ link: '#0A6EBE', gold: yellow.A700, sidebar: '#424242', + pinSidebarButton: { + icon: '#181818', + background: '#BDBDBD', + }, }, }); diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index d11b07d3fe..c38d67d063 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -51,6 +51,10 @@ type PaletteAdditions = { default: string; }; }; + pinSidebarButton: { + icon: string; + background: string; + }; }; export type BackstagePalette = Palette & PaletteAdditions;