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 { User , TeamMemberInfo } 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" ;
12
+ import gitpodIcon from './icons/gitpod.svg' ;
13
+ import CaretDown from "./icons/CaretDown.svg" ;
14
+ import { getGitpodService , gitpodHostUrl } from "./service/service" ;
15
+ import { UserContext } from "./user-context" ;
16
+ import { TeamsContext , getCurrentTeam } from "./teams/teams-context" ;
17
+ import settingsMenu from './settings/settings-menu' ;
18
+ import { adminMenu } from './admin/admin-menu' ;
19
+ import ContextMenu from "./components/ContextMenu" ;
20
+ import Separator from "./components/Separator" ;
21
+ import PillMenuItem from "./components/PillMenuItem" ;
22
+ import TabMenuItem from "./components/TabMenuItem" ;
23
+
24
+ interface Entry {
25
+ title : string ,
26
+ link : string ,
27
+ alternatives ?: string [ ]
28
+ }
29
+
30
+ function isSelected ( entry : Entry , location : Location < any > ) {
31
+ const all = [ entry . link , ...( entry . alternatives || [ ] ) ] ;
32
+ const path = location . pathname . toLowerCase ( ) ;
33
+ return all . some ( n => n === path || n + '/' === path ) ;
34
+ }
35
+
36
+ export default function Menu ( ) {
37
+ const { user } = useContext ( UserContext ) ;
38
+ const { teams } = useContext ( TeamsContext ) ;
39
+ const history = useHistory ( ) ;
40
+ const location = useLocation ( ) ;
41
+
42
+ const userFullName = user ?. fullName || user ?. name || '...' ;
43
+ const showTeamsUI = user ?. rolesOrPermissions ?. includes ( 'teams-and-projects' ) || window . location . hostname . endsWith ( 'gitpod-dev.com' ) || window . location . hostname . endsWith ( 'gitpod-io-dev.com' ) ;
44
+ const team = getCurrentTeam ( location , teams ) ;
45
+
46
+ const [ teamMembers , setTeamMembers ] = useState < Record < string , TeamMemberInfo [ ] > > ( { } ) ;
47
+ useEffect ( ( ) => {
48
+ if ( ! showTeamsUI || ! teams ) {
49
+ return ;
50
+ }
51
+ ( async ( ) => {
52
+ const members : Record < string , TeamMemberInfo [ ] > = { } ;
53
+ await Promise . all ( teams . map ( async ( team ) => {
54
+ const infos = await getGitpodService ( ) . server . getTeamMembers ( team . id ) ;
55
+ members [ team . id ] = infos ;
56
+ } ) ) ;
57
+ setTeamMembers ( members ) ;
58
+ } ) ( ) ;
59
+ } , [ teams ] ) ;
60
+
61
+ const leftMenu = ( ! ! team
62
+ ? [
63
+ {
64
+ title : 'Projects' ,
65
+ link : `/${ team . slug } /projects` ,
66
+ alternatives : [ `/${ team . slug } ` ]
67
+ } ,
68
+ {
69
+ title : 'Members' ,
70
+ link : `/${ team . slug } /members`
71
+ }
72
+ ]
73
+ : [
74
+ {
75
+ title : 'Workspaces' ,
76
+ link : '/workspaces' ,
77
+ alternatives : [ '/' ]
78
+ } ,
79
+ {
80
+ title : 'Settings' ,
81
+ link : '/settings' ,
82
+ alternatives : settingsMenu . flatMap ( e => e . link )
83
+ }
84
+ ]
85
+ ) ;
86
+ const rightMenu = [
87
+ ...( user ?. rolesOrPermissions ?. includes ( 'admin' ) ? [ {
88
+ title : 'Admin' ,
89
+ link : '/admin' ,
90
+ alternatives : adminMenu . flatMap ( e => e . link )
91
+ } ] : [ ] ) ,
92
+ {
93
+ title : 'Docs' ,
94
+ link : 'https://www.gitpod.io/docs/' ,
95
+ } ,
96
+ {
97
+ title : 'Community' ,
98
+ link : 'https://community.gitpod.io/' ,
99
+ }
100
+ ] ;
101
+
102
+ return < >
103
+ < header className = "lg:px-28 px-10 flex flex-col pt-4 space-y-4" >
104
+ < div className = "flex" >
105
+ < div className = "flex justify-between items-center pr-3" >
106
+ < Link to = "/" >
107
+ < img src = { gitpodIcon } className = "h-6" />
108
+ </ Link >
109
+ < div className = "ml-2 text-base" >
110
+ { showTeamsUI
111
+ ? < ContextMenu classes = "w-64 left-0" menuEntries = { [
112
+ {
113
+ title : userFullName ,
114
+ customContent : < div className = "w-full text-gray-400 flex flex-col" >
115
+ < span className = "text-gray-800 dark:text-gray-100 text-base font-semibold" > { userFullName } </ span >
116
+ < span className = "" > Personal Account</ span >
117
+ </ div > ,
118
+ separator : true ,
119
+ onClick : ( ) => history . push ( "/" ) ,
120
+ } ,
121
+ ...( teams || [ ] ) . map ( t => ( {
122
+ title : t . name ,
123
+ customContent : < div className = "w-full text-gray-400 flex flex-col" >
124
+ < span className = "text-gray-800 dark:text-gray-300 text-base font-semibold" > { t . name } </ span >
125
+ < span className = "" > { ! ! teamMembers [ t . id ]
126
+ ? `${ teamMembers [ t . id ] . length } member${ teamMembers [ t . id ] . length === 1 ? '' : 's' } `
127
+ : '...'
128
+ } </ span >
129
+ </ div > ,
130
+ separator : true ,
131
+ onClick : ( ) => history . push ( `/${ t . slug } ` ) ,
132
+ } ) ) . sort ( ( a , b ) => a . title . toLowerCase ( ) > b . title . toLowerCase ( ) ? 1 : - 1 ) ,
133
+ {
134
+ title : 'Create a new team' ,
135
+ customContent : < div className = "w-full text-gray-400 flex items-center" >
136
+ < span className = "flex-1 font-semibold" > New Team</ span >
137
+ < 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 >
138
+ </ div > ,
139
+ onClick : ( ) => history . push ( "/new-team" ) ,
140
+ }
141
+ ] } >
142
+ < div className = "flex p-1.5 pl-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" >
143
+ < span className = "text-base text-gray-600 dark:text-gray-400 font-semibold" > { team ?. name || userFullName } </ span >
144
+ < img className = "m-2 filter-grayscale" src = { CaretDown } />
145
+ </ div >
146
+ </ ContextMenu >
147
+ : < nav className = "flex-1" >
148
+ < ul className = "flex flex-1 items-center justify-between text-base text-gray-700 space-x-2" >
149
+ < li className = "flex-1" > </ li >
150
+ { leftMenu . map ( entry => < li key = { entry . title } >
151
+ < PillMenuItem name = { entry . title } selected = { isSelected ( entry , location ) } link = { entry . link } />
152
+ </ li > ) }
153
+ </ ul >
154
+ </ nav >
155
+ }
156
+ </ div >
157
+ </ div >
158
+ < div className = "flex flex-1 items-center w-auto" id = "menu" >
159
+ < nav className = "flex-1" >
160
+ < ul className = "flex flex-1 items-center justify-between text-base text-gray-700 space-x-2" >
161
+ < li className = "flex-1" > </ li >
162
+ { rightMenu . map ( entry => < li key = { entry . title } >
163
+ < PillMenuItem name = { entry . title } selected = { isSelected ( entry , location ) } link = { entry . link } />
164
+ </ li > ) }
165
+ </ ul >
166
+ </ nav >
167
+ < 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" >
168
+ < ContextMenu menuEntries = { [
169
+ {
170
+ title : ( user && User . getPrimaryEmail ( user ) ) || '' ,
171
+ customFontStyle : 'text-gray-400' ,
172
+ separator : true
173
+ } ,
174
+ {
175
+ title : 'Settings' ,
176
+ link : '/settings' ,
177
+ separator : true
178
+ } ,
179
+ {
180
+ title : 'Logout' ,
181
+ href : gitpodHostUrl . asApiLogout ( ) . toString ( )
182
+ } ,
183
+ ] } >
184
+ < img className = "rounded-full w-6 h-6" src = { user ?. avatarUrl || '' } alt = { user ?. name || 'Anonymous' } />
185
+ </ ContextMenu >
186
+ </ div >
187
+ </ div >
188
+ </ div >
189
+ { showTeamsUI && < div className = "flex" >
190
+ { leftMenu . map ( entry => < TabMenuItem name = { entry . title } selected = { isSelected ( entry , location ) } link = { entry . link } /> ) }
191
+ </ div > }
192
+ </ header >
193
+ { showTeamsUI && < Separator /> }
194
+ </ > ;
195
+ }
0 commit comments