Improve side navigation (#589)
* Implement sidebar items * Add intro component * Add user badge component * Add Sidebar simple story component Co-authored-by: Victor Viale <victor.viale@besedo.com> Co-authored-by: Stefan Ålund <alund@spotify.com>
This commit is contained in:
@@ -19,27 +19,30 @@ import PropTypes from 'prop-types';
|
||||
import { Link, makeStyles, Typography } from '@material-ui/core';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import ExploreIcon from '@material-ui/icons/Explore';
|
||||
import AccountCircle from '@material-ui/icons/AccountCircle';
|
||||
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import AccountTreeIcon from '@material-ui/icons/AccountTree';
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarPage,
|
||||
sidebarConfig,
|
||||
SidebarContext,
|
||||
SidebarItem,
|
||||
SidebarSpacer,
|
||||
SidebarDivider,
|
||||
SidebarSearchField,
|
||||
SidebarSpace,
|
||||
SidebarUserBadge,
|
||||
SidebarThemeToggle,
|
||||
} from '@backstage/core';
|
||||
|
||||
const useSidebarLogoStyles = makeStyles({
|
||||
root: {
|
||||
height: sidebarConfig.drawerWidthClosed,
|
||||
width: sidebarConfig.drawerWidthClosed,
|
||||
height: 3 * sidebarConfig.logoHeight,
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
alignItems: 'center',
|
||||
marginBottom: -14,
|
||||
},
|
||||
logoContainer: {
|
||||
width: sidebarConfig.drawerWidthClosed,
|
||||
@@ -48,9 +51,9 @@ const useSidebarLogoStyles = makeStyles({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontSize: sidebarConfig.logoHeight,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 22,
|
||||
marginLeft: 20,
|
||||
whiteSpace: 'nowrap',
|
||||
color: '#fff',
|
||||
},
|
||||
@@ -61,7 +64,7 @@ const useSidebarLogoStyles = makeStyles({
|
||||
|
||||
const SidebarLogo: FC<{}> = () => {
|
||||
const classes = useSidebarLogoStyles();
|
||||
const isOpen = useContext(SidebarContext);
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
@@ -75,21 +78,30 @@ const SidebarLogo: FC<{}> = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleSearch = (query: string): void => {
|
||||
// XXX (@koroeskohr): for testing purposes
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(query);
|
||||
};
|
||||
|
||||
const Root: FC<{}> = ({ children }) => (
|
||||
<SidebarPage>
|
||||
<Sidebar>
|
||||
<SidebarLogo />
|
||||
<SidebarSpacer />
|
||||
<SidebarSearchField onSearch={handleSearch} />
|
||||
<SidebarDivider />
|
||||
{/* Global nav, not org-specific */}
|
||||
<SidebarItem icon={HomeIcon} to="/" text="Home" />
|
||||
<SidebarItem icon={ExploreIcon} to="/explore" text="Explore" />
|
||||
<SidebarItem icon={CreateComponentIcon} to="/create" text="Create..." />
|
||||
{/* End global nav */}
|
||||
<SidebarDivider />
|
||||
<SidebarItem icon={AccountTreeIcon} to="/catalog" text="Catalog" />
|
||||
<SidebarItem icon={AccountCircle} to="/login" text="Login" />
|
||||
<SidebarDivider />
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarThemeToggle />
|
||||
<SidebarUserBadge />
|
||||
</Sidebar>
|
||||
{children}
|
||||
</SidebarPage>
|
||||
|
||||
@@ -20,7 +20,7 @@ import React, { FC, useRef, useState } from 'react';
|
||||
import { sidebarConfig, SidebarContext } from './config';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
const useStyles = makeStyles<BackstageTheme>((theme) => ({
|
||||
root: {
|
||||
zIndex: 1000,
|
||||
position: 'relative',
|
||||
@@ -115,7 +115,7 @@ export const Sidebar: FC<Props> = ({
|
||||
onBlur={handleClose}
|
||||
data-testid="sidebar-root"
|
||||
>
|
||||
<SidebarContext.Provider value={state === State.Open}>
|
||||
<SidebarContext.Provider value={{ isOpen: state === State.Open }}>
|
||||
<div
|
||||
className={clsx(classes.drawer, {
|
||||
[classes.drawerPeek]: state === State.Peek,
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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, useContext, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { Link, Typography, makeStyles, Collapse } from '@material-ui/core';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import {
|
||||
SIDEBAR_INTRO_LOCAL_STORAGE,
|
||||
SidebarContext,
|
||||
sidebarConfig,
|
||||
} from './config';
|
||||
import { SidebarDivider } from './Items';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
introCard: {
|
||||
color: '#b5b5b5',
|
||||
// XXX (@koroeskohr): should I be using a Mui theme variable?
|
||||
fontSize: 12,
|
||||
width: sidebarConfig.drawerWidthOpen,
|
||||
marginTop: 18,
|
||||
marginBottom: 12,
|
||||
paddingLeft: sidebarConfig.iconPadding,
|
||||
paddingRight: sidebarConfig.iconPadding,
|
||||
},
|
||||
introDismiss: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
marginTop: 12,
|
||||
},
|
||||
introDismissLink: {
|
||||
color: '#dddddd',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: 4,
|
||||
'&:hover': {
|
||||
color: theme.palette.linkHover,
|
||||
transition: theme.transitions.create('color', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.shortest,
|
||||
}),
|
||||
},
|
||||
},
|
||||
introDismissText: {
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
introDismissIcon: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
marginRight: 12,
|
||||
},
|
||||
}));
|
||||
|
||||
type IntroCardProps = {
|
||||
text: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const IntroCard: FC<IntroCardProps> = props => {
|
||||
const classes = useStyles();
|
||||
const { text, onClose } = props;
|
||||
const handleClose = () => onClose();
|
||||
|
||||
return (
|
||||
<div className={classes.introCard}>
|
||||
<Typography variant="subtitle2">{text}</Typography>
|
||||
<div className={classes.introDismiss}>
|
||||
<Link
|
||||
component="button"
|
||||
onClick={handleClose}
|
||||
underline="none"
|
||||
className={classes.introDismissLink}
|
||||
>
|
||||
<CloseIcon className={classes.introDismissIcon} />
|
||||
<Typography component="span" className={classes.introDismissText}>
|
||||
Dismiss
|
||||
</Typography>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type SidebarIntroLocalStorage = {
|
||||
starredItemsDismissed: boolean;
|
||||
recentlyViewedItemsDismissed: boolean;
|
||||
};
|
||||
|
||||
type SidebarIntroCardProps = {
|
||||
text: string;
|
||||
onDismiss: () => void;
|
||||
};
|
||||
|
||||
const SidebarIntroCard: FC<SidebarIntroCardProps> = props => {
|
||||
const {text, onDismiss} = props
|
||||
const [collapsing, setCollapsing] = useState(false)
|
||||
const startDismissing = () => {
|
||||
setCollapsing(true)
|
||||
}
|
||||
return (
|
||||
<Collapse in={!collapsing} onExited={onDismiss}>
|
||||
<IntroCard
|
||||
text={text}
|
||||
onClose={startDismissing}
|
||||
/>
|
||||
</Collapse>
|
||||
);
|
||||
};
|
||||
|
||||
const starredIntroText = `Fun fact! As you explore all the awesome plugins in Backstage, you can actually pin them to this side nav.
|
||||
Keep an eye out for the little star icon (⭐) next to the plugin name and give it a click!`;
|
||||
const recentlyViewedIntroText =
|
||||
'And your recently viewed plugins will pop up here!';
|
||||
|
||||
export const SidebarIntro: FC = () => {
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
const [
|
||||
{ starredItemsDismissed, recentlyViewedItemsDismissed },
|
||||
setDismissedIntro,
|
||||
] = useLocalStorage<SidebarIntroLocalStorage>(SIDEBAR_INTRO_LOCAL_STORAGE, {
|
||||
starredItemsDismissed: false,
|
||||
recentlyViewedItemsDismissed: false,
|
||||
});
|
||||
|
||||
const dismissStarred = () => {
|
||||
setDismissedIntro(state => ({ ...state, starredItemsDismissed: true }));
|
||||
};
|
||||
const dismissRecentlyViewed = () => {
|
||||
setDismissedIntro(state => ({
|
||||
...state,
|
||||
recentlyViewedItemsDismissed: true,
|
||||
}));
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!starredItemsDismissed && (
|
||||
<>
|
||||
<SidebarIntroCard text={starredIntroText} onDismiss={dismissStarred} />
|
||||
<SidebarDivider />
|
||||
</>
|
||||
)}
|
||||
{!recentlyViewedItemsDismissed && (
|
||||
<SidebarIntroCard text={recentlyViewedIntroText} onDismiss={dismissRecentlyViewed} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -14,87 +14,196 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Link, makeStyles, styled, Theme, Typography } from '@material-ui/core';
|
||||
import {
|
||||
makeStyles,
|
||||
styled,
|
||||
TextField,
|
||||
Theme,
|
||||
Typography,
|
||||
Badge,
|
||||
} from '@material-ui/core';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
import clsx from 'clsx';
|
||||
import React, { FC, useContext } from 'react';
|
||||
import React, { FC, useContext, useState, KeyboardEventHandler } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { sidebarConfig, SidebarContext } from './config';
|
||||
import { IconComponent } from '../../icons';
|
||||
|
||||
const useStyles = makeStyles<Theme>((theme) => ({
|
||||
root: {
|
||||
color: '#b5b5b5',
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
alignItems: 'center',
|
||||
height: 40,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
closed: {
|
||||
width: sidebarConfig.drawerWidthClosed,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
open: {
|
||||
width: sidebarConfig.drawerWidthOpen,
|
||||
},
|
||||
label: {
|
||||
fontWeight: 'bolder',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: 1.0,
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
iconContainer: {
|
||||
height: '100%',
|
||||
width: sidebarConfig.drawerWidthClosed,
|
||||
marginRight: -theme.spacing(2),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
const useStyles = makeStyles<Theme>((theme) => {
|
||||
const {
|
||||
selectedIndicatorWidth,
|
||||
drawerWidthClosed,
|
||||
drawerWidthOpen,
|
||||
iconContainerWidth,
|
||||
iconSize,
|
||||
} = sidebarConfig;
|
||||
|
||||
return {
|
||||
root: {
|
||||
color: '#b5b5b5',
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
alignItems: 'center',
|
||||
height: 48,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
closed: {
|
||||
width: drawerWidthClosed,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
open: {
|
||||
width: drawerWidthOpen,
|
||||
},
|
||||
label: {
|
||||
// XXX (@koroeskohr): I can't seem to achieve the desired font-weight from the designs
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: 1.0,
|
||||
},
|
||||
iconContainer: {
|
||||
boxSizing: 'border-box',
|
||||
height: '100%',
|
||||
width: iconContainerWidth,
|
||||
marginRight: -theme.spacing(2),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
icon: {
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
},
|
||||
searchRoot: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
searchField: {
|
||||
color: '#b5b5b5',
|
||||
fontWeight: 'bold',
|
||||
fontSize: theme.typography.fontSize,
|
||||
},
|
||||
searchContainer: {
|
||||
width: drawerWidthOpen - iconContainerWidth,
|
||||
},
|
||||
selected: {
|
||||
'&$root': {
|
||||
borderLeft: `solid ${selectedIndicatorWidth}px #9BF0E1`,
|
||||
color: '#ffffff',
|
||||
},
|
||||
'&$closed': {
|
||||
width: drawerWidthClosed - selectedIndicatorWidth,
|
||||
},
|
||||
'& $iconContainer': {
|
||||
marginLeft: -selectedIndicatorWidth,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type SidebarItemProps = {
|
||||
icon: IconComponent;
|
||||
text: string;
|
||||
text?: string;
|
||||
to?: string;
|
||||
disableSelected?: boolean;
|
||||
hasNotifications?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export const SidebarItem: FC<SidebarItemProps> = ({
|
||||
icon: Icon,
|
||||
text,
|
||||
to,
|
||||
to = '#',
|
||||
disableSelected = false,
|
||||
hasNotifications = false,
|
||||
onClick,
|
||||
children,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const isOpen = useContext(SidebarContext);
|
||||
// XXX (@koroeskohr): unsure this is optimal. But I just really didn't want to have the item component
|
||||
// depend on the current location, and at least have it being optionally forced to selected.
|
||||
// Still waiting on a Q answered to fine tune the implementation
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
|
||||
const itemIcon = (
|
||||
<Badge
|
||||
color="secondary"
|
||||
variant="dot"
|
||||
overlap="circle"
|
||||
invisible={!hasNotifications}
|
||||
>
|
||||
<Icon fontSize="small" className={classes.icon} />
|
||||
</Badge>
|
||||
);
|
||||
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<Link
|
||||
<NavLink
|
||||
className={clsx(classes.root, classes.closed)}
|
||||
href={to}
|
||||
activeClassName={classes.selected}
|
||||
isActive={(match) => match && !disableSelected}
|
||||
exact
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
underline="none"
|
||||
>
|
||||
<Icon fontSize="small" />
|
||||
</Link>
|
||||
{itemIcon}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
<NavLink
|
||||
className={clsx(classes.root, classes.open)}
|
||||
href={to}
|
||||
activeClassName={classes.selected}
|
||||
isActive={(match) => match && !disableSelected}
|
||||
exact
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
underline="none"
|
||||
>
|
||||
<div data-testid="login-button" className={classes.iconContainer}>
|
||||
<Icon fontSize="small" />
|
||||
{itemIcon}
|
||||
</div>
|
||||
<Typography variant="subtitle1" className={classes.label}>
|
||||
{text}
|
||||
</Typography>
|
||||
</Link>
|
||||
{text && (
|
||||
<Typography variant="subtitle2" className={classes.label}>
|
||||
{text}
|
||||
</Typography>
|
||||
)}
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
type SidebarSearchFieldProps = {
|
||||
onSearch: (input: string) => void;
|
||||
};
|
||||
|
||||
export const SidebarSearchField: FC<SidebarSearchFieldProps> = (props) => {
|
||||
const [input, setInput] = useState('');
|
||||
const classes = useStyles();
|
||||
|
||||
const handleEnter: KeyboardEventHandler = (ev) => {
|
||||
if (ev.key === 'Enter') {
|
||||
props.onSearch(input);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInput(ev.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.searchRoot}>
|
||||
<SidebarItem icon={SearchIcon} disableSelected>
|
||||
<TextField
|
||||
placeholder="Search"
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleEnter}
|
||||
className={classes.searchContainer}
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
className: classes.searchField,
|
||||
}}
|
||||
/>
|
||||
</SidebarItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -111,4 +220,5 @@ export const SidebarDivider = styled('hr')({
|
||||
width: '100%',
|
||||
background: '#383838',
|
||||
border: 'none',
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 {
|
||||
Sidebar,
|
||||
SidebarIntro,
|
||||
SidebarItem,
|
||||
SidebarDivider,
|
||||
SidebarSearchField,
|
||||
SidebarSpace,
|
||||
SidebarUserBadge,
|
||||
} from '.';
|
||||
import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined';
|
||||
import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
export default {
|
||||
title: 'Sidebar',
|
||||
component: Sidebar,
|
||||
decorators: [
|
||||
(storyFn: () => JSX.Element) => (
|
||||
<MemoryRouter initialEntries={['/']}>{storyFn()}</MemoryRouter>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
const handleSearch = (input: string) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(input);
|
||||
};
|
||||
|
||||
export const SampleSidebar = () => (
|
||||
<Sidebar>
|
||||
{/* <SidebarLogo /> */}
|
||||
<SidebarSearchField onSearch={handleSearch} />
|
||||
<SidebarDivider />
|
||||
<SidebarItem selected icon={HomeOutlinedIcon} to="#" text="Home" />
|
||||
<SidebarItem icon={HomeOutlinedIcon} to="#" text="Plugins" />
|
||||
<SidebarItem icon={AddCircleOutlineIcon} to="#" text="Create..." />
|
||||
<SidebarDivider />
|
||||
<SidebarIntro />
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarUserBadge />
|
||||
</Sidebar>
|
||||
);
|
||||
@@ -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, { FC, useContext } from 'react';
|
||||
import { Avatar, makeStyles, Theme, Typography } from '@material-ui/core';
|
||||
import People from '@material-ui/icons/People';
|
||||
import { sidebarConfig, SidebarContext } from './config';
|
||||
import { SidebarItem } from './Items';
|
||||
|
||||
const useStyles = makeStyles<Theme>(() => {
|
||||
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 UserBadge: FC<Props> = ({ imageUrl, name, hideName = false }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Avatar alt={name} src={imageUrl} className={classes.avatar} />
|
||||
{!hideName && <Typography variant="subtitle2">{name}</Typography>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SidebarUserBadge: FC = () => {
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
const isUserLoggedIn = false;
|
||||
return isUserLoggedIn ? (
|
||||
<UserBadge
|
||||
imageUrl="https://via.placeholder.com/200/200"
|
||||
name="Victor Viale"
|
||||
hideName={!isOpen}
|
||||
/>
|
||||
) : (
|
||||
<SidebarItem icon={People} text="Log in" to="/login" disableSelected />
|
||||
);
|
||||
};
|
||||
@@ -16,11 +16,34 @@
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
const drawerWidthClosed = 72;
|
||||
const iconPadding = 24;
|
||||
const userBadgePadding = 18;
|
||||
|
||||
export const sidebarConfig = {
|
||||
drawerWidthClosed: 64,
|
||||
drawerWidthOpen: 220,
|
||||
defaultOpenDelayMs: 400,
|
||||
defaultCloseDelayMs: 200,
|
||||
drawerWidthClosed,
|
||||
drawerWidthOpen: 224,
|
||||
// As per NN/g's guidance on timing for exposing hidden content
|
||||
// See https://www.nngroup.com/articles/timing-exposing-content/
|
||||
defaultOpenDelayMs: 300,
|
||||
defaultCloseDelayMs: 0,
|
||||
defaultFadeDuration: 200,
|
||||
logoHeight: 32,
|
||||
iconContainerWidth: drawerWidthClosed,
|
||||
iconSize: drawerWidthClosed - iconPadding * 2,
|
||||
iconPadding,
|
||||
selectedIndicatorWidth: 3,
|
||||
userBadgePadding,
|
||||
userBadgeDiameter: drawerWidthClosed - userBadgePadding * 2,
|
||||
};
|
||||
|
||||
export const SidebarContext = createContext<boolean>(false);
|
||||
export const SIDEBAR_INTRO_LOCAL_STORAGE =
|
||||
'@backstage/core/sidebar-intro-dismissed';
|
||||
|
||||
type SidebarContextType = {
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
export const SidebarContext = createContext<SidebarContextType>({
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
@@ -17,5 +17,7 @@
|
||||
export * from './Bar';
|
||||
export * from './Page';
|
||||
export * from './Items';
|
||||
export * from './Intro';
|
||||
export * from './UserBadge';
|
||||
export * from './config';
|
||||
export { SidebarThemeToggle } from './SidebarThemeToggle';
|
||||
|
||||
@@ -4327,9 +4327,9 @@
|
||||
pretty-format "^25.1.0"
|
||||
|
||||
"@types/testing-library__jest-dom@^5.0.4":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.0.4.tgz#c7bfbafb920cd1ce40506474e70ee73637f33701"
|
||||
integrity sha512-Ns69aaNvlxvXkPxIwsqeaWH5vJpwa/pdBIlf8LGkRnbV3tiqUgifs13moLXg1NQ2AM23qRR5CtHarNshvRyEdA==
|
||||
version "5.6.0"
|
||||
resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.6.0.tgz#325e97aacb7e4a66693e7face8a2c04f936f4a4b"
|
||||
integrity sha512-VRl4kIzvtySjscCpMul3mz0UgEd40nG/jWluaXIYi5UG8cOOLD56u8IIgHZk+gSKmccRCsVv7AAg1HBmE7OQ2w==
|
||||
dependencies:
|
||||
"@types/jest" "*"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user