@jupyterlab/extensionmanager 4.0.0-alpha.2 → 4.0.0-alpha.21

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,299 +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.installed &&
120
- entry.pkg_type == 'source' &&
121
- !entry.blockedExtensionsEntry &&
122
- !(!entry.allowedExtensionsEntry && listMode === 'allow') &&
123
- ListModel.isDisclaimed() && (React.createElement(Button, { onClick: () => props.performAction('install', entry), minimal: true, small: true }, trans.__('Install'))),
124
- ListModel.entryHasUpdate(entry) &&
125
- entry.pkg_type == 'source' &&
126
- !entry.blockedExtensionsEntry &&
127
- !(!entry.allowedExtensionsEntry && listMode === 'allow') &&
128
- ListModel.isDisclaimed() && (React.createElement(Button, { onClick: () => props.performAction('install', entry), minimal: true, small: true }, trans.__('Update'))),
129
- entry.installed && entry.pkg_type == 'source' && (React.createElement(Button, { onClick: () => props.performAction('uninstall', entry), minimal: true, small: true }, trans.__('Uninstall'))),
130
- entry.enabled && entry.pkg_type == 'source' && (React.createElement(Button, { onClick: () => props.performAction('disable', entry), minimal: true, small: true }, trans.__('Disable'))),
131
- entry.installed && entry.pkg_type == 'source' && !entry.enabled && (React.createElement(Button, { onClick: () => props.performAction('enable', entry), minimal: true, small: true }, trans.__('Enable'))),
132
- entry.installed && entry.pkg_type == 'prebuilt' && (React.createElement("div", { className: "jp-extensionmanager-entry-buttons" },
133
- React.createElement(Button, { onClick: () => showDialog({
134
- title,
135
- body: (React.createElement("div", null, getPrebuiltUninstallInstruction(entry, trans))),
136
- buttons: [
137
- Dialog.okButton({
138
- label: trans.__('OK'),
139
- caption: trans.__('OK')
140
- })
141
- ]
142
- }).then(result => {
143
- return result.button.accept;
144
- }), minimal: true, small: true }, trans.__('About')))))))));
145
- }
146
- function getPrebuiltUninstallInstruction(entry, trans) {
147
- var _a, _b;
148
- if ((_a = entry.install) === null || _a === void 0 ? void 0 : _a.uninstallInstructions) {
149
- return (React.createElement("div", null,
150
- React.createElement("p", null, trans.__(`This is a prebuilt extension. To uninstall it, please
151
- apply following instructions.`)),
152
- React.createElement("p", null, trans.__((_b = entry.install) === null || _b === void 0 ? void 0 : _b.uninstallInstructions))));
153
- }
154
- return (React.createElement("div", null,
155
- React.createElement("p", null, trans.__(`This is a prebuilt extension. To uninstall it, please
156
- read the user guide on:`)),
157
- React.createElement("p", null,
158
- 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'))))))))));
159
49
  }
160
50
  /**
161
51
  * List view widget for extensions
162
52
  */
163
- export function ListView(props) {
164
- const translator = props.translator || nullTranslator;
165
- const trans = translator.load('jupyterlab');
166
- const entryViews = [];
167
- for (const entry of props.entries) {
168
- entryViews.push(React.createElement(ListEntry, { entry: entry, listMode: props.listMode, viewType: props.viewType, key: entry.name, performAction: props.performAction, translator: translator }));
169
- }
170
- let pagination;
171
- if (props.numPages > 1) {
172
- pagination = (React.createElement("div", { className: "jp-extensionmanager-pagination" },
173
- 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' })));
174
- }
175
- const listview = (React.createElement("ul", { className: "jp-extensionmanager-listview" }, entryViews));
53
+ function ListView(props) {
54
+ var _a;
55
+ const { canFetch, performAction, supportInstallation, trans } = props;
176
56
  return (React.createElement("div", { className: "jp-extensionmanager-listview-wrapper" },
177
- entryViews.length > 0 ? (listview) : (React.createElement("div", { key: "message", className: "jp-extensionmanager-listview-message" }, trans.__('No entries'))),
178
- 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' })))));
179
60
  }
180
61
  function ErrorMessage(props) {
181
- return (React.createElement("div", { key: "error-msg", className: "jp-extensionmanager-error" }, props.children));
62
+ return React.createElement("div", { className: "jp-extensionmanager-error" }, props.children);
182
63
  }
183
- /**
184
- *
185
- */
186
- export class CollapsibleSection extends React.Component {
187
- constructor(props) {
188
- super(props);
189
- this.state = {
190
- isOpen: props.isOpen ? true : false
191
- };
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');
192
72
  }
193
- /**
194
- * Render the collapsible section using the virtual DOM.
195
- */
196
73
  render() {
197
- let icon = this.state.isOpen ? caretDownIconStyled : caretRightIconStyled;
198
- let isOpen = this.state.isOpen;
199
- let className = 'jp-extensionmanager-headerText';
200
- if (this.props.disabled) {
201
- icon = caretRightIconStyled;
202
- isOpen = false;
203
- className = 'jp-extensionmanager-headerTextDisabled';
204
- }
205
74
  return (React.createElement(React.Fragment, null,
206
- React.createElement("div", { className: "jp-stack-panel-header" },
207
- React.createElement(ToolbarButtonComponent, { icon: icon, onClick: () => {
208
- this.handleCollapse();
209
- } }),
210
- React.createElement("span", { className: className }, this.props.header),
211
- !this.props.disabled && this.props.headerElements),
212
- React.createElement(Collapse, { isOpen: isOpen }, this.props.children)));
213
- }
214
- /**
215
- * Handler for search input changes.
216
- */
217
- handleCollapse() {
218
- this.setState({
219
- isOpen: !this.state.isOpen
220
- }, () => {
221
- if (this.props.onCollapse) {
222
- this.props.onCollapse(this.state.isOpen);
223
- }
224
- });
225
- }
226
- UNSAFE_componentWillReceiveProps(nextProps) {
227
- if (nextProps.forceOpen) {
228
- this.setState({
229
- isOpen: true
230
- });
231
- }
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)))));
232
86
  }
233
87
  }
234
- /**
235
- * The main view for the discovery extension.
236
- */
237
- export class ExtensionView extends VDomRenderer {
238
- constructor(app, serviceManager, settings, translator) {
239
- super(new ListModel(app, serviceManager, settings, translator));
240
- this.translator = translator || nullTranslator;
241
- this._trans = this.translator.load('jupyterlab');
242
- this._settings = settings;
243
- this._forceOpen = false;
244
- this.addClass('jp-extensionmanager-view');
245
- }
246
- /**
247
- * The search input node.
248
- */
249
- get inputNode() {
250
- 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);
251
95
  }
252
- /**
253
- * Render the extension view using the virtual DOM.
254
- */
255
96
  render() {
256
- const model = this.model;
257
- if (!model.listMode) {
258
- return [React.createElement("div", { key: "empty" })];
259
- }
260
- if (model.listMode === 'invalid') {
261
- return [
262
- React.createElement("div", { style: { padding: 8 }, key: "invalid" },
263
- React.createElement("div", null, this._trans
264
- .__(`The extension manager is disabled. Please contact your system
265
- administrator to verify the listings configuration.`)),
266
- React.createElement("div", null,
267
- 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.'))))
268
- ];
269
- }
270
- const pages = Math.ceil(model.totalEntries / model.pagination);
271
- const elements = [
272
- React.createElement(SearchBar, { key: "searchbar", placeholder: this._trans.__('SEARCH'), disabled: !ListModel.isDisclaimed(), settings: this._settings })
273
- ];
274
- if (model.promptBuild) {
275
- elements.push(React.createElement(BuildPrompt, { key: "promt", translator: this.translator, performBuild: () => {
276
- model.performBuild();
277
- }, ignoreBuild: () => {
278
- model.ignoreBuildRecommendation();
279
- } }));
280
- }
281
- // Indicator element for pending actions:
282
- elements.push(React.createElement("div", { key: "pending", className: `jp-extensionmanager-pending ${model.hasPendingActions() ? 'jp-mod-hasPending' : ''}` }));
283
- const content = [];
284
- content.push(React.createElement(CollapsibleSection, { key: "warning-section", isOpen: !ListModel.isDisclaimed(), disabled: false, header: this._trans.__('Warning') },
285
- React.createElement("div", { className: "jp-extensionmanager-disclaimer" },
286
- React.createElement("div", null, this._trans
97
+ return (React.createElement(React.Fragment, null,
98
+ React.createElement("p", null,
99
+ this.trans
287
100
  .__(`The JupyterLab development team is excited to have a robust
288
101
  third-party extension community. However, we do not review
289
102
  third-party extensions, and some extensions may introduce security
290
- risks or contain malicious code that runs on your machine.`)),
291
- React.createElement("div", { style: { paddingTop: 8 } },
292
- ListModel.isDisclaimed() && (React.createElement(Button, { className: "jp-extensionmanager-disclaimer-disable", onClick: (e) => {
293
- this._settings.set('disclaimed', false).catch(reason => {
294
- console.error(`Something went wrong when setting disclaimed.\n${reason}`);
295
- });
296
- } }, this._trans.__('Disable'))),
297
- !ListModel.isDisclaimed() && (React.createElement(Button, { className: "jp-extensionmanager-disclaimer-enable", onClick: (e) => {
298
- this._forceOpen = true;
299
- this._settings.set('disclaimed', true).catch(reason => {
300
- console.error(`Something went wrong when setting disclaimed.\n${reason}`);
301
- });
302
- } }, this._trans.__('Enable')))))));
303
- if (!model.initialized) {
304
- content.push(React.createElement("div", { key: "loading-placeholder", className: "jp-extensionmanager-loader" }, this._trans.__('Updating extensions list')));
305
- }
306
- else if (model.serverConnectionError !== null) {
307
- content.push(React.createElement(ErrorMessage, { key: "error-msg" },
308
- React.createElement("p", null, this._trans
309
- .__(`Error communicating with server extension. Consult the documentation
310
- for how to ensure that it is enabled.`)),
311
- React.createElement("p", null, this._trans.__('Reason given:')),
312
- React.createElement("pre", null, model.serverConnectionError)));
313
- }
314
- else if (model.serverRequirementsError !== null) {
315
- content.push(React.createElement(ErrorMessage, { key: "server-requirements-error" },
316
- React.createElement("p", null, this._trans.__('The server has some missing requirements for installing extensions.')),
317
- React.createElement("p", null, this._trans.__('Details:')),
318
- React.createElement("pre", null, model.serverRequirementsError)));
319
- }
320
- else {
321
- // List installed and discovery sections
322
- const installedContent = [];
323
- if (model.installedError !== null) {
324
- installedContent.push(React.createElement(ErrorMessage, { key: "install-error" }, `Error querying installed extensions${model.installedError ? `: ${model.installedError}` : '.'}`));
325
- }
326
- else {
327
- installedContent.push(React.createElement(ListView, { key: "installed-items", listMode: model.listMode, viewType: 'installed', entries: model.searchResult.length > 0
328
- ? model.installed.filter(entry => model.searchResult.indexOf(entry) > -1)
329
- : model.installed, numPages: 1, translator: this.translator, onPage: value => {
330
- /* no-op */
331
- }, performAction: this.onAction.bind(this) }));
332
- }
333
- 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: () => {
334
- model.refreshInstalled();
335
- }, tooltip: this._trans.__('Refresh extension list') }) }, installedContent));
336
- const searchContent = [];
337
- if (model.searchError !== null) {
338
- searchContent.push(React.createElement(ErrorMessage, { key: "search-error" }, `Error searching for extensions${model.searchError ? `: ${model.searchError}` : '.'}`));
339
- }
340
- else {
341
- searchContent.push(React.createElement(ListView, { key: "search-items", listMode: model.listMode, viewType: 'searchResult',
342
- // Filter out installed extensions:
343
- entries: model.searchResult.filter(entry => model.installed.indexOf(entry) === -1), numPages: pages, onPage: value => {
344
- this.onPage(value);
345
- }, performAction: this.onAction.bind(this), translator: this.translator }));
346
- }
347
- content.push(React.createElement(CollapsibleSection, { key: "search-section", isOpen: ListModel.isDisclaimed(), forceOpen: this._forceOpen, disabled: !ListModel.isDisclaimed(), header: model.query
348
- ? this._trans.__('Search Results')
349
- : this._trans.__('Discover'), onCollapse: (isOpen) => {
350
- if (isOpen && model.query === null) {
351
- model.query = '';
352
- }
353
- } }, searchContent));
354
- }
355
- elements.push(React.createElement("div", { key: "content", className: "jp-extensionmanager-content" }, content));
356
- // Reset the force open for future usage.
357
- this._forceOpen = false;
358
- 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 }))));
359
130
  }
