@jupyterlab/extensionmanager 4.0.0-alpha.13 → 4.0.0-alpha.15

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/widget.js CHANGED
@@ -1,69 +1,19 @@
1
1
  // Copyright (c) Jupyter Development Team.
2
2
  // Distributed under the terms of the Modified BSD License.
3
- import { Dialog, showDialog } from '@jupyterlab/apputils';
4
- import { nullTranslator } from '@jupyterlab/translation';
5
- import { Button, caretDownIcon, caretRightIcon, Collapse, InputGroup, jupyterIcon, listingsInfoIcon, refreshIcon, ToolbarButtonComponent, VDomRenderer } from '@jupyterlab/ui-components';
3
+ import { Button, FilterBox, infoIcon, jupyterIcon, PanelWithToolbar, ReactWidget, refreshIcon, SidePanel, ToolbarButton, ToolbarButtonComponent } from '@jupyterlab/ui-components';
6
4
  import * as React from 'react';
7
5
  import ReactPaginate from 'react-paginate';
8
6
  import { ListModel } from './model';
9
- import { isJupyterOrg } from './npm';
10
- // TODO: Replace pagination with lazy loading of lower search results
11
- /**
12
- * Icons with custom styling bound.
13
- */
14
- const caretDownIconStyled = caretDownIcon.bindprops({
15
- height: 'auto',
16
- width: '20px'
17
- });
18
- const caretRightIconStyled = caretRightIcon.bindprops({
19
- height: 'auto',
20
- width: '20px'
21
- });
22
- const badgeSize = 32;
23
- const badgeQuerySize = Math.floor(devicePixelRatio * badgeSize);
24
- /**
25
- * Search bar VDOM component.
26
- */
27
- export class SearchBar extends React.Component {
28
- constructor(props) {
29
- super(props);
30
- /**
31
- * Handler for search input changes.
32
- */
33
- this.handleChange = (e) => {
34
- const target = e.target;
35
- this.setState({
36
- value: target.value
37
- });
38
- };
39
- this.state = {
40
- value: ''
41
- };
42
- }
43
- /**
44
- * Render the list view using the virtual DOM.
45
- */
46
- render() {
47
- return (React.createElement("div", { className: "jp-extensionmanager-search-bar" },
48
- React.createElement(InputGroup, { className: "jp-extensionmanager-search-wrapper", type: "text", placeholder: this.props.placeholder, onChange: this.handleChange, value: this.state.value, rightIcon: "ui-components:search", disabled: this.props.disabled })));
49
- }
50
- }
51
- /**
52
- * Create a build prompt as a react element.
53
- *
54
- * @param props Configuration of the build prompt.
55
- */
56
- function BuildPrompt(props) {
57
- const translator = props.translator || nullTranslator;
58
- const trans = translator.load('jupyterlab');
59
- return (React.createElement("div", { className: "jp-extensionmanager-buildprompt" },
60
- React.createElement("div", { className: "jp-extensionmanager-buildmessage" }, trans.__('A build is needed to include the latest changes')),
61
- React.createElement(Button, { onClick: props.performBuild, minimal: true, small: true }, trans.__('Rebuild')),
62
- React.createElement(Button, { onClick: props.ignoreBuild, minimal: true, small: true }, trans.__('Ignore'))));
63
- }
7
+ const BADGE_SIZE = 32;
8
+ const BADGE_QUERY_SIZE = Math.floor(devicePixelRatio * BADGE_SIZE);
64
9
  function getExtensionGitHubUser(entry) {
65
- if (entry.url && entry.url.startsWith('https://github.com/')) {
66
- return entry.url.split('/')[3];
10
+ if (entry.homepage_url &&
11
+ entry.homepage_url.startsWith('https://github.com/')) {
12
+ return entry.homepage_url.split('/')[3];
13
+ }
14
+ else if (entry.repository_url &&
15
+ entry.repository_url.startsWith('https://github.com/')) {
16
+ return entry.repository_url.split('/')[3];
67
17
  }
68
18
  return null;
69
19
  }
@@ -71,288 +21,130 @@ function getExtensionGitHubUser(entry) {
71
21
  * VDOM for visualizing an extension entry.
72
22
  */
73
23
  function ListEntry(props) {
74
- var _a;
75
- const { entry, listMode, viewType } = props;
76
- const translator = props.translator || nullTranslator;
77
- const trans = translator.load('jupyterlab');
24
+ const { entry, supportInstallation, trans } = props;
78
25
  const flagClasses = [];
79
26
  if (entry.status && ['ok', 'warning', 'error'].indexOf(entry.status) !== -1) {
80
27
  flagClasses.push(`jp-extensionmanager-entry-${entry.status}`);
81
28
  }
82
- let title = entry.name;
83
- const entryIsJupyterOrg = isJupyterOrg(entry.name);
84
- if (entryIsJupyterOrg) {
85
- title = trans.__('%1 (Developed by Project Jupyter)', entry.name);
86
- }
29
+ const title = entry.name;
87
30
  const githubUser = getExtensionGitHubUser(entry);
88
- if (listMode === 'block' &&
89
- entry.blockedExtensionsEntry &&
90
- viewType === 'searchResult') {
91
- return React.createElement("li", null);
92
- }
93
- if (listMode === 'allow' &&
94
- !entry.allowedExtensionsEntry &&
95
- viewType === 'searchResult') {
96
- return React.createElement("li", null);
97
- }
98
- if (listMode === 'block' && ((_a = entry.blockedExtensionsEntry) === null || _a === void 0 ? void 0 : _a.name)) {
99
- flagClasses.push(`jp-extensionmanager-entry-should-be-uninstalled`);
100
- }
101
- if (listMode === 'allow' && !entry.allowedExtensionsEntry) {
31
+ if (!entry.allowed) {
102
32
  flagClasses.push(`jp-extensionmanager-entry-should-be-uninstalled`);
103
33
  }
104
34
  return (React.createElement("li", { className: `jp-extensionmanager-entry ${flagClasses.join(' ')}`, title: title, style: { display: 'flex' } },
105
- React.createElement("div", { style: { marginRight: '8px' } },
106
- githubUser && (React.createElement("img", { src: `https://github.com/${githubUser}.png?size=${badgeQuerySize}`, style: { width: '32px', height: '32px' } })),
107
- !githubUser && (React.createElement("div", { style: { width: `${badgeSize}px`, height: `${badgeSize}px` } }))),
35
+ React.createElement("div", { style: { marginRight: '8px' } }, githubUser ? (React.createElement("img", { src: `https://github.com/${githubUser}.png?size=${BADGE_QUERY_SIZE}`, style: { width: '32px', height: '32px' } })) : (React.createElement("div", { style: { width: `${BADGE_SIZE}px`, height: `${BADGE_SIZE}px` } }))),
108
36
  React.createElement("div", { className: "jp-extensionmanager-entry-description" },
109
37
  React.createElement("div", { className: "jp-extensionmanager-entry-title" },
110
- React.createElement("div", { className: "jp-extensionmanager-entry-name" }, entry.url ? (React.createElement("a", { href: entry.url, target: "_blank", rel: "noopener noreferrer" }, entry.name)) : (React.createElement("div", null, entry.name))),
111
- entry.blockedExtensionsEntry && (React.createElement(ToolbarButtonComponent, { icon: listingsInfoIcon, iconLabel: trans.__('%1 extension has been blockedExtensions since install. Please uninstall immediately and contact your blockedExtensions administrator.', entry.name), onClick: () => window.open('https://jupyterlab.readthedocs.io/en/latest/user/extensions.html') })),
112
- !entry.allowedExtensionsEntry &&
113
- viewType === 'installed' &&
114
- listMode === 'allow' && (React.createElement(ToolbarButtonComponent, { icon: listingsInfoIcon, iconLabel: trans.__('%1 extension has been removed from the allowedExtensions since installation. Please uninstall immediately and contact your allowedExtensions administrator.', entry.name), onClick: () => window.open('https://jupyterlab.readthedocs.io/en/latest/user/extensions.html') })),
115
- entryIsJupyterOrg && (React.createElement(jupyterIcon.react, { className: "jp-extensionmanager-is-jupyter-org", top: "1px", height: "auto", width: "1em" }))),
38
+ React.createElement("div", { className: "jp-extensionmanager-entry-name" }, entry.homepage_url ? (React.createElement("a", { href: entry.homepage_url, target: "_blank", rel: "noopener noreferrer" }, entry.name)) : (React.createElement("div", null, entry.name))),
39
+ entry.installed && !entry.allowed && (React.createElement(ToolbarButtonComponent, { icon: infoIcon, iconLabel: trans.__('%1 extension is not allowed any more. Please uninstall immediately or contact your administrator.', entry.name), onClick: () => window.open('https://jupyterlab.readthedocs.io/en/latest/user/extensions.html') })),
40
+ entry.approved && (React.createElement(jupyterIcon.react, { className: "jp-extensionmanager-is-approved", top: "1px", height: "auto", width: "1em", title: trans.__('This extension is approved by your security team.') }))),
116
41
  React.createElement("div", { className: "jp-extensionmanager-entry-content" },
117
42
  React.createElement("div", { className: "jp-extensionmanager-entry-description" }, entry.description),
118
- React.createElement("div", { className: "jp-extensionmanager-entry-buttons" },
119
- entry.enabled && (React.createElement(Button, { onClick: () => props.performAction('disable', entry), minimal: true, small: true }, trans.__('Disable'))),
120
- entry.installed && !entry.enabled && (React.createElement(Button, { onClick: () => props.performAction('enable', entry), minimal: true, small: true }, trans.__('Enable'))),
121
- entry.installed && entry.pkg_type == 'prebuilt' && (React.createElement("div", { className: "jp-extensionmanager-entry-buttons" },
122
- React.createElement(Button, { onClick: () => showDialog({
123
- title,
124
- body: (React.createElement("div", null, getPrebuiltUninstallInstruction(entry, trans))),
125
- buttons: [
126
- Dialog.okButton({
127
- label: trans.__('OK'),
128
- caption: trans.__('OK')
129
- })
130
- ]
131
- }).then(result => {
132
- return result.button.accept;
133
- }), minimal: true, small: true }, trans.__('About')))))))));
134
- }
135
- function getPrebuiltUninstallInstruction(entry, trans) {
136
- var _a, _b;
137
- if ((_a = entry.install) === null || _a === void 0 ? void 0 : _a.uninstallInstructions) {
138
- return (React.createElement("div", null,
139
- React.createElement("p", null, trans.__(`This is a prebuilt extension. To uninstall it, please
140
- apply following instructions.`)),
141
- React.createElement("p", null, trans.__((_b = entry.install) === null || _b === void 0 ? void 0 : _b.uninstallInstructions))));
142
- }
143
- return (React.createElement("div", null,
144
- React.createElement("p", null, trans.__(`This is a prebuilt extension. To uninstall it, please
145
- read the user guide on:`)),
146
- React.createElement("p", null,
147
- React.createElement("a", { href: "https://jupyterlab.readthedocs.io/en/latest/user/extensions.html", target: "_blank", rel: "noopener noreferrer" }, "https://jupyterlab.readthedocs.io/en/latest/user/extensions.html"))));
43
+ props.performAction && (React.createElement("div", { className: "jp-extensionmanager-entry-buttons" }, entry.installed ? (React.createElement(React.Fragment, null,
44
+ supportInstallation && (React.createElement(React.Fragment, null,
45
+ ListModel.entryHasUpdate(entry) && (React.createElement(Button, { onClick: () => props.performAction('install', entry), minimal: true, small: true }, trans.__('Update'))),
46
+ React.createElement(Button, { onClick: () => props.performAction('uninstall', entry), minimal: true, small: true }, trans.__('Uninstall')))),
47
+ entry.enabled ? (React.createElement(Button, { onClick: () => props.performAction('disable', entry), minimal: true, small: true }, trans.__('Disable'))) : (React.createElement(Button, { onClick: () => props.performAction('enable', entry), minimal: true, small: true }, trans.__('Enable'))))) : (supportInstallation && (React.createElement(Button, { onClick: () => props.performAction('install', entry), minimal: true, small: true }, trans.__('Install'))))))))));
148
48
  }
