@jupyterlab/extensionmanager 4.0.0-alpha.9 → 4.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dialog.js +2 -2
- package/lib/dialog.js.map +1 -1
- package/lib/index.d.ts +0 -2
- package/lib/index.js +0 -2
- package/lib/index.js.map +1 -1
- package/lib/model.d.ts +90 -193
- package/lib/model.js +210 -451
- package/lib/model.js.map +1 -1
- package/lib/widget.d.ts +18 -207
- package/lib/widget.js +201 -317
- package/lib/widget.js.map +1 -1
- package/package.json +16 -15
- package/src/dialog.tsx +39 -0
- package/src/index.ts +9 -0
- package/src/model.ts +691 -0
- package/src/widget.tsx +760 -0
- package/style/base.css +59 -103
- package/style/index.css +1 -1
- package/style/index.js +1 -1
- package/lib/build-helper.d.ts +0 -9
- package/lib/build-helper.js +0 -60
- package/lib/build-helper.js.map +0 -1
- package/lib/companions.d.ts +0 -88
- package/lib/companions.js +0 -98
- package/lib/companions.js.map +0 -1
- package/lib/listings.d.ts +0 -50
- package/lib/listings.js +0 -80
- package/lib/listings.js.map +0 -1
- package/lib/npm.d.ts +0 -223
- package/lib/npm.js +0 -76
- package/lib/npm.js.map +0 -1
package/src/widget.tsx
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
import { ITranslator, TranslationBundle } from '@jupyterlab/translation';
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
FilterBox,
|
|
8
|
+
infoIcon,
|
|
9
|
+
jupyterIcon,
|
|
10
|
+
PanelWithToolbar,
|
|
11
|
+
ReactWidget,
|
|
12
|
+
refreshIcon,
|
|
13
|
+
SidePanel,
|
|
14
|
+
ToolbarButton,
|
|
15
|
+
ToolbarButtonComponent
|
|
16
|
+
} from '@jupyterlab/ui-components';
|
|
17
|
+
import { Message } from '@lumino/messaging';
|
|
18
|
+
import { AccordionLayout, AccordionPanel } from '@lumino/widgets';
|
|
19
|
+
import * as React from 'react';
|
|
20
|
+
import ReactPaginate from 'react-paginate';
|
|
21
|
+
import { Action, IEntry, ListModel } from './model';
|
|
22
|
+
|
|
23
|
+
const BADGE_SIZE = 32;
|
|
24
|
+
const BADGE_QUERY_SIZE = Math.floor(devicePixelRatio * BADGE_SIZE);
|
|
25
|
+
|
|
26
|
+
function getExtensionGitHubUser(entry: IEntry) {
|
|
27
|
+
if (
|
|
28
|
+
entry.homepage_url &&
|
|
29
|
+
entry.homepage_url.startsWith('https://github.com/')
|
|
30
|
+
) {
|
|
31
|
+
return entry.homepage_url.split('/')[3];
|
|
32
|
+
} else if (
|
|
33
|
+
entry.repository_url &&
|
|
34
|
+
entry.repository_url.startsWith('https://github.com/')
|
|
35
|
+
) {
|
|
36
|
+
return entry.repository_url.split('/')[3];
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* VDOM for visualizing an extension entry.
|
|
43
|
+
*/
|
|
44
|
+
function ListEntry(props: ListEntry.IProperties): React.ReactElement<any> {
|
|
45
|
+
const { canFetch, entry, supportInstallation, trans } = props;
|
|
46
|
+
const flagClasses = [];
|
|
47
|
+
if (entry.status && ['ok', 'warning', 'error'].indexOf(entry.status) !== -1) {
|
|
48
|
+
flagClasses.push(`jp-extensionmanager-entry-${entry.status}`);
|
|
49
|
+
}
|
|
50
|
+
const githubUser = canFetch ? getExtensionGitHubUser(entry) : null;
|
|
51
|
+
|
|
52
|
+
if (!entry.allowed) {
|
|
53
|
+
flagClasses.push(`jp-extensionmanager-entry-should-be-uninstalled`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<li
|
|
58
|
+
className={`jp-extensionmanager-entry ${flagClasses.join(' ')}`}
|
|
59
|
+
style={{ display: 'flex' }}
|
|
60
|
+
>
|
|
61
|
+
<div style={{ marginRight: '8px' }}>
|
|
62
|
+
{githubUser ? (
|
|
63
|
+
<img
|
|
64
|
+
src={`https://github.com/${githubUser}.png?size=${BADGE_QUERY_SIZE}`}
|
|
65
|
+
style={{ width: '32px', height: '32px' }}
|
|
66
|
+
/>
|
|
67
|
+
) : (
|
|
68
|
+
<div
|
|
69
|
+
style={{ width: `${BADGE_SIZE}px`, height: `${BADGE_SIZE}px` }}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
<div className="jp-extensionmanager-entry-description">
|
|
74
|
+
<div className="jp-extensionmanager-entry-title">
|
|
75
|
+
<div className="jp-extensionmanager-entry-name">
|
|
76
|
+
{entry.homepage_url ? (
|
|
77
|
+
<a
|
|
78
|
+
href={entry.homepage_url}
|
|
79
|
+
target="_blank"
|
|
80
|
+
rel="noopener noreferrer"
|
|
81
|
+
title={trans.__('%1 extension home page', entry.name)}
|
|
82
|
+
>
|
|
83
|
+
{entry.name}
|
|
84
|
+
</a>
|
|
85
|
+
) : (
|
|
86
|
+
<div>{entry.name}</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
<div className="jp-extensionmanager-entry-version">
|
|
90
|
+
<div title={trans.__('Version: %1', entry.installed_version)}>
|
|
91
|
+
{entry.installed_version}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
{entry.installed && !entry.allowed && (
|
|
95
|
+
<ToolbarButtonComponent
|
|
96
|
+
icon={infoIcon}
|
|
97
|
+
iconLabel={trans.__(
|
|
98
|
+
'%1 extension is not allowed anymore. Please uninstall it immediately or contact your administrator.',
|
|
99
|
+
entry.name
|
|
100
|
+
)}
|
|
101
|
+
onClick={() =>
|
|
102
|
+
window.open(
|
|
103
|
+
'https://jupyterlab.readthedocs.io/en/latest/user/extensions.html'
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
{entry.approved && (
|
|
109
|
+
<jupyterIcon.react
|
|
110
|
+
className="jp-extensionmanager-is-approved"
|
|
111
|
+
top="1px"
|
|
112
|
+
height="auto"
|
|
113
|
+
width="1em"
|
|
114
|
+
title={trans.__(
|
|
115
|
+
'This extension is approved by your security team.'
|
|
116
|
+
)}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
<div className="jp-extensionmanager-entry-content">
|
|
121
|
+
<div className="jp-extensionmanager-entry-description">
|
|
122
|
+
{entry.description}
|
|
123
|
+
</div>
|
|
124
|
+
{props.performAction && (
|
|
125
|
+
<div className="jp-extensionmanager-entry-buttons">
|
|
126
|
+
{entry.installed ? (
|
|
127
|
+
<>
|
|
128
|
+
{supportInstallation && (
|
|
129
|
+
<>
|
|
130
|
+
{ListModel.entryHasUpdate(entry) && (
|
|
131
|
+
<Button
|
|
132
|
+
onClick={() => props.performAction!('install', entry)}
|
|
133
|
+
title={trans.__(
|
|
134
|
+
'Update "%1" to "%2"',
|
|
135
|
+
entry.name,
|
|
136
|
+
entry.latest_version
|
|
137
|
+
)}
|
|
138
|
+
minimal
|
|
139
|
+
small
|
|
140
|
+
>
|
|
141
|
+
{trans.__('Update to %1', entry.latest_version)}
|
|
142
|
+
</Button>
|
|
143
|
+
)}
|
|
144
|
+
<Button
|
|
145
|
+
onClick={() => props.performAction!('uninstall', entry)}
|
|
146
|
+
title={trans.__('Uninstall "%1"', entry.name)}
|
|
147
|
+
minimal
|
|
148
|
+
small
|
|
149
|
+
>
|
|
150
|
+
{trans.__('Uninstall')}
|
|
151
|
+
</Button>
|
|
152
|
+
</>
|
|
153
|
+
)}
|
|
154
|
+
{entry.enabled ? (
|
|
155
|
+
<Button
|
|
156
|
+
onClick={() => props.performAction!('disable', entry)}
|
|
157
|
+
title={trans.__('Disable "%1"', entry.name)}
|
|
158
|
+
minimal
|
|
159
|
+
small
|
|
160
|
+
>
|
|
161
|
+
{trans.__('Disable')}
|
|
162
|
+
</Button>
|
|
163
|
+
) : (
|
|
164
|
+
<Button
|
|
165
|
+
onClick={() => props.performAction!('enable', entry)}
|
|
166
|
+
title={trans.__('Enable "%1"', entry.name)}
|
|
167
|
+
minimal
|
|
168
|
+
small
|
|
169
|
+
>
|
|
170
|
+
{trans.__('Enable')}
|
|
171
|
+
</Button>
|
|
172
|
+
)}
|
|
173
|
+
</>
|
|
174
|
+
) : (
|
|
175
|
+
supportInstallation && (
|
|
176
|
+
<Button
|
|
177
|
+
onClick={() => props.performAction!('install', entry)}
|
|
178
|
+
title={trans.__('Install "%1"', entry.name)}
|
|
179
|
+
minimal
|
|
180
|
+
small
|
|
181
|
+
>
|
|
182
|
+
{trans.__('Install')}
|
|
183
|
+
</Button>
|
|
184
|
+
)
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</li>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The namespace for extension entry statics.
|
|
196
|
+
*/
|
|
197
|
+
namespace ListEntry {
|
|
198
|
+
export interface IProperties {
|
|
199
|
+
/**
|
|
200
|
+
* Whether thumbnails can be fetched from external webservices or not.
|
|
201
|
+
*/
|
|
202
|
+
canFetch: boolean;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The entry to visualize.
|
|
206
|
+
*/
|
|
207
|
+
entry: IEntry;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Whether the extension can be (un-)install or not.
|
|
211
|
+
*/
|
|
212
|
+
supportInstallation: boolean;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Callback to use for performing an action on the entry.
|
|
216
|
+
*
|
|
217
|
+
* Not provided if actions are not allowed.
|
|
218
|
+
*/
|
|
219
|
+
performAction?: (action: Action, entry: IEntry) => void;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The language translator.
|
|
223
|
+
*/
|
|
224
|
+
trans: TranslationBundle;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* List view widget for extensions
|
|
230
|
+
*/
|
|
231
|
+
function ListView(props: ListView.IProperties): React.ReactElement<any> {
|
|
232
|
+
const { canFetch, performAction, supportInstallation, trans } = props;
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div className="jp-extensionmanager-listview-wrapper">
|
|
236
|
+
{props.entries.length > 0 ? (
|
|
237
|
+
<ul className="jp-extensionmanager-listview">
|
|
238
|
+
{props.entries.map(entry => (
|
|
239
|
+
<ListEntry
|
|
240
|
+
key={entry.name}
|
|
241
|
+
canFetch={canFetch}
|
|
242
|
+
entry={entry}
|
|
243
|
+
performAction={performAction}
|
|
244
|
+
supportInstallation={supportInstallation}
|
|
245
|
+
trans={trans}
|
|
246
|
+
/>
|
|
247
|
+
))}
|
|
248
|
+
</ul>
|
|
249
|
+
) : (
|
|
250
|
+
<div key="message" className="jp-extensionmanager-listview-message">
|
|
251
|
+
{trans.__('No entries')}
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
{props.numPages > 1 && (
|
|
255
|
+
<div className="jp-extensionmanager-pagination">
|
|
256
|
+
<ReactPaginate
|
|
257
|
+
previousLabel={'<'}
|
|
258
|
+
nextLabel={'>'}
|
|
259
|
+
breakLabel="..."
|
|
260
|
+
breakClassName={'break-me'}
|
|
261
|
+
initialPage={(props.initialPage ?? 1) - 1}
|
|
262
|
+
pageCount={props.numPages}
|
|
263
|
+
marginPagesDisplayed={2}
|
|
264
|
+
pageRangeDisplayed={3}
|
|
265
|
+
onPageChange={(data: { selected: number }) =>
|
|
266
|
+
props.onPage(data.selected + 1)
|
|
267
|
+
}
|
|
268
|
+
containerClassName={'pagination'}
|
|
269
|
+
activeClassName={'active'}
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The namespace for list view widget statics.
|
|
279
|
+
*/
|
|
280
|
+
namespace ListView {
|
|
281
|
+
export interface IProperties {
|
|
282
|
+
/**
|
|
283
|
+
* Whether thumbnails can be fetched from external webservices or not.
|
|
284
|
+
*/
|
|
285
|
+
canFetch: boolean;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The extension entries to display.
|
|
289
|
+
*/
|
|
290
|
+
entries: ReadonlyArray<IEntry>;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Active page
|
|
294
|
+
*/
|
|
295
|
+
initialPage?: number;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The number of pages that can be viewed via pagination.
|
|
299
|
+
*/
|
|
300
|
+
numPages: number;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Whether the extension can be (un-)install or not.
|
|
304
|
+
*/
|
|
305
|
+
supportInstallation: boolean;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The language translator.
|
|
309
|
+
*/
|
|
310
|
+
trans: TranslationBundle;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The callback to use for changing the page
|
|
314
|
+
*/
|
|
315
|
+
onPage: (page: number) => void;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Callback to use for performing an action on an entry.
|
|
319
|
+
*
|
|
320
|
+
* Not provided if actions are not allowed.
|
|
321
|
+
*/
|
|
322
|
+
performAction?: (action: Action, entry: IEntry) => void;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function ErrorMessage(props: ErrorMessage.IProperties) {
|
|
327
|
+
return <div className="jp-extensionmanager-error">{props.children}</div>;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
namespace ErrorMessage {
|
|
331
|
+
export interface IProperties {
|
|
332
|
+
children: React.ReactNode;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
class Header extends ReactWidget {
|
|
337
|
+
constructor(
|
|
338
|
+
protected model: ListModel,
|
|
339
|
+
protected trans: TranslationBundle,
|
|
340
|
+
protected searchInputRef: React.RefObject<HTMLInputElement>
|
|
341
|
+
) {
|
|
342
|
+
super();
|
|
343
|
+
model.stateChanged.connect(this.update, this);
|
|
344
|
+
this.addClass('jp-extensionmanager-header');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
render(): JSX.Element {
|
|
348
|
+
return (
|
|
349
|
+
<>
|
|
350
|
+
<div className="jp-extensionmanager-title">
|
|
351
|
+
<span>{this.trans.__('%1 Manager', this.model.name)}</span>
|
|
352
|
+
{this.model.installPath && (
|
|
353
|
+
<infoIcon.react
|
|
354
|
+
className="jp-extensionmanager-path"
|
|
355
|
+
tag="span"
|
|
356
|
+
title={this.trans.__(
|
|
357
|
+
'Extension installation path: %1',
|
|
358
|
+
this.model.installPath
|
|
359
|
+
)}
|
|
360
|
+
></infoIcon.react>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
<FilterBox
|
|
364
|
+
placeholder={this.trans.__('Search')}
|
|
365
|
+
disabled={!this.model.isDisclaimed}
|
|
366
|
+
updateFilter={(fn, query) => {
|
|
367
|
+
this.model.query = query ?? '';
|
|
368
|
+
}}
|
|
369
|
+
useFuzzyFilter={false}
|
|
370
|
+
inputRef={this.searchInputRef}
|
|
371
|
+
/>
|
|
372
|
+
|
|
373
|
+
<div
|
|
374
|
+
className={`jp-extensionmanager-pending ${
|
|
375
|
+
this.model.hasPendingActions() ? 'jp-mod-hasPending' : ''
|
|
376
|
+
}`}
|
|
377
|
+
/>
|
|
378
|
+
{this.model.actionError && (
|
|
379
|
+
<ErrorMessage>
|
|
380
|
+
<p>{this.trans.__('Error when performing an action.')}</p>
|
|
381
|
+
<p>{this.trans.__('Reason given:')}</p>
|
|
382
|
+
<pre>{this.model.actionError}</pre>
|
|
383
|
+
</ErrorMessage>
|
|
384
|
+
)}
|
|
385
|
+
</>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
class Warning extends ReactWidget {
|
|
391
|
+
constructor(protected model: ListModel, protected trans: TranslationBundle) {
|
|
392
|
+
super();
|
|
393
|
+
this.addClass('jp-extensionmanager-disclaimer');
|
|
394
|
+
model.stateChanged.connect(this.update, this);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
render(): JSX.Element {
|
|
398
|
+
return (
|
|
399
|
+
<>
|
|
400
|
+
<p>
|
|
401
|
+
{this.trans
|
|
402
|
+
.__(`The JupyterLab development team is excited to have a robust
|
|
403
|
+
third-party extension community. However, we do not review
|
|
404
|
+
third-party extensions, and some extensions may introduce security
|
|
405
|
+
risks or contain malicious code that runs on your machine. Moreover in order
|
|
406
|
+
to work, this panel needs to fetch data from web services. Do you agree to
|
|
407
|
+
activate this feature?`)}
|
|
408
|
+
<br />
|
|
409
|
+
<a
|
|
410
|
+
href="https://jupyterlab.readthedocs.io/en/latest/privacy_policies.html"
|
|
411
|
+
target="_blank"
|
|
412
|
+
rel="noreferrer"
|
|
413
|
+
>
|
|
414
|
+
{this.trans.__('Please read the privacy policy.')}
|
|
415
|
+
</a>
|
|
416
|
+
</p>
|
|
417
|
+
{this.model.isDisclaimed ? (
|
|
418
|
+
<Button
|
|
419
|
+
className="jp-extensionmanager-disclaimer-disable"
|
|
420
|
+
onClick={(e: React.MouseEvent<Element, MouseEvent>) => {
|
|
421
|
+
this.model.isDisclaimed = false;
|
|
422
|
+
}}
|
|
423
|
+
title={this.trans.__('This will withdraw your consent.')}
|
|
424
|
+
>
|
|
425
|
+
{this.trans.__('No')}
|
|
426
|
+
</Button>
|
|
427
|
+
) : (
|
|
428
|
+
<div>
|
|
429
|
+
<Button
|
|
430
|
+
className="jp-extensionmanager-disclaimer-enable"
|
|
431
|
+
onClick={() => {
|
|
432
|
+
this.model.isDisclaimed = true;
|
|
433
|
+
}}
|
|
434
|
+
>
|
|
435
|
+
{this.trans.__('Yes')}
|
|
436
|
+
</Button>
|
|
437
|
+
<Button
|
|
438
|
+
className="jp-extensionmanager-disclaimer-disable"
|
|
439
|
+
onClick={() => {
|
|
440
|
+
this.model.isEnabled = false;
|
|
441
|
+
}}
|
|
442
|
+
title={this.trans.__(
|
|
443
|
+
'This will disable the extension manager panel; including the listing of installed extension.'
|
|
444
|
+
)}
|
|
445
|
+
>
|
|
446
|
+
{this.trans.__('No, disable')}
|
|
447
|
+
</Button>
|
|
448
|
+
</div>
|
|
449
|
+
)}
|
|
450
|
+
</>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
class InstalledList extends ReactWidget {
|
|
456
|
+
constructor(protected model: ListModel, protected trans: TranslationBundle) {
|
|
457
|
+
super();
|
|
458
|
+
model.stateChanged.connect(this.update, this);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
render(): JSX.Element {
|
|
462
|
+
return (
|
|
463
|
+
<>
|
|
464
|
+
{this.model.installedError !== null ? (
|
|
465
|
+
<ErrorMessage>
|
|
466
|
+
{`Error querying installed extensions${
|
|
467
|
+
this.model.installedError ? `: ${this.model.installedError}` : '.'
|
|
468
|
+
}`}
|
|
469
|
+
</ErrorMessage>
|
|
470
|
+
) : this.model.isLoadingInstalledExtensions ? (
|
|
471
|
+
<div className="jp-extensionmanager-loader">
|
|
472
|
+
{this.trans.__('Updating extensions list…')}
|
|
473
|
+
</div>
|
|
474
|
+
) : (
|
|
475
|
+
<ListView
|
|
476
|
+
canFetch={this.model.isDisclaimed}
|
|
477
|
+
entries={this.model.installed.filter(pkg =>
|
|
478
|
+
new RegExp(this.model.query.toLowerCase()).test(pkg.name)
|
|
479
|
+
)}
|
|
480
|
+
numPages={1}
|
|
481
|
+
trans={this.trans}
|
|
482
|
+
onPage={value => {
|
|
483
|
+
/* no-op */
|
|
484
|
+
}}
|
|
485
|
+
performAction={
|
|
486
|
+
this.model.isDisclaimed ? this.onAction.bind(this) : null
|
|
487
|
+
}
|
|
488
|
+
supportInstallation={
|
|
489
|
+
this.model.canInstall && this.model.isDisclaimed
|
|
490
|
+
}
|
|
491
|
+
/>
|
|
492
|
+
)}
|
|
493
|
+
</>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Callback handler for when the user wants to perform an action on an extension.
|
|
499
|
+
*
|
|
500
|
+
* @param action The action to perform.
|
|
501
|
+
* @param entry The entry to perform the action on.
|
|
502
|
+
*/
|
|
503
|
+
onAction(action: Action, entry: IEntry): Promise<void> {
|
|
504
|
+
switch (action) {
|
|
505
|
+
case 'install':
|
|
506
|
+
return this.model.install(entry);
|
|
507
|
+
case 'uninstall':
|
|
508
|
+
return this.model.uninstall(entry);
|
|
509
|
+
case 'enable':
|
|
510
|
+
return this.model.enable(entry);
|
|
511
|
+
case 'disable':
|
|
512
|
+
return this.model.disable(entry);
|
|
513
|
+
default:
|
|
514
|
+
throw new Error(`Invalid action: ${action}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
class SearchResult extends ReactWidget {
|
|
520
|
+
constructor(protected model: ListModel, protected trans: TranslationBundle) {
|
|
521
|
+
super();
|
|
522
|
+
model.stateChanged.connect(this.update, this);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Callback handler for the user changes the page of the search result pagination.
|
|
527
|
+
*
|
|
528
|
+
* @param value The pagination page number.
|
|
529
|
+
*/
|
|
530
|
+
onPage(value: number): void {
|
|
531
|
+
this.model.page = value;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Callback handler for when the user wants to perform an action on an extension.
|
|
536
|
+
*
|
|
537
|
+
* @param action The action to perform.
|
|
538
|
+
* @param entry The entry to perform the action on.
|
|
539
|
+
*/
|
|
540
|
+
onAction(action: Action, entry: IEntry): Promise<void> {
|
|
541
|
+
switch (action) {
|
|
542
|
+
case 'install':
|
|
543
|
+
return this.model.install(entry);
|
|
544
|
+
case 'uninstall':
|
|
545
|
+
return this.model.uninstall(entry);
|
|
546
|
+
case 'enable':
|
|
547
|
+
return this.model.enable(entry);
|
|
548
|
+
case 'disable':
|
|
549
|
+
return this.model.disable(entry);
|
|
550
|
+
default:
|
|
551
|
+
throw new Error(`Invalid action: ${action}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
render(): JSX.Element {
|
|
556
|
+
return (
|
|
557
|
+
<>
|
|
558
|
+
{this.model.searchError !== null ? (
|
|
559
|
+
<ErrorMessage>
|
|
560
|
+
{`Error searching for extensions${
|
|
561
|
+
this.model.searchError ? `: ${this.model.searchError}` : '.'
|
|
562
|
+
}`}
|
|
563
|
+
</ErrorMessage>
|
|
564
|
+
) : this.model.isSearching ? (
|
|
565
|
+
<div className="jp-extensionmanager-loader">
|
|
566
|
+
{this.trans.__('Updating extensions list…')}
|
|
567
|
+
</div>
|
|
568
|
+
) : (
|
|
569
|
+
<ListView
|
|
570
|
+
canFetch={this.model.isDisclaimed}
|
|
571
|
+
entries={this.model.searchResult}
|
|
572
|
+
initialPage={this.model.page}
|
|
573
|
+
numPages={this.model.lastPage}
|
|
574
|
+
onPage={value => {
|
|
575
|
+
this.onPage(value);
|
|
576
|
+
}}
|
|
577
|
+
performAction={
|
|
578
|
+
this.model.isDisclaimed ? this.onAction.bind(this) : null
|
|
579
|
+
}
|
|
580
|
+
supportInstallation={
|
|
581
|
+
this.model.canInstall && this.model.isDisclaimed
|
|
582
|
+
}
|
|
583
|
+
trans={this.trans}
|
|
584
|
+
/>
|
|
585
|
+
)}
|
|
586
|
+
</>
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
update(): void {
|
|
591
|
+
this.title.label = this.model.query
|
|
592
|
+
? this.trans.__('Search Results')
|
|
593
|
+
: this.trans.__('Discover');
|
|
594
|
+
super.update();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export namespace ExtensionsPanel {
|
|
599
|
+
export interface IOptions {
|
|
600
|
+
model: ListModel;
|
|
601
|
+
translator: ITranslator;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export class ExtensionsPanel extends SidePanel {
|
|
606
|
+
constructor(options: ExtensionsPanel.IOptions) {
|
|
607
|
+
const { model, translator } = options;
|
|
608
|
+
super({ translator });
|
|
609
|
+
this.model = model;
|
|
610
|
+
this._searchInputRef = React.createRef<HTMLInputElement>();
|
|
611
|
+
this.addClass('jp-extensionmanager-view');
|
|
612
|
+
|
|
613
|
+
this.trans = translator.load('jupyterlab');
|
|
614
|
+
|
|
615
|
+
this.header.addWidget(new Header(model, this.trans, this._searchInputRef));
|
|
616
|
+
|
|
617
|
+
const warning = new Warning(model, this.trans);
|
|
618
|
+
warning.title.label = this.trans.__('Warning');
|
|
619
|
+
|
|
620
|
+
this.addWidget(warning);
|
|
621
|
+
|
|
622
|
+
const installed = new PanelWithToolbar();
|
|
623
|
+
installed.addClass('jp-extensionmanager-installedlist');
|
|
624
|
+
installed.title.label = this.trans.__('Installed');
|
|
625
|
+
|
|
626
|
+
installed.toolbar.addItem(
|
|
627
|
+
'refresh',
|
|
628
|
+
new ToolbarButton({
|
|
629
|
+
icon: refreshIcon,
|
|
630
|
+
onClick: () => {
|
|
631
|
+
model.refreshInstalled(true).catch(reason => {
|
|
632
|
+
console.error(
|
|
633
|
+
`Failed to refresh the installed extensions list:\n${reason}`
|
|
634
|
+
);
|
|
635
|
+
});
|
|
636
|
+
},
|
|
637
|
+
tooltip: this.trans.__('Refresh extensions list')
|
|
638
|
+
})
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
installed.addWidget(new InstalledList(model, this.trans));
|
|
642
|
+
|
|
643
|
+
this.addWidget(installed);
|
|
644
|
+
|
|
645
|
+
if (this.model.canInstall) {
|
|
646
|
+
const searchResults = new SearchResult(model, this.trans);
|
|
647
|
+
searchResults.addClass('jp-extensionmanager-searchresults');
|
|
648
|
+
this.addWidget(searchResults);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
this._wasDisclaimed = this.model.isDisclaimed;
|
|
652
|
+
if (this.model.isDisclaimed) {
|
|
653
|
+
(this.content as AccordionPanel).collapse(0);
|
|
654
|
+
(this.content.layout as AccordionLayout).setRelativeSizes([0, 1, 1]);
|
|
655
|
+
} else {
|
|
656
|
+
// If warning is not disclaimed expand only the warning panel
|
|
657
|
+
(this.content as AccordionPanel).expand(0);
|
|
658
|
+
(this.content as AccordionPanel).collapse(1);
|
|
659
|
+
(this.content as AccordionPanel).collapse(2);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
this.model.stateChanged.connect(this._onStateChanged, this);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Dispose of the widget and its descendant widgets.
|
|
667
|
+
*/
|
|
668
|
+
dispose(): void {
|
|
669
|
+
if (this.isDisposed) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
this.model.stateChanged.disconnect(this._onStateChanged, this);
|
|
673
|
+
super.dispose();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Handle the DOM events for the extension manager search bar.
|
|
678
|
+
*
|
|
679
|
+
* @param event - The DOM event sent to the extension manager search bar.
|
|
680
|
+
*
|
|
681
|
+
* #### Notes
|
|
682
|
+
* This method implements the DOM `EventListener` interface and is
|
|
683
|
+
* called in response to events on the search bar's DOM node.
|
|
684
|
+
* It should not be called directly by user code.
|
|
685
|
+
*/
|
|
686
|
+
handleEvent(event: Event): void {
|
|
687
|
+
switch (event.type) {
|
|
688
|
+
case 'focus':
|
|
689
|
+
case 'blur':
|
|
690
|
+
this._toggleFocused();
|
|
691
|
+
break;
|
|
692
|
+
default:
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* A message handler invoked on a `'before-attach'` message.
|
|
699
|
+
*/
|
|
700
|
+
protected onBeforeAttach(msg: Message): void {
|
|
701
|
+
this.node.addEventListener('focus', this, true);
|
|
702
|
+
this.node.addEventListener('blur', this, true);
|
|
703
|
+
super.onBeforeAttach(msg);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
protected onBeforeShow(msg: Message): void {
|
|
707
|
+
if (!this._wasInitialized) {
|
|
708
|
+
this._wasInitialized = true;
|
|
709
|
+
this.model.refreshInstalled().catch(reason => {
|
|
710
|
+
console.log(`Failed to refresh installed extension list:\n${reason}`);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* A message handler invoked on an `'after-detach'` message.
|
|
717
|
+
*/
|
|
718
|
+
protected onAfterDetach(msg: Message): void {
|
|
719
|
+
super.onAfterDetach(msg);
|
|
720
|
+
this.node.removeEventListener('focus', this, true);
|
|
721
|
+
this.node.removeEventListener('blur', this, true);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* A message handler invoked on an `'activate-request'` message.
|
|
726
|
+
*/
|
|
727
|
+
protected onActivateRequest(msg: Message): void {
|
|
728
|
+
if (this.isAttached) {
|
|
729
|
+
const input = this._searchInputRef.current;
|
|
730
|
+
if (input) {
|
|
731
|
+
input.focus();
|
|
732
|
+
input.select();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
super.onActivateRequest(msg);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
private _onStateChanged(): void {
|
|
739
|
+
if (!this._wasDisclaimed && this.model.isDisclaimed) {
|
|
740
|
+
(this.content as AccordionPanel).collapse(0);
|
|
741
|
+
(this.content as AccordionPanel).expand(1);
|
|
742
|
+
(this.content as AccordionPanel).expand(2);
|
|
743
|
+
}
|
|
744
|
+
this._wasDisclaimed = this.model.isDisclaimed;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Toggle the focused modifier based on the input node focus state.
|
|
749
|
+
*/
|
|
750
|
+
private _toggleFocused(): void {
|
|
751
|
+
const focused = document.activeElement === this._searchInputRef.current;
|
|
752
|
+
this.toggleClass('lm-mod-focused', focused);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
protected model: ListModel;
|
|
756
|
+
protected trans: TranslationBundle;
|
|
757
|
+
private _searchInputRef: React.RefObject<HTMLInputElement>;
|
|
758
|
+
private _wasInitialized = false;
|
|
759
|
+
private _wasDisclaimed = true;
|
|
760
|
+
}
|