@jupyterlab/extensionmanager 4.0.0-alpha.12 → 4.0.0-alpha.14

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