Skip to content

Commit

Permalink
[dashboard] Implement Team selector and creation wizard
Browse files Browse the repository at this point in the history
  • Loading branch information
jankeromnes committed Jun 7, 2021
1 parent 4850f7c commit ce2d778
Show file tree
Hide file tree
Showing 18 changed files with 273 additions and 104 deletions.
1 change: 1 addition & 0 deletions .gitpod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ tasks:
vscode:
extensions:
- bajdzis.vscode-database
- bradlc.vscode-tailwindcss
- EditorConfig.EditorConfig
- golang.go
- hangxingliu.vscode-nginx-conf-hint
Expand Down
44 changes: 21 additions & 23 deletions components/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const Account = React.lazy(() => import(/* webpackPrefetch: true */ './settings/
const Notifications = React.lazy(() => import(/* webpackPrefetch: true */ './settings/Notifications'));
const Plans = React.lazy(() => import(/* webpackPrefetch: true */ './settings/Plans'));
const Teams = React.lazy(() => import(/* webpackPrefetch: true */ './settings/Teams'));
const NewTeam = React.lazy(() => import(/* webpackPrefetch: true */ './settings/NewTeam'));
const EnvironmentVariables = React.lazy(() => import(/* webpackPrefetch: true */ './settings/EnvironmentVariables'));
const Integrations = React.lazy(() => import(/* webpackPrefetch: true */ './settings/Integrations'));
const Preferences = React.lazy(() => import(/* webpackPrefetch: true */ './settings/Preferences'));
Expand Down Expand Up @@ -148,6 +149,7 @@ function App() {
<Route path="/notifications" exact component={Notifications} />
<Route path="/plans" exact component={Plans} />
<Route path="/teams" exact component={Teams} />
<Route path="/new-team" exact component={NewTeam} />
<Route path="/variables" exact component={EnvironmentVariables} />
<Route path="/preferences" exact component={Preferences} />
<Route path="/install-github-app" exact component={InstallGitHubApp} />
Expand Down Expand Up @@ -179,7 +181,7 @@ function App() {
</Route>
<Route path="*" render={
(match) => {

return isGitpodIo() ?
// delegate to our website to handle the request
(window.location.host = 'www.gitpod.io') :
Expand Down Expand Up @@ -219,30 +221,25 @@ function getURLHash() {
}

const renderMenu = (user?: User) => {
const left = [
{
title: 'Workspaces',
link: '/workspaces',
alternatives: ['/']
},
{
title: 'Settings',
link: '/settings',
alternatives: settingsMenu.flatMap(e => e.link)
}
];

if (user && user?.rolesOrPermissions?.includes('admin')) {
left.push({
title: 'Admin',
link: '/admin',
alternatives: adminMenu.flatMap(e => e.link)
});
}

return <Menu
left={left}
left={[
{
title: 'Workspaces',
link: '/workspaces',
alternatives: ['/']
},
{
title: 'Settings',
link: '/settings',
alternatives: settingsMenu.flatMap(e => e.link)
}
]}
right={[
...(user?.rolesOrPermissions?.includes('admin') ? [{
title: 'Admin',
link: '/admin',
alternatives: adminMenu.flatMap(e => e.link)
}] : []),
{
title: 'Docs',
link: 'https://www.gitpod.io/docs/',
Expand All @@ -252,6 +249,7 @@ const renderMenu = (user?: User) => {
link: 'https://community.gitpod.io/',
}
]}
showTeams={true}
/>;
}

Expand Down
9 changes: 5 additions & 4 deletions components/dashboard/src/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Link } from 'react-router-dom';
export interface ContextMenuProps {
children: React.ReactChild[] | React.ReactChild;
menuEntries: ContextMenuEntry[];
width?: string;
classes?: string;
}

export interface ContextMenuEntry {
Expand All @@ -21,6 +21,7 @@ export interface ContextMenuEntry {
*/
separator?: boolean;
customFontStyle?: string;
customContent?: React.ReactChild;
onClick?: (event: React.MouseEvent) => void;
href?: string;
link?: string;
Expand Down Expand Up @@ -66,11 +67,11 @@ function ContextMenu(props: ContextMenuProps) {
{props.children}
</div>
{expanded ?
<div className={`mt-2 z-50 ${props.width || 'w-48'} bg-white dark:bg-gray-900 absolute right-0 flex flex-col border border-gray-200 dark:border-gray-800 rounded-lg truncated`}>
<div className={`mt-2 z-50 bg-white dark:bg-gray-900 absolute flex flex-col border border-gray-200 dark:border-gray-800 rounded-lg truncated ${props.classes || 'w-48 right-0'}`}>
{props.menuEntries.map((e, index) => {
const clickable = e.href || e.onClick || e.link;
const entry = <div className={`px-4 flex py-3 ${clickable ? 'hover:bg-gray-200 dark:hover:bg-gray-800' : ''} text-sm leading-1 ${e.customFontStyle || font} ${e.separator ? ' border-b border-gray-200 dark:border-gray-800' : ''}`} >
<div className="truncate w-52">{e.title}</div><div className="flex-1"></div>{e.active ? <div className="pl-1 font-semibold">&#x2713;</div> : null}
const entry = <div className={`px-4 flex py-3 ${clickable ? 'hover:bg-gray-200 dark:hover:bg-gray-800' : ''} text-sm leading-1 ${e.customFontStyle || font} ${e.separator ? ' border-b border-gray-200 dark:border-gray-800' : ''}`} title={e.title}>
{e.customContent || <><div className="truncate w-52">{e.title}</div><div className="flex-1"></div>{e.active ? <div className="pl-1 font-semibold">&#x2713;</div> : null}</>}
</div>
const key = `entry-${menuId}-${index}-${e.title}`;
if (e.link) {
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard/src/components/DropDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function DropDown(props: DropDownProps) {
})
const font = "text-gray-400 dark:text-gray-500 text-sm leading-1 group hover:text-gray-600 dark:hover:text-gray-400 transition ease-in-out"
return (
<ContextMenu menuEntries={enhancedEntries} width={props.contextMenuWidth}>
<ContextMenu menuEntries={enhancedEntries} classes={`${props.contextMenuWidth} right-0`}>
<span className={`py-2 cursor-pointer ${font}`}>{props.prefix}{current}<Arrow up={false}/></span>
</ContextMenu>
);
Expand Down
174 changes: 112 additions & 62 deletions components/dashboard/src/components/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,83 +4,133 @@
* See License-AGPL.txt in the project root for license information.
*/

import { User } from "@gitpod/gitpod-protocol";
import { useContext } from "react";
import { Link } from "react-router-dom";
import { Team, User } from "@gitpod/gitpod-protocol";
import { useContext, useEffect, useState } from "react";
import { Link, useHistory } from "react-router-dom";
import { useLocation } from "react-router";
import { Location } from "history";
import gitpodIcon from '../icons/gitpod.svg';
import { gitpodHostUrl } from "../service/service";
import CaretDown from "../icons/CaretDown.svg";
import { getGitpodService, gitpodHostUrl } from "../service/service";
import { UserContext } from "../user-context";
import ContextMenu from "./ContextMenu";
import { useLocation } from "react-router";
import Separator from "./Separator";
import PillMenuItem from "./PillMenuItem";
import TabMenuItem from "./TabMenuItem";

interface Entry {
title: string,
link: string,
alternatives?: string[]
}

function MenuItem(entry: Entry) {
const location = useLocation();
let classes = "flex block text-sm font-medium dark:text-gray-200 px-3 px-0 py-1.5 rounded-md transition ease-in-out";
function isSelected(entry: Entry, location: Location<any>) {
const all = [entry.link, ...(entry.alternatives||[])];
const path = location.pathname.toLowerCase();
if (all.find( n => n === path || n+'/' === path)) {
classes += " bg-gray-200 dark:bg-gray-700";
} else {
classes += " text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800";
}
return <li key={entry.title}>
{entry.link.startsWith('https://')
? <a className={classes} href={entry.link}>
<div>{entry.title}</div>
</a>
: <Link className={classes} to={entry.link}>
<div>{entry.title}</div>
</Link>}
</li>;
return all.some(n => n === path || n+'/' === path);
}

function Menu(props: { left: Entry[], right: Entry[] }) {
export default function Menu(props: { left: Entry[], right: Entry[], showTeams?: boolean }) {
const { user } = useContext(UserContext);
const history = useHistory();
const location = useLocation();
const [ teams, setTeams ] = useState<Team[]>([]);
useEffect(() => {
getGitpodService().server.getTeams().then(setTeams).catch(error => {
console.error('Could not fetch teams!', error);
});
}, []);

return (
<header className="lg:px-28 px-10 flex flex-wrap items-center py-4">
<div className="flex justify-between items-center pr-3">
<Link to="/">
<img src={gitpodIcon} className="h-6" />
</Link>
</div>
<div className="flex flex-1 items-center w-auto w-full" id="menu">
<nav className="flex-1">
<ul className="flex flex-1 items-center justify-between text-base text-gray-700 space-x-2">
{props.left.map(MenuItem)}
<li className="flex-1"></li>
{props.right.map(MenuItem)}
</ul>
</nav>
<div className="ml-3 flex items-center justify-start mb-0 pointer-cursor m-l-auto rounded-full border-2 border-transparent hover:border-gray-200 dark:hover:border-gray-700 p-0.5 font-medium">
<ContextMenu menuEntries={[
{
title: (user && User.getPrimaryEmail(user)) || '',
customFontStyle: 'text-gray-400',
separator: true
},
{
title: 'Settings',
link: '/settings',
separator: true
},
{
title: 'Logout',
href: gitpodHostUrl.asApiLogout().toString()
},
]}>
<img className="rounded-full w-6 h-6"
src={user?.avatarUrl || ''} alt={user?.name || 'Anonymous'} />
</ContextMenu>
const userFullName = user?.fullName || user?.name || '...';

return <>
<header className="lg:px-28 px-10 flex flex-col pt-4 space-y-4">
<div className="flex">
<div className="flex justify-between items-center pr-3">
<Link to="/">
<img src={gitpodIcon} className="h-6" />
</Link>
<div className="ml-2 text-base">
{!!props.showTeams
? <ContextMenu classes="w-64 left-0" menuEntries={[
{
title: userFullName,
customContent: <div className="w-full text-gray-400 flex flex-col">
<span className="text-gray-800 text-base font-semibold">{userFullName}</span>
<span className="">Personal Account</span>
</div>,
separator: true,
onClick: () => {},
},
...(teams || []).map(t => ({
title: t.name,
customContent: <div className="w-full text-gray-400 flex flex-col">
<span className="text-gray-800 text-base font-semibold">{t.name}</span>
<span className="">N members</span>
</div>,
separator: true,
onClick: () => {},
})).sort((a,b) => a.title.toLowerCase() > b.title.toLowerCase() ? 1 : -1),
{
title: 'Create a new team',
customContent: <div className="w-full text-gray-400 flex items-center">
<span className="flex-1 font-semibold">New Team</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14" className="w-3.5"><path fill="currentColor" fill-rule="evenodd" d="M7 0a1 1 0 011 1v5h5a1 1 0 110 2H8v5a1 1 0 11-2 0V8H1a1 1 0 010-2h5V1a1 1 0 011-1z" clip-rule="evenodd"/></svg>
</div>,
onClick: () => history.push("/new-team"),
}
]}>
<div className="flex p-1.5 pl-3 rounded-lg hover:bg-gray-200">
<span className="text-base text-gray-600 font-semibold">{userFullName}</span>
<img className="m-2 filter-grayscale" src={CaretDown}/>
</div>
</ContextMenu>
: <nav className="flex-1">
<ul className="flex flex-1 items-center justify-between text-base text-gray-700 space-x-2">
<li className="flex-1"></li>
{props.left.map(entry => <li key={entry.title}>
<PillMenuItem name={entry.title} selected={isSelected(entry, location)} link={entry.link}/>
</li>)}
</ul>
</nav>
}
</div>
</div>
<div className="flex flex-1 items-center w-auto" id="menu">
<nav className="flex-1">
<ul className="flex flex-1 items-center justify-between text-base text-gray-700 space-x-2">
<li className="flex-1"></li>
{props.right.map(entry => <li key={entry.title}>
<PillMenuItem name={entry.title} selected={isSelected(entry, location)} link={entry.link}/>
</li>)}
</ul>
</nav>
<div className="ml-3 flex items-center justify-start mb-0 pointer-cursor m-l-auto rounded-full border-2 border-transparent hover:border-gray-200 dark:hover:border-gray-700 p-0.5 font-medium">
<ContextMenu menuEntries={[
{
title: (user && User.getPrimaryEmail(user)) || '',
customFontStyle: 'text-gray-400',
separator: true
},
{
title: 'Settings',
link: '/settings',
separator: true
},
{
title: 'Logout',
href: gitpodHostUrl.asApiLogout().toString()
},
]}>
<img className="rounded-full w-6 h-6" src={user?.avatarUrl || ''} alt={user?.name || 'Anonymous'} />
</ContextMenu>
</div>
</div>
</div>
{!!props.showTeams && <div className="flex">
{props.left.map(entry => <TabMenuItem name={entry.title} selected={isSelected(entry, location)} link={entry.link}/>)}
</div>}
</header>
);
}

export default Menu;
{!!props.showTeams && <Separator />}
</>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function PendingChangesDropdown(props: { workspaceInstance?: Work
if (totalChanges <= 0) {
return <p>No Changes</p>;
}
return <ContextMenu menuEntries={menuEntries} width="w-64 max-h-48 overflow-scroll mx-auto left-0 right-0">
return <ContextMenu menuEntries={menuEntries} classes="w-64 max-h-48 overflow-scroll mx-auto left-0 right-0">
<p className="flex justify-center text-gitpod-red">
<span>{totalChanges} Change{totalChanges === 1 ? '' : 's'}</span>
<img className="m-2" src={CaretDown}/>
Expand Down
22 changes: 22 additions & 0 deletions components/dashboard/src/components/PillMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import { Link } from "react-router-dom";

export default function PillMenuItem(p: {
name: string,
selected: boolean,
link?: string,
onClick?: (event: React.MouseEvent) => void
}) {
const classes = 'flex block text-sm font-medium dark:text-gray-200 px-3 px-0 py-1.5 rounded-md transition ease-in-out ' +
(p.selected
? 'bg-gray-200 dark:bg-gray-700'
: 'text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800');
return ((!p.link || p.link.startsWith('https://'))
? <a className={classes} href={p.link} onClick={p.onClick}>{p.name}</a>
: <Link className={classes} to={p.link} onClick={p.onClick}>{p.name}</Link>);
}
2 changes: 1 addition & 1 deletion components/dashboard/src/components/Separator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
*/

export default function Separator() {
return <div className="border-gray-200 dark:border-gray-800 border-b h-0.5 absolute left-0 w-screen"></div>;
return <div className="border-gray-200 dark:border-gray-800 border-b absolute left-0 w-screen"></div>;
}
22 changes: 22 additions & 0 deletions components/dashboard/src/components/TabMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import { Link } from "react-router-dom";

export default function TabMenuItem(p: {
name: string,
selected: boolean,
link?: string,
onClick?: (event: React.MouseEvent) => void
}) {
const classes = 'cursor-pointer py-2 px-4 border-b-4 border-transparent transition ease-in-out ' +
(p.selected
? 'text-gray-600 dark:text-gray-400 border-gray-700 dark:border-gray-400'
: 'text-gray-400 dark:text-gray-600 hover:border-gray-400 dark:hover:border-gray-600');
return ((!p.link || p.link.startsWith('https://'))
? <a className={classes} href={p.link} onClick={p.onClick}>{p.name}</a>
: <Link className={classes} to={p.link} onClick={p.onClick}>{p.name}</Link>);
}
Loading

0 comments on commit ce2d778

Please sign in to comment.