5
5
*/
6
6
7
7
import { User } from "@gitpod/gitpod-protocol" ;
8
- import { useContext , useEffect , useState } from "react" ;
8
+ import { FC , useCallback , useContext , useEffect , useMemo , useState } from "react" ;
9
9
import { Link } from "react-router-dom" ;
10
10
import { useLocation } from "react-router" ;
11
11
import { Location } from "history" ;
12
12
import { countries } from "countries-list" ;
13
13
import gitpodIcon from "../icons/gitpod.svg" ;
14
14
import { getGitpodService , gitpodHostUrl } from "../service/service" ;
15
15
import { useCurrentUser } from "../user-context" ;
16
- import ContextMenu from "../components/ContextMenu" ;
17
- import Separator from "../components/Separator" ;
16
+ import ContextMenu , { ContextMenuEntry } from "../components/ContextMenu" ;
17
+ import { Separator } from "../components/Separator" ;
18
18
import PillMenuItem from "../components/PillMenuItem" ;
19
19
import { PaymentContext } from "../payment-context" ;
20
20
import FeedbackFormModal from "../feedback-form/FeedbackModal" ;
21
21
import { isGitpodIo } from "../utils" ;
22
22
import OrganizationSelector from "./OrganizationSelector" ;
23
23
import { getAdminTabs } from "../admin/admin.routes" ;
24
+ import classNames from "classnames" ;
24
25
25
26
interface Entry {
26
27
title : string ;
@@ -34,12 +35,6 @@ export default function Menu() {
34
35
const { setCurrency, setIsStudent, setIsChargebeeCustomer } = useContext ( PaymentContext ) ;
35
36
const [ isFeedbackFormVisible , setFeedbackFormVisible ] = useState < boolean > ( false ) ;
36
37
37
- function isSelected ( entry : Entry , location : Location < any > ) {
38
- const all = [ entry . link , ...( entry . alternatives || [ ] ) ] . map ( ( l ) => l . toLowerCase ( ) ) ;
39
- const path = location . pathname . toLowerCase ( ) ;
40
- return all . some ( ( n ) => n === path || n + "/" === path ) ;
41
- }
42
-
43
38
useEffect ( ( ) => {
44
39
const { server } = getGitpodService ( ) ;
45
40
Promise . all ( [
@@ -52,64 +47,49 @@ export default function Menu() {
52
47
] ) . then ( ( setters ) => setters . forEach ( ( s ) => s ( ) ) ) ;
53
48
} , [ setCurrency , setIsChargebeeCustomer , setIsStudent ] ) ;
54
49
55
- const leftMenu : Entry [ ] = [
56
- {
57
- title : "Workspaces" ,
58
- link : "/workspaces" ,
59
- alternatives : [ "/" ] ,
60
- } ,
61
- {
62
- title : "Projects" ,
63
- link : `/projects` ,
64
- alternatives : [ ] as string [ ] ,
65
- } ,
66
- ] ;
67
-
68
- const adminMenu : Entry = {
69
- title : "Admin" ,
70
- link : "/admin" ,
71
- alternatives : [
72
- ...getAdminTabs ( ) . reduce (
73
- ( prevEntry , currEntry ) =>
74
- currEntry . alternatives
75
- ? [ ...prevEntry , ...currEntry . alternatives , currEntry . link ]
76
- : [ ...prevEntry , currEntry . link ] ,
77
- [ ] as string [ ] ,
78
- ) ,
79
- ] ,
80
- } ;
50
+ const adminMenu : Entry = useMemo (
51
+ ( ) => ( {
52
+ title : "Admin" ,
53
+ link : "/admin" ,
54
+ alternatives : [
55
+ ...getAdminTabs ( ) . reduce (
56
+ ( prevEntry , currEntry ) =>
57
+ currEntry . alternatives
58
+ ? [ ...prevEntry , ...currEntry . alternatives , currEntry . link ]
59
+ : [ ...prevEntry , currEntry . link ] ,
60
+ [ ] as string [ ] ,
61
+ ) ,
62
+ ] ,
63
+ } ) ,
64
+ [ ] ,
65
+ ) ;
81
66
82
- const handleFeedbackFormClick = ( ) => {
67
+ const handleFeedbackFormClick = useCallback ( ( ) => {
83
68
setFeedbackFormVisible ( true ) ;
84
- } ;
69
+ } , [ ] ) ;
85
70
86
- const onFeedbackFormClose = ( ) => {
71
+ const onFeedbackFormClose = useCallback ( ( ) => {
87
72
setFeedbackFormVisible ( false ) ;
88
- } ;
73
+ } , [ ] ) ;
89
74
90
75
return (
91
76
< >
92
- < header className = "app-container flex flex-col pt-4 space-y-4" data-analytics = '{"button_type":"menu"}' >
93
- < div className = "flex h-10 mb-3" >
94
- < div className = "flex justify-between items-center pr-3" >
95
- < Link to = "/" className = "pr-3 w-10" >
77
+ < header className = "app-container flex flex-col pt-4" data-analytics = '{"button_type":"menu"}' >
78
+ < div className = "flex justify-between h-10 mb-3 w-full" >
79
+ < div className = "flex items-center" >
80
+ { /* hidden on smaller screens */ }
81
+ < Link to = "/" className = "hidden md:inline pr-3 w-10" >
96
82
< img src = { gitpodIcon } className = "h-6" alt = "Gitpod's logo" />
97
83
</ Link >
98
84
< OrganizationSelector />
99
- < div className = "pl-2 text-base text-gray-500 dark:text-gray-400 flex max-w-lg overflow-hidden" >
100
- { leftMenu . map ( ( entry ) => (
101
- < div className = "p-1" key = { entry . title } >
102
- < PillMenuItem
103
- name = { entry . title }
104
- selected = { isSelected ( entry , location ) }
105
- link = { entry . link }
106
- />
107
- </ div >
108
- ) ) }
85
+ { /* hidden on smaller screens (in it's own menu below on smaller screens) */ }
86
+ < div className = "hidden md:block pl-2" >
87
+ < OrgPagesNav />
109
88
</ div >
110
89
</ div >
111
- < div className = "flex-1 flex items-center w-auto" id = "menu" >
112
- < nav className = "flex-1" >
90
+ < div className = "flex items-center w-auto" id = "menu" >
91
+ { /* hidden on smaller screens - TODO: move to user menu on smaller screen */ }
92
+ < nav className = "hidden md:block flex-1" >
113
93
< ul className = "flex flex-1 items-center justify-between text-base text-gray-500 dark:text-gray-400 space-x-2" >
114
94
< li className = "flex-1" > </ li >
115
95
{ user ?. rolesOrPermissions ?. includes ( "admin" ) && (
@@ -128,52 +108,146 @@ export default function Menu() {
128
108
) }
129
109
</ ul >
130
110
</ nav >
131
- < div
132
- 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 flex-shrink-0"
133
- data-analytics = '{"label":"Account"}'
134
- >
135
- < ContextMenu
136
- menuEntries = { [
137
- {
138
- title : ( user && ( User . getPrimaryEmail ( user ) || user ?. name ) ) || "User" ,
139
- customFontStyle : "text-gray-400" ,
140
- separator : true ,
141
- } ,
142
- {
143
- title : "User Settings" ,
144
- link : "/user/settings" ,
145
- } ,
146
- {
147
- title : "Docs" ,
148
- href : "https://www.gitpod.io/docs/" ,
149
- target : "_blank" ,
150
- rel : "noreferrer" ,
151
- } ,
152
- {
153
- title : "Help" ,
154
- href : "https://www.gitpod.io/support/" ,
155
- target : "_blank" ,
156
- rel : "noreferrer" ,
157
- separator : true ,
158
- } ,
159
- {
160
- title : "Logout" ,
161
- href : gitpodHostUrl . asApiLogout ( ) . toString ( ) ,
162
- } ,
163
- ] }
164
- >
165
- < img
166
- className = "rounded-full w-6 h-6"
167
- src = { user ?. avatarUrl || "" }
168
- alt = { user ?. name || "Anonymous" }
169
- />
170
- </ ContextMenu >
171
- </ div >
111
+ { /* Hide normal user menu on small screens */ }
112
+ < UserMenu user = { user } className = "hidden md:block" />
113
+ { /* Show a user menu w/ admin & feedback links on small screens */ }
114
+ < UserMenu
115
+ user = { user }
116
+ className = "md:hidden"
117
+ withAdminLink
118
+ withFeedbackLink
119
+ onFeedback = { handleFeedbackFormClick }
120
+ />
172
121
</ div >
173
122
{ isFeedbackFormVisible && < FeedbackFormModal onClose = { onFeedbackFormClose } /> }
174
123
</ div >
175
124
</ header >
176
125
< Separator />
126
+ { /* only shown on small screens */ }
127
+ < OrgPagesNav className = "md:hidden app-container flex justify-start py-2" />
128
+ { /* only shown on small screens */ }
129
+ < Separator className = "md:hidden" />
177
130
</ >
178
131
) ;
179
132
}
133
+
134
+ type OrgPagesNavProps = {
135
+ className ?: string ;
136
+ } ;
137
+ const OrgPagesNav : FC < OrgPagesNavProps > = ( { className } ) => {
138
+ const location = useLocation ( ) ;
139
+
140
+ const leftMenu : Entry [ ] = useMemo (
141
+ ( ) => [
142
+ {
143
+ title : "Workspaces" ,
144
+ link : "/workspaces" ,
145
+ alternatives : [ "/" ] ,
146
+ } ,
147
+ {
148
+ title : "Projects" ,
149
+ link : `/projects` ,
150
+ alternatives : [ ] as string [ ] ,
151
+ } ,
152
+ ] ,
153
+ [ ] ,
154
+ ) ;
155
+
156
+ return (
157
+ < div
158
+ className = { classNames (
159
+ "text-base text-gray-500 dark:text-gray-400 flex items-center space-x-1 py-1" ,
160
+ className ,
161
+ ) }
162
+ >
163
+ { leftMenu . map ( ( entry ) => (
164
+ < div key = { entry . title } >
165
+ < PillMenuItem name = { entry . title } selected = { isSelected ( entry , location ) } link = { entry . link } />
166
+ </ div >
167
+ ) ) }
168
+ </ div >
169
+ ) ;
170
+ } ;
171
+
172
+ type UserMenuProps = {
173
+ user ?: User ;
174
+ className ?: string ;
175
+ withAdminLink ?: boolean ;
176
+ withFeedbackLink ?: boolean ;
177
+ onFeedback ?: ( ) => void ;
178
+ } ;
179
+ const UserMenu : FC < UserMenuProps > = ( { user, className, withAdminLink, withFeedbackLink, onFeedback } ) => {
180
+ const extraSection = useMemo ( ( ) => {
181
+ const items : ContextMenuEntry [ ] = [ ] ;
182
+
183
+ if ( withAdminLink && user ?. rolesOrPermissions ?. includes ( "admin" ) ) {
184
+ items . push ( {
185
+ title : "Admin" ,
186
+ link : "/admin" ,
187
+ } ) ;
188
+ }
189
+ if ( withFeedbackLink && isGitpodIo ( ) ) {
190
+ items . push ( {
191
+ title : "Feedback" ,
192
+ onClick : onFeedback ,
193
+ } ) ;
194
+ }
195
+
196
+ // Add a separator to the last item
197
+ if ( items . length > 0 ) {
198
+ items [ items . length - 1 ] . separator = true ;
199
+ }
200
+
201
+ return items ;
202
+ } , [ onFeedback , user ?. rolesOrPermissions , withAdminLink , withFeedbackLink ] ) ;
203
+
204
+ return (
205
+ < div
206
+ className = { classNames (
207
+ "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 flex-shrink-0" ,
208
+ className ,
209
+ ) }
210
+ data-analytics = '{"label":"Account"}'
211
+ >
212
+ < ContextMenu
213
+ menuEntries = { [
214
+ {
215
+ title : ( user && ( User . getPrimaryEmail ( user ) || user ?. name ) ) || "User" ,
216
+ customFontStyle : "text-gray-400" ,
217
+ separator : true ,
218
+ } ,
219
+ {
220
+ title : "User Settings" ,
221
+ link : "/user/settings" ,
222
+ } ,
223
+ {
224
+ title : "Docs" ,
225
+ href : "https://www.gitpod.io/docs/" ,
226
+ target : "_blank" ,
227
+ rel : "noreferrer" ,
228
+ } ,
229
+ {
230
+ title : "Help" ,
231
+ href : "https://www.gitpod.io/support/" ,
232
+ target : "_blank" ,
233
+ rel : "noreferrer" ,
234
+ separator : true ,
235
+ } ,
236
+ ...extraSection ,
237
+ {
238
+ title : "Logout" ,
239
+ href : gitpodHostUrl . asApiLogout ( ) . toString ( ) ,
240
+ } ,
241
+ ] }
242
+ >
243
+ < img className = "rounded-full w-8 h-8" src = { user ?. avatarUrl || "" } alt = { user ?. name || "Anonymous" } />
244
+ </ ContextMenu >
245
+ </ div >
246
+ ) ;
247
+ } ;
248
+
249
+ function isSelected ( entry : Entry , location : Location < any > ) {
250
+ const all = [ entry . link , ...( entry . alternatives || [ ] ) ] . map ( ( l ) => l . toLowerCase ( ) ) ;
251
+ const path = location . pathname . toLowerCase ( ) ;
252
+ return all . some ( ( n ) => n === path || n + "/" === path ) ;
253
+ }
0 commit comments