diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 3945befb73..c26336c18b 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -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 (
@@ -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 }) => (
-
+
+ {/* Global nav, not org-specific */}
+ {/* End global nav */}
-
+
+
{children}
diff --git a/packages/core/src/layout/Sidebar/Bar.tsx b/packages/core/src/layout/Sidebar/Bar.tsx
index b32101a270..a03a33ce09 100644
--- a/packages/core/src/layout/Sidebar/Bar.tsx
+++ b/packages/core/src/layout/Sidebar/Bar.tsx
@@ -20,7 +20,7 @@ import React, { FC, useRef, useState } from 'react';
import { sidebarConfig, SidebarContext } from './config';
import { BackstageTheme } from '@backstage/theme';
-const useStyles = makeStyles
(theme => ({
+const useStyles = makeStyles((theme) => ({
root: {
zIndex: 1000,
position: 'relative',
@@ -115,7 +115,7 @@ export const Sidebar: FC = ({
onBlur={handleClose}
data-testid="sidebar-root"
>
-
+
(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
= props => {
+ const classes = useStyles();
+ const { text, onClose } = props;
+ const handleClose = () => onClose();
+
+ return (
+
+
{text}
+
+
+
+
+ Dismiss
+
+
+
+
+ );
+};
+
+type SidebarIntroLocalStorage = {
+ starredItemsDismissed: boolean;
+ recentlyViewedItemsDismissed: boolean;
+};
+
+type SidebarIntroCardProps = {
+ text: string;
+ onDismiss: () => void;
+};
+
+const SidebarIntroCard: FC = props => {
+ const {text, onDismiss} = props
+ const [collapsing, setCollapsing] = useState(false)
+ const startDismissing = () => {
+ setCollapsing(true)
+ }
+ return (
+
+
+
+ );
+};
+
+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(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 && (
+ <>
+
+
+ >
+ )}
+ {!recentlyViewedItemsDismissed && (
+
+ )}
+ >
+ );
+};
diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx
index 62f02ca58b..60132f0bc9 100644
--- a/packages/core/src/layout/Sidebar/Items.tsx
+++ b/packages/core/src/layout/Sidebar/Items.tsx
@@ -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) => ({
- 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) => {
+ 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 = ({
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 = (
+
+
+
+ );
if (!isOpen) {
return (
- match && !disableSelected}
+ exact
+ to={to}
onClick={onClick}
- underline="none"
>
-
-
+ {itemIcon}
+
);
}
return (
- match && !disableSelected}
+ exact
+ to={to}
onClick={onClick}
- underline="none"
>
-
+ {itemIcon}
-
- {text}
-
-
+ {text && (
+
+ {text}
+
+ )}
+ {children}
+
+ );
+};
+
+type SidebarSearchFieldProps = {
+ onSearch: (input: string) => void;
+};
+
+export const SidebarSearchField: FC = (props) => {
+ const [input, setInput] = useState('');
+ const classes = useStyles();
+
+ const handleEnter: KeyboardEventHandler = (ev) => {
+ if (ev.key === 'Enter') {
+ props.onSearch(input);
+ }
+ };
+
+ const handleInput = (ev: React.ChangeEvent) => {
+ setInput(ev.target.value);
+ };
+
+ return (
+
+
+
+
+
);
};
@@ -111,4 +220,5 @@ export const SidebarDivider = styled('hr')({
width: '100%',
background: '#383838',
border: 'none',
+ margin: 0,
});
diff --git a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx
new file mode 100644
index 0000000000..fc0025caca
--- /dev/null
+++ b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx
@@ -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) => (
+ {storyFn()}
+ ),
+ ],
+};
+
+const handleSearch = (input: string) => {
+ // eslint-disable-next-line no-console
+ console.log(input);
+};
+
+export const SampleSidebar = () => (
+
+ {/* */}
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/packages/core/src/layout/Sidebar/UserBadge.tsx b/packages/core/src/layout/Sidebar/UserBadge.tsx
new file mode 100644
index 0000000000..1614558ab9
--- /dev/null
+++ b/packages/core/src/layout/Sidebar/UserBadge.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, { 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(() => {
+ 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 = ({ imageUrl, name, hideName = false }) => {
+ const classes = useStyles();
+
+ return (
+
+
+ {!hideName &&
{name} }
+
+ );
+};
+
+export const SidebarUserBadge: FC = () => {
+ const { isOpen } = useContext(SidebarContext);
+ const isUserLoggedIn = false;
+ return isUserLoggedIn ? (
+
+ ) : (
+
+ );
+};
diff --git a/packages/core/src/layout/Sidebar/config.ts b/packages/core/src/layout/Sidebar/config.ts
index 7695e695d7..e101153bfb 100644
--- a/packages/core/src/layout/Sidebar/config.ts
+++ b/packages/core/src/layout/Sidebar/config.ts
@@ -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(false);
+export const SIDEBAR_INTRO_LOCAL_STORAGE =
+ '@backstage/core/sidebar-intro-dismissed';
+
+type SidebarContextType = {
+ isOpen: boolean;
+};
+
+export const SidebarContext = createContext({
+ isOpen: false,
+});
diff --git a/packages/core/src/layout/Sidebar/index.ts b/packages/core/src/layout/Sidebar/index.ts
index f3f1a8872b..731d117ce3 100644
--- a/packages/core/src/layout/Sidebar/index.ts
+++ b/packages/core/src/layout/Sidebar/index.ts
@@ -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';
diff --git a/yarn.lock b/yarn.lock
index 281d2fa491..4d4f3a23da 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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" "*"