diff --git a/src/smc-project/jupyter/kernel-data.ts b/src/smc-project/jupyter/kernel-data.ts index e20567b3dc..22c537f92c 100644 --- a/src/smc-project/jupyter/kernel-data.ts +++ b/src/smc-project/jupyter/kernel-data.ts @@ -2,6 +2,8 @@ Use nteracts kernelspecs module to get data about all installed Jupyter kernels. The result is cached for 5s to avoid wasted effort in case of a flurry of calls. + +Specs: https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernel-specs */ import { findAll } from "kernelspecs"; @@ -25,7 +27,10 @@ export async function get_kernel_data(): Promise { v.push({ name: kernel, display_name: value.spec.display_name, - language: value.spec.language + language: value.spec.language, + interrupt_mode: value.spec.interrupt_mode, + env: value.spec.env, + metadata: value.spec.metadata }); } v.sort(field_cmp("display_name")); diff --git a/src/smc-util/db-schema.js b/src/smc-util/db-schema.js index d44609a577..0c7496fe9c 100644 --- a/src/smc-util/db-schema.js +++ b/src/smc-util/db-schema.js @@ -318,7 +318,8 @@ schema.accounts = { jupyter_classic: false, show_exec_warning: true, physical_keyboard: "default", - keyboard_variant: "" + keyboard_variant: "", + ask_jupyter_kernel: true }, other_settings: { katex: true, @@ -1783,8 +1784,7 @@ schema.mentions = { }, error: { type: "string", - desc: - "some sort of error occured handling this mention" + desc: "some sort of error occured handling this mention" }, action: { type: "string", @@ -1802,7 +1802,7 @@ schema.mentions = { path: true, source: "account_id", target: true, - priority:true + priority: true }, required_fields: { project_id: true, diff --git a/src/smc-util/misc.js b/src/smc-util/misc.js index e1ba74b0f1..c9a68f2ff0 100644 --- a/src/smc-util/misc.js +++ b/src/smc-util/misc.js @@ -3173,6 +3173,7 @@ exports.jupyter_language_to_name = function(lang) { // Find the kernel whose name is closest to the given name. exports.closest_kernel_match = function(name, kernel_list) { name = name.toLowerCase().replace("matlab", "octave"); + name = name === "python" ? "python3" : name; let bestValue = -1; let bestMatch = null; for ( @@ -3181,6 +3182,8 @@ exports.closest_kernel_match = function(name, kernel_list) { asc ? i++ : i-- ) { const k = kernel_list.get(i); + // filter out kernels with negative priority (using the priority would be great, though) + if (k.getIn(["metadata", "cocalc", "priority"], 0) < 0) continue; const kernel_name = k.get("name").toLowerCase(); let v = 0; for ( @@ -3194,7 +3197,6 @@ exports.closest_kernel_match = function(name, kernel_list) { break; } } - // TODO: don't use regular name comparison, use compareVersionStrings if ( v > bestValue || (v === bestValue && diff --git a/src/smc-util/test/misc-test.coffee b/src/smc-util/test/misc-test.coffee index 17fef544d6..00a36e305c 100644 --- a/src/smc-util/test/misc-test.coffee +++ b/src/smc-util/test/misc-test.coffee @@ -1518,10 +1518,17 @@ describe 'test closest kernel matching method', -> python3 = immutable.fromJS {name:"python3", display_name:"Python 3", language:"python"} sage8_2 = immutable.fromJS {name:"sage8.2", display_name:"Sagemath 8.2", language:"python"} sage8_10 = immutable.fromJS {name:"sage8.10", display_name:"Sagemath 8.10", language:"python"} - kernels = immutable.fromJS([octave,python3,python3,sage8_2,sage8_10]) + ir = immutable.fromJS {name:"ir", display_name:"R (R-Project)", language:"r"} + ir_old = immutable.fromJS {name:"ir-old", display_name: "R (old)", language: "r", metadata: {cocalc: {priority: -10}}} + kernels = immutable.fromJS([octave, python3, python3, sage8_2, sage8_10, ir, ir_old]) it 'thinks python8 should be python3', -> expect(misc.closest_kernel_match("python8",kernels)).toEqual(python3) it 'replaces "matlab" with "octave"', -> - expect(misc.closest_kernel_match("matlabe",kernels)).toEqual(octave) + expect(misc.closest_kernel_match("matlab",kernels)).toEqual(octave) it 'suggests sage8.10 over sage8.2', -> expect(misc.closest_kernel_match("sage8",kernels)).toEqual(sage8_10) + it 'suggests R over ir35', -> + expect(misc.closest_kernel_match("ir35",kernels)).toEqual(ir) + it 'suggests R over ir-35', -> + expect(misc.closest_kernel_match("ir-35",kernels)).toEqual(ir) + diff --git a/src/smc-webapp/jupyter/actions.ts b/src/smc-webapp/jupyter/actions.ts index 3b3764181f..afdde4ed78 100644 --- a/src/smc-webapp/jupyter/actions.ts +++ b/src/smc-webapp/jupyter/actions.ts @@ -39,7 +39,11 @@ import * as awaiting from "awaiting"; const misc = require("smc-util/misc"); const { required, defaults } = misc; import { Actions } from "../app-framework"; -import { JupyterStoreState, JupyterStore } from "./store"; +import { + JupyterStoreState, + JupyterStore, + show_kernel_selector_reasons +} from "./store"; const util = require("./util"); const parsing = require("./parsing"); const keyboard = require("./keyboard"); @@ -49,14 +53,11 @@ const { cm_options } = require("./cm_options"); const { JUPYTER_CLASSIC_MODERN } = require("smc-util/theme"); // map project_id (string) -> kernels (immutable) -let jupyter_kernels = immutable.Map>(); +import { Kernels, Kernel } from "./util"; +let jupyter_kernels = immutable.Map(); const { IPynbImporter } = require("./import-from-ipynb"); -// DEFAULT_KERNEL = 'python2' -// DEFAULT_KERNEL = "anaconda3"; -const DEFAULT_KERNEL = "sagemath"; - // Using require due to project import path issue... :-( // import { three_way_merge } from "smc-util/sync/editor/generic/util"; const { three_way_merge } = require("smc-util/sync/editor/generic/util"); @@ -410,18 +411,19 @@ export class JupyterActions extends Actions { this.setState({ kernels }); // We must also update the kernel info (e.g., display name), now that we // know the kernels (e.g., maybe it changed or is now known but wasn't before). - this.setState({ - kernel_info: this.store.get_kernel_info(this.store.get("kernel")) - }); + const kernel_info = this.store.get_kernel_info(this.store.get("kernel")); + this.setState({ kernel_info }); }; - set_jupyter_kernels = () => { + set_jupyter_kernels = async () => { const kernels = jupyter_kernels.get(this.store.jupyter_kernel_key()); if (kernels != null) { this.setState({ kernels }); } else { - this.fetch_jupyter_kernels(); + await this.fetch_jupyter_kernels(); } + this.update_select_kernel_data(); + this.check_select_kernel(); }; set_error = (err: any): void => { @@ -931,6 +933,7 @@ export class JupyterActions extends Actions { this.set_backend_kernel_info(); this.set_cm_options(); } + break; } }); @@ -955,6 +958,7 @@ export class JupyterActions extends Actions { if (this._state === "init") { this._state = "ready"; } + this.check_select_kernel(); if (this.store.get("view_mode") === "raw") { this.set_raw_ipynb(); @@ -963,21 +967,38 @@ export class JupyterActions extends Actions { }; _syncdb_init_kernel = (): void => { - const account = this.redux.getStore("account"); - const default_kernel = - account != null - ? // TODO: getIn types - account.getIn( - ["editor_settings", "jupyter", "kernel"] as any, - DEFAULT_KERNEL - ) - : undefined; + // console.log("jupyter::_syncdb_init_kernel", this.store.get("kernel")); if (this.store.get("kernel") == null) { // Creating a new notebook with no kernel set - const kernel = default_kernel || DEFAULT_KERNEL; - this.set_kernel(kernel); + if (!this._is_project) { + // we either let the user select a kernel, or use a stored one + let using_default_kernel = false; + + const account_store = this.redux.getStore("account") as any; + const editor_settings = account_store.get("editor_settings") as any; + if ( + editor_settings != null && + !editor_settings.get("ask_jupyter_kernel") + ) { + const default_kernel = editor_settings.getIn(["jupyter", "kernel"]); + // TODO: check if kernel is actually known + if (default_kernel != null) { + this.set_kernel(default_kernel); + using_default_kernel = true; + } + } + + if (!using_default_kernel) { + // otherwise we let the user choose a kernel + this.show_select_kernel("bad kernel"); + } + // we also finalize the kernel selection check, because it doesn't switch to true + // if there is no kernel at all. + this.setState({ check_select_kernel_init: true }); + } } else { // Opening an existing notebook + const default_kernel = this.store.get_default_kernel(); if (default_kernel == null) { // But user has no default kernel, since they never before explicitly set one. // So we set it. This is so that a user's default @@ -1936,16 +1957,19 @@ export class JupyterActions extends Actions { }; set_kernel = (kernel: any) => { - if (this.syncdb.get_state() != 'ready') { + if (this.syncdb.get_state() != "ready") { console.warn("Jupyter syncdb not yet ready -- not setting kernel"); return; } if (this.store.get("kernel") !== kernel) { - return this._set({ + this._set({ type: "settings", kernel }); } + if (this.store.get("show_kernel_selector")) { + this.hide_select_kernel(); + } }; show_history_viewer = () => { @@ -2713,14 +2737,14 @@ export class JupyterActions extends Actions { set_to_ipynb = async (ipynb: any, data_only = false) => { /* - set_to_ipynb - set from ipynb object. This is - mainly meant to be run on the backend in the project, - but is also run on the frontend too, e.g., - for client-side nbviewer (in which case it won't remove images, etc.). - - See the documentation for load_ipynb_file in project-actions.ts for - documentation about the data_only input variable. - */ + * set_to_ipynb - set from ipynb object. This is + * mainly meant to be run on the backend in the project, + * but is also run on the frontend too, e.g., + * for client-side nbviewer (in which case it won't remove images, etc.). + * + * See the documentation for load_ipynb_file in project-actions.ts for + * documentation about the data_only input variable. + */ //dbg = @dbg("set_to_ipynb") let set, trust; this._state = "load"; @@ -2738,7 +2762,7 @@ export class JupyterActions extends Actions { ipynb.metadata != null ? ipynb.metadata.kernelspec : undefined, x => x.name ) - : DEFAULT_KERNEL; // very like to work since official ipynb file without this kernelspec is invalid. + : undefined; //dbg("kernel in ipynb: name='#{kernel}'") const existing_ids = this.get_cell_list().toJS(); @@ -2912,21 +2936,14 @@ export class JupyterActions extends Actions { }; set_default_kernel = (kernel: any): void => { - let left: any; - if (this._is_project) { - // doesn't make sense for project (right now at least) - return; - } - const s = this.redux.getStore("account") as any; - if (s == null) { - return; - } - const cur = - (left = __guard__(s.getIn(["editor_settings", "jupyter"]), x => - x.toJS() - )) != null - ? left - : {}; + // doesn't make sense for project (right now at least) + if (this._is_project) return; + const account_store = this.redux.getStore("account") as any; + if (account_store == null) return; + const acc_jup = account_store.getIn(["editor_settings", "jupyter"]); + if (acc_jup == null) return; + let tmp: any; + const cur = (tmp = acc_jup.toJS()) != null ? tmp : {}; cur.kernel = kernel; (this.redux.getTable("account") as any).set({ editor_settings: { jupyter: cur } @@ -3284,6 +3301,96 @@ export class JupyterActions extends Actions { // Close the file this.file_action("close_file"); }; + + check_select_kernel = (): void => { + const kernel = this.store.get("kernel"); + if (kernel == null) return; + + let unknown_kernel = false; + + //console.log("jupyter::check_select_kernel", { + // kernels: this.store.get("kernels"), + // info: this.store.get_kernel_info(kernel) + //}); + + if (this.store.get("kernels") != null) + unknown_kernel = this.store.get_kernel_info(kernel) == null; + + // a kernel is set, but we don't know it + if (unknown_kernel) { + this.show_select_kernel("bad kernel"); + } else { + // we got a kernel, close dialog if not requested by user + if ( + this.store.get("show_kernel_selector") && + this.store.get("show_kernel_selector_reason") === "bad kernel" + ) { + this.hide_select_kernel(); + } + } + this.setState({ check_select_kernel_init: true }); + }; + + update_select_kernel_data = (): void => { + const kernels = jupyter_kernels.get(this.store.jupyter_kernel_key()); + if (kernels == null) return; + const kernel_selection = this.store.get_kernel_selection(kernels); + const [ + kernels_by_name, + kernels_by_language + ] = this.store.get_kernels_by_name_or_language(kernels); + const default_kernel = this.store.get_default_kernel(); + // do we have a similar kernel? + let closestKernel: Kernel | undefined = undefined; + const kernel = this.store.get("kernel"); + const kernel_info = this.store.get_kernel_info(kernel); + // unknown kernel, we try to find a close match + if (kernel_info == null && kernel != null) { + // kernel & kernels must be defined + closestKernel = misc.closest_kernel_match(kernel, kernels); + } + this.setState({ + kernel_selection, + kernels_by_name, + kernels_by_language, + default_kernel, + closestKernel + }); + }; + + show_select_kernel = (reason: show_kernel_selector_reasons): void => { + this.update_select_kernel_data(); + // we might not have the "kernels" data yet (but we will, once fetching it is complete) + // the select dialog will show a loading spinner + this.setState({ + show_kernel_selector_reason: reason, + show_kernel_selector: true + }); + }; + + hide_select_kernel = (): void => { + this.setState({ + show_kernel_selector_reason: undefined, + show_kernel_selector: false, + kernel_selection: undefined, + kernels_by_name: undefined + }); + }; + + select_kernel = (kernel_name: string): void => { + this.set_kernel(kernel_name); + this.set_default_kernel(kernel_name); + this.focus(true); + this.hide_select_kernel(); + }; + + kernel_dont_ask_again = (dont_ask: boolean): void => { + // why is "as any" necessary? + const account_table = this.redux.getTable("account") as any; + account_table.set({ + editor_settings: { ask_jupyter_kernel: !dont_ask } + }); + }; } function __guard__(value: any, transform: any) { @@ -3291,7 +3398,7 @@ function __guard__(value: any, transform: any) { ? transform(value) : undefined; } -function __range__(left: any, right: any, inclusive: any) { +function __range__(left: number, right: number, inclusive: boolean) { let range: any[] = []; let ascending = left < right; let end = !inclusive ? right : ascending ? right + 1 : right - 1; @@ -3300,6 +3407,7 @@ function __range__(left: any, right: any, inclusive: any) { } return range; } + function __guardMethod__(obj: any, methodName: any, transform: any) { if ( typeof obj !== "undefined" && diff --git a/src/smc-webapp/jupyter/import-from-ipynb.ts b/src/smc-webapp/jupyter/import-from-ipynb.ts index 97815d386d..ac2672deab 100644 --- a/src/smc-webapp/jupyter/import-from-ipynb.ts +++ b/src/smc-webapp/jupyter/import-from-ipynb.ts @@ -15,20 +15,8 @@ const DEFAULT_IPYNB = { } ], metadata: { - kernelspec: { - display_name: "Python 2", - language: "python", - name: "python2" - }, - language_info: { - codemirror_mode: { name: "ipython", version: 2 }, - file_extension: ".py", - mimetype: "text/x-python", - name: "python", - nbconvert_exporter: "python", - pygments_lexer: "ipython2", - version: "2.7.13" - } + kernelspec: undefined, + language_info: undefined }, nbformat: 4, nbformat_minor: 0 diff --git a/src/smc-webapp/jupyter/main.tsx b/src/smc-webapp/jupyter/main.tsx index 21cd9be25d..d260bfd99e 100644 --- a/src/smc-webapp/jupyter/main.tsx +++ b/src/smc-webapp/jupyter/main.tsx @@ -2,7 +2,7 @@ Top-level react component, which ties everything together */ -import { React, Component, rclass, rtypes } from "../app-framework"; // TODO: this will move +import { React, Component, Rendered, rclass, rtypes } from "../app-framework"; // TODO: this will move import * as immutable from "immutable"; const { ErrorDisplay, Loading } = require("../r_misc"); // React components that implement parts of the Jupyter notebook. @@ -18,10 +18,12 @@ const { EditAttachments } = require("./edit-attachments"); const { EditCellMetadata } = require("./edit-cell-metadata"); const { FindAndReplace } = require("./find-and-replace"); const { ConfirmDialog } = require("./confirm-dialog"); +const { KernelSelector } = require("./select-kernel"); const { KeyboardShortcuts } = require("./keyboard-shortcuts"); const { JSONView } = require("./json-view"); const { RawEditor } = require("./raw-editor"); const { ExamplesDialog } = require("smc-webapp/assistant/dialog"); +import { Kernel as KernelType, Kernels as KernelsType } from "./util"; const KERNEL_STYLE: React.CSSProperties = { position: "absolute", @@ -42,6 +44,8 @@ interface JupyterEditorProps { name: string; // TODO: is this correct? view_mode?: any; // rtypes.oneOf(['normal', 'json', 'raw']) kernel?: string; // string name of the kernel + kernels?: KernelsType; + site_name: string; // error?: string; // TODO: repeated? fatal?: string; // *FATAL* error; user must edit file to fix. toolbar?: boolean; @@ -74,10 +78,18 @@ interface JupyterEditorProps { insert_image?: boolean; // show insert image dialog edit_attachments?: string; edit_cell_metadata?: immutable.Map; - editor_settings?: immutable.Map; + editor_settings: immutable.Map; raw_ipynb?: immutable.Map; metadata?: immutable.Map; trust?: boolean; + kernel_info: immutable.Map; + check_select_kernel_init: boolean; + show_kernel_selector?: boolean; + kernel_selection?: immutable.Map; + kernels_by_name?: immutable.OrderedMap>; + kernels_by_language?: immutable.OrderedMap>; + default_kernel?: string; + closestKernel?: KernelType; } class JupyterEditor0 extends Component { @@ -86,6 +98,7 @@ class JupyterEditor0 extends Component { [name]: { view_mode: rtypes.oneOf(["normal", "json", "raw"]), kernel: rtypes.string, // string name of the kernel + kernels: rtypes.immutable.List, error: rtypes.string, fatal: rtypes.string, // *FATAL* error; user must edit file to fix. toolbar: rtypes.bool, @@ -118,11 +131,20 @@ class JupyterEditor0 extends Component { insert_image: rtypes.bool, // show insert image dialog edit_attachments: rtypes.string, edit_cell_metadata: rtypes.immutable.Map, - editor_settings: rtypes.immutable.Map, raw_ipynb: rtypes.immutable.Map, metadata: rtypes.immutable.Map, - trust: rtypes.bool - } + trust: rtypes.bool, + kernel_info: rtypes.immutable.Map, + check_select_kernel_init: rtypes.bool, + show_kernel_selector: rtypes.bool, + kernel_selection: rtypes.immutable.Map, + kernels_by_name: rtypes.immutable.Map, + kernels_by_language: rtypes.immutable.Map, + default_kernel: rtypes.string, + closestKernel: rtypes.immutable.Map + }, + customize: { site_name: rtypes.string }, + account: { editor_settings: rtypes.immutable.Map } }; } @@ -182,11 +204,25 @@ class JupyterEditor0 extends Component { ); } + render_loading(): Rendered { + return ( + + ); + } + render_cells() { if ( this.props.cell_list == null || this.props.font_size == null || - this.props.cm_options == null + this.props.cm_options == null || + this.props.kernels == null ) { return ( { ); } + render_select_kernel() { + const ask_jupyter_kernel = this.props.editor_settings.get( + "ask_jupyter_kernel" + ); + return ( + + ); + } + render_keyboard_shortcuts() { return ( { } } + render_main() { + if (!this.props.check_select_kernel_init) { + return this.render_loading(); + } else if (this.props.show_kernel_selector) { + return this.render_select_kernel(); + } else { + return ( + <> + {this.render_main_view()} + {this.render_introspect()} + + ); + } + } + render() { if (this.props.fatal) { return this.render_fatal(); @@ -411,8 +482,7 @@ class JupyterEditor0 extends Component { {this.render_assistant_dialog()} {this.render_confirm_dialog()} {this.render_heading()} - {this.render_main_view()} - {this.render_introspect()} + {this.render_main()} ); } diff --git a/src/smc-webapp/jupyter/register.ts b/src/smc-webapp/jupyter/register.ts index ca0bceb704..3c611053f0 100644 --- a/src/smc-webapp/jupyter/register.ts +++ b/src/smc-webapp/jupyter/register.ts @@ -13,7 +13,7 @@ const { webapp_client } = require("../webapp_client"); const { JupyterEditor } = require("./main"); const { JupyterActions } = require("./actions"); -const { JupyterStore } = require("./store"); +const { JupyterStore, initial_jupyter_store_state } = require("./store"); import { syncdb2 as new_syncdb } from "../frame-editors/generic/client"; @@ -34,7 +34,11 @@ export function register() { } const actions = redux.createActions(name, JupyterActions); - const store = redux.createStore(name, JupyterStore); + const store = redux.createStore( + name, + JupyterStore, + initial_jupyter_store_state + ); const sync_path = misc.meta_file(path, "jupyter2"); // a.ipynb --> ".a.ipynb.sage-jupyter2" const syncdb = new_syncdb({ diff --git a/src/smc-webapp/jupyter/select-kernel.tsx b/src/smc-webapp/jupyter/select-kernel.tsx new file mode 100644 index 0000000000..e258fbe004 --- /dev/null +++ b/src/smc-webapp/jupyter/select-kernel.tsx @@ -0,0 +1,379 @@ +/* +help users selecting a kernel +*/ + +import { React, Component, Rendered } from "../app-framework"; // TODO: this will move +import { + Map as ImmutableMap, + List, + OrderedMap /*, List as ImmutableList*/ +} from "immutable"; +import * as misc from "smc-util/misc"; +const { Icon, Loading } = require("../r_misc"); // TODO: import types +const { + Button, + Col, + Row, + ButtonGroup, + Checkbox, + Alert +} = require("react-bootstrap"); // TODO: import types +import { Kernel } from "./util"; +const { COLORS } = require("smc-util/theme"); + +const row_style: React.CSSProperties = { + marginTop: "5px", + marginBottom: "5px" +}; + +const main_style: React.CSSProperties = { + padding: "20px 10px", + overflowY: "auto", + overflowX: "hidden" +}; + +interface KernelSelectorProps { + actions: any; + site_name: string; + kernel?: string; + kernel_info?: any; + default_kernel?: string; + ask_jupyter_kernel?: boolean; + kernel_selection?: ImmutableMap; + kernels_by_name?: OrderedMap>; + kernels_by_language?: OrderedMap>; + closestKernel?: Kernel; +} + +interface KernelSelectorState {} + +export class KernelSelector extends Component< + KernelSelectorProps, + KernelSelectorState +> { + constructor(props: KernelSelectorProps, context: any) { + super(props, context); + this.state = {}; + } + + // the idea here is to not set the kernel, but still render the notebook. + // looks like that's not easy, and well, probably incompatible with classical jupyter. + + /* + + {this.close_button()} + + + close_button() { + return ( + + ); + } + */ + + kernel_name(name: string) { + return this.kernel_attr(name, "display_name"); + } + + kernel_attr(name: string, attr: string): string { + if (this.props.kernels_by_name == null) return ""; + const k = this.props.kernels_by_name.get(name); + if (k == null) return ""; + return k.get(attr, name); + } + + render_suggested_link(cocalc) { + if (cocalc == null) return; + const url: string | undefined = cocalc.get("url"); + const descr: string | undefined = cocalc.get("description", ""); + if (url != null) { + return ( + + {descr} + + ); + } else { + return descr; + } + } + + render_kernel_button( + name: string, + size?: string, + show_icon: boolean = true + ): Rendered { + const lang = this.kernel_attr(name, "language"); + let icon: Rendered | undefined = undefined; + if (lang != null && show_icon) { + if (["python", "r", "sagemath", "octave", "julia"].indexOf(lang) >= 0) { + icon = ; + } else if (lang.startsWith("bash")) { + icon = ; + } + // TODO do other languages have icons? + } + return ( + + ); + } + + render_suggested() { + if ( + this.props.kernel_selection == null || + this.props.kernels_by_name == null + ) + return; + + const entries: Rendered[] = []; + this.props.kernel_selection + .sort((a, b) => this.kernel_name(a).localeCompare(this.kernel_name(b))) + .map((name, lang) => { + const cocalc: ImmutableMap< + string, + any + > = this.props.kernels_by_name!.getIn( + [name, "metadata", "cocalc"], + null + ); + if (cocalc == null) return; + const prio: number = cocalc.get("priority", 0); + if (prio < 10) return; + + entries.push( + + {this.render_kernel_button(name)} + +
{this.render_suggested_link(cocalc)}
+ +
+ ); + }); + + if (entries.length == 0) return; + + return ( + +

Suggested kernels

+ {entries} +
+ ); + } + + // render_all_selected_link() { + // if (this.props.kernels_by_name == null) return; + // const name = this.state.selected_kernel; + // if (name == null) return; + // const cocalc: ImmutableMap = this.props.kernels_by_name.getIn( + // [name, "metadata", "cocalc"], + // null + // ); + // return this.render_suggested_link(cocalc); + // } + + render_all_langs(): Rendered[] | undefined { + if (this.props.kernels_by_language == null) return; + const label: React.CSSProperties = { + fontWeight: "bold", + color: COLORS.GRAY_D + }; + const all: Rendered[] = []; + this.props.kernels_by_language.forEach((names, lang) => { + const kernels = names.map(name => + this.render_kernel_button(name, "small", false) + ); + all.push( + + + {misc.capitalize(lang)} + + + {kernels} + + + ); + return true; + }); + + return all; + } + + render_all() { + if (this.props.kernels_by_language == null) return; + + return ( + +