360
131
  /**
361
- * 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.
362
133
  *
363
- * @param value The new query.
134
+ * @param action The action to perform.
135
+ * @param entry The entry to perform the action on.
364
136
  */
365
- onSearch(value) {
366
- 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);
367
158
  }
368
159
  /**
369
160
  * Callback handler for the user changes the page of the search result pagination.
@@ -393,6 +184,74 @@ risks or contain malicious code that runs on your machine.`)),
393
184
  throw new Error(`Invalid action: ${action}`);
394
185
  }
395
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
+ }
396
255
  /**
397
256
  * Handle the DOM events for the extension manager search bar.
398
257
  *
@@ -405,9 +264,6 @@ risks or contain malicious code that runs on your machine.`)),
405
264
  */
406
265
  handleEvent(event) {
407
266
  switch (event.type) {
408
- case 'input':
409
- this.onSearch(this.inputNode.value);
410
- break;
411
267
  case 'focus':
412
268
  case 'blur':
413
269
  this._toggleFocused();
@@ -420,15 +276,23 @@ risks or contain malicious code that runs on your machine.`)),
420
276
  * A message handler invoked on a `'before-attach'` message.
421
277
  */
422
278
  onBeforeAttach(msg) {
423
- this.node.addEventListener('input', this);
424
279
  this.node.addEventListener('focus', this, true);
425
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
+ }
426
290
  }
427
291
  /**
428
292
  * A message handler invoked on an `'after-detach'` message.
429
293
  */
430
294
  onAfterDetach(msg) {
431
- this.node.removeEventListener('input', this);
295
+ super.onAfterDetach(msg);
432
296
  this.node.removeEventListener('focus', this, true);
433
297
  this.node.removeEventListener('blur', this, true);
434
298
  }
@@ -437,18 +301,27 @@ risks or contain malicious code that runs on your machine.`)),
437
301
  */
438
302
  onActivateRequest(msg) {
439
303
  if (this.isAttached) {
440
- const input = this.inputNode;
304
+ const input = this._searchInputRef.current;
441
305
  if (input) {
442
306
  input.focus();
443
307
  input.select();
444
308
  }
445
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;
446
319
  }
447
320
  /**
448
321
  * Toggle the focused modifier based on the input node focus state.
449
322
  */
450
323
  _toggleFocused() {
451
- const focused = document.activeElement === this.inputNode;
324
+ const focused = document.activeElement === this._searchInputRef.current;
452
325
  this.toggleClass('lm-mod-focused', focused);
453
326
  }
454
327
  }