1
1
// Copyright (c) Jupyter Development Team.
2
2
// Distributed under the terms of the Modified BSD License.
3
3
4
+ import {
5
+ find
6
+ } from '@phosphor/algorithm' ;
7
+
4
8
import {
5
9
PanelLayout , Widget
6
10
} from '@phosphor/widgets' ;
7
11
8
12
import {
9
- ObservableValue
13
+ ToolbarButton
14
+ } from '@jupyterlab/apputils' ;
15
+
16
+ import {
17
+ ObservableValue , URLExt
10
18
} from '@jupyterlab/coreutils' ;
11
19
12
20
import {
@@ -17,6 +25,22 @@ import {
17
25
GitHubDrive
18
26
} from './contents' ;
19
27
28
+
29
+ /**
30
+ * The base url for a mybinder deployment.
31
+ */
32
+ const MY_BINDER_BASE_URL = 'https://mybinder.org/v2/gh' ;
33
+
34
+ /**
35
+ * The GitHub base url.
36
+ */
37
+ const GITHUB_BASE_URL = 'https://github.com' ;
38
+
39
+ /**
40
+ * The className for disabling the mybinder button.
41
+ */
42
+ const MY_BINDER_DISABLED = 'jp-MyBinderButton-disabled' ;
43
+
20
44
/**
21
45
* Widget for hosting the GitHub filebrowser.
22
46
*/
@@ -30,17 +54,60 @@ class GitHubFileBrowser extends Widget {
30
54
this . _browser = browser ;
31
55
this . _drive = drive ;
32
56
33
- const userLabel = new Widget ( ) ;
34
- userLabel . addClass ( 'jp-GitHubUserLabel' ) ;
35
- userLabel . node . textContent = 'User:' ;
36
- this . _browser . toolbar . addItem ( 'label' , userLabel ) ;
37
-
57
+ // Create an editable name for the user/org name.
38
58
this . userName = new GitHubEditableName ( drive . user , '<Edit User>' ) ;
39
59
this . userName . addClass ( 'jp-GitHubEditableUserName' ) ;
40
- this . userName . node . title = 'User ' ;
60
+ this . userName . node . title = 'Click to edit user/organization ' ;
41
61
this . _browser . toolbar . addItem ( 'user' , this . userName ) ;
42
62
this . userName . name . changed . connect ( this . _onUserChanged , this ) ;
43
63
64
+ // Create a button that opens GitHub at the appropriate
65
+ // repo+directory.
66
+ this . _openGitHubButton = new ToolbarButton ( {
67
+ onClick : ( ) => {
68
+ let url = GITHUB_BASE_URL ;
69
+ // If there is no valid user, do nothing.
70
+ if ( ! this . _drive . validUserState . get ( ) ) {
71
+ window . open ( url ) ;
72
+ return ;
73
+ }
74
+ const user = this . _drive . user ;
75
+ const path = this . _browser . model . path ;
76
+ const repo = path . split ( '/' ) [ 0 ] . split ( ':' ) [ 1 ] ;
77
+ url = URLExt . join ( url , user ) ;
78
+ if ( repo ) {
79
+ const dirPath = URLExt . join ( repo , ...path . split ( '/' ) . slice ( 1 ) ) ;
80
+ url = URLExt . join ( url , repo , 'tree' , 'master' , dirPath ) ;
81
+ }
82
+ window . open ( url ) ;
83
+ } ,
84
+ className : 'jp-GitHubIcon' ,
85
+ tooltip : 'Open this repository on GitHub'
86
+ } ) ;
87
+ this . _browser . toolbar . addItem ( 'GitHub' , this . _openGitHubButton ) ;
88
+
89
+ // Create a button the opens MyBinder to the appropriate repo.
90
+ this . _launchBinderButton = new ToolbarButton ( {
91
+ onClick : ( ) => {
92
+ // If binder is not active for this directory, do nothing.
93
+ if ( ! this . _binderActive ) {
94
+ return ;
95
+ }
96
+ const user = this . _drive . user ;
97
+ const repo = this . _browser . model . path . split ( '/' ) [ 0 ] . split ( ':' ) [ 1 ] ;
98
+ const url = URLExt . join ( MY_BINDER_BASE_URL , user , repo , 'master' ) ;
99
+ window . open ( url + '?urlpath=lab' ) ;
100
+ } ,
101
+ tooltip : 'Launch this repository on mybinder.org' ,
102
+ className : 'jp-MyBinderButton'
103
+ } ) ;
104
+ this . _browser . toolbar . addItem ( 'binder' , this . _launchBinderButton ) ;
105
+
106
+ // Set up a listener to check if we can launch mybinder.
107
+ this . _browser . model . pathChanged . connect ( this . _onPathChanged , this ) ;
108
+ // Trigger an initial pathChanged to check for binder state.
109
+ this . _onPathChanged ( ) ;
110
+
44
111
this . _drive . rateLimitedState . changed . connect ( this . _updateErrorPanel , this ) ;
45
112
this . _drive . validUserState . changed . connect ( this . _updateErrorPanel , this ) ;
46
113
}
@@ -70,6 +137,40 @@ class GitHubFileBrowser extends Widget {
70
137
} ) ;
71
138
}
72
139
140
+ /**
141
+ * React to the path changing for the browser.
142
+ */
143
+ private _onPathChanged ( ) : void {
144
+ const path = this . _browser . model . path ;
145
+ // Check for a valid user.
146
+ if ( ! this . _drive . validUserState . get ( ) ) {
147
+ this . _launchBinderButton . addClass ( MY_BINDER_DISABLED ) ;
148
+ this . _binderActive = false ;
149
+ return ;
150
+ }
151
+ // Check for a valid repo.
152
+ const repo = path . split ( '/' ) [ 0 ] . split ( ':' ) [ 1 ] ;
153
+ if ( ! repo ) {
154
+ this . _launchBinderButton . addClass ( MY_BINDER_DISABLED ) ;
155
+ this . _binderActive = false ;
156
+ return ;
157
+ }
158
+ // Check for one of the special values indicating we can
159
+ // launch the repository.
160
+ const item = find ( this . _browser . model . items ( ) , i => {
161
+ return i . name === 'requirements.txt' || i . name === 'environment.yml' ||
162
+ i . name === 'apt.txt' || i . name === 'REQUIRE' ||
163
+ i . name === 'Dockerfile' ;
164
+ } ) ;
165
+ if ( item ) {
166
+ this . _launchBinderButton . removeClass ( MY_BINDER_DISABLED ) ;
167
+ this . _binderActive = true ;
168
+ return ;
169
+ }
170
+ this . _launchBinderButton . addClass ( MY_BINDER_DISABLED ) ;
171
+ this . _binderActive = false ;
172
+ }
173
+
73
174
/**
74
175
* React to a change in the validity of the drive.
75
176
*/
@@ -108,6 +209,9 @@ class GitHubFileBrowser extends Widget {
108
209
private _browser : FileBrowser ;
109
210
private _drive : GitHubDrive ;
110
211
private _errorPanel : GitHubErrorPanel | null ;
212
+ private _openGitHubButton : ToolbarButton ;
213
+ private _launchBinderButton : ToolbarButton ;
214
+ private _binderActive = false ;
111
215
}
112
216
113
217
/**
@@ -121,8 +225,9 @@ class GitHubEditableName extends Widget {
121
225
super ( ) ;
122
226
this . addClass ( 'jp-GitHubEditableName' ) ;
123
227
this . _nameNode = document . createElement ( 'div' ) ;
228
+ this . _nameNode . className = 'jp-GitHubEditableName-display' ;
124
229
this . _editNode = document . createElement ( 'input' ) ;
125
- this . _editNode . className = 'jp-GitHubEditableNameInput ' ;
230
+ this . _editNode . className = 'jp-GitHubEditableName-input ' ;
126
231
127
232
this . _placeholder = placeholder || '<Edit Name>'
128
233
@@ -137,7 +242,9 @@ class GitHubEditableName extends Widget {
137
242
this . _pending = true ;
138
243
Private . changeField ( this . _nameNode , this . _editNode ) . then ( value => {
139
244
this . _pending = false ;
140
- this . name . set ( value ) ;
245
+ if ( this . name . get ( ) !== value ) {
246
+ this . name . set ( value ) ;
247
+ }
141
248
} ) ;
142
249
} ;
143
250
@@ -182,8 +289,6 @@ class GitHubErrorPanel extends Widget {
182
289
}
183
290
184
291
185
-
186
-
187
292
/**
188
293
* A module-Private namespace.
189
294
*/
@@ -202,13 +307,10 @@ namespace Private {
202
307
* or has been canceled.
203
308
*/
204
309
function changeField ( text : HTMLElement , edit : HTMLInputElement ) : Promise < string > {
205
- // Replace the text node with an the input element,
206
- // setting the value and width of the input element to
207
- // the same as the text node.
310
+ // Replace the text node with an the input element.
208
311
let parent = text . parentElement as HTMLElement ;
209
312
let initialValue = text . textContent ;
210
313
edit . value = initialValue ;
211
- parent . style . width = String ( parent . offsetWidth + 3 ) + 'px' ;
212
314
parent . replaceChild ( edit , text ) ;
213
315
edit . focus ( ) ;
214
316
@@ -222,10 +324,8 @@ namespace Private {
222
324
223
325
return new Promise < string > ( ( resolve , reject ) => {
224
326
edit . onblur = ( ) => {
225
- // Restore the correct width and set the
226
- // text content of the original node, then
327
+ // Set the text content of the original node, then
227
328
// replace the node.
228
- parent . style . width = '' ;
229
329
parent . replaceChild ( text , edit ) ;
230
330
text . textContent = edit . value || initialValue ;
231
331
resolve ( edit . value ) ;
0 commit comments