All kernels by language

+ {this.render_all_langs()} +
+ ); + } + + render_last() { + if (this.props.default_kernel == null) return; + return ( + +

Quick selection

+
+ Your most recently selected kernel is{" "} + {this.render_kernel_button(this.props.default_kernel)}. +
+
+ ); + } + + dont_ask_again_click(checked: boolean) { + this.props.actions.kernel_dont_ask_again(checked); + } + + render_dont_ask_again() { + return ( + +
+ this.dont_ask_again_click(e.target.checked)} + > + Do not ask again + + + Check this box to always use your most recent selection. You can + change your kernel any time later, too. + +
+
+ ); + } + + render_top() { + if (this.props.kernel == null || this.props.kernel_info == null) { + let msg: Rendered; + // kernel, but no info means it is not known + if (this.props.kernel != null && this.props.kernel_info == null) { + msg = ( + <> + Your notebook kernel "{this.props.kernel}" does not + exist on {this.props.site_name}. + + ); + } else { + msg = <>This notebook has no kernel.; + } + return ( + + {msg} A working kernel is required in order to + evaluate the code in the notebook. Please select one for the + programming language you want to work with. + + ); + } else { + return ( + + Select a new kernel. The currently selected kernel is{" "} + "{this.kernel_name(this.props.kernel)}". + + ); + } + } + + render_unknown() { + const closestKernel = this.props.closestKernel; + if (this.props.kernel_info != null || closestKernel == null) return; + const closestKernelName = closestKernel.get("name"); + if (closestKernelName == null) return; + + return ( + + +

Unknown Kernel

+
+ A similar kernel might be{" "} + {this.render_kernel_button(closestKernelName)}. +
+
+
+ ); + } + + render_footer(): Rendered { + return ( + + Note: You can always change the selected kernel later + in the »Kernel« menu or by clicking on the kernel information at the top + right. + + ); + } + + render_close_button(): Rendered | undefined { + if (this.props.kernel == null || this.props.kernel_info == null) return; + return ( + + ); + } + + render_body(): Rendered { + if ( + this.props.kernels_by_name == null || + this.props.kernel_selection == null + ) { + return ( + + + + ); + } else { + return ( + <> + {this.render_top()} + {this.render_unknown()} + {this.render_last()} + {this.render_dont_ask_again()} + {this.render_suggested()} + {this.render_all()} +
+ {this.render_footer()} + + ); + } + } + + render_head(): Rendered { + return ( + +

+ {"Select a Kernel"} + {this.render_close_button()} +

+
+ ); + } + + render(): Rendered { + return ( +
+ + {this.render_head()} + {this.render_body()} + +
+ ); + } +} diff --git a/src/smc-webapp/jupyter/status.tsx b/src/smc-webapp/jupyter/status.tsx index 614e083abb..a058e8c10c 100644 --- a/src/smc-webapp/jupyter/status.tsx +++ b/src/smc-webapp/jupyter/status.tsx @@ -30,7 +30,10 @@ class Mode0 extends Component { return ; } return ( -
+
); @@ -128,10 +131,15 @@ class Kernel0 extends Component { render_name() { let display_name = - this.props.kernel_info != null ? this.props.kernel_info.get("display_name") : undefined; + this.props.kernel_info != null + ? this.props.kernel_info.get("display_name") + : undefined; if (display_name == null && this.props.kernels != null) { // Definitely an unknown kernel - const closestKernel = closest_kernel_match(this.props.kernel, this.props.kernels); + const closestKernel = closest_kernel_match( + this.props.kernel, + this.props.kernels + ); const closestKernelDisplayName = closestKernel.get("display_name"); const closestKernelName = closestKernel.get("name"); return ( @@ -139,8 +147,9 @@ class Kernel0 extends Component { style={KERNEL_ERROR_STYLE} onClick={() => this.props.actions.set_kernel(closestKernelName)} > - Unknown kernel {this.props.kernel}, click here - to use {closestKernelDisplayName} instead. + Unknown kernel{" "} + {this.props.kernel}, click + here to use {closestKernelDisplayName} instead. ); } else { @@ -149,7 +158,12 @@ class Kernel0 extends Component { display_name = this.props.kernel; } return ( - {display_name != null ? display_name : "No Kernel"} + this.props.actions.show_select_kernel("user request")} + > + {display_name != null ? display_name : "No Kernel"} + ); } } @@ -270,7 +284,10 @@ class Kernel0 extends Component { // unknown, e.g, not reporting/working or old backend. return; } - if (this.props.backend_state !== "running" && this.props.backend_state !== "starting") { + if ( + this.props.backend_state !== "running" && + this.props.backend_state !== "starting" + ) { // not using resourcesw memory = cpu = 0; } else { @@ -305,8 +322,8 @@ class Kernel0 extends Component {
Does NOT include subprocesses.
- You can clear all memory by selecting Close and Halt from the File menu or restarting your - kernel. + You can clear all memory by selecting Close and Halt from the File menu + or restarting your kernel.
); return ( @@ -315,7 +332,11 @@ class Kernel0 extends Component { CPU: {cpu}%
- Memory: {memory}MB + Memory:{" "} + + {memory} + MB + ); @@ -333,7 +354,10 @@ class Kernel0 extends Component { ); const body = ( -
+
{title} {this.render_backend_state_icon()}
diff --git a/src/smc-webapp/jupyter/store.ts b/src/smc-webapp/jupyter/store.ts index 923c32674d..84acaf1a12 100644 --- a/src/smc-webapp/jupyter/store.ts +++ b/src/smc-webapp/jupyter/store.ts @@ -6,14 +6,23 @@ declare const localStorage: any; const misc = require("smc-util/misc"); import { Store } from "../app-framework"; -import { Set } from "immutable"; +import { + Set, + Map as ImmutableMap, + List as ImmutableList, + OrderedMap, + fromJS as immutableFromJS +} from "immutable"; const { export_to_ipynb } = require("./export-to-ipynb"); const { DEFAULT_COMPUTE_IMAGE } = require("smc-util/compute-images"); +import { Kernels, Kernel } from "./util"; // Used for copy/paste. We make a single global clipboard, so that // copy/paste between different notebooks works. let global_clipboard: any = undefined; +export type show_kernel_selector_reasons = "bad kernel" | "user request"; + export interface JupyterStoreState { nbconvert_dialog: any; cell_toolbar: string; @@ -28,9 +37,9 @@ export interface JupyterStoreState { fatal: string; has_unsaved_changes?: boolean; has_uncommitted_changes?: boolean; - kernel: any | string; - kernels: any; - kernel_info: any; + kernel?: string; + kernels?: Kernels; + kernel_info?: any; max_output_length: number; metadata: any; md_edit_ids: Set; @@ -57,8 +66,23 @@ export interface JupyterStoreState { confirm_dialog: any; insert_image: any; scroll: any; + check_select_kernel_init: boolean; + show_kernel_selector: boolean; + show_kernel_selector_reason?: show_kernel_selector_reasons; + kernel_selection?: ImmutableMap; + kernels_by_name?: OrderedMap>; + kernels_by_language?: OrderedMap>; + default_kernel?: string; + closestKernel?: Kernel; } +export const initial_jupyter_store_state: { + [K in keyof JupyterStoreState]?: JupyterStoreState[K] +} = { + check_select_kernel_init: false, + show_kernel_selector:false +}; + export class JupyterStore extends Store { private _is_project: any; private _more_output: any; @@ -162,7 +186,7 @@ export class JupyterStore extends Store { } }; - get_kernel_info = (kernel: any) => { + get_kernel_info = (kernel: any): any | undefined => { // slow/inefficient, but ok since this is rarely called let info: any = undefined; const kernels = this.get("kernels"); @@ -292,6 +316,98 @@ export class JupyterStore extends Store { } }; + get_default_kernel = (): string | undefined => { + const account = this.redux.getStore("account"); + if (account != null) { + // TODO: getIn types + return account.getIn(["editor_settings", "jupyter", "kernel"] as any); + } else { + return undefined; + } + }; + + /* + * select all kernels, which are ranked highest for a specific language + * and do have a priority weight > 0. + * + * kernel metadata looks like that + * + * "display_name": ..., + * "argv":, ... + * "language": "sagemath", + * "metadata": { + * "cocalc": { + * "priority": 10, + * "description": "Open-source mathematical software system", + * "url": "https://www.sagemath.org/" + * } + * } + * + * Return dict of language <-> kernel_name + */ + get_kernel_selection = (kernels: Kernels): ImmutableMap => { + const data: any = {}; + kernels + .filter(entry => entry.get("language") != null) + .groupBy(entry => entry.get("language")) + .forEach((kernels, lang) => { + const top: any = kernels + .sort((a, b) => { + const va = -a.getIn(["metadata", "cocalc", "priority"], 0); + const vb = -b.getIn(["metadata", "cocalc", "priority"], 0); + return misc.cmp(va, vb); + }) + .first(); + if (top == null || lang == null) return true; + const name = top.get("name"); + if (name == null) return true; + data[lang] = name; + }); + + return ImmutableMap(data); + }; + + get_kernels_by_name_or_language = ( + kernels: Kernels + ): [ + OrderedMap>, + OrderedMap> + ] => { + let data_name: any = {}; + let data_lang: any = {}; + const add_lang = (lang, entry) => { + if (data_lang[lang] == null) data_lang[lang] = []; + data_lang[lang].push(entry); + }; + kernels.map(entry => { + const name = entry.get("name"); + const lang = entry.get("language"); + if (name != null) data_name[name] = entry; + if (lang == null) { + // we collect all kernels without a language under "misc" + add_lang("misc", entry); + } else { + add_lang(lang, entry); + } + }); + const by_name = OrderedMap>( + data_name + ).sortBy((v, k) => { + return v.get("display_name", v.get("name", k)).toLowerCase(); + }); + // data_lang, we're only interested in the kernel names, not the entry itself + data_lang = immutableFromJS(data_lang).map((v, k) => { + v = v + .sortBy(v => v.get("display_name", v.get("name", k)).toLowerCase()) + .map(v => v.get("name")); + return v; + }); + const by_lang = OrderedMap>(data_lang).sortBy( + (_v, k) => k.toLowerCase() + ); + return [by_name, by_lang]; + }; + get_raw_link = (path: any) => { return this.redux .getProjectStore(this.get("project_id")) diff --git a/src/smc-webapp/jupyter/util.ts b/src/smc-webapp/jupyter/util.ts index 6cf0228504..ff93783be5 100644 --- a/src/smc-webapp/jupyter/util.ts +++ b/src/smc-webapp/jupyter/util.ts @@ -8,6 +8,8 @@ part of CoCalc (c) SageMath, Inc., 2017 */ +import * as immutable from "immutable"; + // This list is inspired by OutputArea.output_types in https://github.com/jupyter/notebook/blob/master/notebook/static/notebook/js/outputarea.js // The order matters -- we only keep the left-most type (see import-from-ipynb.coffee) @@ -23,6 +25,9 @@ export const JUPYTER_MIMETYPES = [ "text/plain" ]; +export type Kernel = immutable.Map; +export type Kernels = immutable.List; + export function codemirror_to_jupyter_pos( code: string, pos: { ch: number; line: number } diff --git a/src/smc-webapp/r_account.cjsx b/src/smc-webapp/r_account.cjsx index 98226b8875..482da04e25 100644 --- a/src/smc-webapp/r_account.cjsx +++ b/src/smc-webapp/r_account.cjsx @@ -856,6 +856,7 @@ EDITOR_SETTINGS_CHECKBOXES = extra_button_bar : 'more editing functions (mainly in Sage worksheets)' build_on_save : 'build LaTex file whenever it is saved to disk' show_exec_warning : 'warn that certain files are not directly executable' + ask_jupyter_kernel : 'ask which kernel to use for a new Jupyter Notebook' jupyter_classic : use classical Jupyter notebook (DANGER: this can cause trouble...) EditorSettingsCheckboxes = rclass