@jupyterlab/extensionmanager 4.0.0-alpha.8 → 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/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,140 @@ 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 { canFetch, 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
- }
87
- 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)) {
29
+ const githubUser = canFetch ? getExtensionGitHubUser(entry) : null;
30
+ if (!entry.allowed) {
99
31
  flagClasses.push(`jp-extensionmanager-entry-should-be-uninstalled`);
100
32
  }
101
- if (listMode === 'allow' && !entry.allowedExtensionsEntry) {
102
- flagClasses.push(`jp-extensionmanager-entry-should-be-uninstalled`);
103
- }
104
- 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` } }))),
33
+ return (React.createElement("li", { className: `jp-extensionmanager-entry ${flagClasses.join(' ')}`, style: { display: 'flex' } },
34
+ 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
35
  React.createElement("div", { className: "jp-extensionmanager-entry-description" },
109
36
  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" }))),
37
+ React.createElement("div", { className: "jp-extensionmanager-entry-name" }, entry.homepage_url ? (React.createElement("a", { href: entry.homepage_url, target: "_blank", rel: "noopener noreferrer", title: trans.__('%1 extension home page', entry.name) }, entry.name)) : (React.createElement("div", null, entry.name))),
38
+ React.createElement("div", { className: "jp-extensionmanager-entry-version" },
39
+ React.createElement("div", { title: trans.__('Version: %1', entry.installed_version) }, entry.installed_version)),
40
+ entry.installed && !entry.allowed && (React.createElement(ToolbarButtonComponent, { icon: infoIcon, iconLabel: trans.__('%1 extension is not allowed anymore. Please uninstall it immediately or contact your administrator.', entry.name), onClick: () => window.open('https://jupyterlab.readthedocs.io/en/latest/user/extensions.html') })),
41
+ 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
42
  React.createElement("div", { className: "jp-extensionmanager-entry-content" },
117
43
  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"))));
44
+ props.performAction && (React.createElement("div", { className: "jp-extensionmanager-entry-buttons" }, entry.installed ? (React.createElement(React.Fragment, null,
45
+ supportInstallation && (React.createElement(React.Fragment, null,
46
+ ListModel.entryHasUpdate(entry) && (React.createElement(Button, { onClick: () => props.performAction('install', entry), title: trans.__('Update "%1" to "%2"', entry.name, entry.latest_version), minimal: true, small: true }, trans.__('Update to %1', entry.latest_version))),
47
+ React.createElement(Button, { onClick: () => props.performAction('uninstall', entry), title: trans.__('Uninstall "%1"', entry.name), minimal: true, small: true }, trans.__('Uninstall')))),
48
+ entry.enabled ? (React.createElement(Button, { onClick: () => props.performAction('disable', entry), title: trans.__('Disable "%1"', entry.name), minimal: true, small: true }, trans.__('Disable'))) : (React.createElement(Button, { onClick: () => props.performAction('enable', entry), title: trans.__('Enable "%1"', entry.name), minimal: true, small: true }, trans.__('Enable'))))) : (supportInstallation && (React.createElement(Button, { onClick: () => props.performAction('install', entry), title: trans.__('Install "%1"', entry.name), minimal: true, small: true }, trans.__('Install'))))))))));
148
49
  }
149
50
  /**
150
51
  * List view widget for extensions
151
52
  */
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));
53
+ function ListView(props) {
54
+ var _a;
55
+ const { canFetch, performAction, supportInstallation, trans } = props;
165
56
  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));
57
+ props.entries.length > 0 ? (React.createElement("ul", { className: "jp-extensionmanager-listview" }, props.entries.map(entry => (React.createElement(ListEntry, { key: entry.name, canFetch: canFetch, entry: entry, performAction: performAction, supportInstallation: supportInstallation, trans: trans }))))) : (React.createElement("div", { key: "message", className: "jp-extensionmanager-listview-message" }, trans.__('No entries'))),
58
+ props.numPages > 1 && (React.createElement("div", { className: "jp-extensionmanager-pagination" },
59
+ React.createElement(ReactPaginate, { previousLabel: '<', nextLabel: '>', breakLabel: "...", breakClassName: 'break-me', initialPage: ((_a = props.initialPage) !== null && _a !== void 0 ? _a : 1) - 1, pageCount: props.numPages, marginPagesDisplayed: 2, pageRangeDisplayed: 3, onPageChange: (data) => props.onPage(data.selected + 1), containerClassName: 'pagination', activeClassName: 'active' })))));
168
60
  }
169
61
  function ErrorMessage(props) {
170
- return (React.createElement("div", { key: "error-msg", className: "jp-extensionmanager-error" }, props.children));
62
+ return React.createElement("div", { className: "jp-extensionmanager-error" }, props.children);
171
63
  }
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
- };
64
+ class Header extends ReactWidget {
65
+ constructor(model, trans, searchInputRef) {
66
+ super();
67
+ this.model = model;
68
+ this.trans = trans;
69
+ this.searchInputRef = searchInputRef;
70
+ model.stateChanged.connect(this.update, this);
71
+ this.addClass('jp-extensionmanager-header');
181
72
  }
182
- /**
183
- * Render the collapsible section using the virtual DOM.
184
- */
185
73
  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
74
  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
- }
75
+ React.createElement("div", { className: "jp-extensionmanager-title" },
76
+ React.createElement("span", null, this.trans.__('%1 Manager', this.model.name)),
77
+ this.model.installPath && (React.createElement(infoIcon.react, { className: "jp-extensionmanager-path", tag: "span", title: this.trans.__('Extension installation path: %1', this.model.installPath) }))),
78
+ React.createElement(FilterBox, { placeholder: this.trans.__('Search'), disabled: !this.model.isDisclaimed, updateFilter: (fn, query) => {
79
+ this.model.query = query !== null && query !== void 0 ? query : '';
80
+ }, useFuzzyFilter: false, inputRef: this.searchInputRef }),
81
+ React.createElement("div", { className: `jp-extensionmanager-pending ${this.model.hasPendingActions() ? 'jp-mod-hasPending' : ''}` }),
82
+ this.model.actionError && (React.createElement(ErrorMessage, null,
83
+ React.createElement("p", null, this.trans.__('Error when performing an action.')),
84
+ React.createElement("p", null, this.trans.__('Reason given:')),
85
+ React.createElement("pre", null, this.model.actionError)))));
221
86
  }
222
87
  }
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');
88
+ class Warning extends ReactWidget {
89
+ constructor(model, trans) {
90
+ super();
91
+ this.model = model;
92
+ this.trans = trans;
93
+ this.addClass('jp-extensionmanager-disclaimer');
94
+ model.stateChanged.connect(this.update, this);
240
95
  }
241
- /**
242
- * Render the extension view using the virtual DOM.
243
- */
244
96
  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
97
+ return (React.createElement(React.Fragment, null,
98
+ React.createElement("p", null,
99
+ this.trans
277
100
  .__(`The JupyterLab development team is excited to have a robust
278
101
  third-party extension community. However, we do not review
279
102
  third-party extensions, and some extensions may introduce security
280
- 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;
103
+ risks or contain malicious code that runs on your machine. Moreover in order
104
+ to work, this panel needs to fetch data from web services. Do you agree to
105
+ activate this feature?`),
106
+ React.createElement("br", null),
107
+ React.createElement("a", { href: "https://jupyterlab.readthedocs.io/en/latest/privacy_policies.html", target: "_blank", rel: "noreferrer" }, this.trans.__('Please read the privacy policy.'))),
108
+ this.model.isDisclaimed ? (React.createElement(Button, { className: "jp-extensionmanager-disclaimer-disable", onClick: (e) => {
109
+ this.model.isDisclaimed = false;
110
+ }, title: this.trans.__('This will withdraw your consent.') }, this.trans.__('No'))) : (React.createElement("div", null,
111
+ React.createElement(Button, { className: "jp-extensionmanager-disclaimer-enable", onClick: () => {
112
+ this.model.isDisclaimed = true;
113
+ } }, this.trans.__('Yes')),
114
+ React.createElement(Button, { className: "jp-extensionmanager-disclaimer-disable", onClick: () => {
115
+ this.model.isEnabled = false;
116
+ }, title: this.trans.__('This will disable the extension manager panel; including the listing of installed extension.') }, this.trans.__('No, disable'))))));
117
+ }
118
+ }
119
+ class InstalledList extends ReactWidget {
120
+ constructor(model, trans) {
121
+ super();
122
+ this.model = model;
123
+ this.trans = trans;
124
+ model.stateChanged.connect(this.update, this);
125
+ }
126
+ render() {
127
+ 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, { canFetch: this.model.isDisclaimed, entries: this.model.installed.filter(pkg => new RegExp(this.model.query.toLowerCase()).test(pkg.name)), numPages: 1, trans: this.trans, onPage: value => {
128
+ /* no-op */
129
+ }, performAction: this.model.isDisclaimed ? this.onAction.bind(this) : null, supportInstallation: this.model.canInstall && this.model.isDisclaimed }))));
348
130
  }
349
131
  /**
350
- * Callback handler for the user specifies a new search query.
132
+ * Callback handler for when the user wants to perform an action on an extension.
351
133
  *
352
- * @param value The new query.
134
+ * @param action The action to perform.
135
+ * @param entry The entry to perform the action on.
353
136
  */
354
- onSearch(value) {
355
- this.model.query = value;
137
+ onAction(action, entry) {
138
+ switch (action) {
139
+ case 'install':
140
+ return this.model.install(entry);
141
+ case 'uninstall':
142
+ return this.model.uninstall(entry);
143
+ case 'enable':
144
+ return this.model.enable(entry);
145
+ case 'disable':
146
+ return this.model.disable(entry);
147
+ default:
148
+ throw new Error(`Invalid action: ${action}`);
149
+ }
150
+ }
151
+ }
152
+ class SearchResult extends ReactWidget {
153
+ constructor(model, trans) {
154
+ super();
155
+ this.model = model;
156
+ this.trans = trans;
157
+ model.stateChanged.connect(this.update, this);
356
158
  }
357
159
  /**
358
160
  * Callback handler for the user changes the page of the search result pagination.
@@ -382,6 +184,74 @@ risks or contain malicious code that runs on your machine.`)),
382
184
  throw new Error(`Invalid action: ${action}`);
