@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/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,29 @@ 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._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
- this._totalblockedExtensionsFound = 0;
58
- this._totalallowedExtensionsFound = 0;
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._searchResult = [];
52
+ this._lastSearchResult = [];
63
53
  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();
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._searchResult;
97
+ return this._lastSearchResult;
116
98
  }
117
99
  /**
118
- * The current NPM repository search query.
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 = value;
127
- void this._debouncedUpdate.invoke();
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 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.
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 = value;
142
- void this.update();
126
+ if (this._page !== value) {
127
+ this._page = value;
128
+ void this._debouncedSearch.invoke();
129
+ }
143
130
  }
144
131
  /**
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.
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 = 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;
140
+ if (this._pagination !== value) {
141
+ this._pagination = value;
142
+ void this._debouncedSearch.invoke();
143
+ }
176
144
  }
177
145
  /**
178
- * The total number of allowedExtensions results in the current search.
146
+ * The last page of results in the current search.
179
147
  */
180
- get totalallowedExtensionsFound() {
181
- return this._totalallowedExtensionsFound;
148
+ get lastPage() {
149
+ return this._lastPage;
182
150
  }
183
151
  /**
184
- * Initialize the model.
152
+ * Dispose the extensions list model.
185
153
  */
186
- initialize() {
187
- return this.update()
188
- .then(() => {
189
- this.initialized = true;
190
- this.stateChanged.emit(undefined);
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
- if (entry.installed) {
210
- // Updating
211
- await this._performAction('install', entry).then(data => {
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._performAction('uninstall', entry);
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._performAction('enable', entry);
251
- await this.update();
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._performAction('disable', entry);
263
- await this.update();
213
+ await this.performAction('disable', entry);
214
+ await this.refreshInstalled(true);
264
215
  }
265
216
  /**
266
- * Check for companion packages in kernels or server.
217
+ * Refresh installed packages
267
218
  *
268
- * @param entry An entry indicating which extension to check.
219
+ * @param force Force refreshing the list of installed packages
269
220
  */
270
- checkCompanionPackages(entry) {
271
- return this.searcher
272
- .fetchPackageData(entry.name, entry.latest_version)
273
- .then(data => {
274
- if (!data || !data.jupyterlab || !data.jupyterlab.discovery) {
275
- return true;
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._addPendingAction(completed);
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
- return entries;
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
- return Promise.all(promises).then(() => {
422
- return entries;
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 {Promise<{ [key: string]: IEntry; }>} The search result as a map of entries.
244
+ * @returns The extensions matching the current query.
460
245
  */
461
- async performSearch() {
462
- if (this.query === null) {
463
- this.query = '';
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
- // Start the search without waiting for it:
466
- const search = this.searcher.searchExtensions(this.query, this.page, this.pagination);
467
- const searchMapPromise = this.translateSearchResult(search);
468
- let searchMap;
251
+ this.searchError = null;
252
+ this._isSearching = true;
253
+ this.stateChanged.emit();
469
254
  try {
470
- searchMap = await searchMapPromise;
471
- this.searchError = null;
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
- return searchMap;
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 NPM repository, and the notebook server.
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(refreshInstalled = false) {
506
- if (ListModel.isDisclaimed()) {
507
- const [searchMap, installedMap] = await Promise.all([
508
- this.performSearch(),
509
- this.queryInstalled(refreshInstalled)
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
- _performAction(action, entry) {
539
- const url = new URL(EXTENSION_API_PATH, this.serverConnectionSettings.baseUrl);
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
- completed.then(() => {
553
- this.serverConnectionError = null;
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.serverConnectionError = reason.toString();
338
+ this.actionError = reason.toString();
556
339
  });
557
- this._addPendingAction(completed);
558
- return completed;
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
- _addPendingAction(pending) {
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
- * Match kernel specs against kernel spec regexps
398
+ * Call the API extension
633
399
  *
634
- * @param kernelInfo The info containing the regexp patterns
635
- * @param specs The available kernel specs.
636
- */
637
- function matchSpecs(kernelInfo, specs) {
638
- if (!specs) {
639
- return [];
640
- }
641
- const matches = [];
642
- let reLang = null;
643
- let reName = null;
644
- if (kernelInfo.kernel_spec.language) {
645
- reLang = new RegExp(kernelInfo.kernel_spec.language);
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
- if (kernelInfo.kernel_spec.display_name) {
648
- reName = new RegExp(kernelInfo.kernel_spec.display_name);
414
+ catch (error) {
415
+ throw new ServerConnection.NetworkError(error);
649
416
  }
650
- for (const key of Object.keys(specs.kernelspecs)) {
651
- const spec = specs.kernelspecs[key];
652
- let match = false;
653
- if (reLang) {
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
- if (!match && reName) {
657
- match = reName.test(spec.display_name);
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 Error(`${response.status} (${response.statusText})`);
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 response;
435
+ return [data, links];
677
436
  }
678
- Private.handleError = handleError;
437
+ Private.requestAPI = requestAPI;
679
438
  })(Private || (Private = {}));
680
439
  //# sourceMappingURL=model.js.map