@jupyterlab/extensionmanager
Version:
JupyterLab - Extension Manager
338 lines • 18.7 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { Button, FilterBox, infoIcon, jupyterIcon, PanelWithToolbar, ReactWidget, refreshIcon, SidePanel, ToolbarButton, ToolbarButtonComponent } from '@jupyterlab/ui-components';
import * as React from 'react';
import ReactPaginate from 'react-paginate';
import { ListModel } from './model';
const BADGE_SIZE = 32;
const BADGE_QUERY_SIZE = Math.floor(devicePixelRatio * BADGE_SIZE);
function getExtensionGitHubUser(entry) {
if (entry.homepage_url &&
entry.homepage_url.startsWith('https://github.com/')) {
return entry.homepage_url.split('/')[3];
}
else if (entry.repository_url &&
entry.repository_url.startsWith('https://github.com/')) {
return entry.repository_url.split('/')[3];
}
return null;
}
/**
* VDOM for visualizing an extension entry.
*/
function ListEntry(props) {
const { canFetch, entry, supportInstallation, trans } = props;
const flagClasses = [];
if (entry.status && ['ok', 'warning', 'error'].indexOf(entry.status) !== -1) {
flagClasses.push(`jp-extensionmanager-entry-${entry.status}`);
}
const githubUser = canFetch ? getExtensionGitHubUser(entry) : null;
if (!entry.allowed) {
flagClasses.push(`jp-extensionmanager-entry-should-be-uninstalled`);
}
return (React.createElement("li", { className: `jp-extensionmanager-entry ${flagClasses.join(' ')}`, style: { display: 'flex' } },
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` } }))),
React.createElement("div", { className: "jp-extensionmanager-entry-description" },
React.createElement("div", { className: "jp-extensionmanager-entry-title" },
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))),
React.createElement("div", { className: "jp-extensionmanager-entry-version" },
React.createElement("div", { title: trans.__('Version: %1', entry.installed_version) }, entry.installed_version)),
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/stable/user/extensions.html') })),
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.') }))),
React.createElement("div", { className: "jp-extensionmanager-entry-content" },
React.createElement("div", { className: "jp-extensionmanager-entry-description" }, entry.description),
props.performAction && (React.createElement("div", { className: "jp-extensionmanager-entry-buttons" }, entry.installed ? (React.createElement(React.Fragment, null,
supportInstallation && (React.createElement(React.Fragment, null,
ListModel.entryHasUpdate(entry) && (React.createElement(Button, { onClick: () => props.performAction('install', entry, {
useVersion: entry.latest_version
}), title: trans.__('Update "%1" to "%2"', entry.name, entry.latest_version), minimal: true, small: true }, trans.__('Update to %1', entry.latest_version))),
React.createElement(Button, { onClick: () => props.performAction('uninstall', entry), title: trans.__('Uninstall "%1"', entry.name), minimal: true, small: true }, trans.__('Uninstall')))),
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'))))))))));
}
/**
* List view widget for extensions
*/
function ListView(props) {
var _a;
const { canFetch, performAction, supportInstallation, trans } = props;
return (React.createElement("div", { className: "jp-extensionmanager-listview-wrapper" },
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'))),
props.numPages > 1 && (React.createElement("div", { className: "jp-extensionmanager-pagination" },
React.createElement(ReactPaginate, { previousLabel: '<', nextLabel: '>', breakLabel: "...", breakClassName: 'break', 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), activeClassName: 'active' })))));
}
function ErrorMessage(props) {
return React.createElement("div", { className: "jp-extensionmanager-error" }, props.children);
}
class Header extends ReactWidget {
constructor(model, trans, searchInputRef) {
super();
this.model = model;
this.trans = trans;
this.searchInputRef = searchInputRef;
model.stateChanged.connect(this.update, this);
this.addClass('jp-extensionmanager-header');
}
render() {
return (React.createElement(React.Fragment, null,
React.createElement("div", { className: "jp-extensionmanager-title" },
React.createElement("span", null, this.trans.__('%1 Manager', this.model.name)),
this.model.installPath && (React.createElement(infoIcon.react, { className: "jp-extensionmanager-path", tag: "span", title: this.trans.__('Extension installation path: %1', this.model.installPath) }))),
React.createElement(FilterBox, { placeholder: this.trans.__('Search extensions'), disabled: !this.model.isDisclaimed, updateFilter: (fn, query) => {
this.model.query = query !== null && query !== void 0 ? query : '';
}, useFuzzyFilter: false, inputRef: this.searchInputRef }),
React.createElement("div", { className: `jp-extensionmanager-pending ${this.model.hasPendingActions() ? 'jp-mod-hasPending' : ''}` }),
this.model.actionError && (React.createElement(ErrorMessage, null,
React.createElement("p", null, this.trans.__('Error when performing an action.')),
React.createElement("p", null, this.trans.__('Reason given:')),
React.createElement("pre", null, this.model.actionError)))));
}
}
class Warning extends ReactWidget {
constructor(model, trans) {
super();
this.model = model;
this.trans = trans;
this.addClass('jp-extensionmanager-disclaimer');
model.stateChanged.connect(this.update, this);
}
render() {
return (React.createElement(React.Fragment, null,
React.createElement("p", null,
this.trans
.__(`The JupyterLab development team is excited to have a robust
third-party extension community. However, we do not review
third-party extensions, and some extensions may introduce security
risks or contain malicious code that runs on your machine. Moreover in order
to work, this panel needs to fetch data from web services. Do you agree to
activate this feature?`),
React.createElement("br", null),
React.createElement("a", { href: "https://jupyterlab.readthedocs.io/en/stable/privacy_policies.html", target: "_blank", rel: "noreferrer" }, this.trans.__('Please read the privacy policy.'))),
this.model.isDisclaimed ? (React.createElement(Button, { className: "jp-extensionmanager-disclaimer-disable", onClick: (e) => {
this.model.isDisclaimed = false;
}, title: this.trans.__('This will withdraw your consent.') }, this.trans.__('No'))) : (React.createElement("div", null,
React.createElement(Button, { className: "jp-extensionmanager-disclaimer-enable", onClick: () => {
this.model.isDisclaimed = true;
} }, this.trans.__('Yes')),
React.createElement(Button, { className: "jp-extensionmanager-disclaimer-disable", onClick: () => {
this.model.isEnabled = false;
}, title: this.trans.__('This will disable the extension manager panel; including the listing of installed extension.') }, this.trans.__('No, disable'))))));
}
}
class InstalledList extends ReactWidget {
constructor(model, trans) {
super();
this.model = model;
this.trans = trans;
model.stateChanged.connect(this.update, this);
}
render() {
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 => {
/* no-op */
}, performAction: this.model.isDisclaimed ? this.onAction.bind(this) : null, supportInstallation: this.model.canInstall && this.model.isDisclaimed }))));
}
/**
* Callback handler for when the user wants to perform an action on an extension.
*
* @param action The action to perform.
* @param entry The entry to perform the action on.
* @param actionOptions Additional options for the action.
*/
onAction(action, entry, actionOptions = {}) {
switch (action) {
case 'install':
return this.model.install(entry, actionOptions);
case 'uninstall':
return this.model.uninstall(entry);
case 'enable':
return this.model.enable(entry);
case 'disable':
return this.model.disable(entry);
default:
throw new Error(`Invalid action: ${action}`);
}
}
}
class SearchResult extends ReactWidget {
constructor(model, trans) {
super();
this.model = model;
this.trans = trans;
model.stateChanged.connect(this.update, this);
}
/**
* Callback handler for the user changes the page of the search result pagination.
*
* @param value The pagination page number.
*/
onPage(value) {
this.model.page = value;
}
/**
* Callback handler for when the user wants to perform an action on an extension.
*
* @param action The action to perform.
* @param entry The entry to perform the action on.
* @param actionOptions Additional options for the action.
*/
onAction(action, entry, actionOptions = {}) {
switch (action) {
case 'install':
return this.model.install(entry, actionOptions);
case 'uninstall':
return this.model.uninstall(entry);
case 'enable':
return this.model.enable(entry);
case 'disable':
return this.model.disable(entry);
default:
throw new Error(`Invalid action: ${action}`);
}
}
render() {
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 => {
this.onPage(value);
}, performAction: this.model.isDisclaimed ? this.onAction.bind(this) : null, supportInstallation: this.model.canInstall && this.model.isDisclaimed, trans: this.trans }))));
}
update() {
this.title.label = this.model.query
? this.trans.__('Search Results')
: this.trans.__('Discover');
super.update();
}
}
export class ExtensionsPanel extends SidePanel {
constructor(options) {
const { model, translator } = options;
super({ translator });
this._wasInitialized = false;
this._wasDisclaimed = true;
this.model = model;
this._searchInputRef = React.createRef();
this.addClass('jp-extensionmanager-view');
this.trans = translator.load('jupyterlab');
this.header.addWidget(new Header(model, this.trans, this._searchInputRef));
const warning = new Warning(model, this.trans);
warning.title.label = this.trans.__('Warning');
this.addWidget(warning);
const installed = new PanelWithToolbar();
installed.addClass('jp-extensionmanager-installedlist');
installed.toolbar.node.setAttribute('aria-label', this.trans.__('Extensions panel toolbar'));
installed.title.label = this.trans.__('Installed');
installed.toolbar.addItem('refresh', new ToolbarButton({
icon: refreshIcon,
onClick: () => {
model.refreshInstalled(true).catch(reason => {
console.error(`Failed to refresh the installed extensions list:\n${reason}`);
});
},
tooltip: this.trans.__('Refresh extensions list')
}));
installed.addWidget(new InstalledList(model, this.trans));
this.addWidget(installed);
if (this.model.canInstall) {
const searchResults = new SearchResult(model, this.trans);
searchResults.addClass('jp-extensionmanager-searchresults');
this.addWidget(searchResults);
}
this._wasDisclaimed = this.model.isDisclaimed;
if (this.model.isDisclaimed) {
this.content.collapse(0);
this.content.layout.setRelativeSizes([0, 1, 1]);
}
else {
// If warning is not disclaimed expand only the warning panel
this.content.expand(0);
this.content.collapse(1);
this.content.collapse(2);
}
this.model.stateChanged.connect(this._onStateChanged, this);
}
/**
* Dispose of the widget and its descendant widgets.
*/
dispose() {
if (this.isDisposed) {
return;
}
this.model.stateChanged.disconnect(this._onStateChanged, this);
super.dispose();
}
/**
* Handle the DOM events for the extension manager search bar.
*
* @param event - The DOM event sent to the extension manager search bar.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the search bar's DOM node.
* It should not be called directly by user code.
*/
handleEvent(event) {
switch (event.type) {
case 'focus':
case 'blur':
this._toggleFocused();
break;
default:
break;
}
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
this.node.addEventListener('focus', this, true);
this.node.addEventListener('blur', this, true);
super.onBeforeAttach(msg);
}
onBeforeShow(msg) {
if (!this._wasInitialized) {
this._wasInitialized = true;
this.model.refreshInstalled().catch(reason => {
console.log(`Failed to refresh installed extension list:\n${reason}`);
});
}
}
/**
* A message handler invoked on an `'after-detach'` message.
*/
onAfterDetach(msg) {
super.onAfterDetach(msg);
this.node.removeEventListener('focus', this, true);
this.node.removeEventListener('blur', this, true);
}
/**
* A message handler invoked on an `'activate-request'` message.
*/
onActivateRequest(msg) {
if (this.isAttached) {
const input = this._searchInputRef.current;
if (input) {
// Cover the cases, mainly on initial startup, where the input ref is not an input element
if (input.focus) {
input.focus();
}
if (input.select) {
input.select();
}
}
}
super.onActivateRequest(msg);
}
_onStateChanged() {
if (!this._wasDisclaimed && this.model.isDisclaimed) {
this.content.collapse(0);
this.content.expand(1);
this.content.expand(2);
}
this._wasDisclaimed = this.model.isDisclaimed;
}
/**
* Toggle the focused modifier based on the input node focus state.
*/
_toggleFocused() {
const focused = document.activeElement === this._searchInputRef.current;
this.toggleClass('lm-mod-focused', focused);
}
}
//# sourceMappingURL=widget.js.map