149
49
  /**
150
50
  * List view widget for extensions
151
51
  */
152
- export function ListView(props) {
153
- const translator = props.translator || nullTranslator;
154
- const trans = translator.load('jupyterlab');
155
- const entryViews = [];
156
- for (const entry of props.entries) {
157
- entryViews.push(React.createElement(ListEntry, { entry: entry, listMode: props.listMode, viewType: props.viewType, key: entry.name, performAction: props.performAction, translator: translator }));
158
- }
159
- let pagination;
160
- if (props.numPages > 1) {
161
- pagination = (React.createElement("div", { className: "jp-extensionmanager-pagination" },
162
- React.createElement(ReactPaginate, { previousLabel: '<', nextLabel: '>', breakLabel: React.createElement("a", { href: "" }, "..."), breakClassName: 'break-me', pageCount: props.numPages, marginPagesDisplayed: 2, pageRangeDisplayed: 5, onPageChange: (data) => props.onPage(data.selected), containerClassName: 'pagination', activeClassName: 'active' })));
163
- }
164
- const listview = (React.createElement("ul", { className: "jp-extensionmanager-listview" }, entryViews));
52
+ function ListView(props) {
53
+ var _a;
54
+ const { trans } = props;
165
55
  return (React.createElement("div", { className: "jp-extensionmanager-listview-wrapper" },
166
- entryViews.length > 0 ? (listview) : (React.createElement("div", { key: "message", className: "jp-extensionmanager-listview-message" }, trans.__('No entries'))),
167
- pagination));
56
+ props.entries.length > 0 ? (React.createElement("ul", { className: "jp-extensionmanager-listview" }, props.entries.map(entry => (React.createElement(ListEntry, { key: entry.name, entry: entry, performAction: props.performAction, supportInstallation: props.supportInstallation, trans: trans }))))) : (React.createElement("div", { key: "message", className: "jp-extensionmanager-listview-message" }, trans.__('No entries'))),
57
+ props.numPages > 1 && (React.createElement("div", { className: "jp-extensionmanager-pagination" },
58
+ React.createElement(ReactPaginate, { previousLabel: '<', nextLabel: '>', breakLabel: React.createElement("a", { href: "" }, "..."), breakClassName: 'break-me', initialPage: ((_a = props.initialPage) !== null && _a !== void 0 ? _a : 1) - 1, pageCount: props.numPages, marginPagesDisplayed: 2, pageRangeDisplayed: 5, onPageChange: (data) => props.onPage(data.selected + 1), containerClassName: 'pagination', activeClassName: 'active' })))));
168
59
  }