383
185
  }
384
186
  }
187
+ render() {
188
+ 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, { canFetch: this.model.isDisclaimed, entries: this.model.searchResult, initialPage: this.model.page, numPages: this.model.lastPage, onPage: value => {
189
+ this.onPage(value);
190
+ }, performAction: this.model.isDisclaimed ? this.onAction.bind(this) : null, supportInstallation: this.model.canInstall && this.model.isDisclaimed, trans: this.trans }))));
191
+ }
192
+ update() {
193
+ this.title.label = this.model.query
194
+ ? this.trans.__('Search Results')
195
+ : this.trans.__('Discover');
196
+ super.update();
197
+ }
198
+ }
199
+ export class ExtensionsPanel extends SidePanel {
200
+ constructor(options) {
201
+ const { model, translator } = options;
202
+ super({ translator });
203
+ this._wasInitialized = false;
204
+ this._wasDisclaimed = true;
205
+ this.model = model;
206
+ this._searchInputRef = React.createRef();
207
+ this.addClass('jp-extensionmanager-view');
208
+ this.trans = translator.load('jupyterlab');
209
+ this.header.addWidget(new Header(model, this.trans, this._searchInputRef));
210
+ const warning = new Warning(model, this.trans);
211
+ warning.title.label = this.trans.__('Warning');
212
+ this.addWidget(warning);
213
+ const installed = new PanelWithToolbar();
214
+ installed.addClass('jp-extensionmanager-installedlist');
215
+ installed.title.label = this.trans.__('Installed');
216
+ installed.toolbar.addItem('refresh', new ToolbarButton({
217
+ icon: refreshIcon,
218
+ onClick: () => {
219
+ model.refreshInstalled(true).catch(reason => {
220
+ console.error(`Failed to refresh the installed extensions list:\n${reason}`);
221
+ });
222
+ },
223
+ tooltip: this.trans.__('Refresh extensions list')
224
+ }));
225
+ installed.addWidget(new InstalledList(model, this.trans));
226
+ this.addWidget(installed);
227
+ if (this.model.canInstall) {
228
+ const searchResults = new SearchResult(model, this.trans);
229
+ searchResults.addClass('jp-extensionmanager-searchresults');
230
+ this.addWidget(searchResults);
231
+ }
232
+ this._wasDisclaimed = this.model.isDisclaimed;
233
+ if (this.model.isDisclaimed) {
234
+ this.content.collapse(0);
235
+ this.content.layout.setRelativeSizes([0, 1, 1]);
236
+ }
237
+ else {
238
+ // If warning is not disclaimed expand only the warning panel
239
+ this.content.expand(0);
240
+ this.content.collapse(1);
241
+ this.content.collapse(2);
242
+ }
243
+ this.model.stateChanged.connect(this._onStateChanged, this);
244
+ }
245
+ /**
246
+ * Dispose of the widget and its descendant widgets.
247
+ */
248
+ dispose() {
249
+ if (this.isDisposed) {
250
+ return;
251
+ }
252
+ this.model.stateChanged.disconnect(this._onStateChanged, this);
253
+ super.dispose();
254
+ }
385
255
  /**
386
256
  * Handle the DOM events for the extension manager search bar.
387
257
  *
@@ -394,9 +264,6 @@ risks or contain malicious code that runs on your machine.`)),
394
264
  */
395
265
  handleEvent(event) {
396
266
  switch (event.type) {
397
- case 'input':
398
- this.onSearch(this.inputNode.value);
399
- break;
400
267
  case 'focus':
401
268
  case 'blur':
402
269
  this._toggleFocused();
@@ -409,15 +276,23 @@ risks or contain malicious code that runs on your machine.`)),
409
276
  * A message handler invoked on a `'before-attach'` message.
410
277
  */
411
278
  onBeforeAttach(msg) {
412
- this.node.addEventListener('input', this);
413
279
  this.node.addEventListener('focus', this, true);
414
280
  this.node.addEventListener('blur', this, true);
281
+ super.onBeforeAttach(msg);
282
+ }
283
+ onBeforeShow(msg) {
284
+ if (!this._wasInitialized) {
285
+ this._wasInitialized = true;
286
+ this.model.refreshInstalled().catch(reason => {
287
+ console.log(`Failed to refresh installed extension list:\n${reason}`);
288
+ });
289
+ }
415
290
  }
416
291
  /**
417
292
  * A message handler invoked on an `'after-detach'` message.
418
293
  */
419
294
  onAfterDetach(msg) {
420
- this.node.removeEventListener('input', this);
295
+ super.onAfterDetach(msg);
421
296
  this.node.removeEventListener('focus', this, true);
422
297
  this.node.removeEventListener('blur', this, true);
423
298
  }
@@ -426,18 +301,27 @@ risks or contain malicious code that runs on your machine.`)),
426
301
  */
427
302
  onActivateRequest(msg) {
428
303
  if (this.isAttached) {
429
- const input = this.inputNode;
304
+ const input = this._searchInputRef.current;
430
305
  if (input) {
431
306
  input.focus();
432
307
  input.select();
433
308
  }
434
309
  }
310
+ super.onActivateRequest(msg);
311
+ }
312
+ _onStateChanged() {
313
+ if (!this._wasDisclaimed && this.model.isDisclaimed) {
314
+ this.content.collapse(0);
315
+ this.content.expand(1);
316
+ this.content.expand(2);
317
+ }
318
+ this._wasDisclaimed = this.model.isDisclaimed;
435
319
  }
436
320
  /**
437
321
  * Toggle the focused modifier based on the input node focus state.
438
322
  */
439
323
  _toggleFocused() {
440
- const focused = document.activeElement === this.inputNode;
324
+ const focused = document.activeElement === this._searchInputRef.current;
441
325
  this.toggleClass('lm-mod-focused', focused);
442
326
  }
443
327
  }