|
@@ -3,6 +3,7 @@
|
|
|
|
|
|
import { VDomRenderer, ToolbarButtonComponent } from '@jupyterlab/apputils';
|
|
import { VDomRenderer, ToolbarButtonComponent } from '@jupyterlab/apputils';
|
|
import { ServiceManager } from '@jupyterlab/services';
|
|
import { ServiceManager } from '@jupyterlab/services';
|
|
|
|
+import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
import {
|
|
import {
|
|
Button,
|
|
Button,
|
|
caretDownIcon,
|
|
caretDownIcon,
|
|
@@ -10,6 +11,7 @@ import {
|
|
Collapse,
|
|
Collapse,
|
|
InputGroup,
|
|
InputGroup,
|
|
jupyterIcon,
|
|
jupyterIcon,
|
|
|
|
+ listingsInfoIcon,
|
|
refreshIcon
|
|
refreshIcon
|
|
} from '@jupyterlab/ui-components';
|
|
} from '@jupyterlab/ui-components';
|
|
|
|
|
|
@@ -18,7 +20,7 @@ import * as React from 'react';
|
|
import ReactPaginate from 'react-paginate';
|
|
import ReactPaginate from 'react-paginate';
|
|
|
|
|
|
import { ListModel, IEntry, Action } from './model';
|
|
import { ListModel, IEntry, Action } from './model';
|
|
-import { isJupyterOrg } from './query';
|
|
|
|
|
|
+import { isJupyterOrg } from './npm';
|
|
|
|
|
|
// TODO: Replace pagination with lazy loading of lower search results
|
|
// TODO: Replace pagination with lazy loading of lower search results
|
|
|
|
|
|
@@ -53,16 +55,69 @@ export class SearchBar extends React.Component<
|
|
*/
|
|
*/
|
|
render(): React.ReactNode {
|
|
render(): React.ReactNode {
|
|
return (
|
|
return (
|
|
- <div className="jp-extensionmanager-search-bar">
|
|
|
|
- <InputGroup
|
|
|
|
- className="jp-extensionmanager-search-wrapper"
|
|
|
|
- type="text"
|
|
|
|
- placeholder={this.props.placeholder}
|
|
|
|
- onChange={this.handleChange}
|
|
|
|
- value={this.state.value}
|
|
|
|
- rightIcon="search"
|
|
|
|
- />
|
|
|
|
- </div>
|
|
|
|
|
|
+ <>
|
|
|
|
+ <div className="jp-extensionmanager-search-bar">
|
|
|
|
+ <InputGroup
|
|
|
|
+ className="jp-extensionmanager-search-wrapper"
|
|
|
|
+ type="text"
|
|
|
|
+ placeholder={this.props.placeholder}
|
|
|
|
+ onChange={this.handleChange}
|
|
|
|
+ value={this.state.value}
|
|
|
|
+ rightIcon="search"
|
|
|
|
+ disabled={this.props.disabled}
|
|
|
|
+ />
|
|
|
|
+ </div>
|
|
|
|
+ <CollapsibleSection
|
|
|
|
+ key="warning-section"
|
|
|
|
+ isOpen={true}
|
|
|
|
+ disabled={false}
|
|
|
|
+ header={'Warning'}
|
|
|
|
+ >
|
|
|
|
+ <div className="jp-extensionmanager-disclaimer">
|
|
|
|
+ <div>
|
|
|
|
+ Extensions installed contain arbitrary code that can execute on
|
|
|
|
+ your machine that may contain malicious code.
|
|
|
|
+ </div>
|
|
|
|
+ <div style={{ paddingTop: 8 }}>
|
|
|
|
+ I understand extensions contain arbitrary code.
|
|
|
|
+ </div>
|
|
|
|
+ <div style={{ paddingTop: 8 }}>
|
|
|
|
+ {ListModel.isDisclaimed() && (
|
|
|
|
+ <Button
|
|
|
|
+ className="jp-extensionmanager-disclaimer-disable"
|
|
|
|
+ onClick={(e: React.MouseEvent<Element, MouseEvent>) => {
|
|
|
|
+ this.props.settings
|
|
|
|
+ .set('disclaimed', false)
|
|
|
|
+ .catch(reason => {
|
|
|
|
+ console.error(
|
|
|
|
+ `Something went wrong when setting disclaimed.\n${reason}`
|
|
|
|
+ );
|
|
|
|
+ });
|
|
|
|
+ }}
|
|
|
|
+ >
|
|
|
|
+ Disable
|
|
|
|
+ </Button>
|
|
|
|
+ )}
|
|
|
|
+ {!ListModel.isDisclaimed() && (
|
|
|
|
+ <Button
|
|
|
|
+ className="jp-extensionmanager-disclaimer-enable"
|
|
|
|
+ onClick={(e: React.MouseEvent<Element, MouseEvent>) => {
|
|
|
|
+ this.props.settings
|
|
|
|
+ .set('disclaimed', true)
|
|
|
|
+ .catch(reason => {
|
|
|
|
+ console.error(
|
|
|
|
+ `Something went wrong when setting disclaimed.\n${reason}`
|
|
|
|
+ );
|
|
|
|
+ });
|
|
|
|
+ }}
|
|
|
|
+ >
|
|
|
|
+ Enable
|
|
|
|
+ </Button>
|
|
|
|
+ )}
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </CollapsibleSection>
|
|
|
|
+ </>
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
@@ -89,6 +144,10 @@ export namespace SearchBar {
|
|
* The placeholder string to use in the search bar input field when empty.
|
|
* The placeholder string to use in the search bar input field when empty.
|
|
*/
|
|
*/
|
|
placeholder: string;
|
|
placeholder: string;
|
|
|
|
+
|
|
|
|
+ disabled: boolean;
|
|
|
|
+
|
|
|
|
+ settings: ISettingRegistry.ISettings;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -147,15 +206,34 @@ namespace BuildPrompt {
|
|
* VDOM for visualizing an extension entry.
|
|
* VDOM for visualizing an extension entry.
|
|
*/
|
|
*/
|
|
function ListEntry(props: ListEntry.IProperties): React.ReactElement<any> {
|
|
function ListEntry(props: ListEntry.IProperties): React.ReactElement<any> {
|
|
- const { entry } = props;
|
|
|
|
|
|
+ const { entry, listMode, viewType } = props;
|
|
const flagClasses = [];
|
|
const flagClasses = [];
|
|
if (entry.status && ['ok', 'warning', 'error'].indexOf(entry.status) !== -1) {
|
|
if (entry.status && ['ok', 'warning', 'error'].indexOf(entry.status) !== -1) {
|
|
flagClasses.push(`jp-extensionmanager-entry-${entry.status}`);
|
|
flagClasses.push(`jp-extensionmanager-entry-${entry.status}`);
|
|
}
|
|
}
|
|
let title = entry.name;
|
|
let title = entry.name;
|
|
if (isJupyterOrg(entry.name)) {
|
|
if (isJupyterOrg(entry.name)) {
|
|
- flagClasses.push(`jp-extensionmanager-entry-mod-whitelisted`);
|
|
|
|
- title = `${entry.name} (Developed by Project Jupyter)`;
|
|
|
|
|
|
+ flagClasses.push(`jp-extensionmanager-entry-mod-jupyterlab-org`);
|
|
|
|
+ }
|
|
|
|
+ if (
|
|
|
|
+ listMode === 'black' &&
|
|
|
|
+ entry.blacklistEntry &&
|
|
|
|
+ viewType === 'searchResult'
|
|
|
|
+ ) {
|
|
|
|
+ return <li></li>;
|
|
|
|
+ }
|
|
|
|
+ if (
|
|
|
|
+ listMode === 'white' &&
|
|
|
|
+ !entry.whitelistEntry &&
|
|
|
|
+ viewType === 'searchResult'
|
|
|
|
+ ) {
|
|
|
|
+ return <li></li>;
|
|
|
|
+ }
|
|
|
|
+ if (listMode === 'black' && entry.blacklistEntry?.name) {
|
|
|
|
+ flagClasses.push(`jp-extensionmanager-entry-should-be-uninstalled`);
|
|
|
|
+ }
|
|
|
|
+ if (listMode === 'white' && !entry.whitelistEntry) {
|
|
|
|
+ flagClasses.push(`jp-extensionmanager-entry-should-be-uninstalled`);
|
|
}
|
|
}
|
|
return (
|
|
return (
|
|
<li
|
|
<li
|
|
@@ -168,36 +246,66 @@ function ListEntry(props: ListEntry.IProperties): React.ReactElement<any> {
|
|
{entry.name}
|
|
{entry.name}
|
|
</a>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
- <jupyterIcon.react
|
|
|
|
- className="jp-extensionmanager-entry-jupyter-org"
|
|
|
|
- top="1px"
|
|
|
|
- height="auto"
|
|
|
|
- width="1em"
|
|
|
|
- />
|
|
|
|
|
|
+ {isJupyterOrg(entry.name) && (
|
|
|
|
+ <ToolbarButtonComponent
|
|
|
|
+ icon={jupyterIcon}
|
|
|
|
+ iconLabel={entry.name + ' (Developed by Project Jupyter)'}
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ {entry.blacklistEntry && (
|
|
|
|
+ <ToolbarButtonComponent
|
|
|
|
+ icon={listingsInfoIcon}
|
|
|
|
+ iconLabel={`${entry.name} extension has been blacklisted since install. Please uninstall immediately and contact your blacklist administrator.`}
|
|
|
|
+ onClick={() =>
|
|
|
|
+ window.open(
|
|
|
|
+ 'https://jupyterlab.readthedocs.io/en/stable/user/extensions.html'
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ {!entry.whitelistEntry &&
|
|
|
|
+ viewType === 'installed' &&
|
|
|
|
+ listMode === 'white' && (
|
|
|
|
+ <ToolbarButtonComponent
|
|
|
|
+ icon={listingsInfoIcon}
|
|
|
|
+ iconLabel={`${entry.name} extension has been removed from the whitelist since installation. Please uninstall immediately and contact your whitelist administrator.`}
|
|
|
|
+ onClick={() =>
|
|
|
|
+ window.open(
|
|
|
|
+ 'https://jupyterlab.readthedocs.io/en/stable/user/extensions.html'
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
</div>
|
|
</div>
|
|
<div className="jp-extensionmanager-entry-content">
|
|
<div className="jp-extensionmanager-entry-content">
|
|
<div className="jp-extensionmanager-entry-description">
|
|
<div className="jp-extensionmanager-entry-description">
|
|
{entry.description}
|
|
{entry.description}
|
|
</div>
|
|
</div>
|
|
<div className="jp-extensionmanager-entry-buttons">
|
|
<div className="jp-extensionmanager-entry-buttons">
|
|
- {!entry.installed && (
|
|
|
|
- <Button
|
|
|
|
- onClick={() => props.performAction('install', entry)}
|
|
|
|
- minimal
|
|
|
|
- small
|
|
|
|
- >
|
|
|
|
- Install
|
|
|
|
- </Button>
|
|
|
|
- )}
|
|
|
|
- {ListModel.entryHasUpdate(entry) && (
|
|
|
|
- <Button
|
|
|
|
- onClick={() => props.performAction('install', entry)}
|
|
|
|
- minimal
|
|
|
|
- small
|
|
|
|
- >
|
|
|
|
- Update
|
|
|
|
- </Button>
|
|
|
|
- )}
|
|
|
|
|
|
+ {!entry.installed &&
|
|
|
|
+ !entry.blacklistEntry &&
|
|
|
|
+ !(!entry.whitelistEntry && listMode === 'white') &&
|
|
|
|
+ ListModel.isDisclaimed() && (
|
|
|
|
+ <Button
|
|
|
|
+ onClick={() => props.performAction('install', entry)}
|
|
|
|
+ minimal
|
|
|
|
+ small
|
|
|
|
+ >
|
|
|
|
+ Install
|
|
|
|
+ </Button>
|
|
|
|
+ )}
|
|
|
|
+ {ListModel.entryHasUpdate(entry) &&
|
|
|
|
+ !entry.blacklistEntry &&
|
|
|
|
+ !(!entry.whitelistEntry && listMode === 'white') &&
|
|
|
|
+ ListModel.isDisclaimed() && (
|
|
|
|
+ <Button
|
|
|
|
+ onClick={() => props.performAction('install', entry)}
|
|
|
|
+ minimal
|
|
|
|
+ small
|
|
|
|
+ >
|
|
|
|
+ Update
|
|
|
|
+ </Button>
|
|
|
|
+ )}
|
|
{entry.installed && (
|
|
{entry.installed && (
|
|
<Button
|
|
<Button
|
|
onClick={() => props.performAction('uninstall', entry)}
|
|
onClick={() => props.performAction('uninstall', entry)}
|
|
@@ -241,6 +349,16 @@ export namespace ListEntry {
|
|
*/
|
|
*/
|
|
entry: IEntry;
|
|
entry: IEntry;
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
+ * The list mode to apply.
|
|
|
|
+ */
|
|
|
|
+ listMode: 'black' | 'white' | 'default';
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * The requested view type.
|
|
|
|
+ */
|
|
|
|
+ viewType: 'installed' | 'searchResult';
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* Callback to use for performing an action on the entry.
|
|
* Callback to use for performing an action on the entry.
|
|
*/
|
|
*/
|
|
@@ -257,6 +375,8 @@ export function ListView(props: ListView.IProperties): React.ReactElement<any> {
|
|
entryViews.push(
|
|
entryViews.push(
|
|
<ListEntry
|
|
<ListEntry
|
|
entry={entry}
|
|
entry={entry}
|
|
|
|
+ listMode={props.listMode}
|
|
|
|
+ viewType={props.viewType}
|
|
key={entry.name}
|
|
key={entry.name}
|
|
performAction={props.performAction}
|
|
performAction={props.performAction}
|
|
/>
|
|
/>
|
|
@@ -315,6 +435,16 @@ export namespace ListView {
|
|
*/
|
|
*/
|
|
numPages: number;
|
|
numPages: number;
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
+ * The list mode to apply.
|
|
|
|
+ */
|
|
|
|
+ listMode: 'black' | 'white' | 'default';
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * The requested view type.
|
|
|
|
+ */
|
|
|
|
+ viewType: 'installed' | 'searchResult';
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* The callback to use for changing the page
|
|
* The callback to use for changing the page
|
|
*/
|
|
*/
|
|
@@ -351,7 +481,7 @@ export class CollapsibleSection extends React.Component<
|
|
constructor(props: CollapsibleSection.IProperties) {
|
|
constructor(props: CollapsibleSection.IProperties) {
|
|
super(props);
|
|
super(props);
|
|
this.state = {
|
|
this.state = {
|
|
- isOpen: props.isOpen || true
|
|
|
|
|
|
+ isOpen: props.isOpen ? true : false
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
@@ -359,23 +489,27 @@ export class CollapsibleSection extends React.Component<
|
|
* Render the collapsible section using the virtual DOM.
|
|
* Render the collapsible section using the virtual DOM.
|
|
*/
|
|
*/
|
|
render(): React.ReactNode {
|
|
render(): React.ReactNode {
|
|
|
|
+ let icon = this.state.isOpen ? caretDownIconStyled : caretRightIconStyled;
|
|
|
|
+ let isOpen = this.state.isOpen;
|
|
|
|
+ let className = 'jp-extensionmanager-headerText';
|
|
|
|
+ if (this.props.disabled) {
|
|
|
|
+ icon = caretRightIconStyled;
|
|
|
|
+ isOpen = false;
|
|
|
|
+ className = 'jp-extensionmanager-headerTextDisabled';
|
|
|
|
+ }
|
|
return (
|
|
return (
|
|
<>
|
|
<>
|
|
<header>
|
|
<header>
|
|
<ToolbarButtonComponent
|
|
<ToolbarButtonComponent
|
|
- icon={
|
|
|
|
- this.state.isOpen ? caretDownIconStyled : caretRightIconStyled
|
|
|
|
- }
|
|
|
|
|
|
+ icon={icon}
|
|
onClick={() => {
|
|
onClick={() => {
|
|
this.handleCollapse();
|
|
this.handleCollapse();
|
|
}}
|
|
}}
|
|
/>
|
|
/>
|
|
- <span className="jp-extensionmanager-headerText">
|
|
|
|
- {this.props.header}
|
|
|
|
- </span>
|
|
|
|
- {this.props.headerElements}
|
|
|
|
|
|
+ <span className={className}>{this.props.header}</span>
|
|
|
|
+ {!this.props.disabled && this.props.headerElements}
|
|
</header>
|
|
</header>
|
|
- <Collapse isOpen={this.state.isOpen}>{this.props.children}</Collapse>
|
|
|
|
|
|
+ <Collapse isOpen={isOpen}>{this.props.children}</Collapse>
|
|
</>
|
|
</>
|
|
);
|
|
);
|
|
}
|
|
}
|
|
@@ -429,6 +563,12 @@ export namespace CollapsibleSection {
|
|
* If given, this will be diplayed instead of the children.
|
|
* If given, this will be diplayed instead of the children.
|
|
*/
|
|
*/
|
|
errorMessage?: string | null;
|
|
errorMessage?: string | null;
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * If true, the section will be collapsed and will not respond
|
|
|
|
+ * to open nor close actions.
|
|
|
|
+ */
|
|
|
|
+ disabled?: boolean;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -446,8 +586,13 @@ export namespace CollapsibleSection {
|
|
* The main view for the discovery extension.
|
|
* The main view for the discovery extension.
|
|
*/
|
|
*/
|
|
export class ExtensionView extends VDomRenderer<ListModel> {
|
|
export class ExtensionView extends VDomRenderer<ListModel> {
|
|
- constructor(serviceManager: ServiceManager) {
|
|
|
|
- super(new ListModel(serviceManager));
|
|
|
|
|
|
+ private _settings: ISettingRegistry.ISettings;
|
|
|
|
+ constructor(
|
|
|
|
+ serviceManager: ServiceManager,
|
|
|
|
+ settings: ISettingRegistry.ISettings
|
|
|
|
+ ) {
|
|
|
|
+ super(new ListModel(serviceManager, settings));
|
|
|
|
+ this._settings = settings;
|
|
this.addClass('jp-extensionmanager-view');
|
|
this.addClass('jp-extensionmanager-view');
|
|
}
|
|
}
|
|
|
|
|
|
@@ -466,11 +611,18 @@ export class ExtensionView extends VDomRenderer<ListModel> {
|
|
protected render(): React.ReactElement<any>[] {
|
|
protected render(): React.ReactElement<any>[] {
|
|
const model = this.model!;
|
|
const model = this.model!;
|
|
let pages = Math.ceil(model.totalEntries / model.pagination);
|
|
let pages = Math.ceil(model.totalEntries / model.pagination);
|
|
- let elements = [<SearchBar key="searchbar" placeholder="SEARCH" />];
|
|
|
|
|
|
+ let elements = [
|
|
|
|
+ <SearchBar
|
|
|
|
+ key="searchbar"
|
|
|
|
+ placeholder="SEARCH"
|
|
|
|
+ disabled={!ListModel.isDisclaimed()}
|
|
|
|
+ settings={this._settings}
|
|
|
|
+ />
|
|
|
|
+ ];
|
|
if (model.promptBuild) {
|
|
if (model.promptBuild) {
|
|
elements.push(
|
|
elements.push(
|
|
<BuildPrompt
|
|
<BuildPrompt
|
|
- key="buildpromt"
|
|
|
|
|
|
+ key="promt"
|
|
performBuild={() => {
|
|
performBuild={() => {
|
|
model.performBuild();
|
|
model.performBuild();
|
|
}}
|
|
}}
|
|
@@ -491,7 +643,7 @@ export class ExtensionView extends VDomRenderer<ListModel> {
|
|
);
|
|
);
|
|
const content = [];
|
|
const content = [];
|
|
if (!model.initialized) {
|
|
if (!model.initialized) {
|
|
- void model.initialize();
|
|
|
|
|
|
+ // void model.initialize();
|
|
content.push(
|
|
content.push(
|
|
<div key="loading-placeholder" className="jp-extensionmanager-loader">
|
|
<div key="loading-placeholder" className="jp-extensionmanager-loader">
|
|
Updating extensions list
|
|
Updating extensions list
|
|
@@ -536,6 +688,8 @@ export class ExtensionView extends VDomRenderer<ListModel> {
|
|
installedContent.push(
|
|
installedContent.push(
|
|
<ListView
|
|
<ListView
|
|
key="installed-items"
|
|
key="installed-items"
|
|
|
|
+ listMode={model.listMode}
|
|
|
|
+ viewType={'installed'}
|
|
entries={model.installed}
|
|
entries={model.installed}
|
|
numPages={1}
|
|
numPages={1}
|
|
onPage={value => {
|
|
onPage={value => {
|
|
@@ -549,7 +703,8 @@ export class ExtensionView extends VDomRenderer<ListModel> {
|
|
content.push(
|
|
content.push(
|
|
<CollapsibleSection
|
|
<CollapsibleSection
|
|
key="installed-section"
|
|
key="installed-section"
|
|
- isOpen={true}
|
|
|
|
|
|
+ isOpen={ListModel.isDisclaimed()}
|
|
|
|
+ disabled={!ListModel.isDisclaimed()}
|
|
header="Installed"
|
|
header="Installed"
|
|
headerElements={
|
|
headerElements={
|
|
<ToolbarButtonComponent
|
|
<ToolbarButtonComponent
|
|
@@ -579,6 +734,8 @@ export class ExtensionView extends VDomRenderer<ListModel> {
|
|
searchContent.push(
|
|
searchContent.push(
|
|
<ListView
|
|
<ListView
|
|
key="search-items"
|
|
key="search-items"
|
|
|
|
+ listMode={model.listMode}
|
|
|
|
+ viewType={'searchResult'}
|
|
// Filter out installed extensions:
|
|
// Filter out installed extensions:
|
|
entries={model.searchResult.filter(
|
|
entries={model.searchResult.filter(
|
|
entry => model.installed.indexOf(entry) === -1
|
|
entry => model.installed.indexOf(entry) === -1
|
|
@@ -595,7 +752,8 @@ export class ExtensionView extends VDomRenderer<ListModel> {
|
|
content.push(
|
|
content.push(
|
|
<CollapsibleSection
|
|
<CollapsibleSection
|
|
key="search-section"
|
|
key="search-section"
|
|
- isOpen={false}
|
|
|
|
|
|
+ isOpen={ListModel.isDisclaimed()}
|
|
|
|
+ disabled={!ListModel.isDisclaimed()}
|
|
header={model.query ? 'Search Results' : 'Discover'}
|
|
header={model.query ? 'Search Results' : 'Discover'}
|
|
onCollapse={(isOpen: boolean) => {
|
|
onCollapse={(isOpen: boolean) => {
|
|
if (isOpen && model.query === null) {
|
|
if (isOpen && model.query === null) {
|