169
60
  function ErrorMessage(props) {
170
- return (React.createElement("div", { key: "error-msg", className: "jp-extensionmanager-error" }, props.children));
61
+ return React.createElement("div", { className: "jp-extensionmanager-error" }, props.children);
171
62
  }
172
- /**
173
- *
174
- */
175
- export class CollapsibleSection extends React.Component {
176
- constructor(props) {
177
- super(props);
178
- this.state = {
179
- isOpen: props.isOpen ? true : false
180
- };
63
+ class Header extends ReactWidget {
64
+ constructor(model, trans, searchInputRef) {
65
+ super();
66
+ this.model = model;
67
+ this.trans = trans;
68
+ this.searchInputRef = searchInputRef;
69
+ model.stateChanged.connect(this.update, this);
70
+ this.addClass('jp-extensionmanager-header');
181
71
  }
182
- /**
183
- * Render the collapsible section using the virtual DOM.
184
- */
185
72
  render() {
186
- let icon = this.state.isOpen ? caretDownIconStyled : caretRightIconStyled;
187
- let isOpen = this.state.isOpen;
188
- let className = 'jp-extensionmanager-headerText';
189
- if (this.props.disabled) {
190
- icon = caretRightIconStyled;
191
- isOpen = false;
192
- className = 'jp-extensionmanager-headerTextDisabled';
193
- }
194
73
  return (React.createElement(React.Fragment, null,
195
- React.createElement("div", { className: "jp-stack-panel-header" },
196
- React.createElement(ToolbarButtonComponent, { icon: icon, onClick: () => {
197
- this.handleCollapse();
198
- } }),
199
- React.createElement("span", { className: className }, this.props.header),
200
- !this.props.disabled && this.props.headerElements),
201
- React.createElement(Collapse, { isOpen: isOpen }, this.props.children)));
202
- }
203
- /**
204
- * Handler for search input changes.
205
- */
206
- handleCollapse() {
207
- this.setState({
208
- isOpen: !this.state.isOpen
209
- }, () => {
210
- if (this.props.onCollapse) {
211
- this.props.onCollapse(this.state.isOpen);
212
- }
213
- });
214
- }
215
- UNSAFE_componentWillReceiveProps(nextProps) {
216
- if (nextProps.forceOpen) {
217
- this.setState({
218
- isOpen: true
219
- });
220
- }
74
+ React.createElement("div", { className: "jp-extensionmanager-title" },
75
+ React.createElement("span", null, this.trans.__('%1 Manager', this.model.name)),
76
+ this.model.installPath && (React.createElement(infoIcon.react, { className: "jp-extensionmanager-path", tag: "span", title: this.trans.__('Extension installation path: %1', this.model.installPath) }))),
77
+ React.createElement(FilterBox, { placeholder: this.trans.__('Search'), disabled: !this.model.isDisclaimed, updateFilter: (fn, query) => {
78
+ this.model.query = query !== null && query !== void 0 ? query : '';
79
+ }, useFuzzyFilter: false, inputRef: this.searchInputRef }),
80
+ React.createElement("div", { className: `jp-extensionmanager-pending ${this.model.hasPendingActions() ? 'jp-mod-hasPending' : ''}` }),
81
+ this.model.actionError && (React.createElement(ErrorMessage, null,
82
+ React.createElement("p", null, this.trans.__('Error when performing an action.')),
83
+ React.createElement("p", null, this.trans.__('Reason given:')),
84
+ React.createElement("pre", null, this.model.actionError)))));
221
85
  }
222
86
  }
