diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d3d3e772a5f4..6a8048827258 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,13 +44,14 @@ a keyboard shortcut or automatically on save. ## Submitting a Pull Request Contribution Generally, an issue should be opened describing a piece of proposed work and the -issues it solves before a pull request is opened. +issues it solves before a pull request is opened. ### Issue Management -Opening an issue lets community members participate in the design discussion, -makes others aware of work being done, and sets the stage for a fruitful community -interaction. A pull request should reference the issue it is addressing. Once the -pull request is merged, the issue related to it will also be closed. If there is + +Opening an issue lets community members participate in the design discussion, +makes others aware of work being done, and sets the stage for a fruitful community +interaction. A pull request should reference the issue it is addressing. Once the +pull request is merged, the issue related to it will also be closed. If there is additional discussion around implemementation the issue may be re-opened. Once 30 days have passed with no additional discussion, the [lock bot](https://github.com/apps/lock) will lock the issue. If additional discussion is desired, or if the pull request doesn't fully address the diff --git a/buildutils/package.json b/buildutils/package.json index efd55c58e117..eaae3a4f49b5 100644 --- a/buildutils/package.json +++ b/buildutils/package.json @@ -49,6 +49,7 @@ "mini-css-extract-plugin": "~0.6.0", "package-json": "^6.3.0", "path": "~0.12.7", + "prettier": "^1.18.2", "semver": "^6.1.0", "sort-package-json": "~1.22.1", "typescript": "~3.5.1", @@ -60,6 +61,7 @@ "@types/inquirer": "^6.0.3", "@types/mini-css-extract-plugin": "^0.2.0", "@types/node": "^12.0.2", + "@types/prettier": "^1.16.4", "@types/webpack": "^4.4.32", "rimraf": "~2.6.2" } diff --git a/buildutils/src/build.ts b/buildutils/src/build.ts index 2e14db75aab3..de622b45a756 100644 --- a/buildutils/src/build.ts +++ b/buildutils/src/build.ts @@ -183,10 +183,7 @@ export namespace Build { }, { test: /\.svg/, - use: [ - { loader: 'svg-url-loader', options: {} }, - { loader: 'svgo-loader', options: { plugins: [] } } - ] + use: [{ loader: 'svg-url-loader', options: { encoding: 'none' } }] }, { test: /\.(png|jpg|gif|ttf|woff|woff2|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/, diff --git a/buildutils/src/ensure-package.ts b/buildutils/src/ensure-package.ts index 1c4e4991b1d6..2e007c8f972f 100644 --- a/buildutils/src/ensure-package.ts +++ b/buildutils/src/ensure-package.ts @@ -6,17 +6,48 @@ import * as fs from 'fs-extra'; import * as glob from 'glob'; import * as path from 'path'; +import * as prettier from 'prettier'; import * as ts from 'typescript'; import { getDependency } from './get-dependency'; import * as utils from './utils'; -const CSS_HEADER = ` +const HEADER_TEMPLATE = ` /*----------------------------------------------------------------------------- | Copyright (c) Jupyter Development Team. | Distributed under the terms of the Modified BSD License. |----------------------------------------------------------------------------*/ -/* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ +/* This file was auto-generated by {{funcName}}() in @jupyterlab/buildutils */ +`; + +const ICON_IMPORTS_TEMPLATE = ` +import { Icon } from './interfaces'; + +// icon svg import statements +{{iconImportStatements}} + +// defaultIcons definition +export namespace IconImports { + export const defaultIcons: ReadonlyArray = [ + {{iconModelDeclarations}} + ]; +} +`; + +const ICON_CSS_CLASSES_TEMPLATE = ` +/** + * (DEPRECATED) Support for consuming icons as CSS background images + */ + +/* Icons urls */ + +:root { + {{iconCSSUrls}} +} + +/* Icon CSS class declarations */ + +{{iconCSSDeclarations}} `; /** @@ -138,17 +169,20 @@ export async function ensurePackage( // Template the CSS index file. if (cssImports && fs.existsSync(path.join(pkgPath, 'style/base.css'))) { - let cssIndex = CSS_HEADER.trim(); + const funcName = 'ensurePackage'; + let cssIndexContents = utils.fromTemplate( + HEADER_TEMPLATE, + { funcName }, + { end: '' } + ); cssImports.forEach(cssImport => { - cssIndex += `\n@import url('~${cssImport}');`; + cssIndexContents += `\n@import url('~${cssImport}');`; }); - cssIndex += "\n\n@import url('./base.css');\n"; - const cssPath = path.join(pkgPath, 'style/index.css'); - const prev = fs.readFileSync(cssPath, { encoding: 'utf8' }); - if (prev !== cssIndex) { - messages.push(`Updated ./${data.style}`); - fs.writeFileSync(cssPath, cssIndex); - } + cssIndexContents += "\n\n@import url('./base.css');\n"; + + // write out cssIndexContents, if needed + const cssIndexPath = path.join(pkgPath, 'style/index.css'); + messages.push(...ensureFile(cssIndexPath, cssIndexContents, false)); } // Look for unused packages @@ -290,6 +324,78 @@ export async function ensurePackage( return messages; } +/** + * An extra ensure function just for the @jupyterlab/ui-components package. + * Ensures that the icon svg import statements are synced with the contents + * of ui-components/style/icons. + * + * @param pkgPath - The path to the @jupyterlab/ui-components package. + * + * @returns A list of changes that were made to ensure the package. + */ +export async function ensureUiComponents(pkgPath: string): Promise { + const funcName = 'ensureUiComponents'; + let messages: string[] = []; + + const svgs = glob.sync(path.join(pkgPath, 'style/icons', '**/*.svg')); + + /* support for glob import of icon svgs */ + const iconSrcDir = path.join(pkgPath, 'src/icon'); + + // build the per-icon import code + let _iconImportStatements: string[] = []; + let _iconModelDeclarations: string[] = []; + svgs.forEach(svg => { + const name = utils.stem(svg); + const nameCamel = utils.camelCase(name) + 'Svg'; + _iconImportStatements.push( + `import ${nameCamel} from '${path.relative(iconSrcDir, svg)}';` + ); + _iconModelDeclarations.push(`{ name: '${name}', svg: ${nameCamel} }`); + }); + const iconImportStatements = _iconImportStatements.join('\n'); + const iconModelDeclarations = _iconModelDeclarations.join(',\n'); + + // generate the actual contents of the iconImports file + const iconImportsPath = path.join(iconSrcDir, 'iconImports.ts'); + const iconImportsContents = utils.fromTemplate( + HEADER_TEMPLATE + ICON_IMPORTS_TEMPLATE, + { funcName, iconImportStatements, iconModelDeclarations } + ); + messages.push(...ensureFile(iconImportsPath, iconImportsContents)); + + /* support for deprecated icon CSS classes */ + const iconCSSDir = path.join(pkgPath, 'style'); + + // build the per-icon import code + let _iconCSSUrls: string[] = []; + let _iconCSSDeclarations: string[] = []; + svgs.forEach(svg => { + const name = utils.stem(svg); + const urlName = 'jp-icon-' + name; + const className = 'jp-' + utils.camelCase(name, true) + 'Icon'; + + _iconCSSUrls.push( + `--${urlName}: url('${path.relative(iconCSSDir, svg)}');` + ); + _iconCSSDeclarations.push( + `.${className} {background-image: var(--${urlName})}` + ); + }); + const iconCSSUrls = _iconCSSUrls.join('\n'); + const iconCSSDeclarations = _iconCSSDeclarations.join('\n'); + + // generate the actual contents of the iconCSSClasses file + const iconCSSClassesPath = path.join(iconCSSDir, 'deprecated.css'); + const iconCSSClassesContent = utils.fromTemplate( + HEADER_TEMPLATE + ICON_CSS_CLASSES_TEMPLATE, + { funcName, iconCSSUrls, iconCSSDeclarations } + ); + messages.push(...ensureFile(iconCSSClassesPath, iconCSSClassesContent)); + + return messages; +} + /** * The options used to ensure a package. */ @@ -340,6 +446,53 @@ export interface IEnsurePackageOptions { differentVersions?: string[]; } +/** + * Ensure that contents of a file match a supplied string. If they do match, + * do nothing and return an empty array. If they don't match, overwrite the + * file and return an array with an update message. + * + * @param path: The path to the file being checked. The file must exist, + * or else this function does nothing. + * + * @param contents: The desired file contents. + * + * @param prettify: default = true. If true, format the contents with + * `prettier` before comparing/writing. Set to false only if you already + * know your code won't be modified later by the `prettier` git commit hook. + * + * @returns a string array with 0 or 1 messages. + */ +function ensureFile( + path: string, + contents: string, + prettify: boolean = true +): string[] { + let messages: string[] = []; + + if (!fs.existsSync(path)) { + // bail + messages.push( + `Tried to ensure the contents of ./${path}, but the file does not exist` + ); + return messages; + } + + // run the newly generated contents through prettier before comparing + if (prettify) { + contents = prettier.format(contents, { filepath: path, singleQuote: true }); + } + + const prev = fs.readFileSync(path, { + encoding: 'utf8' + }); + if (prev !== contents) { + fs.writeFileSync(path, contents); + messages.push(`Updated ./${path}`); + } + + return messages; +} + /** * Extract the module imports from a TypeScript source file. * diff --git a/buildutils/src/ensure-repo.ts b/buildutils/src/ensure-repo.ts index b02fa34df2ba..425c502eb521 100644 --- a/buildutils/src/ensure-repo.ts +++ b/buildutils/src/ensure-repo.ts @@ -13,7 +13,11 @@ */ import * as path from 'path'; import * as utils from './utils'; -import { ensurePackage, IEnsurePackageOptions } from './ensure-package'; +import { + ensurePackage, + ensureUiComponents, + IEnsurePackageOptions +} from './ensure-package'; type Dict = { [key: string]: T }; @@ -341,6 +345,16 @@ export async function ensureIntegrity(): Promise { } } + // ensure the icon svg imports + pkgMessages = await ensureUiComponents(pkgPaths['@jupyterlab/ui-components']); + if (pkgMessages.length > 0) { + let pkgName = '@jupyterlab/ui-components'; + if (!messages[pkgName]) { + messages[pkgName] = []; + } + messages[pkgName] = messages[pkgName].concat(pkgMessages); + } + // Handle the top level package. let corePath = path.resolve('.', 'package.json'); let coreData: any = utils.readJSONFile(corePath); diff --git a/buildutils/src/utils.ts b/buildutils/src/utils.ts index 504a9c8e5971..1d7535d93e29 100644 --- a/buildutils/src/utils.ts +++ b/buildutils/src/utils.ts @@ -114,6 +114,54 @@ export function writeJSONFile(filePath: string, data: any): boolean { return false; } +/** + * Simple template substitution for template vars of the form {{name}} + * + * @param templ: the template string. + * Ex: `This header generated by {{funcName}}` + * + * @param subs: an object in which the parameter keys are the template + * variables and the parameter values are the substitutions. + * + * @param options: function options. + * + * @param options.autoindent: default = true. If true, will try to match + * indentation level of {{var}} in substituted template. + * + * @param options.end: default = '\n'. Inserted at the end of + * a template post-substitution and post-trim. + * + * @returns the input template with all {{vars}} substituted, then `.trim`-ed. + */ +export function fromTemplate( + templ: string, + subs: Dict, + options: { autoindent?: boolean; end?: string } = {} +) { + // default options values + const autoindent = + options.autoindent === undefined ? true : options.autoindent; + const end = options.end === undefined ? '\n' : options.end; + + Object.keys(subs).forEach(key => { + const val = subs[key]; + + if (autoindent) { + // try to match the indentation level of the {{var}} in the input template. + templ = templ.split(`{{${key}}}`).reduce((acc, cur) => { + // Regex: 0 or more non-newline whitespaces followed by end of string + let indentRe = acc.match(/([^\S\r\n]*).*$/); + let indent = indentRe ? indentRe[1] : ''; + return acc + val.split('\n').join('\n' + indent) + cur; + }); + } else { + templ = templ.split(`{{${key}}}`).join(val); + } + }); + + return templ.trim() + end; +} + /** * * Call a command, checking its status. @@ -286,3 +334,43 @@ export function ensureUnixPathSep(source: string) { } return source.replace(backSlash, '/'); } + +/** + * Get the last portion of a path, without its extension (if any). + * + * @param path - The file path. + * + * @returns the last part of the path, sans extension. + */ +export function stem(path: string): string { + return path + .split('\\') + .pop() + .split('/') + .pop() + .split('.') + .shift(); +} + +/** + * Given a 'snake-case', 'snake_case', or 'snake case' string, + * will return the camel case version: 'snakeCase'. + * + * @param str: the snake-case input string. + * + * @param upper: default = false. If true, the first letter of the + * returned string will be capitalized. + * + * @returns the camel case version of the input string. + */ +export function camelCase(str: string, upper: boolean = false): string { + return str.replace(/(?:^\w|[A-Z]|\b\w|\s+|-+|_+)/g, function(match, index) { + if (+match === 0 || match[0] === '-') { + return ''; + } else if (index === 0 && !upper) { + return match.toLowerCase(); + } else { + return match.toUpperCase(); + } + }); +} diff --git a/dev_mode/imports.css b/dev_mode/imports.css index 47f1c8393e13..4b5aab968a1c 100644 --- a/dev_mode/imports.css +++ b/dev_mode/imports.css @@ -31,6 +31,7 @@ @import url('~@jupyterlab/tabmanager-extension/style/index.css'); @import url('~@jupyterlab/terminal-extension/style/index.css'); @import url('~@jupyterlab/tooltip-extension/style/index.css'); +@import url('~@jupyterlab/ui-components-extension/style/index.css'); @import url('~@jupyterlab/vdom-extension/style/index.css'); @import url('~@jupyterlab/vega4-extension/style/index.css'); @import url('~@jupyterlab/vega5-extension/style/index.css'); diff --git a/dev_mode/package.json b/dev_mode/package.json index ae0b22b451d3..c7bbeea24c55 100644 --- a/dev_mode/package.json +++ b/dev_mode/package.json @@ -51,6 +51,7 @@ "@jupyterlab/theme-dark-extension": "~1.1.0-alpha.1", "@jupyterlab/theme-light-extension": "~1.1.0-alpha.1", "@jupyterlab/tooltip-extension": "~1.1.0-alpha.1", + "@jupyterlab/ui-components-extension": "~1.1.0-alpha.1", "@jupyterlab/vdom-extension": "~1.1.0-alpha.1", "@jupyterlab/vega4-extension": "~1.1.0-alpha.1", "@jupyterlab/vega5-extension": "~1.1.0-alpha.1" @@ -155,6 +156,7 @@ "@jupyterlab/tooltip": "~1.1.0-alpha.1", "@jupyterlab/tooltip-extension": "~1.1.0-alpha.1", "@jupyterlab/ui-components": "~1.1.0-alpha.1", + "@jupyterlab/ui-components-extension": "~1.1.0-alpha.1", "@jupyterlab/vdom": "~1.1.0-alpha.1", "@jupyterlab/vdom-extension": "~1.1.0-alpha.1", "@jupyterlab/vega4-extension": "~1.1.0-alpha.1", @@ -210,6 +212,7 @@ "@jupyterlab/theme-dark-extension": "", "@jupyterlab/theme-light-extension": "", "@jupyterlab/tooltip-extension": "", + "@jupyterlab/ui-components-extension": "", "@jupyterlab/vdom-extension": "" }, "mimeExtensions": { @@ -333,6 +336,7 @@ "@jupyterlab/tooltip": "../packages/tooltip", "@jupyterlab/tooltip-extension": "../packages/tooltip-extension", "@jupyterlab/ui-components": "../packages/ui-components", + "@jupyterlab/ui-components-extension": "../packages/ui-components-extension", "@jupyterlab/vdom": "../packages/vdom", "@jupyterlab/vdom-extension": "../packages/vdom-extension", "@jupyterlab/vega4-extension": "../packages/vega4-extension", diff --git a/dev_mode/webpack.config.js b/dev_mode/webpack.config.js index 78c1769a515d..5a2612582ddf 100644 --- a/dev_mode/webpack.config.js +++ b/dev_mode/webpack.config.js @@ -189,8 +189,22 @@ module.exports = [ }, { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader' }, { + // in css files, svg is loaded as a url formatted string test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, - use: 'url-loader?limit=10000&mimetype=image/svg+xml' + issuer: { test: /\.css$/ }, + use: { + loader: 'svg-url-loader', + options: { encoding: 'none', limit: 10000 } + } + }, + { + // in ts and tsx files (both of which compile to js), + // svg is loaded as a raw string + test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, + issuer: { test: /\.js$/ }, + use: { + loader: 'raw-loader' + } } ] }, diff --git a/docs/source/getting_started/issue.rst b/docs/source/getting_started/issue.rst index 2430273e2c40..7038674bcd58 100644 --- a/docs/source/getting_started/issue.rst +++ b/docs/source/getting_started/issue.rst @@ -36,7 +36,7 @@ If you find a problem in JupyterLab, please follow the steps below to diagnose a - I can reproduce the issue with the classic Jupyter Notebook: The problem is probably not from JupyterLab. It may be in the `Jupyter Notebook server `__, your kernel, etc. Use your best judgement to file an issue with the appropriate project. - I cannot reproduce the issue in classic Jupyter Notebook: Go to step 4. -4. Try to reproduce the issue in your browser incognito or private browsing modeq. Running in private browser mode ensures your browser state is clean. +4. Try to reproduce the issue in your browser incognito or private browsing mode. Running in private browser mode ensures your browser state is clean. - I cannot reproduce the issue in private browsing mode: Perhaps resetting your cookies or other browser state would help. - I can reproduce the issue in private browsing mode: Go to :ref:`create-issue`. diff --git a/examples/app/index.js b/examples/app/index.js index 4b340f301721..aa9bf195b5d0 100644 --- a/examples/app/index.js +++ b/examples/app/index.js @@ -39,7 +39,8 @@ window.addEventListener('load', async function() { require('@jupyterlab/terminal-extension'), require('@jupyterlab/theme-dark-extension'), require('@jupyterlab/theme-light-extension'), - require('@jupyterlab/tooltip-extension') + require('@jupyterlab/tooltip-extension'), + require('@jupyterlab/ui-components-extension') ]; var lab = new JupyterLab(); lab.registerPluginModules(mods); diff --git a/examples/filebrowser/package.json b/examples/filebrowser/package.json index 1bf960f1e249..9b2d960bae55 100644 --- a/examples/filebrowser/package.json +++ b/examples/filebrowser/package.json @@ -16,6 +16,7 @@ "@jupyterlab/fileeditor": "^1.1.0-alpha.1", "@jupyterlab/services": "^4.1.0-alpha.1", "@jupyterlab/theme-light-extension": "^1.1.0-alpha.1", + "@jupyterlab/ui-components": "^1.1.0-alpha.1", "@phosphor/algorithm": "^1.1.3", "@phosphor/commands": "^1.6.3", "@phosphor/widgets": "^1.8.0", diff --git a/examples/filebrowser/src/index.ts b/examples/filebrowser/src/index.ts index fd22a47a41c9..d93c4e493a64 100644 --- a/examples/filebrowser/src/index.ts +++ b/examples/filebrowser/src/index.ts @@ -21,19 +21,21 @@ import { ServiceManager } from '@jupyterlab/services'; import { Dialog, ToolbarButton, showDialog } from '@jupyterlab/apputils'; -import { FileBrowser, FileBrowserModel } from '@jupyterlab/filebrowser'; +import { + CodeMirrorEditorFactory, + CodeMirrorMimeTypeService +} from '@jupyterlab/codemirror'; import { DocumentManager } from '@jupyterlab/docmanager'; import { DocumentRegistry } from '@jupyterlab/docregistry'; -import { - CodeMirrorEditorFactory, - CodeMirrorMimeTypeService -} from '@jupyterlab/codemirror'; +import { FileBrowser, FileBrowserModel } from '@jupyterlab/filebrowser'; import { FileEditorFactory } from '@jupyterlab/fileeditor'; +import { defaultIconRegistry } from '@jupyterlab/ui-components'; + function main(): void { let manager = new ServiceManager(); void manager.ready.then(() => { @@ -85,7 +87,10 @@ function createApp(manager: ServiceManager.IManager): void { let commands = new CommandRegistry(); - let fbModel = new FileBrowserModel({ manager: docManager }); + let fbModel = new FileBrowserModel({ + manager: docManager, + iconRegistry: defaultIconRegistry + }); let fbWidget = new FileBrowser({ id: 'filebrowser', model: fbModel diff --git a/jupyterlab/commands.py b/jupyterlab/commands.py index da2ecfc009c0..64c781f5383c 100644 --- a/jupyterlab/commands.py +++ b/jupyterlab/commands.py @@ -29,6 +29,7 @@ from .semver import Range, gte, lt, lte, gt, make_semver from .jlpmapp import YARN_PATH, HERE +from .coreconfig import _get_default_core_data # The regex for expecting the webpack output. @@ -283,7 +284,7 @@ def watch_dev(logger=None): return package_procs + [wp_proc] -def watch(app_dir=None, logger=None): +def watch(app_dir=None, logger=None, core_config=None): """Watch the application. Parameters @@ -299,11 +300,11 @@ def watch(app_dir=None, logger=None): """ logger = _ensure_logger(logger) _node_check(logger) - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.watch() -def install_extension(extension, app_dir=None, logger=None): +def install_extension(extension, app_dir=None, logger=None, core_config=None): """Install an extension package into JupyterLab. The extension is first validated. @@ -312,24 +313,24 @@ def install_extension(extension, app_dir=None, logger=None): """ logger = _ensure_logger(logger) _node_check(logger) - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.install_extension(extension) -def uninstall_extension(name=None, app_dir=None, logger=None, all_=False): +def uninstall_extension(name=None, app_dir=None, logger=None, all_=False, core_config=None): """Uninstall an extension by name or path. Returns `True` if a rebuild is recommended, `False` otherwise. """ logger = _ensure_logger(logger) _node_check(logger) - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) if all_ is True: return handler.uninstall_all_extensions() return handler.uninstall_extension(name) -def update_extension(name=None, all_=False, app_dir=None, logger=None): +def update_extension(name=None, all_=False, app_dir=None, logger=None, core_config=None): """Update an extension by name, or all extensions. Either `name` must be given as a string, or `all_` must be `True`. @@ -339,7 +340,7 @@ def update_extension(name=None, all_=False, app_dir=None, logger=None): """ logger = _ensure_logger(logger) _node_check(logger) - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) if all_ is True: return handler.update_all_extensions() return handler.update_extension(name) @@ -363,96 +364,96 @@ def clean(app_dir=None, logger=None): def build(app_dir=None, name=None, version=None, static_url=None, logger=None, command='build:prod', kill_event=None, - clean_staging=False): + clean_staging=False, core_config=None): """Build the JupyterLab application. """ logger = _ensure_logger(logger) _node_check(logger) - handler = _AppHandler(app_dir, logger, kill_event=kill_event) + handler = _AppHandler(app_dir, logger, kill_event=kill_event, core_config=core_config) return handler.build(name=name, version=version, static_url=static_url, command=command, clean_staging=clean_staging) -def get_app_info(app_dir=None, logger=None): +def get_app_info(app_dir=None, logger=None, core_config=None): """Get a dictionary of information about the app. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.info -def enable_extension(extension, app_dir=None, logger=None): +def enable_extension(extension, app_dir=None, logger=None, core_config=None): """Enable a JupyterLab extension. Returns `True` if a rebuild is recommended, `False` otherwise. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.toggle_extension(extension, False) -def disable_extension(extension, app_dir=None, logger=None): +def disable_extension(extension, app_dir=None, logger=None, core_config=None): """Disable a JupyterLab package. Returns `True` if a rebuild is recommended, `False` otherwise. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.toggle_extension(extension, True) -def check_extension(extension, app_dir=None, installed=False, logger=None): +def check_extension(extension, app_dir=None, installed=False, logger=None, core_config=None): """Check if a JupyterLab extension is enabled or disabled. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.check_extension(extension, installed) -def build_check(app_dir=None, logger=None): +def build_check(app_dir=None, logger=None, core_config=None): """Determine whether JupyterLab should be built. Returns a list of messages. """ logger = _ensure_logger(logger) _node_check(logger) - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.build_check() -def list_extensions(app_dir=None, logger=None): +def list_extensions(app_dir=None, logger=None, core_config=None): """List the extensions. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.list_extensions() -def link_package(path, app_dir=None, logger=None): +def link_package(path, app_dir=None, logger=None, core_config=None): """Link a package against the JupyterLab build. Returns `True` if a rebuild is recommended, `False` otherwise. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.link_package(path) -def unlink_package(package, app_dir=None, logger=None): +def unlink_package(package, app_dir=None, logger=None, core_config=None): """Unlink a package from JupyterLab by path or name. Returns `True` if a rebuild is recommended, `False` otherwise. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.unlink_package(package) -def get_app_version(app_dir=None): +def get_app_version(app_dir=None, core_config=None): """Get the application version.""" app_dir = app_dir or get_app_dir() - handler = _AppHandler(app_dir) + handler = _AppHandler(app_dir, core_config=core_config) return handler.info['version'] -def get_latest_compatible_package_versions(names, app_dir=None, logger=None): +def get_latest_compatible_package_versions(names, app_dir=None, logger=None, core_config=None): """Get the latest compatible version of a list of packages. """ app_dir = app_dir or get_app_dir() - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.latest_compatible_package_versions(names) @@ -476,12 +477,15 @@ def read_package(target): class _AppHandler(object): - def __init__(self, app_dir, logger=None, kill_event=None): + def __init__(self, app_dir, logger=None, kill_event=None, core_config=None): """Create a new _AppHandler object """ self.app_dir = app_dir or get_app_dir() self.sys_dir = get_app_dir() self.logger = _ensure_logger(logger) + self.core_data = ( + core_config._data if core_config else _get_default_core_data() + ) self.info = self._get_app_info() self.kill_event = kill_event or Event() # TODO: Make this configurable @@ -713,7 +717,6 @@ def uninstall_extension(self, name): Returns `True` if a rebuild is recommended, `False` otherwise. """ # Allow for uninstalled core extensions. - data = self.info['core_data'] if name in self.info['core_extensions']: config = self._read_build_config() uninstalled = config.get('uninstalled_core_extensions', []) @@ -775,7 +778,7 @@ def update_extension(self, name): Returns `True` if a rebuild is recommended, `False` otherwise. """ if name not in self.info['extensions']: - self.logger.warn('No labextension named "%s" installed' % name) + self.logger.warning('No labextension named "%s" installed' % name) return False return self._update_extension(name) @@ -815,8 +818,8 @@ def link_package(self, path): return self.install_extension(path) # Warn that it is a linked package. - self.logger.warn('Installing %s as a linked package:', path) - [self.logger.warn(m) for m in messages] + self.logger.warning('Installing %s as a linked package:', path) + [self.logger.warning(m) for m in messages] # Add to metadata. config = self._read_build_config() @@ -938,7 +941,7 @@ def _get_app_info(self): """ info = dict() - info['core_data'] = core_data = _get_core_data() + info['core_data'] = core_data = self.core_data info['extensions'] = extensions = self._get_extensions(core_data) page_config = self._read_page_config() info['disabled'] = page_config.get('disabledExtensions', []) @@ -963,7 +966,8 @@ def _get_app_info(self): info['sys_dir'] = self.sys_dir info['app_dir'] = self.app_dir - info['core_extensions'] = core_extensions = _get_core_extensions() + info['core_extensions'] = core_extensions = _get_core_extensions( + self.core_data) disabled_core = [] for key in core_extensions: @@ -1390,9 +1394,8 @@ def _install_extension(self, extension, tempdir): raise ValueError(msg % (extension, '\n'.join(messages))) # Verify package compatibility. - core_data = _get_core_data() deps = data.get('dependencies', dict()) - errors = _validate_compatibility(extension, deps, core_data) + errors = _validate_compatibility(extension, deps, self.core_data) if errors: msg = _format_compatibility_errors( data['name'], data['version'], errors @@ -1628,7 +1631,7 @@ def _node_check(logger): output = subprocess.check_output([node, 'node-version-check.js'], cwd=HERE) logger.debug(output.decode('utf-8')) except Exception: - data = _get_core_data() + data = CoreConfig()._data ver = data['engines']['node'] msg = 'Please install nodejs %s before continuing. nodejs may be installed using conda or directly from the nodejs website.' % ver raise ValueError(msg) @@ -1728,13 +1731,6 @@ def _tarsum(input_file): return h.hexdigest() -def _get_core_data(): - """Get the data for the app template. - """ - with open(pjoin(HERE, 'staging', 'package.json')) as fid: - return json.load(fid) - - def _get_static_data(app_dir): """Get the data for the app static dir. """ @@ -1931,10 +1927,10 @@ def _compat_error_age(errors): return 0 -def _get_core_extensions(): +def _get_core_extensions(core_data): """Get the core extensions. """ - data = _get_core_data()['jupyterlab'] + data = core_data['jupyterlab'] return list(data['extensions']) + list(data['mimeExtensions']) diff --git a/jupyterlab/coreconfig.py b/jupyterlab/coreconfig.py new file mode 100644 index 000000000000..af08f6cdd45b --- /dev/null +++ b/jupyterlab/coreconfig.py @@ -0,0 +1,165 @@ +# coding: utf-8 +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +from collections import defaultdict +from itertools import filterfalse +import json +import os.path as osp + +from .jlpmapp import HERE + + +def pjoin(*args): + """Join paths to create a real path. + """ + return osp.abspath(osp.join(*args)) + + +def _get_default_core_data(): + """Get the data for the app template. + """ + with open(pjoin(HERE, 'staging', 'package.json')) as fid: + return json.load(fid) + + +def _is_lab_package(name): + """Whether a package name is in the lab namespace""" + return name.startswith('@jupyterlab/') + + +def _only_nonlab(collection): + """Filter a dict/sequence to remove all lab packages + + This is useful to take the default values of e.g. singletons and filter + away the '@jupyterlab/' namespace packages, but leave any others (e.g. + phosphor and react). + """ + if isinstance(collection, dict): + return dict( + (k, v) for (k, v) in collection.items() + if not _is_lab_package(k) + ) + elif isinstance(collection, (list, tuple)): + return list(filterfalse(_is_lab_package, collection)) + raise TypeError('collection arg should be either dict or list/tuple') + + +class CoreConfig: + """An object representing a core config. + + This enables custom lab application to override some parts of the core + configuration of the build system. + """ + def __init__(self): + self._data = _get_default_core_data() + + def add(self, name, semver, extension=False, mime_extension=False): + """Remove an extension/singleton. + + If neither extension or mimeExtension is True (the default) + the package is added as a singleton dependency. + + name: string + The npm package name + semver: string + The semver range for the package + extension: bool + Whether the package is an extension + mime_extension: bool + Whether the package is a MIME extension + """ + data = self._data + if not name: + raise ValueError('Missing package name') + if not semver: + raise ValueError('Missing package semver') + if name in data['resolutions']: + raise ValueError('Package already present: %r' % (name,)) + data['resolutions'][name] = semver + + # If both mimeExtension and extensions are True, treat + # as mime extension + if mime_extension: + data['jupyterlab']['mimeExtensions'][name] = "" + data['dependencies'][name] = semver + elif extension: + data['jupyterlab']['extensions'][name] = "" + data['dependencies'][name] = semver + else: + data['jupyterlab']['singletonPackages'].append(name) + + def remove(self, name): + """Remove a package/extension. + + name: string + The npm package name + """ + data = self._data + maps = ( + data['dependencies'], + data['resolutions'], + data['jupyterlab']['extensions'], + data['jupyterlab']['mimeExtensions'], + ) + for m in maps: + try: + del m[name] + except KeyError: + pass + + data['jupyterlab']['singletonPackages'].remove(name) + + def clear_packages(self, lab_only=True): + """Clear the packages/extensions. + """ + data = self._data + # Clear all dependencies + if lab_only: + # Clear all "@jupyterlab/" dependencies + data['dependencies'] = _only_nonlab(data['dependencies']) + data['resolutions'] = _only_nonlab(data['resolutions']) + data['jupyterlab']['extensions'] = _only_nonlab( + data['jupyterlab']['extensions']) + data['jupyterlab']['mimeExtensions'] = _only_nonlab( + data['jupyterlab']['mimeExtensions']) + data['jupyterlab']['singletonPackages'] = _only_nonlab( + data['jupyterlab']['singletonPackages']) + else: + data['dependencies'] = {} + data['resolutions'] = {} + data['jupyterlab']['extensions'] = {} + data['jupyterlab']['mimeExtensions'] = {} + data['jupyterlab']['singletonPackages'] = [] + + @property + def extensions(self): + """A dict mapping all extension names to their semver""" + data = self._data + return dict( + (k, data['resolutions'][k]) + for k in data['jupyterlab']['extensions'].keys()) + + @property + def mime_extensions(self): + """A dict mapping all MIME extension names to their semver""" + data = self._data + return dict( + (k, data['resolutions'][k]) + for k in data['jupyterlab']['mimeExtensions'].keys()) + + @property + def singletons(self): + """A dict mapping all singleton names to their semver""" + data = self._data + return dict( + (k, data['resolutions'].get(k, None)) + for k in data['jupyterlab']['singletonPackages']) + + @property + def static_dir(self): + return self._data['jupyterlab']['staticDir'] + + @static_dir.setter + def static_dir(self, static_dir): + self._data['jupyterlab']['staticDir'] = static_dir diff --git a/jupyterlab/handlers/build_handler.py b/jupyterlab/handlers/build_handler.py index 80cf27e5fa89..c8bfbfec15db 100644 --- a/jupyterlab/handlers/build_handler.py +++ b/jupyterlab/handlers/build_handler.py @@ -21,10 +21,11 @@ class Builder(object): _kill_event = None _future = None - def __init__(self, log, core_mode, app_dir): + def __init__(self, log, core_mode, app_dir, core_config=None): self.log = log self.core_mode = core_mode self.app_dir = app_dir + self.core_config = core_config @gen.coroutine def get_status(self): @@ -34,7 +35,8 @@ def get_status(self): raise gen.Return(dict(status='building', message='')) try: - messages = yield self._run_build_check(self.app_dir, self.log) + messages = yield self._run_build_check( + self.app_dir, self.log, self.core_config) status = 'needed' if messages else 'stable' if messages: self.log.warn('Build recommended') @@ -60,7 +62,8 @@ def build(self): self.building = True self._kill_event = evt = Event() try: - yield self._run_build(self.app_dir, self.log, evt) + yield self._run_build( + self.app_dir, self.log, evt, self.core_config) future.set_result(True) except Exception as e: if str(e) == 'Aborted': @@ -84,12 +87,15 @@ def cancel(self): self.canceled = True @run_on_executor - def _run_build_check(self, app_dir, logger): - return build_check(app_dir=app_dir, logger=logger) + def _run_build_check(self, app_dir, logger, core_config): + return build_check( + app_dir=app_dir, logger=logger, core_config=core_config) @run_on_executor - def _run_build(self, app_dir, logger, kill_event): - kwargs = dict(app_dir=app_dir, logger=logger, kill_event=kill_event, command='build') + def _run_build(self, app_dir, logger, kill_event, core_config): + kwargs = dict( + app_dir=app_dir, logger=logger, kill_event=kill_event, + core_config=core_config, command='build') try: return build(**kwargs) except Exception as e: @@ -114,7 +120,7 @@ def get(self): @web.authenticated @gen.coroutine def delete(self): - self.log.warn('Canceling build') + self.log.warning('Canceling build') try: yield self.builder.cancel() except Exception as e: diff --git a/jupyterlab/handlers/extension_manager_handler.py b/jupyterlab/handlers/extension_manager_handler.py index 3ef873e357ba..df02348df367 100644 --- a/jupyterlab/handlers/extension_manager_handler.py +++ b/jupyterlab/handlers/extension_manager_handler.py @@ -66,9 +66,10 @@ def _build_check_info(app_dir, logger): class ExtensionManager(object): executor = ThreadPoolExecutor(max_workers=1) - def __init__(self, log, app_dir): + def __init__(self, log, app_dir, core_config=None): self.log = log self.app_dir = app_dir + self.core_config = core_config self._outdated = None # To start fetching data on outdated extensions immediately, uncomment: # IOLoop.current().spawn_callback(self._get_outdated) @@ -122,7 +123,9 @@ def list_extensions(self): def install(self, extension): """Handle an install/update request""" try: - install_extension(extension, app_dir=self.app_dir, logger=self.log) + install_extension( + extension, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) except ValueError as e: raise gen.Return(dict(status='error', message=str(e))) raise gen.Return(dict(status='ok',)) @@ -130,19 +133,25 @@ def install(self, extension): @gen.coroutine def uninstall(self, extension): """Handle an uninstall request""" - did_uninstall = uninstall_extension(extension, app_dir=self.app_dir, logger=self.log) + did_uninstall = uninstall_extension( + extension, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) raise gen.Return(dict(status='ok' if did_uninstall else 'error',)) @gen.coroutine def enable(self, extension): """Handle an enable request""" - enable_extension(extension, app_dir=self.app_dir, logger=self.log) + enable_extension( + extension, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) raise gen.Return(dict(status='ok',)) @gen.coroutine def disable(self, extension): """Handle a disable request""" - disable_extension(extension, app_dir=self.app_dir, logger=self.log) + disable_extension( + extension, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) raise gen.Return(dict(status='ok',)) @gen.coroutine @@ -187,6 +196,7 @@ def _load_outdated(self): names, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config, ) raise gen.Return(data) diff --git a/jupyterlab/labapp.py b/jupyterlab/labapp.py index a79cf8ff7e7b..992784d02560 100644 --- a/jupyterlab/labapp.py +++ b/jupyterlab/labapp.py @@ -14,7 +14,7 @@ from jupyterlab_server import slugify, WORKSPACE_EXTENSION from notebook.notebookapp import NotebookApp, aliases, flags from notebook.utils import url_path_join as ujoin -from traitlets import Bool, Unicode +from traitlets import Bool, Instance, Unicode from ._version import __version__ from .debuglog import DebugLogFileMixin @@ -23,6 +23,7 @@ build, clean, get_app_dir, get_app_version, get_user_settings_dir, get_workspaces_dir ) +from .coreconfig import CoreConfig build_aliases = dict(base_aliases) @@ -53,6 +54,9 @@ class LabBuildApp(JupyterApp, DebugLogFileMixin): aliases = build_aliases flags = build_flags + # Not configurable! + core_config = Instance(CoreConfig, allow_none=True) + app_dir = Unicode('', config=True, help="The app directory to build in") @@ -88,7 +92,8 @@ def start(self): clean(self.app_dir) self.log.info('Building in %s', app_dir) build(app_dir=app_dir, name=self.name, version=self.version, - command=command, logger=self.log) + command=command, logger=self.log, + core_config=self.core_config) clean_aliases = dict(base_aliases) @@ -105,6 +110,9 @@ class LabCleanApp(JupyterApp): """ aliases = clean_aliases + # Not configurable! + core_config = Instance(CoreConfig, allow_none=True) + app_dir = Unicode('', config=True, help='The app directory to clean') def start(self): @@ -411,7 +419,7 @@ def init_server_extensions(self): super(LabApp, self).init_server_extensions() msg = 'JupyterLab server extension not enabled, manually loading...' if not self.nbserver_extensions.get('jupyterlab', False): - self.log.warn(msg) + self.log.warning(msg) load_jupyter_server_extension(self) diff --git a/jupyterlab/labextensions.py b/jupyterlab/labextensions.py index bc5071a2c030..6d1984737eb4 100644 --- a/jupyterlab/labextensions.py +++ b/jupyterlab/labextensions.py @@ -11,7 +11,7 @@ from jupyter_core.application import JupyterApp, base_flags, base_aliases -from traitlets import Bool, Unicode +from traitlets import Bool, Instance, Unicode from .commands import ( install_extension, uninstall_extension, list_extensions, @@ -19,6 +19,7 @@ link_package, unlink_package, build, get_app_version, HERE, update_extension, ) +from .coreconfig import CoreConfig from .debuglog import DebugLogFileMixin @@ -65,6 +66,9 @@ class BaseExtensionApp(JupyterApp, DebugLogFileMixin): flags = flags aliases = aliases + # Not configurable! + core_config = Instance(CoreConfig, allow_none=True) + app_dir = Unicode('', config=True, help="The app directory to target") @@ -95,7 +99,8 @@ def start(self): command = ':'.join(parts) build(app_dir=self.app_dir, clean_staging=self.should_clean, - logger=self.log, command=command) + logger=self.log, command=command, + core_config=self.core_config) def run_task(self): pass @@ -111,7 +116,9 @@ class InstallLabExtensionApp(BaseExtensionApp): def run_task(self): self.extra_args = self.extra_args or [os.getcwd()] return any([ - install_extension(arg, self.app_dir, logger=self.log) + install_extension( + arg, self.app_dir, logger=self.log, + core_config=self.core_config) for arg in self.extra_args ]) @@ -128,9 +135,13 @@ def run_task(self): self.log.warn('Specify an extension to update, or use --all to update all extensions') return False if self.all: - return update_extension(all_=True, app_dir=self.app_dir, logger=self.log) + return update_extension( + all_=True, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) return any([ - update_extension(name=arg, app_dir=self.app_dir, logger=self.log) + update_extension( + name=arg, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) for arg in self.extra_args ]) @@ -149,7 +160,9 @@ class LinkLabExtensionApp(BaseExtensionApp): def run_task(self): self.extra_args = self.extra_args or [os.getcwd()] return any([ - link_package(arg, self.app_dir, logger=self.log) + link_package( + arg, self.app_dir, logger=self.log, + core_config=self.core_config) for arg in self.extra_args ]) @@ -160,7 +173,9 @@ class UnlinkLabExtensionApp(BaseExtensionApp): def run_task(self): self.extra_args = self.extra_args or [os.getcwd()] return any([ - unlink_package(arg, self.app_dir, logger=self.log) + unlink_package( + arg, self.app_dir, logger=self.log, + core_config=self.core_config) for arg in self.extra_args ]) @@ -175,7 +190,9 @@ class UninstallLabExtensionApp(BaseExtensionApp): def run_task(self): self.extra_args = self.extra_args or [os.getcwd()] return any([ - uninstall_extension(arg, all_=self.all, app_dir=self.app_dir, logger=self.log) + uninstall_extension( + arg, all_=self.all, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) for arg in self.extra_args ]) @@ -184,14 +201,16 @@ class ListLabExtensionsApp(BaseExtensionApp): description = "List the installed labextensions" def run_task(self): - list_extensions(self.app_dir, logger=self.log) + list_extensions( + self.app_dir, logger=self.log, core_config=self.core_config) class EnableLabExtensionsApp(BaseExtensionApp): description = "Enable labextension(s) by name" def run_task(self): - [enable_extension(arg, self.app_dir, logger=self.log) + [enable_extension( + arg, self.app_dir, logger=self.log, core_config=self.core_config) for arg in self.extra_args] @@ -199,7 +218,8 @@ class DisableLabExtensionsApp(BaseExtensionApp): description = "Disable labextension(s) by name" def run_task(self): - [disable_extension(arg, self.app_dir, logger=self.log) + [disable_extension( + arg, self.app_dir, logger=self.log, core_config=self.core_config) for arg in self.extra_args] @@ -215,7 +235,8 @@ def run_task(self): check_extension( arg, self.app_dir, self.should_check_installed_only, - logger=self.log) + logger=self.log, + core_config=self.core_config) for arg in self.extra_args) if not all_enabled: self.exit(1) diff --git a/jupyterlab/staging/webpack.config.js b/jupyterlab/staging/webpack.config.js index 78c1769a515d..5a2612582ddf 100644 --- a/jupyterlab/staging/webpack.config.js +++ b/jupyterlab/staging/webpack.config.js @@ -189,8 +189,22 @@ module.exports = [ }, { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader' }, { + // in css files, svg is loaded as a url formatted string test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, - use: 'url-loader?limit=10000&mimetype=image/svg+xml' + issuer: { test: /\.css$/ }, + use: { + loader: 'svg-url-loader', + options: { encoding: 'none', limit: 10000 } + } + }, + { + // in ts and tsx files (both of which compile to js), + // svg is loaded as a raw string + test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, + issuer: { test: /\.js$/ }, + use: { + loader: 'raw-loader' + } } ] }, diff --git a/jupyterlab/tests/test_jupyterlab.py b/jupyterlab/tests/test_jupyterlab.py index 415b19c23fd7..73c95f6173a7 100644 --- a/jupyterlab/tests/test_jupyterlab.py +++ b/jupyterlab/tests/test_jupyterlab.py @@ -5,6 +5,7 @@ # Distributed under the terms of the Modified BSD License. import glob import json +import logging import os import shutil import sys @@ -23,9 +24,9 @@ install_extension, uninstall_extension, list_extensions, build, link_package, unlink_package, build_check, disable_extension, enable_extension, get_app_info, - check_extension, _test_overlap, _get_core_data, - update_extension + check_extension, _test_overlap, update_extension ) +from jupyterlab.coreconfig import CoreConfig, _get_default_core_data here = os.path.dirname(os.path.abspath(__file__)) @@ -379,6 +380,51 @@ def test_build_custom(self): assert data['jupyterlab']['version'] == '1.0' assert data['jupyterlab']['staticUrl'] == 'bar' + def test_build_custom_minimal_core_config(self): + default_config = CoreConfig() + core_config = CoreConfig() + core_config.clear_packages() + logger = logging.getLogger('jupyterlab_test_logger') + logger.setLevel('DEBUG') + extensions = ( + '@jupyterlab/application-extension', + '@jupyterlab/apputils-extension', + ) + singletons = ( + "@jupyterlab/application", + "@jupyterlab/apputils", + "@jupyterlab/coreutils", + "@jupyterlab/services", + ) + for name in extensions: + semver = default_config.extensions[name] + core_config.add(name, semver, extension=True) + for name in singletons: + semver = default_config.singletons[name] + core_config.add(name, semver) + + assert install_extension(self.mock_extension) is True + build(core_config=core_config, logger=logger) + + # check static directory. + entry = pjoin(self.app_dir, 'static', 'index.out.js') + with open(entry) as fid: + data = fid.read() + assert self.pkg_names['extension'] in data + + pkg = pjoin(self.app_dir, 'static', 'package.json') + with open(pkg) as fid: + data = json.load(fid) + assert list(data['jupyterlab']['extensions'].keys()) == [ + '@jupyterlab/application-extension', + '@jupyterlab/apputils-extension', + self.pkg_names['extension'], + ] + assert data['jupyterlab']['mimeExtensions'] == {} + for pkg in data['jupyterlab']['singletonPackages']: + if pkg.startswith('@jupyterlab/'): + assert pkg in singletons + def test_load_extension(self): app = NotebookApp() stderr = sys.stderr @@ -477,7 +523,7 @@ def test_compatibility(self): assert _test_overlap('<0.6', '0.1') is None def test_install_compatible(self): - core_data = _get_core_data() + core_data = _get_default_core_data() current_app_dep = core_data['dependencies']['@jupyterlab/application'] def _gen_dep(ver): return { "dependencies": { diff --git a/package.json b/package.json index 0790ee3d7fbd..98273ec575d3 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "lerna": "^3.13.2", "lint-staged": "^8.1.5", "open-cli": "^5.0.0", - "prettier": "^1.17.0", + "prettier": "^1.18.2", "tslint": "^5.15.0", "tslint-config-prettier": "^1.18.0", "tslint-plugin-prettier": "^2.0.1", diff --git a/packages/application/package.json b/packages/application/package.json index cc8c7853c8b4..f1a0ee521bda 100644 --- a/packages/application/package.json +++ b/packages/application/package.json @@ -42,6 +42,7 @@ "@jupyterlab/rendermime": "^1.1.0-alpha.1", "@jupyterlab/rendermime-interfaces": "^1.4.0-alpha.1", "@jupyterlab/services": "^4.1.0-alpha.1", + "@jupyterlab/ui-components": "^1.1.0-alpha.1", "@phosphor/algorithm": "^1.1.3", "@phosphor/application": "^1.6.3", "@phosphor/commands": "^1.6.3", diff --git a/packages/application/src/shell.ts b/packages/application/src/shell.ts index 25d3f821849b..ca9778a0cdae 100644 --- a/packages/application/src/shell.ts +++ b/packages/application/src/shell.ts @@ -5,6 +5,8 @@ import { Debouncer } from '@jupyterlab/coreutils'; import { DocumentRegistry } from '@jupyterlab/docregistry'; +import { DockPanelSvg, TabBarSvg } from '@jupyterlab/ui-components'; + import { ArrayExt, find, IIterator, iter, toArray } from '@phosphor/algorithm'; import { PromiseDelegate, Token } from '@phosphor/coreutils'; @@ -176,7 +178,9 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell { let bottomPanel = (this._bottomPanel = new BoxPanel()); let topPanel = (this._topPanel = new Panel()); let hboxPanel = new BoxPanel(); - let dockPanel = (this._dockPanel = new DockPanel()); + let dockPanel = (this._dockPanel = new DockPanelSvg({ + kind: 'dockPanelBar' + })); let headerPanel = (this._headerPanel = new Panel()); MessageLoop.installMessageHook(dockPanel, this._dockChildHook); @@ -1028,7 +1032,8 @@ namespace Private { * Construct a new side bar handler. */ constructor(side: string) { - this._sideBar = new TabBar({ + this._sideBar = new TabBarSvg({ + kind: 'sideBar', insertBehavior: 'none', removeBehavior: 'none', allowDeselect: true diff --git a/packages/application/style/icons.css b/packages/application/style/icons.css index 3b59b877e285..a21e91197379 100644 --- a/packages/application/style/icons.css +++ b/packages/application/style/icons.css @@ -3,6 +3,204 @@ | Distributed under the terms of the Modified BSD License. |----------------------------------------------------------------------------*/ +/** + * Support for icons as inline SVG HTMLElements + */ + +/* recolor the primary elements of an icon */ +.jp-icon0[fill] { + fill: var(--jp-inverse-layout-color0); +} +.jp-icon1[fill] { + fill: var(--jp-inverse-layout-color1); +} +.jp-icon2[fill] { + fill: var(--jp-inverse-layout-color2); +} +.jp-icon3[fill] { + fill: var(--jp-inverse-layout-color3); +} +.jp-icon4[fill] { + fill: var(--jp-inverse-layout-color4); +} + +.jp-icon0[stroke] { + stroke: var(--jp-inverse-layout-color0); +} +.jp-icon1[stroke] { + stroke: var(--jp-inverse-layout-color1); +} +.jp-icon2[stroke] { + stroke: var(--jp-inverse-layout-color2); +} +.jp-icon3[stroke] { + stroke: var(--jp-inverse-layout-color3); +} +.jp-icon4[stroke] { + stroke: var(--jp-inverse-layout-color4); +} + +/* recolor the accent elements of an icon */ +.jp-icon-accent0[fill] { + fill: var(--jp-layout-color0); +} +.jp-icon-accent1[fill] { + fill: var(--jp-layout-color1); +} +.jp-icon-accent2[fill] { + fill: var(--jp-layout-color2); +} +.jp-icon-accent3[fill] { + fill: var(--jp-layout-color3); +} +.jp-icon-accent4[fill] { + fill: var(--jp-layout-color4); +} + +.jp-icon-accent0[stroke] { + stroke: var(--jp-layout-color0); +} +.jp-icon-accent1[stroke] { + stroke: var(--jp-layout-color1); +} +.jp-icon-accent2[stroke] { + stroke: var(--jp-layout-color2); +} +.jp-icon-accent3[stroke] { + stroke: var(--jp-layout-color3); +} +.jp-icon-accent4[stroke] { + stroke: var(--jp-layout-color4); +} + +/* brand icon colors. Same for light and dark */ +.jp-icon-brand0[fill] { + fill: var(--jp-brand-color0); +} +.jp-icon-brand1[fill] { + fill: var(--jp-brand-color1); +} +.jp-icon-brand2[fill] { + fill: var(--jp-brand-color2); +} +.jp-icon-brand3[fill] { + fill: var(--jp-brand-color3); +} +.jp-icon-brand4[fill] { + fill: var(--jp-brand-color4); +} + +.jp-icon-brand0[stroke] { + stroke: var(--jp-brand-color0); +} +.jp-icon-brand1[stroke] { + stroke: var(--jp-brand-color1); +} +.jp-icon-brand2[stroke] { + stroke: var(--jp-brand-color2); +} +.jp-icon-brand3[stroke] { + stroke: var(--jp-brand-color3); +} +.jp-icon-brand4[stroke] { + stroke: var(--jp-brand-color4); +} + +/* warn icon colors. Same for light and dark */ +.jp-icon-warn0[fill] { + fill: var(--jp-warn-color0); +} +.jp-icon-warn1[fill] { + fill: var(--jp-warn-color1); +} +.jp-icon-warn2[fill] { + fill: var(--jp-warn-color2); +} +.jp-icon-warn3[fill] { + fill: var(--jp-warn-color3); +} + +.jp-icon-warn0[stroke] { + stroke: var(--jp-warn-color0); +} +.jp-icon-warn1[stroke] { + stroke: var(--jp-warn-color1); +} +.jp-icon-warn2[stroke] { + stroke: var(--jp-warn-color2); +} +.jp-icon-warn3[stroke] { + stroke: var(--jp-warn-color3); +} + +/* icon colors that contrast well with each other and most backgrounds */ +.jp-icon-contrast0[fill] { + fill: var(--jp-icon-contrast-color0); +} +.jp-icon-contrast1[fill] { + fill: var(--jp-icon-contrast-color1); +} +.jp-icon-contrast2[fill] { + fill: var(--jp-icon-contrast-color2); +} +.jp-icon-contrast3[fill] { + fill: var(--jp-icon-contrast-color3); +} + +.jp-icon-contrast0[stroke] { + stroke: var(--jp-icon-contrast-color0); +} +.jp-icon-contrast1[stroke] { + stroke: var(--jp-icon-contrast-color1); +} +.jp-icon-contrast2[stroke] { + stroke: var(--jp-icon-contrast-color2); +} +.jp-icon-contrast3[stroke] { + stroke: var(--jp-icon-contrast-color3); +} + +/* CSS for icons in selected items in the settings editor */ +#setting-editor .jp-PluginList .jp-mod-selected .jp-icon-selectable[fill] { + fill: white; +} + +#setting-editor + .jp-PluginList + .jp-mod-selected + .jp-icon-selectable-inverse[fill] { + fill: var(--jp-brand-color1); +} + +/* CSS for icons in selected filebrowser listing items */ +.jp-DirListing-item.jp-mod-selected .jp-icon-selectable[fill] { + fill: white; +} + +.jp-DirListing-item.jp-mod-selected .jp-icon-selectable-inverse[fill] { + fill: var(--jp-brand-color1); +} + +/* CSS for icons in selected tabs in the sidebar tab manager */ +#tab-manager .p-TabBar-tab.jp-mod-active .jp-icon-selectable[fill] { + fill: white; +} + +#tab-manager .p-TabBar-tab.jp-mod-active .jp-icon-selectable-inverse[fill] { + fill: var(--jp-brand-color1); +} + +/* special handling for splash icon CSS. While the theme CSS reloads during + splash, the splash icon can loose theming. To prevent that, we set a + default for its color variable */ +:root { + --jp-warn-color0: var(--md-orange-700); +} + +/** + * (DEPRECATED) Support for icons as CSS `background-images` + */ + .jp-MaterialIcon { min-width: 16px; min-height: 16px; @@ -95,12 +293,8 @@ background-image: var(--jp-icon-ellipses); } -.jp-ExtensionIcon { - background-image: var(--jp-icon-extension); -} - -.jp-FileIcon { - background-image: var(--jp-icon-file); +.jp-FileUploadIcon { + background-image: var(--jp-icon-file-upload); } .jp-FilledCircleIcon { @@ -111,10 +305,6 @@ background-image: var(--jp-icon-filter-list); } -.jp-FolderIcon { - background-image: var(--jp-icon-folder); -} - .jp-HomeIcon { background-image: var(--jp-icon-home); min-width: 16px; @@ -123,14 +313,6 @@ vertical-align: sub; } -.jp-ImageIcon { - background-image: var(--jp-icon-image); -} - -.jp-JSONIcon { - background-image: var(--jp-icon-json); -} - .jp-JupyterIcon { background-image: var(--jp-image-jupyter); background-size: 16px; @@ -140,10 +322,6 @@ padding-right: 0px; } -.jp-KernelIcon { - background-image: var(--jp-icon-kernel); -} - .jp-KeyboardIcon { background-image: var(--jp-icon-keyboard); } @@ -169,10 +347,6 @@ width: 20px; } -.jp-MarkdownIcon { - background-image: var(--jp-icon-markdown); -} - .jp-MoreHorizIcon { background-image: var(--jp-icon-more); } @@ -181,30 +355,10 @@ background-image: var(--jp-icon-new-folder); } -.jp-NotebookIcon { - background-image: var(--jp-icon-notebook); -} - -.jp-FolderIcon { - background-image: var(--jp-icon-folder); -} - -.jp-PaletteIcon { - background-image: var(--jp-icon-palette); -} - .jp-PasteIcon { background-image: var(--jp-icon-paste); } -.jp-PythonIcon { - background-image: var(--jp-icon-python); -} - -.jp-RKernelIcon { - background-image: var(--jp-icon-r); -} - .jp-RefreshIcon { background-image: var(--jp-icon-refresh); } @@ -213,10 +367,6 @@ background-image: var(--jp-icon-run); } -.jp-RunningIcon { - background-image: var(--jp-icon-stop-circle); -} - .jp-SaveIcon { background-image: var(--jp-icon-save); } @@ -225,22 +375,10 @@ background-image: var(--jp-icon-settings); } -.jp-SpreadsheetIcon { - background-image: var(--jp-icon-spreadsheet); -} - .jp-StopIcon { background-image: var(--jp-icon-stop); } -.jp-TabIcon { - background-image: var(--jp-icon-tab); -} - -.jp-TerminalIcon { - background-image: var(--jp-icon-terminal); -} - .jp-TextEditorIcon { background-image: var(--jp-icon-text-editor); } @@ -249,14 +387,6 @@ background-image: var(--jp-icon-undo); } -.jp-FileUploadIcon { - background-image: var(--jp-icon-file-upload); -} - -.jp-YAMLIcon { - background-image: var(--jp-icon-yaml); -} - .jp-VegaIcon { background-image: var(--jp-icon-vega); } diff --git a/packages/application/style/images.css b/packages/application/style/images.css index 6c966fe3036f..51cedf03ee4b 100644 --- a/packages/application/style/images.css +++ b/packages/application/style/images.css @@ -3,6 +3,8 @@ | Distributed under the terms of the Modified BSD License. |----------------------------------------------------------------------------*/ +/* CSS for specific icons */ + .jp-ImageJupyterLab { background-image: var(--jp-image-jupyterlab); } @@ -11,10 +13,6 @@ background-image: var(--jp-image-jupyterlab-wordmark); } -.jp-ImageJupyter-favicon { - background-image: var(--jp-image-jupyter-favicon); -} - .jp-JupyterIcon { background-image: var(--jp-image-jupyter); background-size: 16px; diff --git a/packages/application/style/index.css b/packages/application/style/index.css index 664fa92bba98..6ece208121b6 100644 --- a/packages/application/style/index.css +++ b/packages/application/style/index.css @@ -5,6 +5,7 @@ /* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ @import url('~@phosphor/widgets/style/index.css'); +@import url('~@jupyterlab/ui-components/style/index.css'); @import url('~@jupyterlab/apputils/style/index.css'); @import url('~@jupyterlab/docregistry/style/index.css'); @import url('~font-awesome/css/font-awesome.min.css'); diff --git a/packages/application/tsconfig.json b/packages/application/tsconfig.json index 3dfa87de215c..27ada107b7cf 100644 --- a/packages/application/tsconfig.json +++ b/packages/application/tsconfig.json @@ -23,6 +23,9 @@ }, { "path": "../services" + }, + { + "path": "../ui-components" } ] } diff --git a/packages/apputils-extension/package.json b/packages/apputils-extension/package.json index 2f9ff1262af9..9dac53222948 100644 --- a/packages/apputils-extension/package.json +++ b/packages/apputils-extension/package.json @@ -41,6 +41,7 @@ "@jupyterlab/apputils": "^1.1.0-alpha.1", "@jupyterlab/coreutils": "^3.1.0-alpha.1", "@jupyterlab/mainmenu": "^1.1.0-alpha.1", + "@jupyterlab/ui-components": "^1.1.0-alpha.1", "@phosphor/algorithm": "^1.1.3", "@phosphor/commands": "^1.6.3", "@phosphor/coreutils": "^1.3.1", diff --git a/packages/apputils-extension/src/index.ts b/packages/apputils-extension/src/index.ts index e447f9540b69..c07def7b84f6 100644 --- a/packages/apputils-extension/src/index.ts +++ b/packages/apputils-extension/src/index.ts @@ -34,6 +34,8 @@ import { import { IMainMenu } from '@jupyterlab/mainmenu'; +import { defaultIconRegistry } from '@jupyterlab/ui-components'; + import { PromiseDelegate } from '@phosphor/coreutils'; import { DisposableDelegate } from '@phosphor/disposable'; @@ -291,6 +293,13 @@ const splash: JupyterFrontEndPlugin = { galaxy.id = 'galaxy'; logo.id = 'main-logo'; + defaultIconRegistry.icon({ + name: 'jupyter-favicon', + container: logo, + center: true, + kind: 'splash' + }); + galaxy.appendChild(logo); ['1', '2', '3'].forEach(id => { const moon = document.createElement('div'); diff --git a/packages/apputils-extension/style/images/jupyter-favicon.svg b/packages/apputils-extension/style/images/jupyter-favicon.svg deleted file mode 100644 index 41d846fc9d5f..000000000000 --- a/packages/apputils-extension/style/images/jupyter-favicon.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - logo - Created with Sketch. - - - - - \ No newline at end of file diff --git a/packages/apputils-extension/style/index.css b/packages/apputils-extension/style/index.css index 39f879fb3639..0f1065092855 100644 --- a/packages/apputils-extension/style/index.css +++ b/packages/apputils-extension/style/index.css @@ -5,6 +5,7 @@ /* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ @import url('~@phosphor/widgets/style/index.css'); +@import url('~@jupyterlab/ui-components/style/index.css'); @import url('~@jupyterlab/apputils/style/index.css'); @import url('~@jupyterlab/application/style/index.css'); @import url('~@jupyterlab/mainmenu/style/index.css'); diff --git a/packages/apputils-extension/style/splash.css b/packages/apputils-extension/style/splash.css index c810edccd31b..891adef97267 100644 --- a/packages/apputils-extension/style/splash.css +++ b/packages/apputils-extension/style/splash.css @@ -33,18 +33,6 @@ height: 100%; } -#main-logo { - background-image: url('./images/jupyter-favicon.svg'); - background-repeat: no-repeat; - background-size: 100px; - position: absolute; - background-position: center; - width: 100%; - height: 100%; - z-index: 1; - animation: 0.3s fade-in linear forwards; -} - .planet { background-repeat: no-repeat; background-size: cover; diff --git a/packages/apputils-extension/tsconfig.json b/packages/apputils-extension/tsconfig.json index 17ecbea70fb2..298728e8d135 100644 --- a/packages/apputils-extension/tsconfig.json +++ b/packages/apputils-extension/tsconfig.json @@ -17,6 +17,9 @@ }, { "path": "../mainmenu" + }, + { + "path": "../ui-components" } ] } diff --git a/packages/apputils/src/domutils.ts b/packages/apputils/src/domutils.ts index 8237b231f50f..ee14f99fdc98 100644 --- a/packages/apputils/src/domutils.ts +++ b/packages/apputils/src/domutils.ts @@ -34,6 +34,18 @@ export namespace DOMUtils { return parent.querySelector(`.${className}`) as HTMLElement; } + /** + * Find the first element matching a class name. + */ + export function findElements( + parent: HTMLElement, + className: string + ): HTMLCollectionOf { + return parent.getElementsByClassName(className) as HTMLCollectionOf< + HTMLElement + >; + } + /** * Create a DOM id with prefix "id-" to solve bug for UUIDs beginning with numbers. */ diff --git a/packages/apputils/style/index.css b/packages/apputils/style/index.css index 9a38dcba26bc..38810a9cb009 100644 --- a/packages/apputils/style/index.css +++ b/packages/apputils/style/index.css @@ -4,7 +4,7 @@ |----------------------------------------------------------------------------*/ /* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ -@import url('~@jupyterlab/ui-components/style/index.css'); @import url('~@phosphor/widgets/style/index.css'); +@import url('~@jupyterlab/ui-components/style/index.css'); @import url('./base.css'); diff --git a/packages/codemirror-extension/src/index.ts b/packages/codemirror-extension/src/index.ts index 16c0e1a5f3ad..f07a96c45419 100644 --- a/packages/codemirror-extension/src/index.ts +++ b/packages/codemirror-extension/src/index.ts @@ -71,13 +71,18 @@ const commands: JupyterFrontEndPlugin = { export const editorSyntaxStatus: JupyterFrontEndPlugin = { id: '@jupyterlab/codemirror-extension:editor-syntax-status', autoStart: true, - requires: [IStatusBar, IEditorTracker, ILabShell], + requires: [IEditorTracker, ILabShell], + optional: [IStatusBar], activate: ( app: JupyterFrontEnd, - statusBar: IStatusBar, tracker: IEditorTracker, - labShell: ILabShell + labShell: ILabShell, + statusBar: IStatusBar | null ) => { + if (!statusBar) { + // Automatically disable if statusbar missing + return; + } let item = new EditorSyntaxStatus({ commands: app.commands }); labShell.currentChanged.connect(() => { const current = labShell.currentWidget; diff --git a/packages/coreutils/src/path.ts b/packages/coreutils/src/path.ts index b7ee7919dce7..db1b57d12514 100644 --- a/packages/coreutils/src/path.ts +++ b/packages/coreutils/src/path.ts @@ -61,6 +61,23 @@ export namespace PathExt { return posix.extname(path); } + /** + * Get the last portion of a path, without its extension (if any). + * + * @param path - The file path. + * + * @returns the last part of the path, sans extension. + */ + export function stem(path: string): string { + return path + .split('\\') + .pop() + .split('/') + .pop() + .split('.') + .shift(); + } + /** * Normalize a string path, reducing '..' and '.' parts. * When multiple slashes are found, they're replaced by a single one; when the path contains a trailing slash, it is preserved. On Windows backslashes are used. diff --git a/packages/coreutils/src/text.ts b/packages/coreutils/src/text.ts index ef8de712b5fb..7892a1b11a7e 100644 --- a/packages/coreutils/src/text.ts +++ b/packages/coreutils/src/text.ts @@ -71,6 +71,29 @@ export namespace Text { return jsIdx; } + /** + * Given a 'snake-case', 'snake_case', or 'snake case' string, + * will return the camel case version: 'snakeCase'. + * + * @param str: the snake-case input string. + * + * @param upper: default = false. If true, the first letter of the + * returned string will be capitalized. + * + * @returns the camel case version of the input string. + */ + export function camelCase(str: string, upper: boolean = false): string { + return str.replace(/(?:^\w|[A-Z]|\b\w|\s+|-+|_+)/g, function(match, index) { + if (+match === 0 || match[0] === '-') { + return ''; + } else if (index === 0 && !upper) { + return match.toLowerCase(); + } else { + return match.toUpperCase(); + } + }); + } + /** * Given a string, title case the words in the string. * diff --git a/packages/csvviewer-extension/src/index.ts b/packages/csvviewer-extension/src/index.ts index db69d5773750..a8f21c14b66c 100644 --- a/packages/csvviewer-extension/src/index.ts +++ b/packages/csvviewer-extension/src/index.ts @@ -35,8 +35,13 @@ const FACTORY_TSV = 'TSVTable'; const csv: JupyterFrontEndPlugin = { activate: activateCsv, id: '@jupyterlab/csvviewer-extension:csv', - requires: [ILayoutRestorer, IThemeManager, IMainMenu], - optional: [ISearchProviderRegistry], + requires: [], + optional: [ + ILayoutRestorer, + IThemeManager, + IMainMenu, + ISearchProviderRegistry + ], autoStart: true }; @@ -46,8 +51,13 @@ const csv: JupyterFrontEndPlugin = { const tsv: JupyterFrontEndPlugin = { activate: activateTsv, id: '@jupyterlab/csvviewer-extension:tsv', - requires: [ILayoutRestorer, IThemeManager, IMainMenu], - optional: [ISearchProviderRegistry], + requires: [], + optional: [ + ILayoutRestorer, + IThemeManager, + IMainMenu, + ISearchProviderRegistry + ], autoStart: true }; @@ -66,7 +76,7 @@ function addMenuEntries( title: 'Go to Line', value: 0 }).then(value => { - if (value.button.accept) { + if (value.button.accept && value.value !== null) { widget.content.goToLine(value.value); } }); @@ -79,10 +89,10 @@ function addMenuEntries( */ function activateCsv( app: JupyterFrontEnd, - restorer: ILayoutRestorer, - themeManager: IThemeManager, - mainMenu: IMainMenu, - searchregistry: ISearchProviderRegistry = null + restorer: ILayoutRestorer | null, + themeManager: IThemeManager | null, + mainMenu: IMainMenu | null, + searchregistry: ISearchProviderRegistry | null ): void { const factory = new CSVViewerFactory({ name: FACTORY_CSV, @@ -98,12 +108,14 @@ function activateCsv( let style: DataGrid.IStyle = Private.LIGHT_STYLE; let rendererConfig: TextRenderConfig = Private.LIGHT_TEXT_CONFIG; - // Handle state restoration. - void restorer.restore(tracker, { - command: 'docmanager:open', - args: widget => ({ path: widget.context.path, factory: FACTORY_CSV }), - name: widget => widget.context.path - }); + if (restorer) { + // Handle state restoration. + void restorer.restore(tracker, { + command: 'docmanager:open', + args: widget => ({ path: widget.context.path, factory: FACTORY_CSV }), + name: widget => widget.context.path + }); + } app.docRegistry.addWidgetFactory(factory); let ft = app.docRegistry.getFileType('csv'); @@ -116,8 +128,8 @@ function activateCsv( }); if (ft) { - widget.title.iconClass = ft.iconClass; - widget.title.iconLabel = ft.iconLabel; + widget.title.iconClass = ft.iconClass!; + widget.title.iconLabel = ft.iconLabel!; } // Set the theme for the new widget. widget.content.style = style; @@ -126,7 +138,10 @@ function activateCsv( // Keep the themes up-to-date. const updateThemes = () => { - const isLight = themeManager.isLight(themeManager.theme); + const isLight = + themeManager && themeManager.theme + ? themeManager.isLight(themeManager.theme) + : true; style = isLight ? Private.LIGHT_STYLE : Private.DARK_STYLE; rendererConfig = isLight ? Private.LIGHT_TEXT_CONFIG @@ -136,9 +151,13 @@ function activateCsv( grid.content.rendererConfig = rendererConfig; }); }; - themeManager.themeChanged.connect(updateThemes); + if (themeManager) { + themeManager.themeChanged.connect(updateThemes); + } - addMenuEntries(mainMenu, tracker); + if (mainMenu) { + addMenuEntries(mainMenu, tracker); + } if (searchregistry) { searchregistry.register('csv', CSVSearchProvider); } @@ -149,10 +168,10 @@ function activateCsv( */ function activateTsv( app: JupyterFrontEnd, - restorer: ILayoutRestorer, - themeManager: IThemeManager, - mainMenu: IMainMenu, - searchregistry: ISearchProviderRegistry = null + restorer: ILayoutRestorer | null, + themeManager: IThemeManager | null, + mainMenu: IMainMenu | null, + searchregistry: ISearchProviderRegistry | null ): void { const factory = new TSVViewerFactory({ name: FACTORY_TSV, @@ -168,12 +187,14 @@ function activateTsv( let style: DataGrid.IStyle = Private.LIGHT_STYLE; let rendererConfig: TextRenderConfig = Private.LIGHT_TEXT_CONFIG; - // Handle state restoration. - void restorer.restore(tracker, { - command: 'docmanager:open', - args: widget => ({ path: widget.context.path, factory: FACTORY_TSV }), - name: widget => widget.context.path - }); + if (restorer) { + // Handle state restoration. + void restorer.restore(tracker, { + command: 'docmanager:open', + args: widget => ({ path: widget.context.path, factory: FACTORY_TSV }), + name: widget => widget.context.path + }); + } app.docRegistry.addWidgetFactory(factory); let ft = app.docRegistry.getFileType('tsv'); @@ -186,8 +207,8 @@ function activateTsv( }); if (ft) { - widget.title.iconClass = ft.iconClass; - widget.title.iconLabel = ft.iconLabel; + widget.title.iconClass = ft.iconClass!; + widget.title.iconLabel = ft.iconLabel!; } // Set the theme for the new widget. widget.content.style = style; @@ -196,7 +217,10 @@ function activateTsv( // Keep the themes up-to-date. const updateThemes = () => { - const isLight = themeManager.isLight(themeManager.theme); + const isLight = + themeManager && themeManager.theme + ? themeManager.isLight(themeManager.theme) + : true; style = isLight ? Private.LIGHT_STYLE : Private.DARK_STYLE; rendererConfig = isLight ? Private.LIGHT_TEXT_CONFIG @@ -206,9 +230,13 @@ function activateTsv( grid.content.rendererConfig = rendererConfig; }); }; - themeManager.themeChanged.connect(updateThemes); + if (themeManager) { + themeManager.themeChanged.connect(updateThemes); + } - addMenuEntries(mainMenu, tracker); + if (mainMenu) { + addMenuEntries(mainMenu, tracker); + } if (searchregistry) { searchregistry.register('tsv', CSVSearchProvider); } diff --git a/packages/docmanager-extension/src/index.ts b/packages/docmanager-extension/src/index.ts index d1fec3d07a71..0e52205b8457 100644 --- a/packages/docmanager-extension/src/index.ts +++ b/packages/docmanager-extension/src/index.ts @@ -167,13 +167,18 @@ const docManagerPlugin: JupyterFrontEndPlugin = { export const savingStatusPlugin: JupyterFrontEndPlugin = { id: '@jupyterlab/docmanager-extension:saving-status', autoStart: true, - requires: [IStatusBar, IDocumentManager, ILabShell], + requires: [IDocumentManager, ILabShell], + optional: [IStatusBar], activate: ( _: JupyterFrontEnd, - statusBar: IStatusBar, docManager: IDocumentManager, - labShell: ILabShell + labShell: ILabShell, + statusBar: IStatusBar | null ) => { + if (!statusBar) { + // Automatically disable if statusbar missing + return; + } const saving = new SavingStatus({ docManager }); // Keep the currently active widget synchronized. @@ -197,13 +202,18 @@ export const savingStatusPlugin: JupyterFrontEndPlugin = { export const pathStatusPlugin: JupyterFrontEndPlugin = { id: '@jupyterlab/docmanager-extension:path-status', autoStart: true, - requires: [IStatusBar, IDocumentManager, ILabShell], + requires: [IDocumentManager, ILabShell], + optional: [IStatusBar], activate: ( _: JupyterFrontEnd, - statusBar: IStatusBar, docManager: IDocumentManager, - labShell: ILabShell + labShell: ILabShell, + statusBar: IStatusBar | null ) => { + if (!statusBar) { + // Automatically disable if statusbar missing + return; + } const path = new PathStatus({ docManager }); // Keep the file path widget up-to-date with the application active widget. diff --git a/packages/docregistry/src/registry.ts b/packages/docregistry/src/registry.ts index f234672f5963..cebc01a43648 100644 --- a/packages/docregistry/src/registry.ts +++ b/packages/docregistry/src/registry.ts @@ -1120,7 +1120,7 @@ export namespace DocumentRegistry { name: 'default', extensions: [], mimeTypes: [], - iconClass: 'jp-MaterialIcon jp-FileIcon', + iconClass: 'jp-FileIcon', iconLabel: '', contentType: 'file', fileFormat: 'text' @@ -1171,7 +1171,7 @@ export namespace DocumentRegistry { extensions: ['.ipynb'], contentType: 'notebook', fileFormat: 'json', - iconClass: 'jp-MaterialIcon jp-NotebookIcon' + iconClass: 'jp-NotebookIcon' }; /** @@ -1183,7 +1183,7 @@ export namespace DocumentRegistry { extensions: [], mimeTypes: ['text/directory'], contentType: 'directory', - iconClass: 'jp-MaterialIcon jp-FolderIcon' + iconClass: 'jp-FolderIcon' }; /** @@ -1198,56 +1198,56 @@ export namespace DocumentRegistry { displayName: 'Markdown File', extensions: ['.md'], mimeTypes: ['text/markdown'], - iconClass: 'jp-MaterialIcon jp-MarkdownIcon' + iconClass: 'jp-MarkdownIcon' }, { name: 'python', displayName: 'Python File', extensions: ['.py'], mimeTypes: ['text/x-python'], - iconClass: 'jp-MaterialIcon jp-PythonIcon' + iconClass: 'jp-PythonIcon' }, { name: 'json', displayName: 'JSON File', extensions: ['.json'], mimeTypes: ['application/json'], - iconClass: 'jp-MaterialIcon jp-JSONIcon' + iconClass: 'jp-JsonIcon' }, { name: 'csv', displayName: 'CSV File', extensions: ['.csv'], mimeTypes: ['text/csv'], - iconClass: 'jp-MaterialIcon jp-SpreadsheetIcon' + iconClass: 'jp-SpreadsheetIcon' }, { name: 'tsv', displayName: 'TSV File', extensions: ['.tsv'], mimeTypes: ['text/csv'], - iconClass: 'jp-MaterialIcon jp-SpreadsheetIcon' + iconClass: 'jp-SpreadsheetIcon' }, { name: 'r', displayName: 'R File', mimeTypes: ['text/x-rsrc'], extensions: ['.r'], - iconClass: 'jp-MaterialIcon jp-RKernelIcon' + iconClass: 'jp-RKernelIcon' }, { name: 'yaml', displayName: 'YAML File', mimeTypes: ['text/x-yaml', 'text/yaml'], extensions: ['.yaml', '.yml'], - iconClass: 'jp-MaterialIcon jp-YAMLIcon' + iconClass: 'jp-YamlIcon' }, { name: 'svg', displayName: 'Image', mimeTypes: ['image/svg+xml'], extensions: ['.svg'], - iconClass: 'jp-MaterialIcon jp-ImageIcon', + iconClass: 'jp-ImageIcon', fileFormat: 'base64' }, { @@ -1255,7 +1255,7 @@ export namespace DocumentRegistry { displayName: 'Image', mimeTypes: ['image/tiff'], extensions: ['.tif', '.tiff'], - iconClass: 'jp-MaterialIcon jp-ImageIcon', + iconClass: 'jp-ImageIcon', fileFormat: 'base64' }, { @@ -1263,7 +1263,7 @@ export namespace DocumentRegistry { displayName: 'Image', mimeTypes: ['image/jpeg'], extensions: ['.jpg', '.jpeg'], - iconClass: 'jp-MaterialIcon jp-ImageIcon', + iconClass: 'jp-ImageIcon', fileFormat: 'base64' }, { @@ -1271,7 +1271,7 @@ export namespace DocumentRegistry { displayName: 'Image', mimeTypes: ['image/gif'], extensions: ['.gif'], - iconClass: 'jp-MaterialIcon jp-ImageIcon', + iconClass: 'jp-ImageIcon', fileFormat: 'base64' }, { @@ -1279,7 +1279,7 @@ export namespace DocumentRegistry { displayName: 'Image', mimeTypes: ['image/png'], extensions: ['.png'], - iconClass: 'jp-MaterialIcon jp-ImageIcon', + iconClass: 'jp-ImageIcon', fileFormat: 'base64' }, { @@ -1287,7 +1287,7 @@ export namespace DocumentRegistry { displayName: 'Image', mimeTypes: ['image/bmp'], extensions: ['.bmp'], - iconClass: 'jp-MaterialIcon jp-ImageIcon', + iconClass: 'jp-ImageIcon', fileFormat: 'base64' } ]; diff --git a/packages/filebrowser-extension/package.json b/packages/filebrowser-extension/package.json index eb2d4dded6fb..944e08b9d03c 100644 --- a/packages/filebrowser-extension/package.json +++ b/packages/filebrowser-extension/package.json @@ -45,6 +45,7 @@ "@jupyterlab/mainmenu": "^1.1.0-alpha.1", "@jupyterlab/services": "^4.1.0-alpha.1", "@jupyterlab/statusbar": "^1.1.0-alpha.1", + "@jupyterlab/ui-components": "^1.1.0-alpha.1", "@phosphor/algorithm": "^1.1.3", "@phosphor/commands": "^1.6.3", "@phosphor/messaging": "^1.2.3", diff --git a/packages/filebrowser-extension/src/index.ts b/packages/filebrowser-extension/src/index.ts index 50f5b6b4db22..9ec7efe84655 100644 --- a/packages/filebrowser-extension/src/index.ts +++ b/packages/filebrowser-extension/src/index.ts @@ -43,6 +43,8 @@ import { Contents } from '@jupyterlab/services'; import { IStatusBar } from '@jupyterlab/statusbar'; +import { IIconRegistry } from '@jupyterlab/ui-components'; + import { IIterator, map, reduce, toArray } from '@phosphor/algorithm'; import { CommandRegistry } from '@phosphor/commands'; @@ -128,7 +130,7 @@ const factory: JupyterFrontEndPlugin = { activate: activateFactory, id: '@jupyterlab/filebrowser-extension:factory', provides: IFileBrowserFactory, - requires: [IDocumentManager, IStateDB] + requires: [IIconRegistry, IDocumentManager, IStateDB] }; /** @@ -155,12 +157,17 @@ const shareFile: JupyterFrontEndPlugin = { export const fileUploadStatus: JupyterFrontEndPlugin = { id: '@jupyterlab/filebrowser-extension:file-upload-status', autoStart: true, - requires: [IStatusBar, IFileBrowserFactory], + requires: [IFileBrowserFactory], + optional: [IStatusBar], activate: ( app: JupyterFrontEnd, - statusBar: IStatusBar, - browser: IFileBrowserFactory + browser: IFileBrowserFactory, + statusBar: IStatusBar | null ) => { + if (!statusBar) { + // Automatically disable if statusbar missing + return; + } const item = new FileUploadStatus({ tracker: browser.tracker }); @@ -200,6 +207,7 @@ export default plugins; */ function activateFactory( app: JupyterFrontEnd, + icoReg: IIconRegistry, docManager: IDocumentManager, state: IStateDB ): IFileBrowserFactory { @@ -210,6 +218,7 @@ function activateFactory( options: IFileBrowserFactory.IOptions = {} ) => { const model = new FileBrowserModel({ + iconRegistry: icoReg, manager: docManager, driveName: options.driveName || '', refreshInterval: options.refreshInterval, diff --git a/packages/filebrowser-extension/tsconfig.json b/packages/filebrowser-extension/tsconfig.json index ccbf069617a4..863de97f9c19 100644 --- a/packages/filebrowser-extension/tsconfig.json +++ b/packages/filebrowser-extension/tsconfig.json @@ -32,6 +32,9 @@ }, { "path": "../statusbar" + }, + { + "path": "../ui-components" } ] } diff --git a/packages/filebrowser/package.json b/packages/filebrowser/package.json index 51a41f50e91b..7f31b361607f 100644 --- a/packages/filebrowser/package.json +++ b/packages/filebrowser/package.json @@ -41,6 +41,7 @@ "@jupyterlab/docregistry": "^1.1.0-alpha.1", "@jupyterlab/services": "^4.1.0-alpha.1", "@jupyterlab/statusbar": "^1.1.0-alpha.1", + "@jupyterlab/ui-components": "^1.1.0-alpha.1", "@phosphor/algorithm": "^1.1.3", "@phosphor/coreutils": "^1.3.1", "@phosphor/disposable": "^1.2.0", diff --git a/packages/filebrowser/src/crumbs.ts b/packages/filebrowser/src/crumbs.ts index 542677e4cbc8..7aa2989c45d1 100644 --- a/packages/filebrowser/src/crumbs.ts +++ b/packages/filebrowser/src/crumbs.ts @@ -17,6 +17,8 @@ import { PathExt, PageConfig } from '@jupyterlab/coreutils'; import { renameFile } from '@jupyterlab/docmanager'; +import { defaultIconRegistry } from '@jupyterlab/ui-components'; + import { FileBrowserModel } from './model'; /** @@ -30,10 +32,15 @@ const MATERIAL_CLASS = 'jp-MaterialIcon'; const BREADCRUMB_CLASS = 'jp-BreadCrumbs'; /** - * The class name added to add the folder icon for the breadcrumbs + * The class name for the folder icon for the breadcrumbs home */ const BREADCRUMB_HOME = 'jp-FolderIcon'; +/** + * The class name for the breadcrumbs home node + */ +const BREADCRUMB_HOME_CLASS = 'jp-BreadCrumbs-home'; + /** * The class named associated to the ellipses icon */ @@ -159,7 +166,10 @@ export class BreadCrumbs extends Widget { // Find a valid click target. let node = event.target as HTMLElement; while (node && node !== this.node) { - if (node.classList.contains(BREADCRUMB_ITEM_CLASS)) { + if ( + node.classList.contains(BREADCRUMB_ITEM_CLASS) || + node.classList.contains(BREADCRUMB_HOME_CLASS) + ) { let index = ArrayExt.findFirstIndex( this._crumbs, value => value === node @@ -353,7 +363,12 @@ namespace Private { */ export function createCrumbs(): ReadonlyArray { let home = document.createElement('span'); - home.className = `${MATERIAL_CLASS} ${BREADCRUMB_HOME} ${BREADCRUMB_ITEM_CLASS}`; + defaultIconRegistry.icon({ + name: BREADCRUMB_HOME, + className: BREADCRUMB_HOME_CLASS, + container: home, + kind: 'breadCrumb' + }); home.title = PageConfig.getOption('serverRoot') || 'Jupyter Server Root'; let ellipsis = document.createElement('span'); ellipsis.className = diff --git a/packages/filebrowser/src/listing.ts b/packages/filebrowser/src/listing.ts index ff507164f9cc..98aa5425ead6 100644 --- a/packages/filebrowser/src/listing.ts +++ b/packages/filebrowser/src/listing.ts @@ -20,6 +20,8 @@ import { DocumentRegistry } from '@jupyterlab/docregistry'; import { Contents } from '@jupyterlab/services'; +import { IIconRegistry } from '@jupyterlab/ui-components'; + import { ArrayExt, ArrayIterator, @@ -186,7 +188,9 @@ export class DirListing extends Widget { */ constructor(options: DirListing.IOptions) { super({ - node: (options.renderer || DirListing.defaultRenderer).createNode() + node: (options.renderer = + options.renderer || + new DirListing.Renderer(options.model.iconRegistry)).createNode() }); this.addClass(DIR_LISTING_CLASS); this._model = options.model; @@ -196,7 +200,7 @@ export class DirListing extends Widget { this._editNode = document.createElement('input'); this._editNode.className = EDITOR_CLASS; this._manager = this._model.manager; - this._renderer = options.renderer || DirListing.defaultRenderer; + this._renderer = options.renderer; const headerNode = DOMUtils.findElement(this.node, HEADER_CLASS); this._renderer.populateHeaderNode(headerNode); @@ -736,11 +740,14 @@ export class DirListing extends Widget { content.appendChild(node); } - // Remove extra classes from the nodes. + // Remove extra classes/data from the nodes. nodes.forEach(item => { item.classList.remove(SELECTED_CLASS); item.classList.remove(RUNNING_CLASS); item.classList.remove(CUT_CLASS); + if (item.children[0]) { + delete (item.children[0] as HTMLElement).dataset.icon; + } }); // Add extra classes to item nodes based on widget state. @@ -1638,6 +1645,10 @@ export namespace DirListing { * The default implementation of an `IRenderer`. */ export class Renderer implements IRenderer { + constructor(icoReg: IIconRegistry) { + this._iconRegistry = icoReg; + } + /** * Create the DOM node for a dir listing. */ @@ -1757,9 +1768,23 @@ export namespace DirListing { let modified = DOMUtils.findElement(node, ITEM_MODIFIED_CLASS); if (fileType) { - icon.textContent = fileType.iconLabel || ''; - icon.className = `${ITEM_ICON_CLASS} ${fileType.iconClass || ''}`; + // add icon as svg node. Can be styled using CSS + if ( + !this._iconRegistry.icon({ + name: fileType.iconClass, + className: ITEM_ICON_CLASS, + title: fileType.iconLabel, + container: icon, + center: true, + kind: 'listing' + }) + ) { + // add icon as CSS background image. Can't be styled using CSS + icon.className = `${ITEM_ICON_CLASS} ${fileType.iconClass || ''}`; + icon.textContent = fileType.iconLabel || ''; + } } else { + // use default icon as CSS background image icon.textContent = ''; icon.className = ITEM_ICON_CLASS; } @@ -1843,12 +1868,8 @@ export namespace DirListing { node.appendChild(icon); return node; } + _iconRegistry: IIconRegistry; } - - /** - * The default `IRenderer` instance. - */ - export const defaultRenderer = new Renderer(); } /** diff --git a/packages/filebrowser/src/model.ts b/packages/filebrowser/src/model.ts index 8f458a0c9555..5f6392759a5c 100644 --- a/packages/filebrowser/src/model.ts +++ b/packages/filebrowser/src/model.ts @@ -1,6 +1,8 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. +import { showDialog, Dialog } from '@jupyterlab/apputils'; + import { IChangedArgs, IStateDB, @@ -13,6 +15,8 @@ import { IDocumentManager, shouldOverwrite } from '@jupyterlab/docmanager'; import { Contents, Kernel, Session } from '@jupyterlab/services'; +import { IIconRegistry } from '@jupyterlab/ui-components'; + import { ArrayIterator, each, @@ -28,8 +32,6 @@ import { IDisposable } from '@phosphor/disposable'; import { ISignal, Signal } from '@phosphor/signaling'; -import { showDialog, Dialog } from '@jupyterlab/apputils'; - /** * The default duration of the auto-refresh in ms */ @@ -68,6 +70,7 @@ export class FileBrowserModel implements IDisposable { * Construct a new file browser model. */ constructor(options: FileBrowserModel.IOptions) { + this.iconRegistry = options.iconRegistry; this.manager = options.manager; this._driveName = options.driveName || ''; let rootPath = this._driveName ? this._driveName + ':' : ''; @@ -109,6 +112,11 @@ export class FileBrowserModel implements IDisposable { }); } + /** + * The icon registry instance used by the file browser model. + */ + readonly iconRegistry: IIconRegistry; + /** * The document manager instance used by the file browser model. */ @@ -637,6 +645,11 @@ export namespace FileBrowserModel { * An options object for initializing a file browser. */ export interface IOptions { + /** + * An icon registry instance. + */ + iconRegistry: IIconRegistry; + /** * A document manager instance. */ @@ -650,15 +663,15 @@ export namespace FileBrowserModel { driveName?: string; /** - * An optional state database. If provided, the model will restore which - * folder was last opened when it is restored. + * The time interval for browser refreshing, in ms. */ - state?: IStateDB; + refreshInterval?: number; /** - * The time interval for browser refreshing, in ms. + * An optional state database. If provided, the model will restore which + * folder was last opened when it is restored. */ - refreshInterval?: number; + state?: IStateDB; } } diff --git a/packages/filebrowser/style/base.css b/packages/filebrowser/style/base.css index 4290aa23651e..8253a2287bb4 100644 --- a/packages/filebrowser/style/base.css +++ b/packages/filebrowser/style/base.css @@ -225,10 +225,7 @@ color: limegreen; content: '\25CF'; font-size: 8px; - position: relative; - width: 100%; - height: 100%; - top: -2px; + position: absolute; left: -8px; } @@ -256,7 +253,7 @@ background-image: var(--jp-icon-python-selected); } -.jp-DirListing-item.jp-mod-selected .jp-DirListing-itemIcon.jp-JSONIcon { +.jp-DirListing-item.jp-mod-selected .jp-DirListing-itemIcon.jp-JsonIcon { background-image: var(--jp-icon-json-selected); } @@ -268,7 +265,7 @@ background-image: var(--jp-icon-r-selected); } -.jp-DirListing-item.jp-mod-selected .jp-DirListing-itemIcon.jp-YAMLIcon { +.jp-DirListing-item.jp-mod-selected .jp-DirListing-itemIcon.jp-YamlIcon { background-image: var(--jp-icon-yaml-selected); } diff --git a/packages/filebrowser/style/index.css b/packages/filebrowser/style/index.css index a04ddc3c646f..edbcab7a21dd 100644 --- a/packages/filebrowser/style/index.css +++ b/packages/filebrowser/style/index.css @@ -5,6 +5,7 @@ /* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ @import url('~@phosphor/widgets/style/index.css'); +@import url('~@jupyterlab/ui-components/style/index.css'); @import url('~@jupyterlab/apputils/style/index.css'); @import url('~@phosphor/dragdrop/style/index.css'); @import url('~@jupyterlab/docregistry/style/index.css'); diff --git a/packages/filebrowser/tsconfig.json b/packages/filebrowser/tsconfig.json index cb06e46e40c7..19a64feeabb3 100644 --- a/packages/filebrowser/tsconfig.json +++ b/packages/filebrowser/tsconfig.json @@ -23,6 +23,9 @@ }, { "path": "../statusbar" + }, + { + "path": "../ui-components" } ] } diff --git a/packages/fileeditor-extension/src/index.ts b/packages/fileeditor-extension/src/index.ts index 2379f659544c..0454f9cf2eea 100644 --- a/packages/fileeditor-extension/src/index.ts +++ b/packages/fileeditor-extension/src/index.ts @@ -54,7 +54,7 @@ const EDITOR_ICON_CLASS = 'jp-MaterialIcon jp-TextEditorIcon'; /** * The class name for the text editor icon from the default theme. */ -const MARKDOWN_ICON_CLASS = 'jp-MaterialIcon jp-MarkdownIcon'; +const MARKDOWN_ICON_CLASS = 'jp-MarkdownIcon'; /** * The name of the factory that creates editor widgets. @@ -114,13 +114,18 @@ const plugin: JupyterFrontEndPlugin = { export const tabSpaceStatus: JupyterFrontEndPlugin = { id: '@jupyterlab/fileeditor-extension:tab-space-status', autoStart: true, - requires: [IStatusBar, IEditorTracker, ISettingRegistry], + requires: [IEditorTracker, ISettingRegistry], + optional: [IStatusBar], activate: ( app: JupyterFrontEnd, - statusBar: IStatusBar, editorTracker: IEditorTracker, - settingRegistry: ISettingRegistry + settingRegistry: ISettingRegistry, + statusBar: IStatusBar | null ) => { + if (!statusBar) { + // Automatically disable if statusbar missing + return; + } // Create a menu for switching tabs vs spaces. const menu = new Menu({ commands: app.commands }); const command = 'fileeditor:change-tabs'; diff --git a/packages/htmlviewer-extension/src/index.tsx b/packages/htmlviewer-extension/src/index.tsx index 4d6d6ed24589..59b1876bb301 100644 --- a/packages/htmlviewer-extension/src/index.tsx +++ b/packages/htmlviewer-extension/src/index.tsx @@ -20,9 +20,9 @@ import { } from '@jupyterlab/htmlviewer'; /** - * The CSS class for an HTML5 icon. + * The name for an HTML5 icon. */ -const CSS_ICON_CLASS = 'jp-MaterialIcon jp-HTMLIcon'; +const ICON_NAME = 'html5'; /** * Command IDs used by the plugin. @@ -58,7 +58,7 @@ function activateHTMLViewer( displayName: 'HTML File', extensions: ['.html'], mimeTypes: ['text/html'], - iconClass: CSS_ICON_CLASS + iconClass: ICON_NAME }; app.docRegistry.addFileType(ft); @@ -98,6 +98,7 @@ function activateHTMLViewer( app.commands.notifyCommandChanged(CommandIDs.trustHTML); }); + // widget.node.appendChild(HTML5Icon); widget.title.iconClass = ft.iconClass; widget.title.iconLabel = ft.iconLabel; }); diff --git a/packages/htmlviewer-extension/style/base.css b/packages/htmlviewer-extension/style/base.css deleted file mode 100644 index 8ef6cabcc54a..000000000000 --- a/packages/htmlviewer-extension/style/base.css +++ /dev/null @@ -1,9 +0,0 @@ -/*----------------------------------------------------------------------------- -| Copyright (c) Jupyter Development Team. -| Distributed under the terms of the Modified BSD License. -|----------------------------------------------------------------------------*/ - -/* Document icon */ -.jp-HTMLIcon { - background-image: url('./html5-icon.svg'); -} diff --git a/packages/htmlviewer-extension/style/html5-icon.svg b/packages/htmlviewer-extension/style/html5-icon.svg deleted file mode 100644 index b8b075fa046b..000000000000 --- a/packages/htmlviewer-extension/style/html5-icon.svg +++ /dev/null @@ -1,8 +0,0 @@ - - HTML5 Logo - - - - - - \ No newline at end of file diff --git a/packages/htmlviewer-extension/style/index.css b/packages/htmlviewer-extension/style/index.css index cbc7122d5fbc..32422713a5d6 100644 --- a/packages/htmlviewer-extension/style/index.css +++ b/packages/htmlviewer-extension/style/index.css @@ -8,5 +8,3 @@ @import url('~@jupyterlab/docregistry/style/index.css'); @import url('~@jupyterlab/application/style/index.css'); @import url('~@jupyterlab/htmlviewer/style/index.css'); - -@import url('./base.css'); diff --git a/packages/json-extension/style/index.css b/packages/json-extension/style/index.css index 3a4b711feb1e..bacc5f067317 100644 --- a/packages/json-extension/style/index.css +++ b/packages/json-extension/style/index.css @@ -4,8 +4,8 @@ |----------------------------------------------------------------------------*/ /* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ -@import url('~@jupyterlab/ui-components/style/index.css'); @import url('~@phosphor/widgets/style/index.css'); +@import url('~@jupyterlab/ui-components/style/index.css'); @import url('~@jupyterlab/apputils/style/index.css'); @import url('./base.css'); diff --git a/packages/launcher/package.json b/packages/launcher/package.json index 9a64f1aedddd..7d87d19a3445 100644 --- a/packages/launcher/package.json +++ b/packages/launcher/package.json @@ -36,6 +36,7 @@ }, "dependencies": { "@jupyterlab/apputils": "^1.1.0-alpha.1", + "@jupyterlab/ui-components": "^1.1.0-alpha.1", "@phosphor/algorithm": "^1.1.3", "@phosphor/commands": "^1.6.3", "@phosphor/coreutils": "^1.3.1", diff --git a/packages/launcher/src/index.tsx b/packages/launcher/src/index.tsx index 4bc885919dec..021fefa89f76 100644 --- a/packages/launcher/src/index.tsx +++ b/packages/launcher/src/index.tsx @@ -7,6 +7,12 @@ import { VDomRenderer } from '@jupyterlab/apputils'; +import { + combineClasses, + DefaultIconReact, + defaultIconRegistry +} from '@jupyterlab/ui-components'; + import { ArrayExt, ArrayIterator, @@ -188,17 +194,31 @@ export class Launcher extends VDomRenderer { // Now create the sections for each category orderedCategories.forEach(cat => { const item = categories[cat][0] as ILauncher.IItemOptions; - let iconClass = - `${this._commands.iconClass(item.command, { - ...item.args, - cwd: this.cwd - })} ` + 'jp-Launcher-sectionIcon jp-Launcher-icon'; + let iconClass = this._commands.iconClass(item.command, { + ...item.args, + cwd: this.cwd + }); let kernel = KERNEL_CATEGORIES.indexOf(cat) > -1; if (cat in categories) { section = (
- {kernel &&
} + {kernel && defaultIconRegistry.contains(iconClass) ? ( + + ) : ( +
+ )}

