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 { useContext , useEffect , useState } from "react" ;
8+ import { getGitpodService , gitpodHostUrl } from "../service/service" ;
9+ import { iconForAuthProvider , openAuthorizeWindow , simplifyProviderName } from "../provider-utils" ;
10+ import { AuthProviderInfo , ProviderRepository , Team } from "@gitpod/gitpod-protocol" ;
11+ import { TeamsContext } from "../teams/teams-context" ;
12+ import { useLocation } from "react-router" ;
13+
14+ export default function NewProject ( ) {
15+
16+ const { teams } = useContext ( TeamsContext ) ;
17+
18+ const [ provider , setProvider ] = useState < string > ( "github.com" ) ;
19+ const [ reposInAccounts , setReposInAccounts ] = useState < ProviderRepository [ ] > ( [ ] ) ;
20+ const [ selectedAccount , setSelectedAccount ] = useState < string | undefined > ( undefined ) ;
21+ const [ noOrgs , setNoOrgs ] = useState < boolean > ( false ) ;
22+ const [ showGitProviders , setShowGitProviders ] = useState < boolean > ( false ) ;
23+ const [ selectedRepo , setSelectedRepo ] = useState < string | undefined > ( undefined ) ;
24+ const [ selectedTeam , setSelectedTeam ] = useState < Team | undefined > ( undefined ) ;
25+ const [ authProviders , setAuthProviders ] = useState < AuthProviderInfo [ ] > ( [ ] ) ;
26+
27+ const location = useLocation ( ) ;
28+
29+ useEffect ( ( ) => {
30+ const params = new URLSearchParams ( location . search ) ;
31+ const teamParam = params . get ( "team" ) ;
32+ if ( teamParam ) {
33+ window . history . replaceState ( { } , '' , window . location . pathname ) ;
34+ const team = teams ?. find ( t => t . slug === teamParam ) ;
35+ setSelectedTeam ( team ) ;
36+ }
37+
38+ ( async ( ) => {
39+ updateOrgsState ( ) ;
40+ const repos = await updateReposInAccounts ( ) ;
41+ const first = repos [ 0 ] ;
42+ if ( first ) {
43+ setSelectedAccount ( first . account ) ;
44+ }
45+ setAuthProviders ( await getGitpodService ( ) . server . getAuthProviders ( ) ) ;
46+ } ) ( ) ;
47+ } , [ ] ) ;
48+
49+ const isGitHub = ( ) => provider === "github.com" ;
50+
51+ const updateReposInAccounts = async ( installationId ?: string ) => {
52+ try {
53+ const repos = await getGitpodService ( ) . server . getProviderRepositoriesForUser ( { provider, hints : { installationId } } ) ;
54+ setReposInAccounts ( repos ) ;
55+ return repos ;
56+ } catch ( error ) {
57+ setReposInAccounts ( [ ] ) ;
58+ console . log ( error ) ;
59+ }
60+ return [ ] ;
61+ }
62+
63+ const getToken = async ( host : string ) => {
64+ return getGitpodService ( ) . server . getToken ( { host } ) ;
65+ }
66+
67+ const updateOrgsState = async ( ) => {
68+ try {
69+ const ghToken = await getToken ( provider ) ;
70+ setNoOrgs ( ghToken ?. scopes . includes ( "read:org" ) !== true ) ;
71+ } catch {
72+ }
73+ }
74+
75+ const reconfigure = ( ) => {
76+ openReconfigureWindow ( {
77+ account : selectedAccount , onSuccess : ( p : { installationId : string , setupAction ?: string } ) => {
78+ updateReposInAccounts ( p . installationId ) ;
79+ }
80+ } ) ;
81+ }
82+
83+ const grantReadOrgPermissions = async ( ) => {
84+ try {
85+ await openAuthorizeWindow ( {
86+ host : "github.com" ,
87+ scopes : [ "read:org" ] ,
88+ onSuccess : ( ) => {
89+ updateReposInAccounts ( ) ;
90+ updateOrgsState ( ) ;
91+ }
92+ } )
93+ } catch ( error ) {
94+ console . log ( error ) ;
95+ }
96+ }
97+
98+ const selectRepo = ( params : { name : string , account : string } ) => {
99+ setSelectedRepo ( params . name ) ;
100+
101+ if ( selectedTeam ) {
102+ createProject ( selectedTeam , params . name ) ;
103+ }
104+ }
105+
106+ const createProject = async ( team : Team , selectedRepo : string ) => {
107+ const repo = reposInAccounts . find ( r => r . account === selectedAccount && r . name === selectedRepo ) ;
108+ if ( ! repo ) {
109+ console . error ( "No repo selected!" )
110+ return ;
111+ }
112+
113+ await getGitpodService ( ) . server . createProject ( {
114+ name : repo . name ,
115+ cloneUrl : repo . cloneUrl ,
116+ account : repo . account ,
117+ provider,
118+ appInstallationId : String ( repo . installationId ) ,
119+ teamId : team . id
120+ } ) ;
121+ // todo@alex clarify redirect target
122+ // window.location.href = `/${team.slug}/${repo.name}`;
123+ window . location . href = `/${ team . slug } /projects` ;
124+ }
125+
126+ const onSelectAccount = ( value : string ) => {
127+ if ( value === "selectProvider" ) {
128+ setShowGitProviders ( true ) ;
129+ } else if ( value === "addOrg" ) {
130+ reconfigure ( ) ;
131+ } else {
132+ setSelectedAccount ( value ) ;
133+ }
134+ }
135+
136+ const toSimpleName = ( fullName : string ) => {
137+ const splitted = fullName . split ( "/" ) ;
138+ if ( splitted . length < 2 ) {
139+ return fullName ;
140+ }
141+ return splitted . shift ( ) && splitted . join ( "/" ) ;
142+ }
143+
144+ const selectProvider = async ( ap : AuthProviderInfo ) => {
145+ const token = await getToken ( ap . host ) ;
146+ if ( token ) {
147+ setShowGitProviders ( false ) ;
148+ setProvider ( ap . host ) ;
149+ return ;
150+ }
151+ await openAuthorizeWindow ( {
152+ host : ap . host ,
153+ scopes : ap . requirements ?. default ,
154+ onSuccess : ( ) => {
155+ setShowGitProviders ( false ) ;
156+ setProvider ( ap . host ) ;
157+ } ,
158+ onError : ( error ) => {
159+ console . log ( error ) ;
160+ }
161+ } ) ;
162+ }
163+
164+ const reposToRender = Array . from ( reposInAccounts ) . filter ( r => r . account === selectedAccount ) ;
165+ const accounts = Array . from ( new Set ( Array . from ( reposInAccounts ) . map ( r => r . account ) ) ) ;
166+
167+ const renderSelectRepository = ( ) => ( < >
168+ < h3 className = "pb-2 mt-8" > Select Repository</ h3 >
169+
170+ < div className = "mt-8 w-96 border rounded-xl border-gray-100 flex-col" >
171+ { showGitProviders && ( < >
172+ < div className = "p-6" >
173+ < div className = "text-center text-gray-500" >
174+ Select a Git provider first and continue with your repositories.
175+ </ div >
176+ < div className = "mt-6 flex flex-col space-y-3 items-center" >
177+ { authProviders . map ( ap => {
178+ return (
179+ < button key = { "button" + ap . host } className = "btn-login flex-none w-56 h-10 p-0 inline-flex" onClick = { ( ) => selectProvider ( ap ) } >
180+ { iconForAuthProvider ( ap . authProviderType ) }
181+ < span className = "pt-2 pb-2 mr-3 text-sm my-auto font-medium truncate overflow-ellipsis" > Continue with { simplifyProviderName ( ap . host ) } </ span >
182+ </ button >
183+ ) ;
184+ } ) }
185+ </ div >
186+ </ div >
187+ </ > ) }
188+ { ! showGitProviders && ( < >
189+ < div className = "p-6" >
190+ < div className = "flex flex-col space-y-2" >
191+ < label htmlFor = "selectAccount" className = "font-medium" > Account</ label >
192+ < select name = "selectAccount" value = { selectedAccount } className = "w-full"
193+ onChange = { ( e ) => onSelectAccount ( e . target . value ) } >
194+ { accounts . map ( a => ( < option key = { `account-${ a } ` } value = { a } > { `${ a } (${ provider } )` } </ option > ) ) }
195+ { isGitHub ( ) && ( < option value = "addOrg" > Add GH Org</ option > ) }
196+ < option value = "selectProvider" > Select Git Provider</ option >
197+ </ select >
198+ </ div >
199+ < div className = "flex-col mt-4" >
200+ { reposToRender . length > 0 && (
201+ < div className = "overscroll-contain max-h-96 overflow-y-auto" >
202+ { reposToRender . map ( r => (
203+ < div key = { `repo-${ r . name } ` } className = "rounded-md whitespace-nowrap flex space-x-2 py-2 px-3 w-full justify-between hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gitpod-kumquat-light transition ease-in-out group" >
204+ < div className = "w-8/12 text-gray-400 flex flex-col" >
205+ < span className = "text-gray-800 dark:text-gray-300 text-base font-semibold overflow-ellipsis truncate" > { toSimpleName ( r . name ) } </ span >
206+ < span className = "" > Updated __ ago</ span >
207+ </ div >
208+ < div className = "w-4/12 flex justify-end" >
209+ < div className = "flex self-center hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md cursor-pointer opacity-0 group-hover:opacity-100" >
210+ < button className = "primary py-1" onClick = { ( ) => selectRepo ( r ) } > Select</ button >
211+ </ div >
212+ </ div >
213+ </ div >
214+ ) ) }
215+ </ div >
216+ ) }
217+ </ div >
218+ </ div >
219+ < div className = "p-3 bg-gray-100" >
220+ < div className = "text-gray-500 text-center" >
221+ Repository not found? < a href = "javascript:void(0)" onClick = { e => reconfigure ( ) } className = "text-gray-400 underline underline-thickness-thin underline-offset-small hover:text-gray-600" > Reconfigure</ a >
222+ </ div >
223+ { noOrgs && (
224+ < div className = "text-gray-500 mx-auto text-center" >
225+ Missing organizations? < a href = "javascript:void(0)" onClick = { e => grantReadOrgPermissions ( ) } className = "text-gray-400 underline underline-thickness-thin underline-offset-small hover:text-gray-600" > Grant permissions</ a >
226+ </ div >
227+ ) }
228+ </ div >
229+ </ > ) }
230+ </ div >
231+ </ > ) ;
232+
233+ const renderSelectTeam = ( ) => ( < >
234+ < h3 className = "pb-2 mt-8" > Select Team</ h3 >
235+ < h4 className = "pb-2" > Adding < strong > { selectedRepo } </ strong > </ h4 >
236+
237+ < div className = "mt-8 w-96 border rounded-xl border-gray-100 flex-col" >
238+ { ( teams || [ ] ) . map ( ( t ) => (
239+ < div key = { `team-${ t . name } ` } className = { `border-b p-6 flex space-x-2 w-full justify-between dark:hover:bg-gray-800 focus:bg-gitpod-kumquat-light transition ease-in-out group` } >
240+ < div className = "w-8/12 m-auto overflow-ellipsis truncate" > { t . name } </ div >
241+ < div className = "w-4/12 flex justify-end" >
242+ < div className = "flex self-center hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md cursor-pointer opacity-0 group-hover:opacity-100" >
243+ < button className = "primary py-1" onClick = { ( ) => createProject ( t , selectedRepo ! ) } > Select</ button >
244+ </ div >
245+ </ div >
246+ </ div >
247+ ) ) }
248+ < div className = "p-6 flex text-gray-500" >
249+ New Team
250+ </ div >
251+ </ div >
252+ </ > ) ;
253+
254+ return ( < div className = "flex flex-col w-96 mt-16 mx-auto items-center" >
255+ < h1 > New Project</ h1 >
256+ < p className = "text-gray-500 text-center text-base" > Project allow you to set up and acess Prebuilds.</ p >
257+
258+ { ! selectedRepo && renderSelectRepository ( ) }
259+
260+ { selectedRepo && renderSelectTeam ( ) }
261+
262+ </ div > ) ;
263+
264+ }
265+
266+ async function openReconfigureWindow ( params : { account ?: string , onSuccess : ( p : any ) => void } ) {
267+ const { account, onSuccess } = params ;
268+ const state = btoa ( JSON . stringify ( { from : "/reconfigure" , next : "/new" } ) ) ;
269+ const url = gitpodHostUrl . withApi ( {
270+ pathname : '/apps/github/reconfigure' ,
271+ search : `account=${ account } &state=${ encodeURIComponent ( state ) } `
272+ } ) . toString ( ) ;
273+
274+ // Optimistically assume that the new window was opened.
275+ window . open ( url , "gitpod-github-window" , "width=800,height=800,status=yes,scrollbars=yes,resizable=yes" ) ;
276+
277+ const eventListener = ( event : MessageEvent ) => {
278+ // todo: check event.origin
279+
280+ const killWindow = ( ) => {
281+ window . removeEventListener ( "message" , eventListener ) ;
282+
283+ if ( event . source && "close" in event . source && event . source . close ) {
284+ console . log ( `Received Window Result. Closing Window.` ) ;
285+ event . source . close ( ) ;
286+ }
287+ }
288+
289+ if ( typeof event . data === "string" && event . data . startsWith ( "payload:" ) ) {
290+ killWindow ( ) ;
291+ try {
292+ let payload : { installationId : string , setupAction ?: string } = JSON . parse ( atob ( event . data . substring ( "payload:" . length ) ) ) ;
293+ onSuccess && onSuccess ( payload ) ;
294+ } catch ( error ) {
295+ console . log ( error ) ;
296+ }
297+ }
298+ if ( typeof event . data === "string" && event . data . startsWith ( "error:" ) ) {
299+ let error : string | { error : string , description ?: string } = atob ( event . data . substring ( "error:" . length ) ) ;
300+ try {
301+ const payload = JSON . parse ( error ) ;
302+ if ( typeof payload === "object" && payload . error ) {
303+ error = { error : payload . error , description : payload . description } ;
304+ }
305+ } catch ( error ) {
306+ console . log ( error ) ;
307+ }
308+
309+ killWindow ( ) ;
310+ // onError && onError(error);
311+ }
312+ } ;
313+ window . addEventListener ( "message" , eventListener ) ;
314+ }
0 commit comments