223
- /**
224
- * The main view for the discovery extension.
225
- */
226
- export class ExtensionView extends VDomRenderer {
227
- constructor(app, serviceManager, settings, translator) {
228
- super(new ListModel(app, serviceManager, settings, translator));
229
- this.translator = translator || nullTranslator;
230
- this._trans = this.translator.load('jupyterlab');
231
- this._settings = settings;
232
- this._forceOpen = false;
233
- this.addClass('jp-extensionmanager-view');
234
- }
235
- /**
236
- * The search input node.
237
- */
238
- get inputNode() {
239
- return this.node.querySelector('.jp-extensionmanager-search-wrapper input');
87
+ class Warning extends ReactWidget {
88
+ constructor(model, trans) {
89
+ super();
90
+ this.model = model;
91
+ this.trans = trans;
92
+ this.addClass('jp-extensionmanager-disclaimer');
93
+ model.stateChanged.connect(this.update, this);
240
94
  }
241
- /**
242
- * Render the extension view using the virtual DOM.
243
- */
244
95
  render() {
245
- var _a, _b;
246
- const model = this.model;
247
- if (!model.listMode) {
248
- return [React.createElement("div", { key: "empty" })];
249
- }
250
- if (model.listMode === 'invalid') {
251
- return [
252
- React.createElement("div", { style: { padding: 8 }, key: "invalid" },
253
- React.createElement("div", null, this._trans
254
- .__(`The extension manager is disabled. Please contact your system
255
- administrator to verify the listings configuration.`)),
256
- React.createElement("div", null,
257
- React.createElement("a", { href: "https://jupyterlab.readthedocs.io/en/latest/user/extensions.html", target: "_blank", rel: "noopener noreferrer" }, this._trans.__('Read more in the JupyterLab documentation.'))))
258
- ];
259
- }
260
- const pages = Math.ceil(model.totalEntries / model.pagination);
261
- const elements = [
262
- React.createElement(SearchBar, { key: "searchbar", placeholder: this._trans.__('SEARCH'), disabled: !ListModel.isDisclaimed(), settings: this._settings })
263
- ];
264
- if (model.promptBuild) {
265
- elements.push(React.createElement(BuildPrompt, { key: "promt", translator: this.translator, performBuild: () => {
266
- model.performBuild();
267
- }, ignoreBuild: () => {
268
- model.ignoreBuildRecommendation();
269
- } }));
270
- }
271
- // Indicator element for pending actions:
272
- elements.push(React.createElement("div", { key: "pending", className: `jp-extensionmanager-pending ${model.hasPendingActions() ? 'jp-mod-hasPending' : ''}` }));
273
- const content = [];
274
- content.push(React.createElement(CollapsibleSection, { key: "warning-section", isOpen: !ListModel.isDisclaimed(), disabled: false, header: this._trans.__('Warning') },
275
- React.createElement("div", { className: "jp-extensionmanager-disclaimer" },
276
- React.createElement("div", null, this._trans
277
- .__(`The JupyterLab development team is excited to have a robust
96
+ return (React.createElement(React.Fragment, null,
97
+ React.createElement("p", null, this.trans
98
+ .__(`The JupyterLab development team is excited to have a robust
278
99
  third-party extension community. However, we do not review
279
100
  third-party extensions, and some extensions may introduce security
280
101
  risks or contain malicious code that runs on your machine.`)),
281
- React.createElement("div", { style: { paddingTop: 8 } },
282
- ListModel.isDisclaimed() && (React.createElement(Button, { className: "jp-extensionmanager-disclaimer-disable", onClick: (e) => {
283
- this._settings.set('disclaimed', false).catch(reason => {
284
- console.error(`Something went wrong when setting disclaimed.\n${reason}`);
285
- });
286
- } }, this._trans.__('Disable'))),
287
- !ListModel.isDisclaimed() && (React.createElement(Button, { className: "jp-extensionmanager-disclaimer-enable", onClick: (e) => {
288
- this._forceOpen = true;
289
- this._settings.set('disclaimed', true).catch(reason => {
290
- console.error(`Something went wrong when setting disclaimed.\n${reason}`);
291
- });
292
- } }, this._trans.__('Enable')))))));
293
- if (!model.initialized) {
294
- content.push(React.createElement("div", { key: "loading-placeholder", className: "jp-extensionmanager-loader" }, this._trans.__('Updating extensions list')));
295
- }
296
- else if (model.serverConnectionError !== null) {
297
- content.push(React.createElement(ErrorMessage, { key: "error-msg" },
298
- React.createElement("p", null, this._trans
299
- .__(`Error communicating with server extension. Consult the documentation
300
- for how to ensure that it is enabled.`)),
301
- React.createElement("p", null, this._trans.__('Reason given:')),
302
- React.createElement("pre", null, model.serverConnectionError)));
303
- }
304
- else if (model.serverRequirementsError !== null) {
305
- content.push(React.createElement(ErrorMessage, { key: "server-requirements-error" },
306
- React.createElement("p", null, this._trans.__('The server has some missing requirements for installing extensions.')),
307
- React.createElement("p", null, this._trans.__('Details:')),
308
- React.createElement("pre", null, model.serverRequirementsError)));
309
- }
310
- else {
311
- // List installed and discovery sections
312
- const installedContent = [];
313
- if (model.installedError !== null) {
314
- installedContent.push(React.createElement(ErrorMessage, { key: "install-error" }, `Error querying installed extensions${model.installedError ? `: ${model.installedError}` : '.'}`));
315
- }
316
- else {
317
- const query = new RegExp((_b = (_a = model.query) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : '');
318
- installedContent.push(React.createElement(ListView, { key: "installed-items", listMode: model.listMode, viewType: 'installed', entries: model.installed.filter(pkg => !model.query || query.test(pkg.name)), numPages: 1, translator: this.translator, onPage: value => {
319
- /* no-op */
320
- }, performAction: this.onAction.bind(this) }));
321
- }
322
- content.push(React.createElement(CollapsibleSection, { key: "installed-section", isOpen: ListModel.isDisclaimed(), forceOpen: this._forceOpen, disabled: !ListModel.isDisclaimed(), header: this._trans.__('Installed'), headerElements: React.createElement(ToolbarButtonComponent, { key: "refresh-button", icon: refreshIcon, onClick: () => {
323
- model.refreshInstalled();
324
- }, tooltip: this._trans.__('Refresh extension list') }) }, installedContent));
325
- const searchContent = [];
326
- if (model.searchError !== null) {
327
- searchContent.push(React.createElement(ErrorMessage, { key: "search-error" }, `Error searching for extensions${model.searchError ? `: ${model.searchError}` : '.'}`));
328
- }
329
- else {
330
- searchContent.push(React.createElement(ListView, { key: "search-items", listMode: model.listMode, viewType: 'searchResult',
331
- // Filter out installed extensions:
332
- entries: model.searchResult.filter(entry => model.installed.indexOf(entry) === -1), numPages: pages, onPage: value => {
333
- this.onPage(value);
334
- }, performAction: this.onAction.bind(this), translator: this.translator }));
335
- }
336
- content.push(React.createElement(CollapsibleSection, { key: "search-section", isOpen: ListModel.isDisclaimed(), forceOpen: this._forceOpen, disabled: !ListModel.isDisclaimed(), header: model.query
337
- ? this._trans.__('Search Results')
338
- : this._trans.__('Discover'), onCollapse: (isOpen) => {
339
- if (isOpen && model.query === null) {
340
- model.query = '';
341
- }
342
- } }, searchContent));
343
- }
344
- elements.push(React.createElement("div", { key: "content", className: "jp-extensionmanager-content" }, content));
345
- // Reset the force open for future usage.
346
- this._forceOpen = false;
347
- return elements;
102
+ this.model.isDisclaimed ? (React.createElement(Button, { className: "jp-extensionmanager-disclaimer-disable", onClick: (e) => {
103
+ this.model.isDisclaimed = false;
104
+ } }, this.trans.__('Disable'))) : (React.createElement(Button, { className: "jp-extensionmanager-disclaimer-enable", onClick: (e) => {
105
+ this.model.isDisclaimed = true;
106
+ } }, this.trans.__('Enable')))));
107
+ }
108
+ }
109
+ class InstalledList extends ReactWidget {
110
+ constructor(model, trans) {
111
+ super();
112
+ this.model = model;
113
+ this.trans = trans;
114
+ model.stateChanged.connect(this.update, this);
115
+ }
116
+ render() {
117
+ return (React.createElement(React.Fragment, null, this.model.installedError !== null ? (React.createElement(ErrorMessage, null, `Error querying installed extensions${this.model.installedError ? `: ${this.model.installedError}` : '.'}`)) : this.model.isLoadingInstalledExtensions ? (React.createElement("div", { className: "jp-extensionmanager-loader" }, this.trans.__('Updating extensions list…'))) : (React.createElement(ListView, { entries: this.model.installed.filter(pkg => new RegExp(this.model.query.toLowerCase()).test(pkg.name)), numPages: 1, trans: this.trans, onPage: value => {
118
+ /* no-op */
119
+ }, performAction: this.model.isDisclaimed ? this.onAction.bind(this) : null, supportInstallation: this.model.canInstall && this.model.isDisclaimed }))));
348
120
  }
349
121
  /**
350
- * Callback handler for the user specifies a new search query.
122
+ * Callback handler for when the user wants to perform an action on an extension.
351
123
  *
352
- * @param value The new query.
124
+ * @param action The action to perform.
125
+ * @param entry The entry to perform the action on.
353
126
  */
354
- onSearch(value) {
355
- this.model.query = value;
127
+ onAction(action, entry) {
128
+ switch (action) {
129
+ case 'install':
130
+ return this.model.install(entry);
131
+ case 'uninstall':
132
+ return this.model.uninstall(entry);
133
+ case 'enable':
134
+ return this.model.enable(entry);
135
+ case 'disable':
136
+ return this.model.disable(entry);
137
+ default:
138
+ throw new Error(`Invalid action: ${action}`);
139
+ }
140
+ }
141
+ }
142
+ class SearchResult extends ReactWidget {
143
+ constructor(model, trans) {
144
+ super();
145
+ this.model = model;
146
+ this.trans = trans;
147
+ model.stateChanged.connect(this.update, this);
356
148
  }
357
149
  /**
358
150
  * Callback handler for the user changes the page of the search result pagination.
@@ -382,6 +174,74 @@ risks or contain malicious code that runs on your machine.`)),
382
174
  throw new Error(`Invalid action: ${action}`);
383
175
  }
384
176
  }
177
+ render() {
178
+ return (React.createElement(React.Fragment, null, this.model.searchError !== null ? (React.createElement(ErrorMessage, null, `Error searching for extensions${this.model.searchError ? `: ${this.model.searchError}` : '.'}`)) : this.model.isSearching ? (React.createElement("div", { className: "jp-extensionmanager-loader" }, this.trans.__('Updating extensions list…'))) : (React.createElement(ListView, { entries: this.model.searchResult, initialPage: this.model.page, numPages: this.model.lastPage, onPage: value => {
179
+ this.onPage(value);
180
+ }, performAction: this.model.isDisclaimed ? this.onAction.bind(this) : null, supportInstallation: this.model.canInstall && this.model.isDisclaimed, trans: this.trans }))));
181
+ }
182
+ update() {
183
+ this.title.label = this.model.query
184
+ ? this.trans.__('Search Results')
185
+ : this.trans.__('Discover');
186
+ super.update();
187
+ }
188
+ }
189
+ export class ExtensionsPanel extends SidePanel {
190
+ constructor(options) {
191
+ const { model, translator } = options;
192
+ super({ translator });
193
+ this._wasInitialized = false;
194
+ this._wasDisclaimed = true;
195
+ this.model = model;
196
+ this._searchInputRef = React.createRef();
197
+ this.addClass('jp-extensionmanager-view');
198
+ this.trans = translator.load('jupyterlab');
199
+ this.header.addWidget(new Header(model, this.trans, this._searchInputRef));
200
+ const warning = new Warning(model, this.trans);
201
+ warning.title.label = this.trans.__('Warning');
202
+ this.addWidget(warning);
203
+ const installed = new PanelWithToolbar();
204
+ installed.addClass('jp-extensionmanager-installedlist');
205
+ installed.title.label = this.trans.__('Installed');
206
+ installed.toolbar.addItem('refresh', new ToolbarButton({
207
+ icon: refreshIcon,
208
+ onClick: () => {
209
+ model.refreshInstalled(true).catch(reason => {
210
+ console.error(`Failed to refresh the installed extensions list:\n${reason}`);
211
+ });
212
+ },
213
+ tooltip: this.trans.__('Refresh extensions list')
214
+ }));
215
+ installed.addWidget(new InstalledList(model, this.trans));
216
+ this.addWidget(installed);
217
+ if (this.model.canInstall) {
218
+ const searchResults = new SearchResult(model, this.trans);
219
+ searchResults.addClass('jp-extensionmanager-searchresults');
220
+ this.addWidget(searchResults);
221
+ }
222
+ this._wasDisclaimed = this.model.isDisclaimed;
223
+ if (this.model.isDisclaimed) {
224
+ this.content.collapse(0);
225
+ this.content.layout.setRelativeSizes([0, 1, 1]);
226
+ }
227
+ else {
228
+ // If warning is not disclaimed expand only the warning panel
229
+ this.content.expand(0);
230
+ this.content.collapse(1);
231
+ this.content.collapse(2);
232
+ }
233
+ this.model.stateChanged.connect(this._onDisclaimedChanged, this);
234
+ }
235
+ /**
236
+ * Dispose of the widget and its descendant widgets.
237
+ */
238
+ dispose() {
239
+ if (this.isDisposed) {
240
+ return;
241
+ }
242
+ this.model.stateChanged.disconnect(this._onDisclaimedChanged, this);
243
+ super.dispose();
244
+ }
385
245
  /**
386
246
  * Handle the DOM events for the extension manager search bar.
387
247
  *
@@ -394,9 +254,6 @@ risks or contain malicious code that runs on your machine.`)),
394
254
  */
395
255
  handleEvent(event) {
396
256
  switch (event.type) {
397
- case 'input':
398
- this.onSearch(this.inputNode.value);
399
- break;
400
257
  case 'focus':
401
258
  case 'blur':
402
259
  this._toggleFocused();
@@ -409,15 +266,23 @@ risks or contain malicious code that runs on your machine.`)),
409
266
  * A message handler invoked on a `'before-attach'` message.
410
267
  */
411
268
  onBeforeAttach(msg) {
412
- this.node.addEventListener('input', this);
413
269
  this.node.addEventListener('focus', this, true);
414
270
  this.node.addEventListener('blur', this, true);
271
+ super.onBeforeAttach(msg);
272
+ }
273
+ onBeforeShow(msg) {
274
+ if (!this._wasInitialized) {
275
+ this._wasInitialized = true;
276
+ this.model.refreshInstalled().catch(reason => {
277
+ console.log(`Failed to refresh installed extension list:\n${reason}`);
278
+ });
279
+ }
415
280
  }
416
281
  /**
417
282
  * A message handler invoked on an `'after-detach'` message.
418
283
  */
419
284
  onAfterDetach(msg) {
420
- this.node.removeEventListener('input', this);
285
+ super.onAfterDetach(msg);
421
286
  this.node.removeEventListener('focus', this, true);
422
287
  this.node.removeEventListener('blur', this, true);
423
288
  }
@@ -426,18 +291,27 @@ risks or contain malicious code that runs on your machine.`)),
426
291
  */
427
292
  onActivateRequest(msg) {
428
293
  if (this.isAttached) {
429
- const input = this.inputNode;
294
+ const input = this._searchInputRef.current;
430
295
  if (input) {
431
296
  input.focus();
432
297
  input.select();
433
298
  }
434
299
  }
300
+ super.onActivateRequest(msg);
301
+ }
302
+ _onDisclaimedChanged() {
303
+ if (!this._wasDisclaimed && this.model.isDisclaimed) {
304
+ this.content.collapse(0);
305
+ this.content.expand(1);
306
+ this.content.expand(2);
307
+ this.content.layout.setRelativeSizes([0, 1, 1]);
308
+ }
435
309
  }
436
310
  /**
437
311
  * Toggle the focused modifier based on the input node focus state.
438
312
  */
439
313
  _toggleFocused() {
440
- const focused = document.activeElement === this.inputNode;
314
+ const focused = document.activeElement === this._searchInputRef.current;
441
315
  this.toggleClass('lm-mod-focused', focused);
442
316
  }
443
317
  }