From 2e25330126bef69b5a5c38c4b0c7c14301d1850e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 18 Dec 2019 17:16:27 -0600 Subject: [PATCH] wip add property inspector integrity finish property inspector lint --- buildutils/src/ensure-package.ts | 3 + dev_mode/package.json | 2 + packages/application-extension/package.json | 1 + packages/application-extension/src/index.tsx | 26 +- packages/application-extension/style/base.css | 5 + .../application-extension/style/index.css | 8 +- packages/application-extension/tsconfig.json | 3 + packages/metapackage/package.json | 1 + packages/metapackage/tsconfig.json | 3 + packages/notebook-extension/package.json | 1 + packages/notebook-extension/src/index.ts | 55 +--- packages/notebook-extension/tsconfig.json | 3 + packages/property-inspector/package.json | 51 +++ packages/property-inspector/src/index.ts | 299 ++++++++++++++++++ packages/property-inspector/src/token.ts | 62 ++++ packages/property-inspector/style/base.css | 17 + packages/property-inspector/style/index.css | 11 + packages/property-inspector/tsconfig.json | 16 + 18 files changed, 521 insertions(+), 46 deletions(-) create mode 100644 packages/application-extension/style/base.css create mode 100644 packages/property-inspector/package.json create mode 100644 packages/property-inspector/src/index.ts create mode 100644 packages/property-inspector/src/token.ts create mode 100644 packages/property-inspector/style/base.css create mode 100644 packages/property-inspector/style/index.css create mode 100644 packages/property-inspector/tsconfig.json diff --git a/buildutils/src/ensure-package.ts b/buildutils/src/ensure-package.ts index 2c2702190247..3b64d1446803 100644 --- a/buildutils/src/ensure-package.ts +++ b/buildutils/src/ensure-package.ts @@ -193,6 +193,9 @@ export async function ensurePackage( // write out cssIndexContents, if needed const cssIndexPath = path.join(pkgPath, 'style/index.css'); + if (!fs.existsSync(cssIndexPath)) { + fs.ensureFileSync(cssIndexPath); + } messages.push(...ensureFile(cssIndexPath, cssIndexContents, false)); } diff --git a/dev_mode/package.json b/dev_mode/package.json index a2e2b5a7ef40..fd2d7a46458f 100644 --- a/dev_mode/package.json +++ b/dev_mode/package.json @@ -143,6 +143,7 @@ "@jupyterlab/observables": "~3.0.0-alpha.4", "@jupyterlab/outputarea": "~2.0.0-alpha.4", "@jupyterlab/pdf-extension": "~2.0.0-alpha.4", + "@jupyterlab/property-inspector": "~2.0.0-alpha.4", "@jupyterlab/rendermime": "~2.0.0-alpha.4", "@jupyterlab/rendermime-extension": "~2.0.0-alpha.4", "@jupyterlab/rendermime-interfaces": "~2.0.0-alpha.4", @@ -328,6 +329,7 @@ "@jupyterlab/observables": "../packages/observables", "@jupyterlab/outputarea": "../packages/outputarea", "@jupyterlab/pdf-extension": "../packages/pdf-extension", + "@jupyterlab/property-inspector": "../packages/property-inspector", "@jupyterlab/rendermime": "../packages/rendermime", "@jupyterlab/rendermime-extension": "../packages/rendermime-extension", "@jupyterlab/rendermime-interfaces": "../packages/rendermime-interfaces", diff --git a/packages/application-extension/package.json b/packages/application-extension/package.json index c2d8a8c86d1b..227a99242453 100644 --- a/packages/application-extension/package.json +++ b/packages/application-extension/package.json @@ -39,6 +39,7 @@ "@jupyterlab/application": "^2.0.0-alpha.4", "@jupyterlab/apputils": "^2.0.0-alpha.4", "@jupyterlab/coreutils": "^4.0.0-alpha.4", + "@jupyterlab/property-inspector": "^2.0.0-alpha.4", "@lumino/algorithm": "^1.2.1", "@lumino/widgets": "^1.9.4", "react": "~16.9.0" diff --git a/packages/application-extension/src/index.tsx b/packages/application-extension/src/index.tsx index 8e2eec0f749e..ecc8b3ab6e06 100644 --- a/packages/application-extension/src/index.tsx +++ b/packages/application-extension/src/index.tsx @@ -31,6 +31,11 @@ import { URLExt } from '@jupyterlab/coreutils'; +import { + IPropertyInspectorProvider, + SideBarPropertyInspectorProvider +} from '@jupyterlab/property-inspector'; + import { each, iter, toArray } from '@lumino/algorithm'; import { Widget, DockLayout } from '@lumino/widgets'; @@ -765,6 +770,24 @@ const paths: JupyterFrontEndPlugin = { provides: JupyterFrontEnd.IPaths }; +/** + * Initialization data for the property_inspector extension. + */ +const propertyInspector: JupyterFrontEndPlugin = { + id: '@jupyterlab/application-extension:property-inspector', + autoStart: true, + requires: [ILabShell], + provides: IPropertyInspectorProvider, + activate: (app: JupyterFrontEnd, labshell: ILabShell) => { + const widget = new SideBarPropertyInspectorProvider(labshell); + widget.title.iconClass = 'jp-BuildIcon jp-SideBar-tabIcon'; + widget.title.caption = 'Property Inspector'; + widget.id = 'jp-property-inspector'; + labshell.add(widget, 'left'); + return widget; + } +}; + /** * Export the plugins as default. */ @@ -779,7 +802,8 @@ const plugins: JupyterFrontEndPlugin[] = [ shell, status, info, - paths + paths, + propertyInspector ]; export default plugins; diff --git a/packages/application-extension/style/base.css b/packages/application-extension/style/base.css new file mode 100644 index 000000000000..270565dd9750 --- /dev/null +++ b/packages/application-extension/style/base.css @@ -0,0 +1,5 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2014-2017, Jupyter Development Team. +| +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ diff --git a/packages/application-extension/style/index.css b/packages/application-extension/style/index.css index d2a1da0c33f9..1e65b648f035 100644 --- a/packages/application-extension/style/index.css +++ b/packages/application-extension/style/index.css @@ -1,7 +1,11 @@ /*----------------------------------------------------------------------------- -| Copyright (c) 2014-2017, Jupyter Development Team. -| +| Copyright (c) Jupyter Development Team. | Distributed under the terms of the Modified BSD License. |----------------------------------------------------------------------------*/ +/* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ +@import url('~@lumino/widgets/style/index.css'); @import url('~@jupyterlab/application/style/index.css'); +@import url('~@jupyterlab/property-inspector/style/index.css'); + +@import url('./base.css'); diff --git a/packages/application-extension/tsconfig.json b/packages/application-extension/tsconfig.json index 7dd0222b0eb9..12c04cc94302 100644 --- a/packages/application-extension/tsconfig.json +++ b/packages/application-extension/tsconfig.json @@ -14,6 +14,9 @@ }, { "path": "../coreutils" + }, + { + "path": "../property-inspector" } ] } diff --git a/packages/metapackage/package.json b/packages/metapackage/package.json index 18d3a1011211..23a899614206 100644 --- a/packages/metapackage/package.json +++ b/packages/metapackage/package.json @@ -86,6 +86,7 @@ "@jupyterlab/observables": "^3.0.0-alpha.4", "@jupyterlab/outputarea": "^2.0.0-alpha.4", "@jupyterlab/pdf-extension": "^2.0.0-alpha.4", + "@jupyterlab/property-inspector": "^2.0.0-alpha.4", "@jupyterlab/rendermime": "^2.0.0-alpha.4", "@jupyterlab/rendermime-extension": "^2.0.0-alpha.4", "@jupyterlab/rendermime-interfaces": "^2.0.0-alpha.4", diff --git a/packages/metapackage/tsconfig.json b/packages/metapackage/tsconfig.json index 8c699ff19235..7f7563ea2b88 100644 --- a/packages/metapackage/tsconfig.json +++ b/packages/metapackage/tsconfig.json @@ -168,6 +168,9 @@ { "path": "../pdf-extension" }, + { + "path": "../property-inspector" + }, { "path": "../rendermime" }, diff --git a/packages/notebook-extension/package.json b/packages/notebook-extension/package.json index 563ab5b6b071..b5e4ff22fabf 100644 --- a/packages/notebook-extension/package.json +++ b/packages/notebook-extension/package.json @@ -46,6 +46,7 @@ "@jupyterlab/launcher": "^2.0.0-alpha.4", "@jupyterlab/mainmenu": "^2.0.0-alpha.4", "@jupyterlab/notebook": "^2.0.0-alpha.4", + "@jupyterlab/property-inspector": "^2.0.0-alpha.4", "@jupyterlab/rendermime": "^2.0.0-alpha.4", "@jupyterlab/services": "^5.0.0-alpha.4", "@jupyterlab/statusbar": "^2.0.0-alpha.4", diff --git a/packages/notebook-extension/src/index.ts b/packages/notebook-extension/src/index.ts index c273c2e05c29..6d2d7409c1d8 100644 --- a/packages/notebook-extension/src/index.ts +++ b/packages/notebook-extension/src/index.ts @@ -77,6 +77,8 @@ import { Message, MessageLoop } from '@lumino/messaging'; import { Panel, Menu } from '@lumino/widgets'; import { CommandRegistry } from '@lumino/commands'; +import { IPropertyInspectorProvider } from '@jupyterlab/property-inspector'; + /** * The command IDs used by the notebook plugin. */ @@ -226,11 +228,6 @@ const NOTEBOOK_ICON_CLASS = 'jp-NotebookIcon'; */ const FACTORY = 'Notebook'; -/** - * The rank of the notebook tools tab in the sidebar - */ -const NOTEBOOK_TOOLS_RANK = 400; - /** * The exluded Export To ... * (returned from nbconvert's export list) @@ -302,7 +299,12 @@ const tools: JupyterFrontEndPlugin = { provides: INotebookTools, id: '@jupyterlab/notebook-extension:tools', autoStart: true, - requires: [INotebookTracker, IEditorServices, IStateDB], + requires: [ + INotebookTracker, + IEditorServices, + IStateDB, + IPropertyInspectorProvider + ], optional: [ILabShell] }; @@ -405,6 +407,7 @@ function activateNotebookTools( tracker: INotebookTracker, editorServices: IEditorServices, state: IStateDB, + inspectorProvider: IPropertyInspectorProvider, labShell: ILabShell | null ): INotebookTools { const id = 'notebook-tools'; @@ -475,43 +478,9 @@ function activateNotebookTools( MessageLoop.installMessageHook(notebookTools, hook); - // Wait until the application has finished restoring before rendering. - void Promise.all([state.fetch(id), app.restored]).then(([value]) => { - const open = !!( - value && ((value as ReadonlyJSONObject)['open'] as boolean) - ); - - // After initial restoration, check if the notebook tools should render. - if (tracker.size) { - app.shell.add(notebookTools, 'left', { rank: NOTEBOOK_TOOLS_RANK }); - if (open) { - app.shell.activateById(notebookTools.id); - } - } - - const updateTools = () => { - // If there are any open notebooks, add notebook tools to the side panel if - // it is not already there. - if (labShell && tracker.size) { - if (!notebookTools.isAttached) { - labShell.add(notebookTools, 'left', { rank: NOTEBOOK_TOOLS_RANK }); - } - return; - } - // If there are no notebooks, close notebook tools. - notebookTools.close(); - }; - - // For all subsequent widget changes, check if the notebook tools should render. - if (labShell) { - labShell.currentChanged.connect((sender, args) => { - updateTools(); - }); - } - // A notebook widget could be closed without a change to labShell.currentWidget - tracker.currentChanged.connect((sender, args) => { - updateTools(); - }); + tracker.widgetAdded.connect((sender, panel) => { + const inspector = inspectorProvider.register(panel); + inspector.render(notebookTools); }); return notebookTools; diff --git a/packages/notebook-extension/tsconfig.json b/packages/notebook-extension/tsconfig.json index ff5d92896b36..3a5126da7f84 100644 --- a/packages/notebook-extension/tsconfig.json +++ b/packages/notebook-extension/tsconfig.json @@ -36,6 +36,9 @@ { "path": "../notebook" }, + { + "path": "../property-inspector" + }, { "path": "../rendermime" }, diff --git a/packages/property-inspector/package.json b/packages/property-inspector/package.json new file mode 100644 index 000000000000..154ac76c1671 --- /dev/null +++ b/packages/property-inspector/package.json @@ -0,0 +1,51 @@ +{ + "name": "@jupyterlab/property-inspector", + "version": "2.0.0-alpha.4", + "description": "A property inspector display for widgets", + "homepage": "https://github.com/jupyterlab/jupyterlab", + "bugs": { + "url": "https://github.com/jupyterlab/jupyterlab/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/jupyterlab/jupyterlab.git" + }, + "license": "BSD-3-Clause", + "author": "Project Jupyter", + "files": [ + "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", + "schema/*.json", + "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" + ], + "sideEffects": [ + "style/**/*" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "style": "style/index.css", + "directories": { + "lib": "lib/" + }, + "scripts": { + "build": "tsc", + "clean": "rimraf lib", + "prepublishOnly": "npm run build", + "watch": "tsc -w --listEmittedFiles" + }, + "dependencies": { + "@jupyterlab/application": "^2.0.0-alpha.4", + "@jupyterlab/apputils": "^2.0.0-alpha.4", + "@lumino/coreutils": "^1.4.0", + "@lumino/disposable": "^1.3.2", + "@lumino/signaling": "^1.3.2", + "@lumino/widgets": "^1.9.4", + "react": "~16.12.0" + }, + "devDependencies": { + "rimraf": "~3.0.0", + "typescript": "~3.7.3" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/property-inspector/src/index.ts b/packages/property-inspector/src/index.ts new file mode 100644 index 000000000000..002f54d82f0e --- /dev/null +++ b/packages/property-inspector/src/index.ts @@ -0,0 +1,299 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { ILabShell } from '@jupyterlab/application'; + +import { ReactWidget } from '@jupyterlab/apputils'; + +import { Signal, ISignal } from '@lumino/signaling'; + +import { Widget, FocusTracker, SingletonLayout } from '@lumino/widgets'; + +import * as React from 'react'; + +import { IPropertyInspector, IPropertyInspectorProvider } from './token'; + +export { IPropertyInspector, IPropertyInspectorProvider }; + +/** + * The implementation of the PropertyInspector. + */ +abstract class PropertyInspectorProvider extends Widget + implements IPropertyInspectorProvider { + /** + * Constrcut a new Property Inspector. + */ + constructor() { + super(); + this.addClass('jp-PropertyInspector'); + this._tracker = new FocusTracker(); + this._tracker.currentChanged.connect(this._onCurrentChanged, this); + } + + /** + * Register a widget in the property inspector provider. + * + * @param widget The owner widget to register. + */ + register(widget: Widget): IPropertyInspector { + if (this._inspectors.has(widget)) { + throw new Error('Widget is already registered'); + } + const inspector = new Private.PropertyInspector(widget); + widget.disposed.connect(this._onWidgetDisposed, this); + this._inspectors.set(widget, inspector); + inspector.onAction.connect(this._onInspectorAction, this); + this._tracker.add(widget); + return inspector; + } + + /** + * The current widget being tracked by the inspector. + */ + protected get currentWidget(): Widget { + return this._tracker.currentWidget; + } + + /** + * Refresh the content for the current widget. + */ + protected refresh(): void { + const current = this._tracker.currentWidget; + if (!current) { + this.setContent(null); + return; + } + const inspector = this._inspectors.get(current); + if (inspector) { + this.setContent(inspector.content); + } + } + + /** + * Show the provider panel. + */ + protected abstract showPanel(): void; + + /** + * Set the content of the provider. + */ + protected abstract setContent(content: Widget | null): void; + + /** + * Handle the disposal of a widget. + */ + private _onWidgetDisposed(sender: Widget): void { + const inspector = this._inspectors.get(sender); + if (inspector) { + inspector.dispose(); + this._inspectors.delete(sender); + } + } + + /** + * Handle inspector actions. + */ + private _onInspectorAction( + sender: Private.PropertyInspector, + action: Private.PropertyInspectorAction + ) { + const owner = sender.owner; + const current = this._tracker.currentWidget; + switch (action) { + case 'content': + if (current === owner) { + this.setContent(sender.content); + } + break; + case 'dispose': + this._tracker.remove(owner); + this._inspectors.delete(owner); + break; + case 'showPanel': + if (current === owner) { + this.showPanel(); + } + break; + default: + throw new Error('Unsupported inspector action'); + } + } + + /** + * Handle a change to the current widget in the tracker. + */ + private _onCurrentChanged(): void { + const current = this._tracker.currentWidget; + if (current) { + const inspector = this._inspectors.get(current); + const content = inspector!.content; + this.setContent(content); + } else { + this.setContent(null); + } + } + + private _tracker = new FocusTracker(); + private _inspectors = new Map(); +} + +/** + * A class that adds a property inspector provider to the + * JupyterLab sidebar. + */ +export class SideBarPropertyInspectorProvider extends PropertyInspectorProvider { + constructor(labshell: ILabShell, placeholder?: Widget) { + super(); + this._labshell = labshell; + const layout = (this.layout = new SingletonLayout()); + if (placeholder) { + this._placeholder = placeholder; + } else { + this._placeholder = new Widget(); + this._placeholder.node.textContent = 'No properties to inspect.'; + this._placeholder.addClass('jp-PropertyInspector-placeholder'); + } + layout.widget = this._placeholder; + labshell.currentChanged.connect(this._onShellCurrentChanged, this); + } + + /** + * Set the content of the sidebar panel. + */ + protected setContent(content: Widget | null): void { + const layout = this.layout as SingletonLayout; + if (layout.widget) { + layout.widget.removeClass('jp-PropertyInspector-content'); + layout.removeWidget(layout.widget); + } + if (!content) { + content = this._placeholder; + } + content.addClass('jp-PropertyInspector-content'); + layout.widget = content; + } + + /** + * Show the sidebar panel. + */ + showPanel(): void { + this._labshell.activateById(this.id); + } + + /** + * Handle the case when the current widget is not in our tracker. + */ + private _onShellCurrentChanged(): void { + const current = this.currentWidget; + if (!current) { + this.setContent(null); + return; + } + const currentShell = this._labshell.currentWidget; + if (currentShell?.node.contains(current.node)) { + this.refresh(); + } else { + this.setContent(null); + } + } + + private _labshell: ILabShell; + private _placeholder: Widget; +} + +/** + * A namespace for module private data. + */ +namespace Private { + /** + * A type alias for the actions a property inspector can take. + */ + export type PropertyInspectorAction = 'content' | 'dispose' | 'showPanel'; + + /** + * An implementation of the property inspector used by the + * property inspector provider. + */ + export class PropertyInspector implements IPropertyInspector { + /** + * Construct a new property inspector. + */ + constructor(owner: Widget) { + this._owner = owner; + } + + /** + * The owner widget for the property inspector. + */ + get owner(): Widget | null { + return this._owner; + } + + /** + * The current content for the property inspector. + */ + get content(): Widget | null { + return this._content; + } + + /** + * Whether the property inspector is disposed. + */ + get isDisposed() { + return this._isDisposed; + } + + /** + * A signal used for actions related to the property inspector. + */ + get onAction(): ISignal { + return this._onAction; + } + + /** + * Show the property inspector panel. + */ + showPanel(): void { + if (this._isDisposed) { + return; + } + this._onAction.emit('showPanel'); + } + + /** + * Render the property inspector content. + */ + render(widget: Widget | React.ReactElement): void { + if (this._isDisposed) { + return; + } + if (widget instanceof Widget) { + this._content = widget; + } else { + this._content = ReactWidget.create(widget); + } + this._onAction.emit('content'); + } + + /** + * Dispose of the property inspector. + */ + dispose(): void { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + this._content = null; + this._owner = null; + Signal.clearData(this); + } + + private _isDisposed = false; + private _content: Widget | null = null; + private _owner: Widget | null = null; + private _onAction = new Signal< + PropertyInspector, + Private.PropertyInspectorAction + >(this); + } +} diff --git a/packages/property-inspector/src/token.ts b/packages/property-inspector/src/token.ts new file mode 100644 index 000000000000..91c31b569aa6 --- /dev/null +++ b/packages/property-inspector/src/token.ts @@ -0,0 +1,62 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { Token } from '@lumino/coreutils'; + +import { IDisposable } from '@lumino/disposable'; + +import { Widget } from '@lumino/widgets'; + +import * as React from 'react'; + +/** + * A property inspector interface provided when registering + * to a property inspector provider. Allows an owner widget + * to set the property inspector content for itself. + */ +export interface IPropertyInspector extends IDisposable { + /* + * Render the property inspector content. + * + * If the owner widget is not the most recently focused, + * The content will not be shown until that widget + * is focused. + * + * @param content - the widget or react element to render. + */ + render(content: Widget | React.ReactElement): void; + + /** + * Show the property inspector panel. + * + * If the owner widget is not the most recently focused, + * this is a no-op. It should be triggered by a user + * action. + */ + showPanel(): void; +} + +/** + * A provider for property inspectors. + */ +export interface IPropertyInspectorProvider { + /** + * Register a widget in the property inspector provider. + * + * @param widget The owner widget whose properties will be inspected. + * + * ## Notes + * Only one property inspector can be provided for each widget. + * Registering the same widget twice will result in an error. + * A widget can be unregistered by disposing of its property + * inspector. + */ + register(widget: Widget): IPropertyInspector; +} + +/** + * The property inspector provider token. + */ +export const IPropertyInspectorProvider = new Token( + '@sagemaker-ui:IPropertyInspectorProvider' +); diff --git a/packages/property-inspector/style/base.css b/packages/property-inspector/style/base.css new file mode 100644 index 000000000000..aee3212643c2 --- /dev/null +++ b/packages/property-inspector/style/base.css @@ -0,0 +1,17 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2014-2019, Jupyter Development Team. +| +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +.jp-PropertyInspector { + display: flex; + align-items: center; + justify-content: center; + color: var(--jp-ui-font-color1); + background: var(--jp-layout-color1); +} + +.jp-PropertyInspector-placeholder { + padding: 8px; +} diff --git a/packages/property-inspector/style/index.css b/packages/property-inspector/style/index.css new file mode 100644 index 000000000000..d94c5e43bec3 --- /dev/null +++ b/packages/property-inspector/style/index.css @@ -0,0 +1,11 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +/* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ +@import url('~@lumino/widgets/style/index.css'); +@import url('~@jupyterlab/apputils/style/index.css'); +@import url('~@jupyterlab/application/style/index.css'); + +@import url('./base.css'); diff --git a/packages/property-inspector/tsconfig.json b/packages/property-inspector/tsconfig.json new file mode 100644 index 000000000000..ed60ff37dc2a --- /dev/null +++ b/packages/property-inspector/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfigbase", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "include": ["src/*"], + "references": [ + { + "path": "../application" + }, + { + "path": "../apputils" + } + ] +}