{cat}

@@ -384,6 +404,7 @@ function Card( }; // Return the VDOM element. + const iconClass = kernel ? '' : commands.iconClass(command, args); return (
-
- {item.kernelIconUrl && kernel && ( - - )} - {!item.kernelIconUrl && !kernel && ( -
- )} - {!item.kernelIconUrl && kernel && ( -
- {label[0].toUpperCase()} -
- )} -
+ {kernel ? ( +
+ {item.kernelIconUrl ? ( + + ) : ( +
+ {label[0].toUpperCase()} +
+ )} +
+ ) : defaultIconRegistry.contains(iconClass) ? ( + + ) : ( +
+
+
+ )}

{label}

diff --git a/packages/launcher/style/index.css b/packages/launcher/style/index.css index 546e18daa8a5..bacc5f067317 100644 --- a/packages/launcher/style/index.css +++ b/packages/launcher/style/index.css @@ -5,6 +5,7 @@ /* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ @import url('~@phosphor/widgets/style/index.css'); +@import url('~@jupyterlab/ui-components/style/index.css'); @import url('~@jupyterlab/apputils/style/index.css'); @import url('./base.css'); diff --git a/packages/launcher/tsconfig.json b/packages/launcher/tsconfig.json index 1a4f97efe5ff..58ebc16e0a36 100644 --- a/packages/launcher/tsconfig.json +++ b/packages/launcher/tsconfig.json @@ -8,6 +8,9 @@ "references": [ { "path": "../apputils" + }, + { + "path": "../ui-components" } ] } diff --git a/packages/metapackage/package.json b/packages/metapackage/package.json index 80baee2a34fa..dd0f3a67767c 100644 --- a/packages/metapackage/package.json +++ b/packages/metapackage/package.json @@ -101,6 +101,7 @@ "@jupyterlab/tooltip": "^1.1.0-alpha.1", "@jupyterlab/tooltip-extension": "^1.1.0-alpha.1", "@jupyterlab/ui-components": "^1.1.0-alpha.1", + "@jupyterlab/ui-components-extension": "^1.1.0-alpha.1", "@jupyterlab/vdom": "^1.1.0-alpha.1", "@jupyterlab/vdom-extension": "^1.1.0-alpha.1", "@jupyterlab/vega4-extension": "^1.1.0-alpha.1", diff --git a/packages/metapackage/tsconfig.json b/packages/metapackage/tsconfig.json index dc0d758dfaf5..2c99cf8e1b42 100644 --- a/packages/metapackage/tsconfig.json +++ b/packages/metapackage/tsconfig.json @@ -213,6 +213,9 @@ { "path": "../ui-components" }, + { + "path": "../ui-components-extension" + }, { "path": "../vdom" }, diff --git a/packages/notebook-extension/src/index.ts b/packages/notebook-extension/src/index.ts index e88150e6230a..2e769795de7c 100644 --- a/packages/notebook-extension/src/index.ts +++ b/packages/notebook-extension/src/index.ts @@ -306,12 +306,17 @@ const tools: JupyterFrontEndPlugin = { export const commandEditItem: JupyterFrontEndPlugin = { id: '@jupyterlab/notebook-extension:mode-status', autoStart: true, - requires: [IStatusBar, INotebookTracker], + requires: [INotebookTracker], + optional: [IStatusBar], activate: ( app: JupyterFrontEnd, - statusBar: IStatusBar, - tracker: INotebookTracker + tracker: INotebookTracker, + statusBar: IStatusBar | null ) => { + if (!statusBar) { + // Automatically disable if statusbar missing + return; + } const { shell } = app; const item = new CommandEditStatus(); @@ -339,12 +344,17 @@ export const commandEditItem: JupyterFrontEndPlugin = { export const notebookTrustItem: JupyterFrontEndPlugin = { id: '@jupyterlab/notebook-extension:trust-status', autoStart: true, - requires: [IStatusBar, INotebookTracker, ILabShell], + requires: [INotebookTracker], + optional: [IStatusBar], activate: ( app: JupyterFrontEnd, - statusBar: IStatusBar, - tracker: INotebookTracker + tracker: INotebookTracker, + statusBar: IStatusBar | null ) => { + if (!statusBar) { + // Automatically disable if statusbar missing + return; + } const { shell } = app; const item = new NotebookTrustStatus(); diff --git a/packages/notebook/src/truststatus.tsx b/packages/notebook/src/truststatus.tsx index 99d7b0d0e242..e6761424c35f 100644 --- a/packages/notebook/src/truststatus.tsx +++ b/packages/notebook/src/truststatus.tsx @@ -6,7 +6,7 @@ import { INotebookModel, Notebook } from '.'; import { Cell } from '@jupyterlab/cells'; -import { IconItem } from '@jupyterlab/statusbar'; +import { DefaultIconReact } from '@jupyterlab/ui-components'; import { toArray } from '@phosphor/algorithm'; @@ -44,8 +44,13 @@ function cellTrust( function NotebookTrustComponent( props: NotebookTrustComponent.IProps ): React.ReactElement { - const source = cellTrust(props)[1]; - return ; + if (props.allCellsTrusted) { + return ; + } else { + return ( + + ); + } } /** diff --git a/packages/notebook/style/base.css b/packages/notebook/style/base.css index b289c121f49a..0f75309ccfac 100644 --- a/packages/notebook/style/base.css +++ b/packages/notebook/style/base.css @@ -19,7 +19,6 @@ |----------------------------------------------------------------------------*/ @import './toolbar.css'; -@import './status.css'; /*----------------------------------------------------------------------------- | Notebook diff --git a/packages/notebook/style/index.css b/packages/notebook/style/index.css index 9927f8adda6c..7b77c736221c 100644 --- a/packages/notebook/style/index.css +++ b/packages/notebook/style/index.css @@ -4,8 +4,8 @@ |----------------------------------------------------------------------------*/ /* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ -@import url('~@jupyterlab/ui-components/style/index.css'); @import url('~@phosphor/widgets/style/index.css'); +@import url('~@jupyterlab/ui-components/style/index.css'); @import url('~@jupyterlab/apputils/style/index.css'); @import url('~@phosphor/dragdrop/style/index.css'); @import url('~@jupyterlab/codeeditor/style/index.css'); diff --git a/packages/notebook/style/not-trusted-icon-dark.svg b/packages/notebook/style/not-trusted-icon-dark.svg deleted file mode 100644 index 8439a93c6e70..000000000000 --- a/packages/notebook/style/not-trusted-icon-dark.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/notebook/style/not-trusted-icon-light.svg b/packages/notebook/style/not-trusted-icon-light.svg deleted file mode 100644 index cb63b74c2de6..000000000000 --- a/packages/notebook/style/not-trusted-icon-light.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/notebook/style/status.css b/packages/notebook/style/status.css deleted file mode 100644 index 6a6fd3f01564..000000000000 --- a/packages/notebook/style/status.css +++ /dev/null @@ -1,28 +0,0 @@ -/*----------------------------------------------------------------------------- -| Copyright (c) Jupyter Development Team. -| Distributed under the terms of the Modified BSD License. -|----------------------------------------------------------------------------*/ - -/* - * icons for light themes - */ - -[data-jp-theme-light='true'] .jp-StatusItem-untrusted { - background-image: url('./not-trusted-icon-light.svg'); -} - -[data-jp-theme-light='true'] .jp-StatusItem-trusted { - background-image: url('./trusted-icon-light.svg'); -} - -/* - * icons for dark themes - */ - -[data-jp-theme-light='false'] .jp-StatusItem-untrusted { - background-image: url('./not-trusted-icon-dark.svg'); -} - -[data-jp-theme-light='false'] .jp-StatusItem-trusted { - background-image: url('./trusted-icon-dark.svg'); -} diff --git a/packages/notebook/style/trusted-icon-dark.svg b/packages/notebook/style/trusted-icon-dark.svg deleted file mode 100644 index b92040c37a58..000000000000 --- a/packages/notebook/style/trusted-icon-dark.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/notebook/style/trusted-icon-light.svg b/packages/notebook/style/trusted-icon-light.svg deleted file mode 100644 index 7f4fc23bac83..000000000000 --- a/packages/notebook/style/trusted-icon-light.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/rendermime-extension/src/index.ts b/packages/rendermime-extension/src/index.ts index abb205600cb8..3796135852c9 100644 --- a/packages/rendermime-extension/src/index.ts +++ b/packages/rendermime-extension/src/index.ts @@ -26,8 +26,8 @@ namespace CommandIDs { */ const plugin: JupyterFrontEndPlugin = { id: '@jupyterlab/rendermime-extension:plugin', - requires: [IDocumentManager], - optional: [ILatexTypesetter], + requires: [], + optional: [IDocumentManager, ILatexTypesetter], provides: IRenderMimeRegistry, activate: activate, autoStart: true @@ -43,45 +43,49 @@ export default plugin; */ function activate( app: JupyterFrontEnd, - docManager: IDocumentManager, + docManager: IDocumentManager | null, latexTypesetter: ILatexTypesetter | null ) { - app.commands.addCommand(CommandIDs.handleLink, { - label: 'Handle Local Link', - execute: args => { - const path = args['path'] as string | undefined | null; - const id = args['id'] as string | undefined | null; - if (!path) { - return; - } - // First check if the path exists on the server. - return docManager.services.contents - .get(path, { content: false }) - .then(() => { - // Open the link with the default rendered widget factory, - // if applicable. - const factory = docManager.registry.defaultRenderedWidgetFactory( - path - ); - const widget = docManager.openOrReveal(path, factory.name); + if (docManager) { + app.commands.addCommand(CommandIDs.handleLink, { + label: 'Handle Local Link', + execute: args => { + const path = args['path'] as string | undefined | null; + const id = args['id'] as string | undefined | null; + if (!path) { + return; + } + // First check if the path exists on the server. + return docManager.services.contents + .get(path, { content: false }) + .then(() => { + // Open the link with the default rendered widget factory, + // if applicable. + const factory = docManager.registry.defaultRenderedWidgetFactory( + path + ); + const widget = docManager.openOrReveal(path, factory.name); - // Handle the hash if one has been provided. - if (widget && id) { - widget.setFragment(id); - } - }); - } - }); + // Handle the hash if one has been provided. + if (widget && id) { + widget.setFragment(id); + } + }); + } + }); + } return new RenderMimeRegistry({ initialFactories: standardRendererFactories, - linkHandler: { - handleLink: (node: HTMLElement, path: string, id?: string) => { - app.commandLinker.connectNode(node, CommandIDs.handleLink, { - path, - id - }); - } - }, + linkHandler: !docManager + ? null + : { + handleLink: (node: HTMLElement, path: string, id?: string) => { + app.commandLinker.connectNode(node, CommandIDs.handleLink, { + path, + id + }); + } + }, latexTypesetter }); } diff --git a/packages/settingeditor/package.json b/packages/settingeditor/package.json index 62f775ef96c1..7629367600be 100644 --- a/packages/settingeditor/package.json +++ b/packages/settingeditor/package.json @@ -40,6 +40,7 @@ "@jupyterlab/coreutils": "^3.1.0-alpha.1", "@jupyterlab/inspector": "^1.1.0-alpha.1", "@jupyterlab/rendermime": "^1.1.0-alpha.1", + "@jupyterlab/ui-components": "^1.1.0-alpha.1", "@phosphor/commands": "^1.6.3", "@phosphor/coreutils": "^1.3.1", "@phosphor/messaging": "^1.2.3", diff --git a/packages/settingeditor/src/pluginlist.tsx b/packages/settingeditor/src/pluginlist.tsx index 1f7dd5977e68..5daada29c4d2 100644 --- a/packages/settingeditor/src/pluginlist.tsx +++ b/packages/settingeditor/src/pluginlist.tsx @@ -5,6 +5,12 @@ import { ISettingRegistry } from '@jupyterlab/coreutils'; +import { + combineClasses, + DefaultIconReact, + defaultIconRegistry +} from '@jupyterlab/ui-components'; + import { Message } from '@phosphor/messaging'; import { ISignal, Signal } from '@phosphor/signaling'; @@ -253,9 +259,11 @@ namespace Private { const { id, schema, version } = plugin; const itemTitle = `${schema.description}\n${id}\n${version}`; const image = getHint(ICON_CLASS_KEY, registry, plugin); - const iconClass = `jp-MaterialIcon jp-PluginList-icon${ - image ? ' ' + image : '' - }`; + const iconClass = combineClasses( + image, + 'jp-PluginList-icon', + 'jp-MaterialIcon' + ); const iconTitle = getHint(ICON_LABEL_KEY, registry, plugin); return ( @@ -265,7 +273,17 @@ namespace Private { key={id} title={itemTitle} > - + {defaultIconRegistry.contains(image) ? ( + + ) : ( + + )} {schema.title || id} ); diff --git a/packages/settingeditor/style/index.css b/packages/settingeditor/style/index.css index 8e4c3c1c1782..fd84f4f05eda 100644 --- a/packages/settingeditor/style/index.css +++ b/packages/settingeditor/style/index.css @@ -5,6 +5,7 @@ /* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ @import url('~@phosphor/widgets/style/index.css'); +@import url('~@jupyterlab/ui-components/style/index.css'); @import url('~@jupyterlab/apputils/style/index.css'); @import url('~@jupyterlab/codeeditor/style/index.css'); @import url('~@jupyterlab/rendermime/style/index.css'); diff --git a/packages/settingeditor/tsconfig.json b/packages/settingeditor/tsconfig.json index e38e704a2b31..75553a5273ab 100644 --- a/packages/settingeditor/tsconfig.json +++ b/packages/settingeditor/tsconfig.json @@ -20,6 +20,9 @@ }, { "path": "../rendermime" + }, + { + "path": "../ui-components" } ] } diff --git a/packages/statusbar/package.json b/packages/statusbar/package.json index 82f488fa2961..7dc5e6123832 100644 --- a/packages/statusbar/package.json +++ b/packages/statusbar/package.json @@ -35,6 +35,7 @@ "@jupyterlab/codeeditor": "^1.1.0-alpha.1", "@jupyterlab/coreutils": "^3.1.0-alpha.1", "@jupyterlab/services": "^4.1.0-alpha.1", + "@jupyterlab/ui-components": "^1.1.0-alpha.1", "@phosphor/algorithm": "^1.1.3", "@phosphor/coreutils": "^1.3.1", "@phosphor/disposable": "^1.2.0", diff --git a/packages/statusbar/src/components/icon.tsx b/packages/statusbar/src/components/icon.tsx deleted file mode 100644 index 74bcd444c583..000000000000 --- a/packages/statusbar/src/components/icon.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import * as React from 'react'; - -import { classes, style } from 'typestyle/lib'; - -import icon from '../style/icon'; - -/** - * A namespace for IconItem statics. - */ -export namespace IconItem { - /** - * Props for an IconItem - */ - export interface IProps { - /** - * A CSS class name for the icon. - */ - source: string; - } -} - -/** - * A functional tsx component for an icon. - */ -export function IconItem( - props: IconItem.IProps & React.HTMLAttributes -): React.ReactElement { - const { source, className, ...rest } = props; - return ( -
- ); -} diff --git a/packages/statusbar/src/components/index.ts b/packages/statusbar/src/components/index.ts index 1f2699acab31..bd165a2abf5a 100644 --- a/packages/statusbar/src/components/index.ts +++ b/packages/statusbar/src/components/index.ts @@ -1,7 +1,6 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -export * from './icon'; export * from './progressBar'; export * from './text'; export * from './group'; diff --git a/packages/statusbar/src/defaults/lineCol.tsx b/packages/statusbar/src/defaults/lineCol.tsx index 2240c2da940c..d46425ace093 100644 --- a/packages/statusbar/src/defaults/lineCol.tsx +++ b/packages/statusbar/src/defaults/lineCol.tsx @@ -7,7 +7,11 @@ import { VDomRenderer, VDomModel, ReactWidget } from '@jupyterlab/apputils'; import { CodeEditor } from '@jupyterlab/codeeditor'; -import { interactiveItem, showPopup, Popup, TextItem } from '..'; +import { DefaultIconReact } from '@jupyterlab/ui-components'; + +import { classes } from 'typestyle/lib'; + +import { interactiveItem, Popup, showPopup, TextItem } from '..'; import { lineFormWrapper, @@ -15,11 +19,11 @@ import { lineFormSearch, lineFormWrapperFocusWithin, lineFormCaption, + lineFormButtonDiv, + lineFormButtonIcon, lineFormButton } from '../style/lineForm'; -import { classes } from 'typestyle/lib'; - /** * A namespace for LineFormComponent statics. */ @@ -111,12 +115,14 @@ class LineFormComponent extends React.Component< this._textInput = input; }} /> - - +
+ + +
} /> @@ -69,18 +69,11 @@ export const InputGroup = (props: IInputGroupProps & CommonProps) => { return ( ); }; -export const Icon = (props: IIconProps) => ( - -); - export const Collapse = (props: ICollapseProps & CommonProps) => ( ); @@ -88,13 +81,13 @@ export const Collapse = (props: ICollapseProps & CommonProps) => ( export const HTMLSelect = (props: IHTMLSelectProps & CommonProps) => ( ); export const Select = (props: ISelectProps & CommonProps) => ( ); diff --git a/packages/ui-components/src/icon/iconimports.ts b/packages/ui-components/src/icon/iconimports.ts new file mode 100644 index 000000000000..3a9151eae78a --- /dev/null +++ b/packages/ui-components/src/icon/iconimports.ts @@ -0,0 +1,62 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +/* This file was auto-generated by ensureUiComponents() in @jupyterlab/buildutils */ + +import { Icon } from './interfaces'; + +// icon svg import statements +import fileSvg from '../../style/icons/filetype/file.svg'; +import folderSvg from '../../style/icons/filetype/folder.svg'; +import html5Svg from '../../style/icons/filetype/html5.svg'; +import imageSvg from '../../style/icons/filetype/image.svg'; +import jsonSvg from '../../style/icons/filetype/json.svg'; +import markdownSvg from '../../style/icons/filetype/markdown.svg'; +import notebookSvg from '../../style/icons/filetype/notebook.svg'; +import pythonSvg from '../../style/icons/filetype/python.svg'; +import rKernelSvg from '../../style/icons/filetype/r-kernel.svg'; +import reactSvg from '../../style/icons/filetype/react.svg'; +import spreadsheetSvg from '../../style/icons/filetype/spreadsheet.svg'; +import yamlSvg from '../../style/icons/filetype/yaml.svg'; +import buildSvg from '../../style/icons/sidebar/build.svg'; +import extensionSvg from '../../style/icons/sidebar/extension.svg'; +import paletteSvg from '../../style/icons/sidebar/palette.svg'; +import runningSvg from '../../style/icons/sidebar/running.svg'; +import tabSvg from '../../style/icons/sidebar/tab.svg'; +import jupyterFaviconSvg from '../../style/icons/splash/jupyter-favicon.svg'; +import kernelSvg from '../../style/icons/statusbar/kernel.svg'; +import lineFormSvg from '../../style/icons/statusbar/line-form.svg'; +import notTrustedSvg from '../../style/icons/statusbar/not-trusted.svg'; +import terminalSvg from '../../style/icons/statusbar/terminal.svg'; +import trustedSvg from '../../style/icons/statusbar/trusted.svg'; + +// defaultIcons definition +export namespace IconImports { + export const defaultIcons: ReadonlyArray = [ + { name: 'file', svg: fileSvg }, + { name: 'folder', svg: folderSvg }, + { name: 'html5', svg: html5Svg }, + { name: 'image', svg: imageSvg }, + { name: 'json', svg: jsonSvg }, + { name: 'markdown', svg: markdownSvg }, + { name: 'notebook', svg: notebookSvg }, + { name: 'python', svg: pythonSvg }, + { name: 'r-kernel', svg: rKernelSvg }, + { name: 'react', svg: reactSvg }, + { name: 'spreadsheet', svg: spreadsheetSvg }, + { name: 'yaml', svg: yamlSvg }, + { name: 'build', svg: buildSvg }, + { name: 'extension', svg: extensionSvg }, + { name: 'palette', svg: paletteSvg }, + { name: 'running', svg: runningSvg }, + { name: 'tab', svg: tabSvg }, + { name: 'jupyter-favicon', svg: jupyterFaviconSvg }, + { name: 'kernel', svg: kernelSvg }, + { name: 'line-form', svg: lineFormSvg }, + { name: 'not-trusted', svg: notTrustedSvg }, + { name: 'terminal', svg: terminalSvg }, + { name: 'trusted', svg: trustedSvg } + ]; +} diff --git a/packages/ui-components/src/icon/iconregistry.tsx b/packages/ui-components/src/icon/iconregistry.tsx new file mode 100644 index 000000000000..d2a5d70b6bfe --- /dev/null +++ b/packages/ui-components/src/icon/iconregistry.tsx @@ -0,0 +1,212 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import React from 'react'; +import { classes } from 'typestyle/lib'; + +import { Text } from '@jupyterlab/coreutils'; + +import { IIconRegistry, Icon } from './interfaces'; +import { IconImports } from './iconimports'; +import { iconStyle, iconStyleFlat } from '../style/icon'; + +import badSvg from '../../style/debug/bad.svg'; +import blankSvg from '../../style/debug/blank.svg'; + +/** + * The icon registry class. + */ +export class IconRegistry implements IIconRegistry { + constructor(options: IconRegistry.IOptions = {}) { + this._debug = !!options.debug; + + let icons = options.initialIcons || IconImports.defaultIcons; + this.addIcon(...icons); + + // add the bad state and blank icons + this.addIcon( + { name: 'bad', svg: badSvg }, + { name: 'blank', svg: blankSvg } + ); + } + + addIcon(...icons: Icon.IModel[]): void { + icons.forEach((icon: Icon.IModel) => { + let className = icon.className + ? icon.className + : IconRegistry.iconClassName(icon.name); + this._classNameToName[className] = icon.name; + this._nameToClassName[icon.name] = className; + this._svg[icon.name] = icon.svg; + }); + } + + contains(name: string): boolean { + return name in this._svg || name in this._classNameToName; + } + + /** + * Get the icon as an HTMLElement of tag + */ + icon( + props: Icon.INodeOptions & { container?: HTMLElement } + ): HTMLElement | null { + const { name, className, title, container, ...propsStyle } = props; + + // we may have been handed a className in place of name + let resolvedName = this.resolveName(name); + if ( + !resolvedName || + (container && + container.dataset.icon && + container.dataset.icon === resolvedName) + ) { + // bail if failing silently or icon node is already set + return; + } + + let svgNode = Private.parseSvg(this.svg(resolvedName)); + + if (title) { + Private.setTitleSvg(svgNode, title); + } + + if (container) { + // clear any existing icon in container (and all other child elements) + container.textContent = ''; + container.dataset.icon = resolvedName; + container.appendChild(svgNode); + + let styleClass = propsStyle ? iconStyle(propsStyle) : ''; + if (className || className === '') { + // override the className, if explicitly passed + container.className = classes(className, styleClass); + } else if (!container.classList.contains(styleClass)) { + // add icon styling class to the container's class, if not already present + container.className = classes(container.className, styleClass); + } + } else { + // add icon styling class directly to the svg node + svgNode.setAttribute( + 'class', + classes(className, propsStyle ? iconStyleFlat(propsStyle) : '') + ); + } + + return svgNode; + } + + /** + * Get the icon as a ReactElement of tag + * TODO: figure out how to remove the unnecessary outer + */ + iconReact( + props: Icon.INodeOptions & { tag?: 'div' | 'span' } + ): React.ReactElement { + const { name, className, title, tag, ...propsStyle } = props; + const Tag = tag || 'div'; + + // we may have been handed a className in place of name + let resolvedName = this.resolveName(name); + if (!resolvedName) { + // bail if failing silently + return <>; + } + + return ( + + ); + } + + resolveName(name: string): string { + if (!(name in this._svg)) { + // assume name is really a className, split the className into parts and check each part + for (let className of name.split(/\s+/)) { + if (className in this._classNameToName) { + return this._classNameToName[className]; + } + } + if (this._debug) { + // couldn't resolve name, mark as bad and warn + console.error(`Invalid icon name: ${name}`); + return 'bad'; + } else { + // couldn't resolve name, fail silently + return ''; + } + } + + return name; + } + + svg(name: string): string { + return this._svg[name]; + } + + static iconClassName(name: string): string { + return 'jp-' + Text.camelCase(name, true) + 'Icon'; + } + + private _classNameToName: { [key: string]: string } = Object.create(null); + private _debug: boolean = false; + private _nameToClassName: { [key: string]: string } = Object.create(null); + private _svg: { [key: string]: string } = Object.create(null); +} + +/** + * The defaultIconRegistry instance. + */ +export const defaultIconRegistry: IconRegistry = new IconRegistry(); + +/** + * Alias for defaultIconRegistry.iconReact that can be used as a React component + */ +export const DefaultIconReact = ( + props: Icon.INodeOptions & { tag?: 'div' | 'span' } +): React.ReactElement => { + return defaultIconRegistry.iconReact(props); +}; + +export namespace IconRegistry { + /** + * The options used to create an icon registry. + */ + export interface IOptions { + /** + * The initial icons for the registry. + * The [[Icon.defaultIcons]] will be used if not given. + */ + initialIcons?: Icon.IModel[]; + + /** + * If the debug flag is set, missing icons will raise a warning + * and be visually marked with an "X". Otherwise, missing icons + * will fail silently. + */ + debug?: boolean; + } +} + +namespace Private { + export function parseSvg(svg: string): HTMLElement { + let parser = new DOMParser(); + return parser.parseFromString(svg, 'image/svg+xml').documentElement; + } + + export function setTitleSvg(svgNode: HTMLElement, title: string): void { + // add a title node to the top level svg node + let titleNodes = svgNode.getElementsByTagName('title'); + if (titleNodes) { + titleNodes[0].textContent = title; + } else { + let titleNode = document.createElement('title'); + titleNode.textContent = title; + svgNode.appendChild(titleNode); + } + } +} diff --git a/packages/ui-components/src/icon/index.ts b/packages/ui-components/src/icon/index.ts new file mode 100644 index 000000000000..c64a470388c7 --- /dev/null +++ b/packages/ui-components/src/icon/index.ts @@ -0,0 +1,7 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +export * from './iconimports'; +export * from './iconregistry'; +export * from './interfaces'; +export * from './tabbarsvg'; diff --git a/packages/ui-components/src/icon/interfaces.ts b/packages/ui-components/src/icon/interfaces.ts new file mode 100644 index 000000000000..e1f6e7c6c74d --- /dev/null +++ b/packages/ui-components/src/icon/interfaces.ts @@ -0,0 +1,63 @@ +import { Token } from '@phosphor/coreutils'; + +import { IIconStyle } from '../style/icon'; +import React from 'react'; + +/** + * The interface for an object that keeps a registry of inline + * svg icons. Has methods for setting up inline svg icons as + * either `HTMLElement` or `ReactElement` + */ +export interface IIconRegistry { + /** + * Add the raw text representation of an svg icon to this registry + */ + addIcon(...icons: Icon.IModel[]): void; + + /** + * Check if any icon of name `name` has been registered. + * Exact matches only + */ + contains(name: string): boolean; + + /** + * Get the icon as an HTMLElement of tag + */ + icon( + props: Icon.INodeOptions & { container: HTMLElement } + ): HTMLElement | null; + + /** + * Get the icon as a ReactElement of tag + */ + iconReact( + props: Icon.INodeOptions & { tag?: 'div' | 'span' } + ): React.ReactElement; +} + +/** + * The IIconRegistry token. + */ +export const IIconRegistry = new Token( + '@jupyterlab/ui-components:IIconRegistry' +); + +export namespace Icon { + /** + * A representation of the resources underlying an inline svg icon + */ + export interface IModel { + name: string; + className?: string; + svg: string; + } + + /** + * The options used when creating an icon node + */ + export interface INodeOptions extends IIconStyle { + name: string; + className?: string; + title?: string; + } +} diff --git a/packages/ui-components/src/icon/tabbarsvg.ts b/packages/ui-components/src/icon/tabbarsvg.ts new file mode 100644 index 000000000000..f48845193340 --- /dev/null +++ b/packages/ui-components/src/icon/tabbarsvg.ts @@ -0,0 +1,140 @@ +import { Message } from '@phosphor/messaging'; + +import { h, VirtualElement } from '@phosphor/virtualdom'; + +import { DockPanel, TabBar, Widget } from '@phosphor/widgets'; + +import { IconKindType } from '../style/icon'; +import { defaultIconRegistry } from './iconregistry'; + +/** + * A widget which displays titles as a single row or column of tabs. Tweaked + * to enable the use of inline svgs as tab icons. + */ +export class TabBarSvg extends TabBar { + /** + * Construct a new tab bar. Sets the (icon) kind and overrides + * the default renderer. + * + * @param options - The options for initializing the tab bar. + */ + constructor(options: TabBarSvg.IOptions) { + options.renderer = options.renderer || TabBarSvg.defaultRenderer; + super(options); + + this._kind = options.kind; + } + + /** + * A message handler invoked on an `'update-request'` message. Adds svg + * nodes to icon nodes as appropriate + */ + protected onUpdateRequest(msg: Message): void { + super.onUpdateRequest(msg); + + for (let itab in this.contentNode.children) { + let tab = this.contentNode.children[itab]; + let title = this.titles[itab]; + let iconNode = tab.children ? (tab.children[0] as HTMLElement) : null; + + if (iconNode) { + // add the svg node, if not already present + defaultIconRegistry.icon({ + name: title.iconClass, + className: '', + container: iconNode, + center: true, + kind: this._kind + }); + } + } + } + + protected _kind: IconKindType; +} + +export namespace TabBarSvg { + export interface IOptions extends TabBar.IOptions { + /** + * The kind of icon this tab bar widget should render. + * Adds preset styling to the icons. + */ + kind?: IconKindType; + } + + /** + * A modified implementation of the TabBar Renderer. + */ + export class Renderer extends TabBar.Renderer { + /** + * Render the icon element for a tab. This version avoids clobbering + * the icon node's children. + * + * @param data - The data to use for rendering the tab. + * + * @returns A virtual element representing the tab icon. + */ + renderIcon(data: TabBar.IRenderData): VirtualElement { + let className = this.createIconClass(data); + return h.div({ className }); + } + } + + export const defaultRenderer = new Renderer(); +} + +/** + * A widget which provides a flexible docking area for widgets.Tweaked + * to enable the use of inline svgs as tab icons. + */ +export class DockPanelSvg extends DockPanel { + /** + * Construct a new dock panel. Overrides the default renderer + * and sets the (icon) kind + * + * @param options - The options for initializing the panel. + */ + constructor(options: DockPanelSvg.IOptions) { + if (!options.renderer) { + options.renderer = new DockPanelSvg.Renderer(options.kind); + } + + super(options); + } +} + +export namespace DockPanelSvg { + export interface IOptions extends DockPanel.IOptions { + /** + * The kind of icon this dock panel widget should render. + * Adds preset styling to the icons. + */ + kind?: IconKindType; + } + + /** + * A modified implementation of the DockPanel Renderer. + */ + export class Renderer extends DockPanel.Renderer { + constructor(kind?: IconKindType) { + super(); + this._kind = kind; + } + + /** + * Create a new tab bar (with inline svg icons enabled + * for use with a dock panel. + * + * @returns A new tab bar for a dock panel. + */ + createTabBar(): TabBarSvg { + let bar = new TabBarSvg({ + kind: this._kind + }); + bar.addClass('p-DockPanel-tabBar'); + return bar; + } + + _kind: IconKindType; + } +} diff --git a/packages/ui-components/src/index.ts b/packages/ui-components/src/index.ts new file mode 100644 index 000000000000..cabb6173cab6 --- /dev/null +++ b/packages/ui-components/src/index.ts @@ -0,0 +1,6 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +export * from './blueprint'; +export * from './icon'; +export * from './utils'; diff --git a/packages/ui-components/src/style/icon.ts b/packages/ui-components/src/style/icon.ts new file mode 100644 index 000000000000..b4f9910eec7f --- /dev/null +++ b/packages/ui-components/src/style/icon.ts @@ -0,0 +1,260 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { cssRule, style } from 'typestyle/lib'; +import { NestedCSSProperties } from 'typestyle/lib/types'; + +/** + * - breadCrumb: The path icons above the filebrowser + * - dockPanelBar: The tab icons above the main area + * - launcherCard: The icons for the cards at the bottom of the launcher + * - launcherSection: The icons to left of the Launcher section headers + * - listing: The icons to the left of the filebrowser listing items + * - settingsEditor: The icons to the left of each section of the settings editor + * - sideBar: The icons for the sidebar (default to the left of the main window) + * - splash: The icon used for the splash screen + * - tabManager: The icons for the tabManager in the sidebar + */ +export type IconKindType = + | 'breadCrumb' + | 'dockPanelBar' + | 'launcherCard' + | 'launcherSection' + | 'listing' + | 'settingsEditor' + | 'sideBar' + | 'splash' + | 'statusBar' + | 'tabManager' + | 'unset'; + +export interface IIconStyle extends NestedCSSProperties { + /** + * center the icon svg in its container + */ + center?: boolean; + + /** + * the kind of the icon, associated with a default stylesheet + */ + kind?: IconKindType; +} + +/** + * styles for centering node inside of containers + */ +const containerCSSCenter: NestedCSSProperties = { + alignItems: 'center', + display: 'flex' +}; + +const iconCSSCenter: NestedCSSProperties = { + display: 'block', + margin: '0 auto', + width: '100%' +}; + +/** + * icon kind specific styles + */ +const iconCSSBreadCrumb: NestedCSSProperties = { + borderRadius: 'var(--jp-border-radius)', + cursor: 'pointer', + margin: '0px 2px', + padding: '0px 2px', + height: '16px', + width: '16px', + verticalAlign: 'middle', + $nest: { + '&:hover': { + backgroundColor: 'var(--jp-layout-color2)' + }, + '&:first-child': { + marginLeft: '0px' + }, + ['.jp-mod-dropTarget']: { + backgroundColor: 'var(--jp-brand-color2)', + opacity: 0.7 + } + } +}; + +const iconCSSDockPanelBar: NestedCSSProperties = { + height: '14px', + width: '14px' +}; + +const iconCSSLauncherCard: NestedCSSProperties = { + height: 'var(--jp-private-launcher-large-icon-size)', + width: 'var(--jp-private-launcher-large-icon-size)' +}; + +const iconCSSLauncherSection: NestedCSSProperties = { + marginRight: '12px', + height: 'var(--jp-private-launcher-small-icon-size)', + width: 'var(--jp-private-launcher-small-icon-size)' +}; + +const iconCSSListing: NestedCSSProperties = { + height: '16px', + width: '16px' +}; + +const iconCSSSettingsEditor: NestedCSSProperties = { + height: '16px', + width: '16px' +}; + +const iconCSSSideBar: NestedCSSProperties = { + width: '20px' +}; + +const iconCSSSplash: NestedCSSProperties = { + width: '100px' +}; + +const iconCSSStatusBar: NestedCSSProperties = { + left: '0px', + top: '0px', + height: '18px', + width: '20px', + position: 'relative' +}; + +const iconCSSTabManager: NestedCSSProperties = { + height: '16px', + width: '16px' +}; + +const iconCSSKind: { [k in IconKindType]: NestedCSSProperties } = { + breadCrumb: iconCSSBreadCrumb, + dockPanelBar: iconCSSDockPanelBar, + launcherCard: iconCSSLauncherCard, + launcherSection: iconCSSLauncherSection, + listing: iconCSSListing, + settingsEditor: iconCSSSettingsEditor, + sideBar: iconCSSSideBar, + splash: iconCSSSplash, + statusBar: iconCSSStatusBar, + tabManager: iconCSSTabManager, + unset: {} +}; + +/** + * container kind specific styles + */ +const containerCSSDockPanelBar: NestedCSSProperties = { + marginRight: '4px' +}; + +const containerCSSLauncherCard: NestedCSSProperties = { + height: 'var(--jp-private-launcher-card-icon-height)' +}; + +const containerCSSListing: NestedCSSProperties = { + flex: '0 0 20px', + marginRight: '4px', + position: 'relative' +}; + +const containerCSSSettingsEditor: NestedCSSProperties = { + display: 'inline-block', + flex: '0 0 20px', + marginLeft: '2px', + marginRight: '1px', + position: 'relative', + height: '20px', + width: '20px' +}; + +const containerCSSSideBar: NestedCSSProperties = { + transform: 'rotate(90deg)' +}; + +const containerCSSSplash: NestedCSSProperties = { + animation: '0.3s fade-in linear forwards', + height: '100%', + width: '100%', + zIndex: 1 +}; + +const containerCSSTabManager: NestedCSSProperties = { + marginRight: '2px', + position: 'relative' +}; + +const containerCSSKind: { [k in IconKindType]: NestedCSSProperties } = { + breadCrumb: {}, + dockPanelBar: containerCSSDockPanelBar, + launcherCard: containerCSSLauncherCard, + launcherSection: {}, + listing: containerCSSListing, + settingsEditor: containerCSSSettingsEditor, + sideBar: containerCSSSideBar, + splash: containerCSSSplash, + statusBar: {}, + tabManager: containerCSSTabManager, + unset: {} +}; + +/** + * for putting together the icon kind style with any user input styling, + * as well as styling from optional flags like `center` + */ +function iconCSS(props: IIconStyle): NestedCSSProperties { + const { kind, center, ...propsCSS } = props; + + return { + ...(center ? iconCSSCenter : {}), + ...(kind ? iconCSSKind[kind] : {}), + ...propsCSS + }; +} + +/** + * for putting together the container kind style with any + * styling from optional flags like `center` + */ +function containerCSS(props: IIconStyle): NestedCSSProperties { + const { kind, center } = props; + + return { + ...(center ? containerCSSCenter : {}), + ...(kind ? containerCSSKind[kind] : {}) + }; +} + +/** + * for setting the style on the container of an svg node representing an icon + */ +export const iconStyle = (props: IIconStyle): string => { + return style({ + ...containerCSS(props), + $nest: { + ['svg']: iconCSS(props) + } + }); +}; + +/** + * for setting the style directly on the svg node representing an icon + */ +export const iconStyleFlat = (props: IIconStyle): string => { + return style(iconCSS(props)); +}; + +// TODO: Figure out a better cludge for styling current sidebar tab selection +cssRule( + `.p-TabBar-tab.p-mod-current .${iconStyle({ + center: true, + kind: 'sideBar' + })}`, + { + transform: + 'rotate(90deg)\n' + + ' translate(\n' + + ' calc(-0.5 * var(--jp-border-width)),\n' + + ' calc(-0.5 * var(--jp-border-width))\n' + + ' )' + } +); diff --git a/packages/ui-components/src/svg.d.ts b/packages/ui-components/src/svg.d.ts new file mode 100644 index 000000000000..90e4deda5555 --- /dev/null +++ b/packages/ui-components/src/svg.d.ts @@ -0,0 +1,58 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +// including this file in a package allows for the use of import statements +// with svg files. Example: `import xSvg from 'path/xSvg.svg'` + +// for use with raw-loader in Webpack. +// The svg will be imported as a raw string + +declare module '*.svg' { + const value: string; + export default value; +} + +// for use with svg-react-loader in Webpack. +// The svg will be imported as a ReactElement + +// declare module '*.svg' { +// import { HTMLAttributes } from 'react'; +// const value: React.ComponentType>; +// export default value; +// } + +// as an alternative to importing svgs one at a time, you can do a glob import +// using `context.requires`. This is a Webpack only extension. Implementation: + +// import { PathExt } from '@jupyterlab/coreutils'; +// +// /** +// * Import all svgs from a directory. The input argument should be +// * of the form `require.context('raw-loader!', true, /\.svg$/)`. +// * should be a string literal path, as this is needed by `require`. +// */ +// export function importSvgs(r: any, exclude: string[] = []): IModel[] { +// const excset = new Set(exclude); +// +// return r.keys().reduce((svgs: IModel[], item: string, index: number) => { +// const name = PathExt.stem(item); +// if (!excset.has(name)) { +// svgs.push({ name: name, svg: r(item).default }); +// } +// return svgs; +// }, []); +// } +// +// // create the array of default icon models +// let icons: IModel[]; +// try { +// // require.context is supplied by Webpack, and doesn't play nice with jest +// icons = importSvgs( +// require.context('raw-loader!../../style/icons', true, /\.svg$/), +// ['bad', 'blank'] +// ); +// } catch (e) { +// // fallback for jest tests +// icons = []; +// } +// export const defaultIcons: ReadonlyArray = icons; diff --git a/packages/ui-components/src/utils.ts b/packages/ui-components/src/utils.ts index 69ba135d38a8..ba5a549a732b 100644 --- a/packages/ui-components/src/utils.ts +++ b/packages/ui-components/src/utils.ts @@ -1,6 +1,6 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -export function combineClassNames(...classNames: (string | undefined)[]) { - return classNames.join(' '); +export function combineClasses(...classNames: (string | undefined)[]) { + return classNames.filter(c => !!c).join(' '); } diff --git a/packages/ui-components/style/base.css b/packages/ui-components/style/base.css index 8adb7976093f..d8730de3cf3f 100644 --- a/packages/ui-components/style/base.css +++ b/packages/ui-components/style/base.css @@ -112,3 +112,7 @@ input::placeholder { select { box-sizing: border-box; } + +/* Sibling imports */ +@import './deprecated.css'; +@import './deprecatedExtra.css'; diff --git a/packages/ui-components/style/debug/bad.svg b/packages/ui-components/style/debug/bad.svg new file mode 100644 index 000000000000..1fe7dcd245ef --- /dev/null +++ b/packages/ui-components/style/debug/bad.svg @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui-components/style/debug/blank.svg b/packages/ui-components/style/debug/blank.svg new file mode 100644 index 000000000000..d44952fd4948 --- /dev/null +++ b/packages/ui-components/style/debug/blank.svg @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui-components/style/deprecated.css b/packages/ui-components/style/deprecated.css new file mode 100644 index 000000000000..70ec78758d03 --- /dev/null +++ b/packages/ui-components/style/deprecated.css @@ -0,0 +1,110 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +/* This file was auto-generated by ensureUiComponents() in @jupyterlab/buildutils */ + +/** + * (DEPRECATED) Support for consuming icons as CSS background images + */ + +/* Icons urls */ + +:root { + --jp-icon-file: url('icons/filetype/file.svg'); + --jp-icon-folder: url('icons/filetype/folder.svg'); + --jp-icon-html5: url('icons/filetype/html5.svg'); + --jp-icon-image: url('icons/filetype/image.svg'); + --jp-icon-json: url('icons/filetype/json.svg'); + --jp-icon-markdown: url('icons/filetype/markdown.svg'); + --jp-icon-notebook: url('icons/filetype/notebook.svg'); + --jp-icon-python: url('icons/filetype/python.svg'); + --jp-icon-r-kernel: url('icons/filetype/r-kernel.svg'); + --jp-icon-react: url('icons/filetype/react.svg'); + --jp-icon-spreadsheet: url('icons/filetype/spreadsheet.svg'); + --jp-icon-yaml: url('icons/filetype/yaml.svg'); + --jp-icon-build: url('icons/sidebar/build.svg'); + --jp-icon-extension: url('icons/sidebar/extension.svg'); + --jp-icon-palette: url('icons/sidebar/palette.svg'); + --jp-icon-running: url('icons/sidebar/running.svg'); + --jp-icon-tab: url('icons/sidebar/tab.svg'); + --jp-icon-jupyter-favicon: url('icons/splash/jupyter-favicon.svg'); + --jp-icon-kernel: url('icons/statusbar/kernel.svg'); + --jp-icon-line-form: url('icons/statusbar/line-form.svg'); + --jp-icon-not-trusted: url('icons/statusbar/not-trusted.svg'); + --jp-icon-terminal: url('icons/statusbar/terminal.svg'); + --jp-icon-trusted: url('icons/statusbar/trusted.svg'); +} + +/* Icon CSS class declarations */ + +.jp-FileIcon { + background-image: var(--jp-icon-file); +} +.jp-FolderIcon { + background-image: var(--jp-icon-folder); +} +.jp-Html5Icon { + background-image: var(--jp-icon-html5); +} +.jp-ImageIcon { + background-image: var(--jp-icon-image); +} +.jp-JsonIcon { + background-image: var(--jp-icon-json); +} +.jp-MarkdownIcon { + background-image: var(--jp-icon-markdown); +} +.jp-NotebookIcon { + background-image: var(--jp-icon-notebook); +} +.jp-PythonIcon { + background-image: var(--jp-icon-python); +} +.jp-RKernelIcon { + background-image: var(--jp-icon-r-kernel); +} +.jp-ReactIcon { + background-image: var(--jp-icon-react); +} +.jp-SpreadsheetIcon { + background-image: var(--jp-icon-spreadsheet); +} +.jp-YamlIcon { + background-image: var(--jp-icon-yaml); +} +.jp-BuildIcon { + background-image: var(--jp-icon-build); +} +.jp-ExtensionIcon { + background-image: var(--jp-icon-extension); +} +.jp-PaletteIcon { + background-image: var(--jp-icon-palette); +} +.jp-RunningIcon { + background-image: var(--jp-icon-running); +} +.jp-TabIcon { + background-image: var(--jp-icon-tab); +} +.jp-JupyterFaviconIcon { + background-image: var(--jp-icon-jupyter-favicon); +} +.jp-KernelIcon { + background-image: var(--jp-icon-kernel); +} +.jp-LineFormIcon { + background-image: var(--jp-icon-line-form); +} +.jp-NotTrustedIcon { + background-image: var(--jp-icon-not-trusted); +} +.jp-TerminalIcon { + background-image: var(--jp-icon-terminal); +} +.jp-TrustedIcon { + background-image: var(--jp-icon-trusted); +} diff --git a/packages/theme-dark-extension/style/icons/jupyter/csv_selected.svg b/packages/ui-components/style/deprecated/selected/csv_selected.svg similarity index 100% rename from packages/theme-dark-extension/style/icons/jupyter/csv_selected.svg rename to packages/ui-components/style/deprecated/selected/csv_selected.svg diff --git a/packages/theme-dark-extension/style/icons/jupyter/file_selected.svg b/packages/ui-components/style/deprecated/selected/file_selected.svg similarity index 100% rename from packages/theme-dark-extension/style/icons/jupyter/file_selected.svg rename to packages/ui-components/style/deprecated/selected/file_selected.svg diff --git a/packages/theme-dark-extension/style/icons/md/ic_folder_24px_selected.svg b/packages/ui-components/style/deprecated/selected/ic_folder_24px_selected.svg similarity index 100% rename from packages/theme-dark-extension/style/icons/md/ic_folder_24px_selected.svg rename to packages/ui-components/style/deprecated/selected/ic_folder_24px_selected.svg diff --git a/packages/theme-dark-extension/style/icons/md/ic_memory_24px_selected.svg b/packages/ui-components/style/deprecated/selected/ic_memory_24px_selected.svg similarity index 100% rename from packages/theme-dark-extension/style/icons/md/ic_memory_24px_selected.svg rename to packages/ui-components/style/deprecated/selected/ic_memory_24px_selected.svg diff --git a/packages/theme-dark-extension/style/icons/jupyter/image_selected.svg b/packages/ui-components/style/deprecated/selected/image_selected.svg similarity index 100% rename from packages/theme-dark-extension/style/icons/jupyter/image_selected.svg rename to packages/ui-components/style/deprecated/selected/image_selected.svg diff --git a/packages/theme-dark-extension/style/icons/jupyter/json_selected.svg b/packages/ui-components/style/deprecated/selected/json_selected.svg similarity index 100% rename from packages/theme-dark-extension/style/icons/jupyter/json_selected.svg rename to packages/ui-components/style/deprecated/selected/json_selected.svg diff --git a/packages/theme-dark-extension/style/icons/jupyter/markdown_selected.svg b/packages/ui-components/style/deprecated/selected/markdown_selected.svg similarity index 100% rename from packages/theme-dark-extension/style/icons/jupyter/markdown_selected.svg rename to packages/ui-components/style/deprecated/selected/markdown_selected.svg diff --git a/packages/theme-dark-extension/style/icons/jupyter/notebook_selected.svg b/packages/ui-components/style/deprecated/selected/notebook_selected.svg similarity index 100% rename from packages/theme-dark-extension/style/icons/jupyter/notebook_selected.svg rename to packages/ui-components/style/deprecated/selected/notebook_selected.svg diff --git a/packages/theme-dark-extension/style/icons/jupyter/python_selected.svg b/packages/ui-components/style/deprecated/selected/python_selected.svg similarity index 100% rename from packages/theme-dark-extension/style/icons/jupyter/python_selected.svg rename to packages/ui-components/style/deprecated/selected/python_selected.svg diff --git a/packages/theme-dark-extension/style/icons/jupyter/r_selected.svg b/packages/ui-components/style/deprecated/selected/r_selected.svg similarity index 100% rename from packages/theme-dark-extension/style/icons/jupyter/r_selected.svg rename to packages/ui-components/style/deprecated/selected/r_selected.svg diff --git a/packages/theme-dark-extension/style/icons/jupyter/yml_selected.svg b/packages/ui-components/style/deprecated/selected/yml_selected.svg similarity index 100% rename from packages/theme-dark-extension/style/icons/jupyter/yml_selected.svg rename to packages/ui-components/style/deprecated/selected/yml_selected.svg diff --git a/packages/ui-components/style/deprecatedExtra.css b/packages/ui-components/style/deprecatedExtra.css new file mode 100644 index 000000000000..25bd79e43ebb --- /dev/null +++ b/packages/ui-components/style/deprecatedExtra.css @@ -0,0 +1,25 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +/** + * This file contains support for deprecated CSS not covered in the + * auto-generated `deprecated.css`. + */ + +/* vars for consuming "selected" icons as CSS background images */ + +:root { + --jp-icon-file-selected: url('deprecated/selected/file_selected.svg'); + --jp-icon-folder-selected: url('deprecated/selected/ic_folder_24px_selected.svg'); + --jp-icon-image-selected: url('deprecated/selected/image_selected.svg'); + --jp-icon-json-selected: url('deprecated/selected/json_selected.svg'); + --jp-icon-kernel-selected: url('deprecated/selected/ic_memory_24px_selected.svg'); + --jp-icon-markdown-selected: url('deprecated/selected/markdown_selected.svg'); + --jp-icon-notebook-selected: url('deprecated/selected/notebook_selected.svg'); + --jp-icon-python-selected: url('deprecated/selected/python_selected.svg'); + --jp-icon-r-selected: url('deprecated/selected/r_selected.svg'); + --jp-icon-spreadsheet-selected: url('deprecated/selected/csv_selected.svg'); + --jp-icon-yaml-selected: url('deprecated/selected/yml_selected.svg'); +} diff --git a/packages/ui-components/style/icons/filetype/file.svg b/packages/ui-components/style/icons/filetype/file.svg new file mode 100644 index 000000000000..0526ea36ef24 --- /dev/null +++ b/packages/ui-components/style/icons/filetype/file.svg @@ -0,0 +1,7 @@ + + + + diff --git a/packages/ui-components/style/icons/filetype/folder.svg b/packages/ui-components/style/icons/filetype/folder.svg new file mode 100644 index 000000000000..e4d6d832d610 --- /dev/null +++ b/packages/ui-components/style/icons/filetype/folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui-components/style/icons/filetype/html5.svg b/packages/ui-components/style/icons/filetype/html5.svg new file mode 100644 index 000000000000..d4c7978c5d4c --- /dev/null +++ b/packages/ui-components/style/icons/filetype/html5.svg @@ -0,0 +1,11 @@ + + + + + + + + + + > + diff --git a/packages/ui-components/style/icons/filetype/image.svg b/packages/ui-components/style/icons/filetype/image.svg new file mode 100644 index 000000000000..7035ee0c4747 --- /dev/null +++ b/packages/ui-components/style/icons/filetype/image.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/ui-components/style/icons/filetype/json.svg b/packages/ui-components/style/icons/filetype/json.svg new file mode 100644 index 000000000000..b840e84d4718 --- /dev/null +++ b/packages/ui-components/style/icons/filetype/json.svg @@ -0,0 +1,16 @@ + + + + diff --git a/packages/ui-components/style/icons/filetype/markdown.svg b/packages/ui-components/style/icons/filetype/markdown.svg new file mode 100644 index 000000000000..691d2037ef02 --- /dev/null +++ b/packages/ui-components/style/icons/filetype/markdown.svg @@ -0,0 +1,10 @@ + + + + diff --git a/packages/ui-components/style/icons/filetype/notebook.svg b/packages/ui-components/style/icons/filetype/notebook.svg new file mode 100644 index 000000000000..cbf8bba2f6d2 --- /dev/null +++ b/packages/ui-components/style/icons/filetype/notebook.svg @@ -0,0 +1,8 @@ + + + + diff --git a/packages/ui-components/style/icons/filetype/python.svg b/packages/ui-components/style/icons/filetype/python.svg new file mode 100644 index 000000000000..57e750cc0281 --- /dev/null +++ b/packages/ui-components/style/icons/filetype/python.svg @@ -0,0 +1,13 @@ + + + + diff --git a/packages/ui-components/style/icons/filetype/r-kernel.svg b/packages/ui-components/style/icons/filetype/r-kernel.svg new file mode 100644 index 000000000000..28353efbe037 --- /dev/null +++ b/packages/ui-components/style/icons/filetype/r-kernel.svg @@ -0,0 +1,9 @@ + + + + diff --git a/packages/ui-components/style/icons/filetype/react.svg b/packages/ui-components/style/icons/filetype/react.svg new file mode 100644 index 000000000000..be0691e3aa1c --- /dev/null +++ b/packages/ui-components/style/icons/filetype/react.svg @@ -0,0 +1,32 @@ + + + diff --git a/packages/ui-components/style/icons/filetype/spreadsheet.svg b/packages/ui-components/style/icons/filetype/spreadsheet.svg new file mode 100644 index 000000000000..97b81167ca2a --- /dev/null +++ b/packages/ui-components/style/icons/filetype/spreadsheet.svg @@ -0,0 +1,6 @@ + + + + diff --git a/packages/ui-components/style/icons/filetype/yaml.svg b/packages/ui-components/style/icons/filetype/yaml.svg new file mode 100644 index 000000000000..9a1772a39826 --- /dev/null +++ b/packages/ui-components/style/icons/filetype/yaml.svg @@ -0,0 +1,10 @@ + + + + diff --git a/packages/ui-components/style/icons/sidebar/build.svg b/packages/ui-components/style/icons/sidebar/build.svg new file mode 100755 index 000000000000..32ddc5709bea --- /dev/null +++ b/packages/ui-components/style/icons/sidebar/build.svg @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui-components/style/icons/sidebar/extension.svg b/packages/ui-components/style/icons/sidebar/extension.svg new file mode 100644 index 000000000000..e7c930bdad64 --- /dev/null +++ b/packages/ui-components/style/icons/sidebar/extension.svg @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui-components/style/icons/sidebar/palette.svg b/packages/ui-components/style/icons/sidebar/palette.svg new file mode 100644 index 000000000000..bccca056d078 --- /dev/null +++ b/packages/ui-components/style/icons/sidebar/palette.svg @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui-components/style/icons/sidebar/running.svg b/packages/ui-components/style/icons/sidebar/running.svg new file mode 100644 index 000000000000..b10ef9c297cb --- /dev/null +++ b/packages/ui-components/style/icons/sidebar/running.svg @@ -0,0 +1,7 @@ + + > + diff --git a/packages/ui-components/style/icons/sidebar/tab.svg b/packages/ui-components/style/icons/sidebar/tab.svg new file mode 100644 index 000000000000..4ce0b473dcb6 --- /dev/null +++ b/packages/ui-components/style/icons/sidebar/tab.svg @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui-components/style/icons/splash/jupyter-favicon.svg b/packages/ui-components/style/icons/splash/jupyter-favicon.svg new file mode 100644 index 000000000000..755b30eec9ca --- /dev/null +++ b/packages/ui-components/style/icons/splash/jupyter-favicon.svg @@ -0,0 +1,5 @@ + + Jupyter Favicon + + + diff --git a/packages/ui-components/style/icons/statusbar/kernel.svg b/packages/ui-components/style/icons/statusbar/kernel.svg new file mode 100644 index 000000000000..e97ce48f2349 --- /dev/null +++ b/packages/ui-components/style/icons/statusbar/kernel.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/statusbar/style/line-form.svg b/packages/ui-components/style/icons/statusbar/line-form.svg similarity index 57% rename from packages/statusbar/style/line-form.svg rename to packages/ui-components/style/icons/statusbar/line-form.svg index 7491cb849bab..b4fd1ef77088 100644 --- a/packages/statusbar/style/line-form.svg +++ b/packages/ui-components/style/icons/statusbar/line-form.svg @@ -1,4 +1,4 @@ - + diff --git a/packages/ui-components/style/icons/statusbar/not-trusted.svg b/packages/ui-components/style/icons/statusbar/not-trusted.svg new file mode 100644 index 000000000000..4a0936fee37a --- /dev/null +++ b/packages/ui-components/style/icons/statusbar/not-trusted.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui-components/style/icons/statusbar/terminal.svg b/packages/ui-components/style/icons/statusbar/terminal.svg new file mode 100644 index 000000000000..e8d49b4e91a1 --- /dev/null +++ b/packages/ui-components/style/icons/statusbar/terminal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/ui-components/style/icons/statusbar/trusted.svg b/packages/ui-components/style/icons/statusbar/trusted.svg new file mode 100644 index 000000000000..8da29db6c18f --- /dev/null +++ b/packages/ui-components/style/icons/statusbar/trusted.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/ui-components/style/index.css b/packages/ui-components/style/index.css index ee417b10abe9..f7e375acb680 100644 --- a/packages/ui-components/style/index.css +++ b/packages/ui-components/style/index.css @@ -5,7 +5,7 @@ /* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ @import url('~@blueprintjs/core/lib/css/blueprint.css'); -@import url('~@blueprintjs/icons/lib/css/blueprint-icons.css'); @import url('~@blueprintjs/select/lib/css/blueprint-select.css'); +@import url('~@phosphor/widgets/style/index.css'); @import url('./base.css'); diff --git a/packages/ui-components/tdoptions.json b/packages/ui-components/tdoptions.json index 60d100d16c84..330558b24eac 100644 --- a/packages/ui-components/tdoptions.json +++ b/packages/ui-components/tdoptions.json @@ -7,7 +7,8 @@ "lib.es2015.d.ts", "lib.es2015.collection.d.ts", "lib.es2015.promise.d.ts", - "lib.dom.d.ts" + "lib.dom.d.ts", + "lib.dom.iterable.d.ts" ], "out": "../../docs/api/ui-components", "baseUrl": ".", @@ -16,5 +17,5 @@ }, "esModuleInterop": true, "jsx": "react", - "types": ["webpack-env"] + "types": ["webpack-env", "node"] } diff --git a/packages/ui-components/tsconfig.json b/packages/ui-components/tsconfig.json index b25ed53167d9..3460c40ab1d8 100644 --- a/packages/ui-components/tsconfig.json +++ b/packages/ui-components/tsconfig.json @@ -2,9 +2,13 @@ "extends": "../../tsconfigbase", "compilerOptions": { "outDir": "lib", - "types": ["webpack-env"], + "types": ["webpack-env", "node"], "rootDir": "src" }, - "include": ["src/*"], - "references": [] + "include": ["src/**/*"], + "references": [ + { + "path": "../coreutils" + } + ] } diff --git a/packages/vdom-extension/src/index.ts b/packages/vdom-extension/src/index.ts index 682cd834130a..8b009b2cf1cd 100644 --- a/packages/vdom-extension/src/index.ts +++ b/packages/vdom-extension/src/index.ts @@ -18,9 +18,9 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { RenderedVDOM, IVDOMTracker } from '@jupyterlab/vdom'; /** - * The CSS class for a VDOM icon. + * The name for a VDOM icon. */ -const CSS_ICON_CLASS = 'jp-MaterialIcon jp-VDOMIcon'; +const ICON_NAME = 'react'; /** * The MIME type for VDOM. @@ -34,14 +34,15 @@ const FACTORY_NAME = 'VDOM'; const plugin: JupyterFrontEndPlugin = { id: '@jupyterlab/vdom-extension:factory', - requires: [IRenderMimeRegistry, INotebookTracker, ILayoutRestorer], + requires: [IRenderMimeRegistry], + optional: [INotebookTracker, ILayoutRestorer], provides: IVDOMTracker, autoStart: true, activate: ( app: JupyterFrontEnd, rendermime: IRenderMimeRegistry, - notebooks: INotebookTracker, - restorer: ILayoutRestorer + notebooks: INotebookTracker | null, + restorer: ILayoutRestorer | null ) => { const tracker = new WidgetTracker({ namespace: 'vdom-widget' @@ -57,29 +58,31 @@ const plugin: JupyterFrontEndPlugin = { 0 ); - notebooks.widgetAdded.connect((sender, panel) => { - // Get the notebook's context and rendermime; - const { - context, - content: { rendermime } - } = panel; - - // Add the renderer factory to the notebook's rendermime registry; - rendermime.addFactory( - { - safe: false, - mimeTypes: [MIME_TYPE], - createRenderer: options => new RenderedVDOM(options, context) - }, - 0 - ); - }); + if (notebooks) { + notebooks.widgetAdded.connect((sender, panel) => { + // Get the notebook's context and rendermime; + const { + context, + content: { rendermime } + } = panel; + + // Add the renderer factory to the notebook's rendermime registry; + rendermime.addFactory( + { + safe: false, + mimeTypes: [MIME_TYPE], + createRenderer: options => new RenderedVDOM(options, context) + }, + 0 + ); + }); + } app.docRegistry.addFileType({ name: 'vdom', mimeTypes: [MIME_TYPE], extensions: ['.vdom', '.vdom.json'], - iconClass: CSS_ICON_CLASS + iconClass: ICON_NAME }); const factory = new MimeDocumentFactory({ @@ -87,7 +90,7 @@ const plugin: JupyterFrontEndPlugin = { dataType: 'json', rendermime, name: FACTORY_NAME, - primaryFileType: app.docRegistry.getFileType('vdom'), + primaryFileType: app.docRegistry.getFileType('vdom')!, fileTypes: ['vdom', 'json'], defaultFor: ['vdom'] }); @@ -102,15 +105,17 @@ const plugin: JupyterFrontEndPlugin = { // Add widget factory to document registry. app.docRegistry.addWidgetFactory(factory); - // Handle state restoration. - void restorer.restore(tracker, { - command: 'docmanager:open', - args: widget => ({ - path: widget.context.path, - factory: FACTORY_NAME - }), - name: widget => widget.context.path - }); + if (restorer) { + // Handle state restoration. + void restorer.restore(tracker, { + command: 'docmanager:open', + args: widget => ({ + path: widget.context.path, + factory: FACTORY_NAME + }), + name: widget => widget.context.path + }); + } return tracker; } diff --git a/packages/vdom-extension/style/base.css b/packages/vdom-extension/style/base.css deleted file mode 100644 index 3aa74aba24ad..000000000000 --- a/packages/vdom-extension/style/base.css +++ /dev/null @@ -1,29 +0,0 @@ -/** - Copyright (c) Jupyter Development Team. - Distributed under the terms of the Modified BSD License. -*/ - -/* Add CSS variables to :root */ -:root { - --jp-icon-vdom: url('./react.svg'); -} - -/* Base styles */ -.jp-RenderedVDOM { - width: 100%; - height: 100%; - padding: 0; - overflow: auto; -} - -/* Document styles */ -.jp-MimeDocument .jp-RenderedVDOM { - padding: 5px; -} - -/* Document icon */ -.jp-VDOMIcon { - background-image: var(--jp-icon-vdom); - background-size: 24px; - background-position: center !important; -} diff --git a/packages/vdom-extension/style/index.css b/packages/vdom-extension/style/index.css index c6533b9403f0..6300595458a5 100644 --- a/packages/vdom-extension/style/index.css +++ b/packages/vdom-extension/style/index.css @@ -10,5 +10,3 @@ @import url('~@jupyterlab/application/style/index.css'); @import url('~@jupyterlab/notebook/style/index.css'); @import url('~@jupyterlab/vdom/style/index.css'); - -@import url('./base.css'); diff --git a/packages/vdom-extension/style/react.svg b/packages/vdom-extension/style/react.svg deleted file mode 100644 index 5592ebec6d60..000000000000 --- a/packages/vdom-extension/style/react.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - diff --git a/packages/vdom/style/base.css b/packages/vdom/style/base.css index 3aa74aba24ad..850eccd402f0 100644 --- a/packages/vdom/style/base.css +++ b/packages/vdom/style/base.css @@ -3,11 +3,6 @@ Distributed under the terms of the Modified BSD License. */ -/* Add CSS variables to :root */ -:root { - --jp-icon-vdom: url('./react.svg'); -} - /* Base styles */ .jp-RenderedVDOM { width: 100%; @@ -20,10 +15,3 @@ .jp-MimeDocument .jp-RenderedVDOM { padding: 5px; } - -/* Document icon */ -.jp-VDOMIcon { - background-image: var(--jp-icon-vdom); - background-size: 24px; - background-position: center !important; -} diff --git a/packages/vdom/style/react.svg b/packages/vdom/style/react.svg deleted file mode 100644 index 5592ebec6d60..000000000000 --- a/packages/vdom/style/react.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - diff --git a/tests/test-filebrowser/package.json b/tests/test-filebrowser/package.json index e198a68a1c9a..e04486bc16e4 100644 --- a/tests/test-filebrowser/package.json +++ b/tests/test-filebrowser/package.json @@ -22,6 +22,7 @@ "@jupyterlab/filebrowser": "^1.1.0-alpha.1", "@jupyterlab/services": "^4.1.0-alpha.1", "@jupyterlab/testutils": "^1.1.0-alpha.1", + "@jupyterlab/ui-components": "^1.1.0-alpha.1", "@phosphor/algorithm": "^1.1.3", "@phosphor/coreutils": "^1.3.1", "@phosphor/messaging": "^1.2.3", diff --git a/tests/test-filebrowser/src/crumbs.spec.ts b/tests/test-filebrowser/src/crumbs.spec.ts index df06b2607ee6..8ce03e54c59a 100644 --- a/tests/test-filebrowser/src/crumbs.spec.ts +++ b/tests/test-filebrowser/src/crumbs.spec.ts @@ -7,19 +7,23 @@ import { DocumentManager, IDocumentManager } from '@jupyterlab/docmanager'; import { DocumentRegistry, TextModelFactory } from '@jupyterlab/docregistry'; +import { BreadCrumbs, FileBrowserModel } from '@jupyterlab/filebrowser'; + import { ServiceManager } from '@jupyterlab/services'; +import { framePromise, signalToPromise } from '@jupyterlab/testutils'; + +import { defaultIconRegistry, IIconRegistry } from '@jupyterlab/ui-components'; + import { Message, MessageLoop } from '@phosphor/messaging'; import { Widget } from '@phosphor/widgets'; import { simulate } from 'simulate-event'; -import { BreadCrumbs, FileBrowserModel } from '@jupyterlab/filebrowser'; - -import { framePromise, signalToPromise } from '@jupyterlab/testutils'; - +const HOME_ITEM_CLASS = 'jp-BreadCrumbs-home'; const ITEM_CLASS = 'jp-BreadCrumbs-item'; +const ITEM_QUERY = `.${HOME_ITEM_CLASS}, .${ITEM_CLASS}`; class LogCrumbs extends BreadCrumbs { methods: string[] = []; @@ -47,6 +51,7 @@ class LogCrumbs extends BreadCrumbs { } describe('filebrowser/model', () => { + let iconRegistry: IIconRegistry; let manager: IDocumentManager; let serviceManager: ServiceManager.IManager; let registry: DocumentRegistry; @@ -68,6 +73,7 @@ describe('filebrowser/model', () => { textModelFactory: new TextModelFactory() }); serviceManager = new ServiceManager({ standby: 'never' }); + iconRegistry = defaultIconRegistry; manager = new DocumentManager({ registry, opener, @@ -91,7 +97,7 @@ describe('filebrowser/model', () => { }); beforeEach(async () => { - model = new FileBrowserModel({ manager }); + model = new FileBrowserModel({ iconRegistry, manager }); await model.cd(path); crumbs = new LogCrumbs({ model }); }); @@ -105,7 +111,7 @@ describe('filebrowser/model', () => { it('should create a new BreadCrumbs instance', () => { const bread = new BreadCrumbs({ model }); expect(bread).to.be.an.instanceof(BreadCrumbs); - const items = crumbs.node.getElementsByClassName(ITEM_CLASS); + const items = crumbs.node.querySelectorAll(ITEM_QUERY); expect(items.length).to.equal(1); }); @@ -119,26 +125,26 @@ describe('filebrowser/model', () => { it('should switch to the parent directory', async () => { Widget.attach(crumbs, document.body); MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest); - let items = crumbs.node.getElementsByClassName(ITEM_CLASS); + let items = crumbs.node.querySelectorAll(ITEM_QUERY); expect(items.length).to.equal(4); const promise = signalToPromise(model.pathChanged); expect(items[2].textContent).to.equal(second); simulate(items[2], 'click'); await promise; MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest); - items = crumbs.node.getElementsByClassName(ITEM_CLASS); + items = crumbs.node.querySelectorAll(ITEM_QUERY); expect(items.length).to.equal(3); }); it('should switch to the home directory', async () => { Widget.attach(crumbs, document.body); MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest); - let items = crumbs.node.getElementsByClassName(ITEM_CLASS); + let items = crumbs.node.querySelectorAll(ITEM_QUERY); const promise = signalToPromise(model.pathChanged); simulate(items[0], 'click'); await promise; MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest); - items = crumbs.node.getElementsByClassName(ITEM_CLASS); + items = crumbs.node.querySelectorAll(ITEM_QUERY); expect(items.length).to.equal(1); expect(model.path).to.equal(''); }); @@ -146,12 +152,12 @@ describe('filebrowser/model', () => { it('should switch to the grandparent directory', async () => { Widget.attach(crumbs, document.body); MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest); - let items = crumbs.node.getElementsByClassName(ITEM_CLASS); + let items = crumbs.node.querySelectorAll(ITEM_QUERY); const promise = signalToPromise(model.pathChanged); simulate(items[1], 'click'); await promise; MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest); - items = crumbs.node.getElementsByClassName(ITEM_CLASS); + items = crumbs.node.querySelectorAll(ITEM_QUERY); expect(items.length).to.equal(2); expect(model.path).to.equal(first); }); @@ -159,13 +165,13 @@ describe('filebrowser/model', () => { it('should refresh the current directory', async () => { Widget.attach(crumbs, document.body); MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest); - let items = crumbs.node.getElementsByClassName(ITEM_CLASS); + let items = crumbs.node.querySelectorAll(ITEM_QUERY); const promise = signalToPromise(model.refreshed); expect(items[3].textContent).to.equal(third); simulate(items[3], 'click'); await promise; MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest); - items = crumbs.node.getElementsByClassName(ITEM_CLASS); + items = crumbs.node.querySelectorAll(ITEM_QUERY); expect(items.length).to.equal(4); expect(model.path).to.equal(path); }); @@ -198,14 +204,14 @@ describe('filebrowser/model', () => { describe('#onUpdateRequest()', () => { it('should be called when the model updates', async () => { - const model = new FileBrowserModel({ manager }); + const model = new FileBrowserModel({ iconRegistry, manager }); await model.cd(path); crumbs = new LogCrumbs({ model }); await model.cd('..'); await framePromise(); expect(crumbs.methods).to.contain('onUpdateRequest'); - const items = crumbs.node.getElementsByClassName(ITEM_CLASS); + const items = crumbs.node.querySelectorAll(ITEM_QUERY); expect(items.length).to.equal(3); model.dispose(); }); diff --git a/tests/test-filebrowser/src/model.spec.ts b/tests/test-filebrowser/src/model.spec.ts index b9886109012b..cd2542ba0c79 100644 --- a/tests/test-filebrowser/src/model.spec.ts +++ b/tests/test-filebrowser/src/model.spec.ts @@ -11,24 +11,27 @@ import { DocumentManager, IDocumentManager } from '@jupyterlab/docmanager'; import { DocumentRegistry, TextModelFactory } from '@jupyterlab/docregistry'; -import { - Contents, - ContentsManager, - ServiceManager -} from '@jupyterlab/services'; - import { FileBrowserModel, LARGE_FILE_SIZE, CHUNK_SIZE } from '@jupyterlab/filebrowser'; +import { + Contents, + ContentsManager, + ServiceManager +} from '@jupyterlab/services'; + import { acceptDialog, dismissDialog, signalToPromises, sleep } from '@jupyterlab/testutils'; + +import { defaultIconRegistry, IIconRegistry } from '@jupyterlab/ui-components'; + import { toArray } from '@phosphor/algorithm'; /** @@ -55,6 +58,7 @@ class DelayedContentsManager extends ContentsManager { } describe('filebrowser/model', () => { + let iconRegistry: IIconRegistry; let manager: IDocumentManager; let serviceManager: ServiceManager.IManager; let registry: DocumentRegistry; @@ -73,6 +77,7 @@ describe('filebrowser/model', () => { textModelFactory: new TextModelFactory() }); serviceManager = new ServiceManager({ standby: 'never' }); + iconRegistry = defaultIconRegistry; manager = new DocumentManager({ registry, opener, @@ -83,7 +88,7 @@ describe('filebrowser/model', () => { beforeEach(async () => { await state.clear(); - model = new FileBrowserModel({ manager, state }); + model = new FileBrowserModel({ iconRegistry, manager, state }); const contents = await manager.newUntitled({ type: 'file' }); name = contents.name; return model.cd(); @@ -96,7 +101,7 @@ describe('filebrowser/model', () => { describe('FileBrowserModel', () => { describe('#constructor()', () => { it('should construct a new file browser model', () => { - model = new FileBrowserModel({ manager }); + model = new FileBrowserModel({ iconRegistry, manager }); expect(model).to.be.an.instanceof(FileBrowserModel); }); }); @@ -260,7 +265,7 @@ describe('filebrowser/model', () => { opener, manager: delayedServiceManager }); - model = new FileBrowserModel({ manager, state }); // Should delay 1000ms + model = new FileBrowserModel({ iconRegistry, manager, state }); // Should delay 1000ms // An initial refresh is called in the constructor. // If it is too slow, it can come in after the directory change, @@ -280,7 +285,7 @@ describe('filebrowser/model', () => { describe('#restore()', () => { it('should restore based on ID', async () => { const id = 'foo'; - const model2 = new FileBrowserModel({ manager, state }); + const model2 = new FileBrowserModel({ iconRegistry, manager, state }); await model.restore(id); await model.cd('src'); expect(model.path).to.equal('src'); @@ -292,7 +297,7 @@ describe('filebrowser/model', () => { it('should be safe to call multiple times', async () => { const id = 'bar'; - const model2 = new FileBrowserModel({ manager, state }); + const model2 = new FileBrowserModel({ iconRegistry, manager, state }); await model.restore(id); await model.cd('src'); expect(model.path).to.equal('src'); diff --git a/tests/test-filebrowser/tsconfig.json b/tests/test-filebrowser/tsconfig.json index bae9b844966a..fe415b7aed6a 100644 --- a/tests/test-filebrowser/tsconfig.json +++ b/tests/test-filebrowser/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "../../testutils" + }, + { + "path": "../../packages/ui-components" } ] } diff --git a/yarn.lock b/yarn.lock index 252bdcf304e0..e1caed24660d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -641,7 +641,7 @@ resize-observer-polyfill "^1.5.0" tslib "^1.9.0" -"@blueprintjs/icons@^3.3.0", "@blueprintjs/icons@^3.8.0": +"@blueprintjs/icons@^3.8.0": version "3.9.0" resolved "https://registry.yarnpkg.com/@blueprintjs/icons/-/icons-3.9.0.tgz#173f70b0d6a573d2a97066c54b4d110ffadeee51" integrity sha512-kq1Bh6PtOF4PcuxcDme8NmnSlkfO0IV89FriZGo6zSA1+OOzSwzvoKqa6S7vJe8xCPPLO5r7lE9AjeOuGeH97g== @@ -2018,6 +2018,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/prettier@^1.16.4": + version "1.18.0" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.18.0.tgz#d2dbe4d5f76b455138f13a2d881278e2c06a733d" + integrity sha512-5N6WK/XXs9PLPpge2KOmOSaIym2vIo32GsrxM5YOFs7uZ8R9L/acg+hQzWsfwoHEpasqQkH0+3LzLTbiF1GFLQ== + "@types/prop-types@*": version "15.7.1" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6" @@ -7617,7 +7622,7 @@ karma-ie-launcher@^1.0.0: resolved "https://registry.yarnpkg.com/karma-ie-launcher/-/karma-ie-launcher-1.0.0.tgz#497986842c490190346cd89f5494ca9830c6d59c" integrity sha1-SXmGhCxJAZA0bNifVJTKmDDG1Zw= dependencies: - lodash "^4.6.2" + lodash "^4.6.1" karma-mocha-reporter@^2.2.5: version "2.2.5" @@ -9723,7 +9728,7 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^1.17.0: +prettier@^1.17.0, prettier@^1.18.2: version "1.18.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== @@ -10689,7 +10694,7 @@ sanitize-html@~1.20.1: lodash.escaperegexp "^4.1.2" lodash.isplainobject "^4.0.6" lodash.isstring "^4.0.1" - lodash.mergewith "^4.6.2" + lodash.mergewith "^4.6.1" postcss "^7.0.5" srcset "^1.0.0" xtend "^4.0.1"