@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/lib/model.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
// Copyright (c) Jupyter Development Team.
|
|
2
2
|
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
/* global RequestInit */
|
|
4
|
+
import { Dialog, showDialog } from '@jupyterlab/apputils';
|
|
5
|
+
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
3
6
|
import { ServerConnection } from '@jupyterlab/services';
|
|
4
7
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
5
8
|
import { VDomModel } from '@jupyterlab/ui-components';
|
|
6
9
|
import { Debouncer } from '@lumino/polling';
|
|
7
10
|
import * as semver from 'semver';
|
|
8
|
-
import { doBuild } from './build-helper';
|
|
9
|
-
import { presentCompanions } from './companions';
|
|
10
11
|
import { reportInstallError } from './dialog';
|
|
11
|
-
import { Lister } from './listings';
|
|
12
|
-
import { isJupyterOrg, Searcher } from './npm';
|
|
13
12
|
/**
|
|
14
13
|
* The server API path for querying/modifying installed extensions.
|
|
15
14
|
*/
|
|
@@ -18,8 +17,9 @@ const EXTENSION_API_PATH = 'lab/api/extensions';
|
|
|
18
17
|
* Model for an extension list.
|
|
19
18
|
*/
|
|
20
19
|
export class ListModel extends VDomModel {
|
|
21
|
-
constructor(
|
|
20
|
+
constructor(serviceManager, translator) {
|
|
22
21
|
super();
|
|
22
|
+
this.actionError = null;
|
|
23
23
|
/**
|
|
24
24
|
* Contains an error message if an error occurred when querying installed extensions.
|
|
25
25
|
*/
|
|
@@ -29,78 +29,29 @@ export class ListModel extends VDomModel {
|
|
|
29
29
|
*/
|
|
30
30
|
this.searchError = null;
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* Whether a reload should be considered due to actions taken.
|
|
33
33
|
*/
|
|
34
|
-
this.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.
|
|
43
|
-
/**
|
|
44
|
-
* Whether the model has finished async initialization.
|
|
45
|
-
*/
|
|
46
|
-
this.initialized = false;
|
|
47
|
-
/**
|
|
48
|
-
* Whether a fresh build should be considered due to actions taken.
|
|
49
|
-
*/
|
|
50
|
-
this.promptBuild = false;
|
|
51
|
-
this.lister = new Lister();
|
|
52
|
-
this._query = ''; // TODO: we may not need the null case?
|
|
53
|
-
this._page = 0;
|
|
54
|
-
this._pagination = 250;
|
|
55
|
-
this._totalEntries = 0;
|
|
34
|
+
this.promptReload = false;
|
|
35
|
+
this._isDisclaimed = false;
|
|
36
|
+
this._isEnabled = false;
|
|
37
|
+
this._isLoadingInstalledExtensions = false;
|
|
38
|
+
this._isSearching = false;
|
|
39
|
+
this._query = '';
|
|
40
|
+
this._page = 1;
|
|
41
|
+
this._pagination = 30;
|
|
42
|
+
this._lastPage = 1;
|
|
56
43
|
this._pendingActions = [];
|
|
57
|
-
|
|
58
|
-
|
|
44
|
+
const metadata = JSON.parse(
|
|
45
|
+
// The page config option may not be defined; e.g. in the federated example
|
|
46
|
+
PageConfig.getOption('extensionManager') || '{}');
|
|
47
|
+
this.name = metadata.name;
|
|
48
|
+
this.canInstall = metadata.can_install;
|
|
49
|
+
this.installPath = metadata.install_path;
|
|
59
50
|
this.translator = translator || nullTranslator;
|
|
60
|
-
this._app = app;
|
|
61
51
|
this._installed = [];
|
|
62
|
-
this.
|
|
52
|
+
this._lastSearchResult = [];
|
|
63
53
|
this.serviceManager = serviceManager;
|
|
64
|
-
this.
|
|
65
|
-
this._debouncedUpdate = new Debouncer(this.update.bind(this), 1000);
|
|
66
|
-
this.lister.listingsLoaded.connect(this._listingIsLoaded, this);
|
|
67
|
-
this.searcher = new Searcher(settings.composite['npmRegistry'], settings.composite['npmCdn'], settings.composite['enableCdn']);
|
|
68
|
-
_isDisclaimed = settings.composite['disclaimed'] === true;
|
|
69
|
-
settings.changed.connect(() => {
|
|
70
|
-
_isDisclaimed = settings.composite['disclaimed'] === true;
|
|
71
|
-
this.searcher = new Searcher(settings.composite['npmRegistry'], settings.composite['npmCdn'], settings.composite['enableCdn']);
|
|
72
|
-
void this.update();
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
_listingIsLoaded(_, listings) {
|
|
76
|
-
this._listMode = listings.mode;
|
|
77
|
-
this._blockedExtensionsArray = new Array();
|
|
78
|
-
if (this._listMode === 'block') {
|
|
79
|
-
listings.entries.map(e => {
|
|
80
|
-
this._blockedExtensionsArray.push({
|
|
81
|
-
name: e.name,
|
|
82
|
-
regexp: new RegExp(e.name),
|
|
83
|
-
type: e.type,
|
|
84
|
-
reason: e.reason,
|
|
85
|
-
creation_date: e.creation_date,
|
|
86
|
-
last_update_date: e.last_update_date
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
this._allowedExtensionsArray = new Array();
|
|
91
|
-
if (this._listMode === 'allow') {
|
|
92
|
-
listings.entries.map(e => {
|
|
93
|
-
this._allowedExtensionsArray.push({
|
|
94
|
-
name: e.name,
|
|
95
|
-
regexp: new RegExp(e.name),
|
|
96
|
-
type: e.type,
|
|
97
|
-
reason: e.reason,
|
|
98
|
-
creation_date: e.creation_date,
|
|
99
|
-
last_update_date: e.last_update_date
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
void this.initialize();
|
|
54
|
+
this._debouncedSearch = new Debouncer(this.search.bind(this), 1000);
|
|
104
55
|
}
|
|
105
56
|
/**
|
|
106
57
|
* A readonly array of the installed extensions.
|
|
@@ -108,14 +59,45 @@ export class ListModel extends VDomModel {
|
|
|
108
59
|
get installed() {
|
|
109
60
|
return this._installed;
|
|
110
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether the warning is disclaimed or not.
|
|
64
|
+
*/
|
|
65
|
+
get isDisclaimed() {
|
|
66
|
+
return this._isDisclaimed;
|
|
67
|
+
}
|
|
68
|
+
set isDisclaimed(v) {
|
|
69
|
+
if (v !== this._isDisclaimed) {
|
|
70
|
+
this._isDisclaimed = v;
|
|
71
|
+
this.stateChanged.emit();
|
|
72
|
+
void this._debouncedSearch.invoke();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether the extension manager is enabled or not.
|
|
77
|
+
*/
|
|
78
|
+
get isEnabled() {
|
|
79
|
+
return this._isEnabled;
|
|
80
|
+
}
|
|
81
|
+
set isEnabled(v) {
|
|
82
|
+
if (v !== this._isEnabled) {
|
|
83
|
+
this._isEnabled = v;
|
|
84
|
+
this.stateChanged.emit();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
get isLoadingInstalledExtensions() {
|
|
88
|
+
return this._isLoadingInstalledExtensions;
|
|
89
|
+
}
|
|
90
|
+
get isSearching() {
|
|
91
|
+
return this._isSearching;
|
|
92
|
+
}
|
|
111
93
|
/**
|
|
112
94
|
* A readonly array containing the latest search result
|
|
113
95
|
*/
|
|
114
96
|
get searchResult() {
|
|
115
|
-
return this.
|
|
97
|
+
return this._lastSearchResult;
|
|
116
98
|
}
|
|
117
99
|
/**
|
|
118
|
-
* The
|
|
100
|
+
* The search query.
|
|
119
101
|
*
|
|
120
102
|
* Setting its value triggers a new search.
|
|
121
103
|
*/
|
|
@@ -123,29 +105,31 @@ export class ListModel extends VDomModel {
|
|
|
123
105
|
return this._query;
|
|
124
106
|
}
|
|
125
107
|
set query(value) {
|
|
126
|
-
this._query
|
|
127
|
-
|
|
108
|
+
if (this._query !== value) {
|
|
109
|
+
this._query = value;
|
|
110
|
+
this._page = 1;
|
|
111
|
+
void this._debouncedSearch.invoke();
|
|
112
|
+
}
|
|
128
113
|
}
|
|
129
114
|
/**
|
|
130
|
-
* The current
|
|
131
|
-
*
|
|
132
|
-
* The npm repository search is paginated by the `pagination` attribute.
|
|
133
|
-
* The `page` value selects which page is used.
|
|
115
|
+
* The current search page.
|
|
134
116
|
*
|
|
135
117
|
* Setting its value triggers a new search.
|
|
118
|
+
*
|
|
119
|
+
* ### Note
|
|
120
|
+
* First page is 1.
|
|
136
121
|
*/
|
|
137
122
|
get page() {
|
|
138
123
|
return this._page;
|
|
139
124
|
}
|
|
140
125
|
set page(value) {
|
|
141
|
-
this._page
|
|
142
|
-
|
|
126
|
+
if (this._page !== value) {
|
|
127
|
+
this._page = value;
|
|
128
|
+
void this._debouncedSearch.invoke();
|
|
129
|
+
}
|
|
143
130
|
}
|
|
144
131
|
/**
|
|
145
|
-
* The
|
|
146
|
-
*
|
|
147
|
-
* The npm repository search is paginated by the `pagination` attribute.
|
|
148
|
-
* The `page` value selects which page is used.
|
|
132
|
+
* The search pagination.
|
|
149
133
|
*
|
|
150
134
|
* Setting its value triggers a new search.
|
|
151
135
|
*/
|
|
@@ -153,46 +137,26 @@ export class ListModel extends VDomModel {
|
|
|
153
137
|
return this._pagination;
|
|
154
138
|
}
|
|
155
139
|
set pagination(value) {
|
|
156
|
-
this._pagination
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
* The total number of results in the current search.
|
|
161
|
-
*/
|
|
162
|
-
get totalEntries() {
|
|
163
|
-
return this._totalEntries;
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* The list mode.
|
|
167
|
-
*/
|
|
168
|
-
get listMode() {
|
|
169
|
-
return this._listMode;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* The total number of blockedExtensions results in the current search.
|
|
173
|
-
*/
|
|
174
|
-
get totalblockedExtensionsFound() {
|
|
175
|
-
return this._totalblockedExtensionsFound;
|
|
140
|
+
if (this._pagination !== value) {
|
|
141
|
+
this._pagination = value;
|
|
142
|
+
void this._debouncedSearch.invoke();
|
|
143
|
+
}
|
|
176
144
|
}
|
|
177
145
|
/**
|
|
178
|
-
* The
|
|
146
|
+
* The last page of results in the current search.
|
|
179
147
|
*/
|
|
180
|
-
get
|
|
181
|
-
return this.
|
|
148
|
+
get lastPage() {
|
|
149
|
+
return this._lastPage;
|
|
182
150
|
}
|
|
183
151
|
/**
|
|
184
|
-
*
|
|
152
|
+
* Dispose the extensions list model.
|
|
185
153
|
*/
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
.catch(() => {
|
|
193
|
-
this.initialized = true;
|
|
194
|
-
this.stateChanged.emit(undefined);
|
|
195
|
-
});
|
|
154
|
+
dispose() {
|
|
155
|
+
if (this.isDisposed) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this._debouncedSearch.dispose();
|
|
159
|
+
super.dispose();
|
|
196
160
|
}
|
|
197
161
|
/**
|
|
198
162
|
* Whether there are currently any actions pending.
|
|
@@ -206,24 +170,11 @@ export class ListModel extends VDomModel {
|
|
|
206
170
|
* @param entry An entry indicating which extension to install.
|
|
207
171
|
*/
|
|
208
172
|
async install(entry) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (data.status !== 'ok') {
|
|
213
|
-
reportInstallError(entry.name, data.message, this.translator);
|
|
214
|
-
}
|
|
215
|
-
return this.update();
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
await this.checkCompanionPackages(entry).then(shouldInstall => {
|
|
219
|
-
if (shouldInstall) {
|
|
220
|
-
return this._performAction('install', entry).then(data => {
|
|
221
|
-
if (data.status !== 'ok') {
|
|
222
|
-
reportInstallError(entry.name, data.message, this.translator);
|
|
223
|
-
}
|
|
224
|
-
return this.update();
|
|
225
|
-
});
|
|
173
|
+
await this.performAction('install', entry).then(data => {
|
|
174
|
+
if (data.status !== 'ok') {
|
|
175
|
+
reportInstallError(entry.name, data.message, this.translator);
|
|
226
176
|
}
|
|
177
|
+
return this.update(true);
|
|
227
178
|
});
|
|
228
179
|
}
|
|
229
180
|
/**
|
|
@@ -235,8 +186,8 @@ export class ListModel extends VDomModel {
|
|
|
235
186
|
if (!entry.installed) {
|
|
236
187
|
throw new Error(`Not installed, cannot uninstall: ${entry.name}`);
|
|
237
188
|
}
|
|
238
|
-
await this.
|
|
239
|
-
return this.update();
|
|
189
|
+
await this.performAction('uninstall', entry);
|
|
190
|
+
return this.update(true);
|
|
240
191
|
}
|
|
241
192
|
/**
|
|
242
193
|
* Enable an extension.
|
|
@@ -247,8 +198,8 @@ export class ListModel extends VDomModel {
|
|
|
247
198
|
if (entry.enabled) {
|
|
248
199
|
throw new Error(`Already enabled: ${entry.name}`);
|
|
249
200
|
}
|
|
250
|
-
await this.
|
|
251
|
-
await this.
|
|
201
|
+
await this.performAction('enable', entry);
|
|
202
|
+
await this.refreshInstalled(true);
|
|
252
203
|
}
|
|
253
204
|
/**
|
|
254
205
|
* Disable an extension.
|
|
@@ -259,275 +210,87 @@ export class ListModel extends VDomModel {
|
|
|
259
210
|
if (!entry.enabled) {
|
|
260
211
|
throw new Error(`Already disabled: ${entry.name}`);
|
|
261
212
|
}
|
|
262
|
-
await this.
|
|
263
|
-
await this.
|
|
213
|
+
await this.performAction('disable', entry);
|
|
214
|
+
await this.refreshInstalled(true);
|
|
264
215
|
}
|
|
265
216
|
/**
|
|
266
|
-
*
|
|
217
|
+
* Refresh installed packages
|
|
267
218
|
*
|
|
268
|
-
* @param
|
|
219
|
+
* @param force Force refreshing the list of installed packages
|
|
269
220
|
*/
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const discovery = data.jupyterlab.discovery;
|
|
278
|
-
const kernelCompanions = [];
|
|
279
|
-
if (discovery.kernel) {
|
|
280
|
-
// match specs
|
|
281
|
-
for (const kernelInfo of discovery.kernel) {
|
|
282
|
-
const matches = Private.matchSpecs(kernelInfo, this.serviceManager.kernelspecs.specs);
|
|
283
|
-
kernelCompanions.push({ kernelInfo, kernels: matches });
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (kernelCompanions.length < 1 && !discovery.server) {
|
|
287
|
-
return true;
|
|
288
|
-
}
|
|
289
|
-
return presentCompanions(kernelCompanions, discovery.server, this.translator);
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Trigger a build check to incorporate actions taken.
|
|
294
|
-
*/
|
|
295
|
-
triggerBuildCheck() {
|
|
296
|
-
const builder = this.serviceManager.builder;
|
|
297
|
-
if (builder.isAvailable && !this.promptBuild) {
|
|
298
|
-
const completed = builder.getStatus().then(response => {
|
|
299
|
-
if (response.status === 'building') {
|
|
300
|
-
// Piggy-back onto existing build
|
|
301
|
-
// TODO: Can this cause dialog collision on build completion?
|
|
302
|
-
return doBuild(this._app, builder);
|
|
303
|
-
}
|
|
304
|
-
if (response.status !== 'needed') {
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
if (!this.promptBuild) {
|
|
308
|
-
this.promptBuild = true;
|
|
309
|
-
this.stateChanged.emit(undefined);
|
|
310
|
-
}
|
|
221
|
+
async refreshInstalled(force = false) {
|
|
222
|
+
this.installedError = null;
|
|
223
|
+
this._isLoadingInstalledExtensions = true;
|
|
224
|
+
this.stateChanged.emit();
|
|
225
|
+
try {
|
|
226
|
+
const [extensions] = await Private.requestAPI({
|
|
227
|
+
refresh: force ? 1 : 0
|
|
311
228
|
});
|
|
312
|
-
this.
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Perform a build on the server
|
|
317
|
-
*/
|
|
318
|
-
performBuild() {
|
|
319
|
-
if (this.promptBuild) {
|
|
320
|
-
this.promptBuild = false;
|
|
321
|
-
this.stateChanged.emit(undefined);
|
|
322
|
-
}
|
|
323
|
-
const completed = doBuild(this._app, this.serviceManager.builder);
|
|
324
|
-
this._addPendingAction(completed);
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Ignore a build recommendation
|
|
328
|
-
*/
|
|
329
|
-
ignoreBuildRecommendation() {
|
|
330
|
-
if (this.promptBuild) {
|
|
331
|
-
this.promptBuild = false;
|
|
332
|
-
this.stateChanged.emit(undefined);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Ignore a build recommendation
|
|
337
|
-
*/
|
|
338
|
-
refreshInstalled() {
|
|
339
|
-
const refresh = this.update(true);
|
|
340
|
-
this._addPendingAction(refresh);
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Translate search results from an npm repository query into entries
|
|
344
|
-
* and remove entries with 'deprecated' in the keyword list
|
|
345
|
-
*
|
|
346
|
-
* @param res Promise to an npm query result.
|
|
347
|
-
*/
|
|
348
|
-
async translateSearchResult(res) {
|
|
349
|
-
const entries = {};
|
|
350
|
-
this._totalblockedExtensionsFound = 0;
|
|
351
|
-
this._totalallowedExtensionsFound = 0;
|
|
352
|
-
this._totalEntries = 0;
|
|
353
|
-
for (const obj of (await res).objects) {
|
|
354
|
-
const pkg = obj.package;
|
|
355
|
-
if (pkg.keywords.indexOf('deprecated') >= 0) {
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
this._totalEntries = this._totalEntries + 1;
|
|
359
|
-
const isblockedExtensions = this.isListed(pkg.name, this._blockedExtensionsArray);
|
|
360
|
-
if (isblockedExtensions) {
|
|
361
|
-
this._totalblockedExtensionsFound =
|
|
362
|
-
this._totalblockedExtensionsFound + 1;
|
|
363
|
-
}
|
|
364
|
-
const isallowedExtensions = this.isListed(pkg.name, this._allowedExtensionsArray);
|
|
365
|
-
if (isallowedExtensions) {
|
|
366
|
-
this._totalallowedExtensionsFound =
|
|
367
|
-
this._totalallowedExtensionsFound + 1;
|
|
368
|
-
}
|
|
369
|
-
entries[pkg.name] = {
|
|
370
|
-
name: pkg.name,
|
|
371
|
-
description: pkg.description,
|
|
372
|
-
url: 'homepage' in pkg.links
|
|
373
|
-
? pkg.links.homepage
|
|
374
|
-
: 'repository' in pkg.links
|
|
375
|
-
? pkg.links.repository
|
|
376
|
-
: pkg.links.npm,
|
|
377
|
-
installed: false,
|
|
378
|
-
enabled: false,
|
|
379
|
-
status: null,
|
|
380
|
-
latest_version: pkg.version,
|
|
381
|
-
installed_version: '',
|
|
382
|
-
blockedExtensionsEntry: isblockedExtensions,
|
|
383
|
-
allowedExtensionsEntry: isallowedExtensions,
|
|
384
|
-
pkg_type: 'source',
|
|
385
|
-
install: undefined
|
|
386
|
-
};
|
|
229
|
+
this._installed = extensions.sort(Private.comparator);
|
|
387
230
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Translate installed extensions information from the server into entries.
|
|
392
|
-
*
|
|
393
|
-
* @param res Promise to the server reply data.
|
|
394
|
-
*/
|
|
395
|
-
async translateInstalled(res) {
|
|
396
|
-
const promises = [];
|
|
397
|
-
const entries = {};
|
|
398
|
-
for (const pkg of await res) {
|
|
399
|
-
promises.push(res.then(info => {
|
|
400
|
-
var _a, _b, _c;
|
|
401
|
-
entries[pkg.name] = {
|
|
402
|
-
name: pkg.name,
|
|
403
|
-
description: pkg.description,
|
|
404
|
-
url: pkg.url,
|
|
405
|
-
installed: pkg.installed !== false,
|
|
406
|
-
enabled: pkg.enabled,
|
|
407
|
-
status: pkg.status,
|
|
408
|
-
latest_version: pkg.latest_version,
|
|
409
|
-
installed_version: pkg.installed_version,
|
|
410
|
-
blockedExtensionsEntry: this.isListed(pkg.name, this._blockedExtensionsArray),
|
|
411
|
-
allowedExtensionsEntry: this.isListed(pkg.name, this._allowedExtensionsArray),
|
|
412
|
-
pkg_type: pkg.pkg_type,
|
|
413
|
-
install: {
|
|
414
|
-
packageManager: (_a = pkg.install) === null || _a === void 0 ? void 0 : _a.packageManager,
|
|
415
|
-
packageName: (_b = pkg.install) === null || _b === void 0 ? void 0 : _b.packageName,
|
|
416
|
-
uninstallInstructions: (_c = pkg.install) === null || _c === void 0 ? void 0 : _c.uninstallInstructions
|
|
417
|
-
}
|
|
418
|
-
};
|
|
419
|
-
}));
|
|
231
|
+
catch (reason) {
|
|
232
|
+
this.installedError = reason.toString();
|
|
420
233
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
isListed(name, listArray) {
|
|
426
|
-
let entry = undefined;
|
|
427
|
-
listArray.forEach((listEntry) => {
|
|
428
|
-
var _a;
|
|
429
|
-
if (listEntry.regexp && ((_a = listEntry.regexp) === null || _a === void 0 ? void 0 : _a.test(name))) {
|
|
430
|
-
entry = listEntry;
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
return entry;
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Make a request to the server for info about its installed extensions.
|
|
437
|
-
*/
|
|
438
|
-
fetchInstalled(refreshInstalled = false) {
|
|
439
|
-
const url = new URL(EXTENSION_API_PATH, this.serverConnectionSettings.baseUrl);
|
|
440
|
-
if (refreshInstalled) {
|
|
441
|
-
url.searchParams.append('refresh', '1');
|
|
234
|
+
finally {
|
|
235
|
+
this._isLoadingInstalledExtensions = false;
|
|
236
|
+
this.stateChanged.emit();
|
|
442
237
|
}
|
|
443
|
-
const request = ServerConnection.makeRequest(url.toString(), {}, this.serverConnectionSettings).then(response => {
|
|
444
|
-
Private.handleError(response);
|
|
445
|
-
return response.json();
|
|
446
|
-
});
|
|
447
|
-
request.then(() => {
|
|
448
|
-
this.serverConnectionError = null;
|
|
449
|
-
}, reason => {
|
|
450
|
-
this.serverConnectionError = reason.toString();
|
|
451
|
-
});
|
|
452
|
-
return request;
|
|
453
238
|
}
|
|
454
239
|
/**
|
|
455
240
|
* Search with current query.
|
|
456
241
|
*
|
|
457
242
|
* Sets searchError and totalEntries as appropriate.
|
|
458
243
|
*
|
|
459
|
-
* @returns
|
|
244
|
+
* @returns The extensions matching the current query.
|
|
460
245
|
*/
|
|
461
|
-
async
|
|
462
|
-
|
|
463
|
-
|
|
246
|
+
async search(force = false) {
|
|
247
|
+
var _a, _b;
|
|
248
|
+
if (!this.isDisclaimed) {
|
|
249
|
+
return Promise.reject('Installation warning is not disclaimed.');
|
|
464
250
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
let searchMap;
|
|
251
|
+
this.searchError = null;
|
|
252
|
+
this._isSearching = true;
|
|
253
|
+
this.stateChanged.emit();
|
|
469
254
|
try {
|
|
470
|
-
|
|
471
|
-
|
|
255
|
+
const [extensions, links] = await Private.requestAPI({
|
|
256
|
+
query: (_a = this.query) !== null && _a !== void 0 ? _a : '',
|
|
257
|
+
page: this.page,
|
|
258
|
+
per_page: this.pagination,
|
|
259
|
+
refresh: force ? 1 : 0
|
|
260
|
+
});
|
|
261
|
+
const lastURL = links['last'];
|
|
262
|
+
if (lastURL) {
|
|
263
|
+
const lastPage = URLExt.queryStringToObject((_b = URLExt.parse(lastURL).search) !== null && _b !== void 0 ? _b : '')['page'];
|
|
264
|
+
if (lastPage) {
|
|
265
|
+
this._lastPage = parseInt(lastPage, 10);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const installedNames = this._installed.map(pkg => pkg.name);
|
|
269
|
+
this._lastSearchResult = extensions
|
|
270
|
+
.filter(pkg => !installedNames.includes(pkg.name))
|
|
271
|
+
.sort(Private.comparator);
|
|
472
272
|
}
|
|
473
273
|
catch (reason) {
|
|
474
|
-
searchMap = {};
|
|
475
274
|
this.searchError = reason.toString();
|
|
476
275
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
* Query the installed extensions.
|
|
481
|
-
*
|
|
482
|
-
* Sets installedError as appropriate.
|
|
483
|
-
*
|
|
484
|
-
* @returns {Promise<{ [key: string]: IEntry; }>} A map of installed extensions.
|
|
485
|
-
*/
|
|
486
|
-
async queryInstalled(refreshInstalled) {
|
|
487
|
-
let installedMap;
|
|
488
|
-
try {
|
|
489
|
-
installedMap = await this.translateInstalled(this.fetchInstalled(refreshInstalled));
|
|
490
|
-
this.installedError = null;
|
|
491
|
-
}
|
|
492
|
-
catch (reason) {
|
|
493
|
-
installedMap = {};
|
|
494
|
-
this.installedError = reason.toString();
|
|
276
|
+
finally {
|
|
277
|
+
this._isSearching = false;
|
|
278
|
+
this.stateChanged.emit();
|
|
495
279
|
}
|
|
496
|
-
return installedMap;
|
|
497
280
|
}
|
|
498
281
|
/**
|
|
499
282
|
* Update the current model.
|
|
500
283
|
*
|
|
501
|
-
* This will query the
|
|
284
|
+
* This will query the packages repository, and the notebook server.
|
|
502
285
|
*
|
|
503
286
|
* Emits the `stateChanged` signal on successful completion.
|
|
504
287
|
*/
|
|
505
|
-
async update(
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
]);
|
|
511
|
-
// Map results to attributes:
|
|
512
|
-
const installed = [];
|
|
513
|
-
for (const key of Object.keys(installedMap)) {
|
|
514
|
-
installed.push(installedMap[key]);
|
|
515
|
-
}
|
|
516
|
-
this._installed = installed.sort(Private.comparator);
|
|
517
|
-
const searchResult = [];
|
|
518
|
-
for (const key of Object.keys(searchMap)) {
|
|
519
|
-
// Filter out installed entries from search results:
|
|
520
|
-
if (installedMap[key] === undefined) {
|
|
521
|
-
searchResult.push(searchMap[key]);
|
|
522
|
-
}
|
|
523
|
-
else {
|
|
524
|
-
searchResult.push(installedMap[key]);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
this._searchResult = searchResult.sort(Private.comparator);
|
|
288
|
+
async update(force = false) {
|
|
289
|
+
if (this.isDisclaimed) {
|
|
290
|
+
// First refresh the installed list - so the search results are correctly filtered
|
|
291
|
+
await this.refreshInstalled(force);
|
|
292
|
+
await this.search();
|
|
528
293
|
}
|
|
529
|
-
// Signal updated state
|
|
530
|
-
this.stateChanged.emit(undefined);
|
|
531
294
|
}
|
|
532
295
|
/**
|
|
533
296
|
* Send a request to the server to perform an action on an extension.
|
|
@@ -535,34 +298,54 @@ export class ListModel extends VDomModel {
|
|
|
535
298
|
* @param action A valid action to perform.
|
|
536
299
|
* @param entry The extension to perform the action on.
|
|
537
300
|
*/
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
const request = {
|
|
301
|
+
performAction(action, entry) {
|
|
302
|
+
const actionRequest = Private.requestAPI({}, {
|
|
541
303
|
method: 'POST',
|
|
542
304
|
body: JSON.stringify({
|
|
543
305
|
cmd: action,
|
|
544
306
|
extension_name: entry.name
|
|
545
307
|
})
|
|
546
|
-
};
|
|
547
|
-
const completed = ServerConnection.makeRequest(url.toString(), request, this.serverConnectionSettings).then(response => {
|
|
548
|
-
Private.handleError(response);
|
|
549
|
-
this.triggerBuildCheck();
|
|
550
|
-
return response.json();
|
|
551
308
|
});
|
|
552
|
-
|
|
553
|
-
|
|
309
|
+
actionRequest.then(([reply]) => {
|
|
310
|
+
const trans = this.translator.load('jupyterlab');
|
|
311
|
+
if (reply.needs_restart.includes('server')) {
|
|
312
|
+
void showDialog({
|
|
313
|
+
title: trans.__('Information'),
|
|
314
|
+
body: trans.__('You will need to restart JupyterLab to apply the changes.'),
|
|
315
|
+
buttons: [Dialog.okButton({ label: trans.__('Ok') })]
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
const followUps = [];
|
|
320
|
+
if (reply.needs_restart.includes('frontend')) {
|
|
321
|
+
followUps.push(
|
|
322
|
+
// @ts-expect-error isElectron is not a standard attribute
|
|
323
|
+
window.isElectron
|
|
324
|
+
? trans.__('reload JupyterLab')
|
|
325
|
+
: trans.__('refresh the web page'));
|
|
326
|
+
}
|
|
327
|
+
if (reply.needs_restart.includes('kernel')) {
|
|
328
|
+
followUps.push(trans.__('install the extension in all kernels and restart them'));
|
|
329
|
+
}
|
|
330
|
+
void showDialog({
|
|
331
|
+
title: trans.__('Information'),
|
|
332
|
+
body: trans.__('You will need to %1 to apply the changes.', followUps.join(trans.__(' and '))),
|
|
333
|
+
buttons: [Dialog.okButton({ label: trans.__('Ok') })]
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
this.actionError = null;
|
|
554
337
|
}, reason => {
|
|
555
|
-
this.
|
|
338
|
+
this.actionError = reason.toString();
|
|
556
339
|
});
|
|
557
|
-
this.
|
|
558
|
-
return
|
|
340
|
+
this.addPendingAction(actionRequest);
|
|
341
|
+
return actionRequest.then(([reply]) => reply);
|
|
559
342
|
}
|
|
560
343
|
/**
|
|
561
344
|
* Add a pending action.
|
|
562
345
|
*
|
|
563
346
|
* @param pending A promise that resolves when the action is completed.
|
|
564
347
|
*/
|
|
565
|
-
|
|
348
|
+
addPendingAction(pending) {
|
|
566
349
|
// Add to pending actions collection
|
|
567
350
|
this._pendingActions.push(pending);
|
|
568
351
|
// Ensure action is removed when resolved
|
|
@@ -576,7 +359,6 @@ export class ListModel extends VDomModel {
|
|
|
576
359
|
this.stateChanged.emit(undefined);
|
|
577
360
|
}
|
|
578
361
|
}
|
|
579
|
-
let _isDisclaimed = false;
|
|
580
362
|
/**
|
|
581
363
|
* ListModel statics.
|
|
582
364
|
*/
|
|
@@ -593,14 +375,6 @@ let _isDisclaimed = false;
|
|
|
593
375
|
return semver.lt(entry.installed_version, entry.latest_version);
|
|
594
376
|
}
|
|
595
377
|
ListModel.entryHasUpdate = entryHasUpdate;
|
|
596
|
-
function isDisclaimed() {
|
|
597
|
-
return _isDisclaimed;
|
|
598
|
-
}
|
|
599
|
-
ListModel.isDisclaimed = isDisclaimed;
|
|
600
|
-
function toogleDisclaimed() {
|
|
601
|
-
_isDisclaimed = !_isDisclaimed;
|
|
602
|
-
}
|
|
603
|
-
ListModel.toogleDisclaimed = toogleDisclaimed;
|
|
604
378
|
})(ListModel || (ListModel = {}));
|
|
605
379
|
/**
|
|
606
380
|
* A namespace for private functionality.
|
|
@@ -614,67 +388,52 @@ var Private;
|
|
|
614
388
|
if (a.name === b.name) {
|
|
615
389
|
return 0;
|
|
616
390
|
}
|
|
617
|
-
const testA = isJupyterOrg(a.name);
|
|
618
|
-
const testB = isJupyterOrg(b.name);
|
|
619
|
-
if (testA === testB) {
|
|
620
|
-
// Retain sort-order from API
|
|
621
|
-
return 0;
|
|
622
|
-
}
|
|
623
|
-
else if (testA && !testB) {
|
|
624
|
-
return -1;
|
|
625
|
-
}
|
|
626
391
|
else {
|
|
627
|
-
return 1;
|
|
392
|
+
return a.name > b.name ? 1 : -1;
|
|
628
393
|
}
|
|
629
394
|
}
|
|
630
395
|
Private.comparator = comparator;
|
|
396
|
+
const LINK_PARSER = /<([^>]+)>; rel="([^"]+)",?/g;
|
|
631
397
|
/**
|
|
632
|
-
*
|
|
398
|
+
* Call the API extension
|
|
633
399
|
*
|
|
634
|
-
* @param
|
|
635
|
-
* @param
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
400
|
+
* @param queryArgs Query arguments
|
|
401
|
+
* @param init Initial values for the request
|
|
402
|
+
* @returns The response body interpreted as JSON and the response link header
|
|
403
|
+
*/
|
|
404
|
+
async function requestAPI(queryArgs = {}, init = {}) {
|
|
405
|
+
var _a;
|
|
406
|
+
// Make request to Jupyter API
|
|
407
|
+
const settings = ServerConnection.makeSettings();
|
|
408
|
+
const requestUrl = URLExt.join(settings.baseUrl, EXTENSION_API_PATH // API Namespace
|
|
409
|
+
);
|
|
410
|
+
let response;
|
|
411
|
+
try {
|
|
412
|
+
response = await ServerConnection.makeRequest(requestUrl + URLExt.objectToQueryString(queryArgs), init, settings);
|
|
646
413
|
}
|
|
647
|
-
|
|
648
|
-
|
|
414
|
+
catch (error) {
|
|
415
|
+
throw new ServerConnection.NetworkError(error);
|
|
649
416
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
match = reLang.test(spec.language);
|
|
417
|
+
let data = await response.text();
|
|
418
|
+
if (data.length > 0) {
|
|
419
|
+
try {
|
|
420
|
+
data = JSON.parse(data);
|
|
655
421
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
}
|
|
659
|
-
if (match) {
|
|
660
|
-
matches.push(spec);
|
|
661
|
-
continue;
|
|
422
|
+
catch (error) {
|
|
423
|
+
console.log('Not a JSON response body.', response);
|
|
662
424
|
}
|
|
663
425
|
}
|
|
664
|
-
return matches;
|
|
665
|
-
}
|
|
666
|
-
Private.matchSpecs = matchSpecs;
|
|
667
|
-
/**
|
|
668
|
-
* Convert a response to an exception on error.
|
|
669
|
-
*
|
|
670
|
-
* @param response The response to inspect.
|
|
671
|
-
*/
|
|
672
|
-
function handleError(response) {
|
|
673
426
|
if (!response.ok) {
|
|
674
|
-
throw new
|
|
427
|
+
throw new ServerConnection.ResponseError(response, data.message || data);
|
|
428
|
+
}
|
|
429
|
+
const link = (_a = response.headers.get('Link')) !== null && _a !== void 0 ? _a : '';
|
|
430
|
+
const links = {};
|
|
431
|
+
let match = null;
|
|
432
|
+
while ((match = LINK_PARSER.exec(link)) !== null) {
|
|
433
|
+
links[match[2]] = match[1];
|
|
675
434
|
}
|
|
676
|
-
return
|
|
435
|
+
return [data, links];
|
|
677
436
|
}
|
|
678
|
-
Private.
|
|
437
|
+
Private.requestAPI = requestAPI;
|
|
679
438
|
})(Private || (Private = {}));
|
|
680
439
|
//# sourceMappingURL=model.js.map
|