Skip to content

Commit ce2d778

Browse files
committed
[dashboard] Implement Team selector and creation wizard
1 parent 4850f7c commit ce2d778

18 files changed

+273
-104
lines changed

.gitpod.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ tasks:
3939
vscode:
4040
extensions:
4141
- bajdzis.vscode-database
42+
- bradlc.vscode-tailwindcss
4243
- EditorConfig.EditorConfig
4344
- golang.go
4445
- hangxingliu.vscode-nginx-conf-hint

components/dashboard/src/App.tsx

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const Account = React.lazy(() => import(/* webpackPrefetch: true */ './settings/
2525
const Notifications = React.lazy(() => import(/* webpackPrefetch: true */ './settings/Notifications'));
2626
const Plans = React.lazy(() => import(/* webpackPrefetch: true */ './settings/Plans'));
2727
const Teams = React.lazy(() => import(/* webpackPrefetch: true */ './settings/Teams'));
28+
const NewTeam = React.lazy(() => import(/* webpackPrefetch: true */ './settings/NewTeam'));
2829
const EnvironmentVariables = React.lazy(() => import(/* webpackPrefetch: true */ './settings/EnvironmentVariables'));
2930
const Integrations = React.lazy(() => import(/* webpackPrefetch: true */ './settings/Integrations'));
3031
const Preferences = React.lazy(() => import(/* webpackPrefetch: true */ './settings/Preferences'));
@@ -148,6 +149,7 @@ function App() {
148149
<Route path="/notifications" exact component={Notifications} />
149150
<Route path="/plans" exact component={Plans} />
150151
<Route path="/teams" exact component={Teams} />
152+
<Route path="/new-team" exact component={NewTeam} />
151153
<Route path="/variables" exact component={EnvironmentVariables} />
152154
<Route path="/preferences" exact component={Preferences} />
153155
<Route path="/install-github-app" exact component={InstallGitHubApp} />
@@ -179,7 +181,7 @@ function App() {
179181
</Route>
180182
<Route path="*" render={
181183
(match) => {
182-
184+
183185
return isGitpodIo() ?
184186
// delegate to our website to handle the request
185187
(window.location.host = 'www.gitpod.io') :
@@ -219,30 +221,25 @@ function getURLHash() {
219221
}
220222

221223
const renderMenu = (user?: User) => {
222-
const left = [
223-
{
224-
title: 'Workspaces',
225-
link: '/workspaces',
226-
alternatives: ['/']
227-
},
228-
{
229-
title: 'Settings',
230-
link: '/settings',
231-
alternatives: settingsMenu.flatMap(e => e.link)
232-
}
233-
];
234-
235-
if (user && user?.rolesOrPermissions?.includes('admin')) {
236-
left.push({
237-
title: 'Admin',
238-
link: '/admin',
239-
alternatives: adminMenu.flatMap(e => e.link)
240-
});
241-
}
242-
243224
return <Menu
244-
left={left}
225+
left={[
226+
{
227+
title: 'Workspaces',
228+
link: '/workspaces',
229+
alternatives: ['/']
230+
},
231+
{
232+
title: 'Settings',
233+
link: '/settings',
234+
alternatives: settingsMenu.flatMap(e => e.link)
235+
}
236+
]}
245237
right={[
238+
...(user?.rolesOrPermissions?.includes('admin') ? [{
239+
title: 'Admin',
240+
link: '/admin',
241+
alternatives: adminMenu.flatMap(e => e.link)
242+
}] : []),
246243
{
247244
title: 'Docs',
248245
link: 'https://www.gitpod.io/docs/',
@@ -252,6 +249,7 @@ const renderMenu = (user?: User) => {
252249
link: 'https://community.gitpod.io/',
253250
}
254251
]}
252+
showTeams={true}
255253
/>;
256254
}
257255

components/dashboard/src/components/ContextMenu.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Link } from 'react-router-dom';
1010
export interface ContextMenuProps {
1111
children: React.ReactChild[] | React.ReactChild;
1212
menuEntries: ContextMenuEntry[];
13-
width?: string;
13+
classes?: string;
1414
}
1515

1616
export interface ContextMenuEntry {
@@ -21,6 +21,7 @@ export interface ContextMenuEntry {
2121
*/
2222
separator?: boolean;
2323
customFontStyle?: string;
24+
customContent?: React.ReactChild;
2425
onClick?: (event: React.MouseEvent) => void;
2526
href?: string;
2627
link?: string;
@@ -66,11 +67,11 @@ function ContextMenu(props: ContextMenuProps) {
6667
{props.children}
6768
</div>
6869
{expanded ?
69-
<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`}>
70+
<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'}`}>
7071
{props.menuEntries.map((e, index) => {
7172
const clickable = e.href || e.onClick || e.link;
72-
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' : ''}`} >
73-
<div className="truncate w-52">{e.title}</div><div className="flex-1"></div>{e.active ? <div className="pl-1 font-semibold">&#x2713;</div> : null}
73+
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}>
74+
{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}</>}
7475
</div>
7576
const key = `entry-${menuId}-${index}-${e.title}`;
7677
if (e.link) {

components/dashboard/src/components/DropDown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function DropDown(props: DropDownProps) {
3838
})
3939
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"
4040
return (
41-
<ContextMenu menuEntries={enhancedEntries} width={props.contextMenuWidth}>
41+
<ContextMenu menuEntries={enhancedEntries} classes={`${props.contextMenuWidth} right-0`}>
4242
<span className={`py-2 cursor-pointer ${font}`}>{props.prefix}{current}<Arrow up={false}/></span>
4343
</ContextMenu>
4444
);

components/dashboard/src/components/Menu.tsx

Lines changed: 112 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,83 +4,133 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7-
import { User } from "@gitpod/gitpod-protocol";
8-
import { useContext } from "react";
9-
import { Link } from "react-router-dom";
7+
import { Team, User } from "@gitpod/gitpod-protocol";
8+
import { useContext, useEffect, useState } from "react";
9+
import { Link, useHistory } from "react-router-dom";
10+
import { useLocation } from "react-router";
11+
import { Location } from "history";
1012
import gitpodIcon from '../icons/gitpod.svg';
11-
import { gitpodHostUrl } from "../service/service";
13+
import CaretDown from "../icons/CaretDown.svg";
14+
import { getGitpodService, gitpodHostUrl } from "../service/service";
1215
import { UserContext } from "../user-context";
1316
import ContextMenu from "./ContextMenu";
14-
import { useLocation } from "react-router";
17+
import Separator from "./Separator";
18+
import PillMenuItem from "./PillMenuItem";
19+
import TabMenuItem from "./TabMenuItem";
20+
1521
interface Entry {
1622
title: string,
1723
link: string,
1824
alternatives?: string[]
1925
}
2026

21-
function MenuItem(entry: Entry) {
22-
const location = useLocation();
23-
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";
27+
function isSelected(entry: Entry, location: Location<any>) {
2428
const all = [entry.link, ...(entry.alternatives||[])];
2529
const path = location.pathname.toLowerCase();
26-
if (all.find( n => n === path || n+'/' === path)) {
27-
classes += " bg-gray-200 dark:bg-gray-700";
28-
} else {
29-
classes += " text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800";
30-
}
31-
return <li key={entry.title}>
32-
{entry.link.startsWith('https://')
33-
? <a className={classes} href={entry.link}>
34-
<div>{entry.title}</div>
35-
</a>
36-
: <Link className={classes} to={entry.link}>
37-
<div>{entry.title}</div>
38-
</Link>}
39-
</li>;
30+
return all.some(n => n === path || n+'/' === path);
4031
}
4132

42-
function Menu(props: { left: Entry[], right: Entry[] }) {
33+
export default function Menu(props: { left: Entry[], right: Entry[], showTeams?: boolean }) {
4334
const { user } = useContext(UserContext);
35+
const history = useHistory();
36+
const location = useLocation();
37+
const [ teams, setTeams ] = useState<Team[]>([]);
38+
useEffect(() => {
39+
getGitpodService().server.getTeams().then(setTeams).catch(error => {
40+
console.error('Could not fetch teams!', error);
41+
});
42+
}, []);
4443

45-
return (
46-
<header className="lg:px-28 px-10 flex flex-wrap items-center py-4">
47-
<div className="flex justify-between items-center pr-3">
48-
<Link to="/">
49-
<img src={gitpodIcon} className="h-6" />
50-
</Link>
51-
</div>
52-
<div className="flex flex-1 items-center w-auto w-full" id="menu">
53-
<nav className="flex-1">
54-
<ul className="flex flex-1 items-center justify-between text-base text-gray-700 space-x-2">
55-
{props.left.map(MenuItem)}
56-
<li className="flex-1"></li>
57-
{props.right.map(MenuItem)}
58-
</ul>
59-
</nav>
60-
<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">
61-
<ContextMenu menuEntries={[
62-
{
63-
title: (user && User.getPrimaryEmail(user)) || '',
64-
customFontStyle: 'text-gray-400',
65-
separator: true
66-
},
67-
{
68-
title: 'Settings',
69-
link: '/settings',
70-
separator: true
71-
},
72-
{
73-
title: 'Logout',
74-
href: gitpodHostUrl.asApiLogout().toString()
75-
},
76-
]}>
77-
<img className="rounded-full w-6 h-6"
78-
src={user?.avatarUrl || ''} alt={user?.name || 'Anonymous'} />
79-
</ContextMenu>
44+
const userFullName = user?.fullName || user?.name || '...';
45+
46+
return <>
47+
<header className="lg:px-28 px-10 flex flex-col pt-4 space-y-4">
48+
<div className="flex">
49+
<div className="flex justify-between items-center pr-3">
50+
<Link to="/">
51+
<img src={gitpodIcon} className="h-6" />
52+
</Link>
53+
<div className="ml-2 text-base">
54+
{!!props.showTeams
55+
? <ContextMenu classes="w-64 left-0" menuEntries={[
56+
{
57+
title: userFullName,
58+
customContent: <div className="w-full text-gray-400 flex flex-col">
59+
<span className="text-gray-800 text-base font-semibold">{userFullName}</span>
60+
<span className="">Personal Account</span>
61+
</div>,
62+
separator: true,
63+
onClick: () => {},
64+
},
65+
...(teams || []).map(t => ({
66+
title: t.name,
67+
customContent: <div className="w-full text-gray-400 flex flex-col">
68+
<span className="text-gray-800 text-base font-semibold">{t.name}</span>
69+
<span className="">N members</span>
70+
</div>,
71+
separator: true,
72+
onClick: () => {},
73+
})).sort((a,b) => a.title.toLowerCase() > b.title.toLowerCase() ? 1 : -1),
74+
{
75+
title: 'Create a new team',
76+
customContent: <div className="w-full text-gray-400 flex items-center">
77+
<span className="flex-1 font-semibold">New Team</span>
78+
<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>
79+
</div>,
80+
onClick: () => history.push("/new-team"),
81+
}
82+
]}>
83+
<div className="flex p-1.5 pl-3 rounded-lg hover:bg-gray-200">
84+
<span className="text-base text-gray-600 font-semibold">{userFullName}</span>
85+
<img className="m-2 filter-grayscale" src={CaretDown}/>
86+
</div>
87+
</ContextMenu>
88+
: <nav className="flex-1">
89+
<ul className="flex flex-1 items-center justify-between text-base text-gray-700 space-x-2">
90+
<li className="flex-1"></li>
91+
{props.left.map(entry => <li key={entry.title}>
92+
<PillMenuItem name={entry.title} selected={isSelected(entry, location)} link={entry.link}/>
93+
</li>)}
94+
</ul>
95+
</nav>
96+
}
97+
</div>
98+
</div>
99+
<div className="flex flex-1 items-center w-auto" id="menu">
100+
<nav className="flex-1">
101+
<ul className="flex flex-1 items-center justify-between text-base text-gray-700 space-x-2">
102+
<li className="flex-1"></li>
103+
{props.right.map(entry => <li key={entry.title}>
104+
<PillMenuItem name={entry.title} selected={isSelected(entry, location)} link={entry.link}/>
105+
</li>)}
106+
</ul>
107+
</nav>
108+
<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">
109+
<ContextMenu menuEntries={[
110+
{
111+
title: (user && User.getPrimaryEmail(user)) || '',
112+
customFontStyle: 'text-gray-400',
113+
separator: true
114+
},
115+
{
116+
title: 'Settings',
117+
link: '/settings',
118+
separator: true
119+
},
120+
{
121+
title: 'Logout',
122+
href: gitpodHostUrl.asApiLogout().toString()
123+
},
124+
]}>
125+
<img className="rounded-full w-6 h-6" src={user?.avatarUrl || ''} alt={user?.name || 'Anonymous'} />
126+
</ContextMenu>
127+
</div>
80128
</div>
81129
</div>
130+
{!!props.showTeams && <div className="flex">
131+
{props.left.map(entry => <TabMenuItem name={entry.title} selected={isSelected(entry, location)} link={entry.link}/>)}
132+
</div>}
82133
</header>
83-
);
84-
}
85-
86-
export default Menu;
134+
{!!props.showTeams && <Separator />}
135+
</>;
136+
}

components/dashboard/src/components/PendingChangesDropdown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export default function PendingChangesDropdown(props: { workspaceInstance?: Work
3434
if (totalChanges <= 0) {
3535
return <p>No Changes</p>;
3636
}
37-
return <ContextMenu menuEntries={menuEntries} width="w-64 max-h-48 overflow-scroll mx-auto left-0 right-0">
37+
return <ContextMenu menuEntries={menuEntries} classes="w-64 max-h-48 overflow-scroll mx-auto left-0 right-0">
3838
<p className="flex justify-center text-gitpod-red">
3939
<span>{totalChanges} Change{totalChanges === 1 ? '' : 's'}</span>
4040
<img className="m-2" src={CaretDown}/>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { Link } from "react-router-dom";
8+
9+
export default function PillMenuItem(p: {
10+
name: string,
11+
selected: boolean,
12+
link?: string,
13+
onClick?: (event: React.MouseEvent) => void
14+
}) {
15+
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 ' +
16+
(p.selected
17+
? 'bg-gray-200 dark:bg-gray-700'
18+
: 'text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800');
19+
return ((!p.link || p.link.startsWith('https://'))
20+
? <a className={classes} href={p.link} onClick={p.onClick}>{p.name}</a>
21+
: <Link className={classes} to={p.link} onClick={p.onClick}>{p.name}</Link>);
22+
}

components/dashboard/src/components/Separator.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
*/
66

77
export default function Separator() {
8-
return <div className="border-gray-200 dark:border-gray-800 border-b h-0.5 absolute left-0 w-screen"></div>;
8+
return <div className="border-gray-200 dark:border-gray-800 border-b absolute left-0 w-screen"></div>;
99
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { Link } from "react-router-dom";
8+
9+
export default function TabMenuItem(p: {
10+
name: string,
11+
selected: boolean,
12+
link?: string,
13+
onClick?: (event: React.MouseEvent) => void
14+
}) {
15+
const classes = 'cursor-pointer py-2 px-4 border-b-4 border-transparent transition ease-in-out ' +
16+
(p.selected
17+
? 'text-gray-600 dark:text-gray-400 border-gray-700 dark:border-gray-400'
18+
: 'text-gray-400 dark:text-gray-600 hover:border-gray-400 dark:hover:border-gray-600');
19+
return ((!p.link || p.link.startsWith('https://'))
20+
? <a className={classes} href={p.link} onClick={p.onClick}>{p.name}</a>
21+
: <Link className={classes} to={p.link} onClick={p.onClick}>{p.name}</Link>);
22+
}

0 commit comments

Comments
 (0)