diff --git a/src/smc-util/misc.js b/src/smc-util/misc.js index e1fb17ba40..40b25df3e2 100644 --- a/src/smc-util/misc.js +++ b/src/smc-util/misc.js @@ -3165,6 +3165,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 ( diff --git a/src/smc-webapp/jupyter/actions.ts b/src/smc-webapp/jupyter/actions.ts index c59c61a6c5..bf14f9e682 100644 --- a/src/smc-webapp/jupyter/actions.ts +++ b/src/smc-webapp/jupyter/actions.ts @@ -381,13 +381,14 @@ export class JupyterActions extends Actions { 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(); }; set_error = (err: any): void => { @@ -888,10 +889,6 @@ export class JupyterActions extends Actions { this.set_cm_options(); } - // unkown kernel name - if (!this._is_project && obj.kernel_info == null) { - this.show_select_kernel(); - } break; } }); @@ -917,13 +914,14 @@ export class JupyterActions extends Actions { this._state = "ready"; } - if (this.store.get("kernel") == null) { + const kernel = this.store.get("kernel"); + const no_kernel: boolean = kernel == null; + const unknown_kernel: boolean = !no_kernel + ? this.store.get_kernel_info(kernel) == null + : false; + if (no_kernel || unknown_kernel) { this.show_select_kernel(); - } else { - // we have a kernel - this.hide_select_kernel(); } - if (this.store.get("view_mode") === "raw") { this.set_raw_ipynb(); } @@ -3178,28 +3176,42 @@ export class JupyterActions extends Actions { this.file_action("close_file"); }; - show_select_kernel = (): void => { + 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 = this.store.get_kernels_by_name(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: TKernel | 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) { + if (kernel_info == null && kernel != null && kernels != null) { + // kernel & kernels must be defined closestKernel = misc.closest_kernel_match(kernel, kernels); } this.setState({ - show_kernel_selector: true, kernel_selection, kernels_by_name, + kernels_by_language, default_kernel, closestKernel }); }; + show_select_kernel = (): 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: true + }); + }; + hide_select_kernel = (): void => { this.setState({ show_kernel_selector: false, diff --git a/src/smc-webapp/jupyter/main.tsx b/src/smc-webapp/jupyter/main.tsx index 4289362397..899e074392 100644 --- a/src/smc-webapp/jupyter/main.tsx +++ b/src/smc-webapp/jupyter/main.tsx @@ -80,6 +80,7 @@ interface JupyterEditorProps { show_kernel_selector?: boolean; kernel_selection?: immutable.Map; kernels_by_name?: immutable.OrderedMap>; + kernels_by_language?: immutable.OrderedMap>; default_kernel?: string; closestKernel?: TKernel; } @@ -130,6 +131,7 @@ class JupyterEditor0 extends Component { 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 } @@ -349,6 +351,7 @@ class JupyterEditor0 extends Component { kernel_info={this.props.kernel_info} kernel_selection={this.props.kernel_selection} kernels_by_name={this.props.kernels_by_name} + kernels_by_language = {this.props.kernels_by_language} default_kernel={this.props.default_kernel} closestKernel={this.props.closestKernel} /> diff --git a/src/smc-webapp/jupyter/select-kernel.tsx b/src/smc-webapp/jupyter/select-kernel.tsx index eb2ee62485..3c02c64dc4 100644 --- a/src/smc-webapp/jupyter/select-kernel.tsx +++ b/src/smc-webapp/jupyter/select-kernel.tsx @@ -5,25 +5,35 @@ 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 { Kernels } from "./util"; -const { Icon, Markdown, Space, Loading } = require("../r_misc"); // TODO: import types +import * as misc from "smc-util/misc"; +const { Icon, Markdown, /*Space,*/ Loading } = require("../r_misc"); // TODO: import types const { Button, Col, Row, - MenuItem, - DropdownButton, + ButtonGroup, + /* MenuItem, + DropdownButton, */ Alert } = require("react-bootstrap"); // TODO: import types import { TKernel } from "./util"; +const { COLORS } = require("smc-util/theme"); const row_style: React.CSSProperties = { marginTop: "5px", marginBottom: "5px" }; +const main_style: React.CSSProperties = { + padding: "20px 40px", + overflowY: "auto", + overflowX: "hidden", + height: "90%" +}; + interface IKernelSelectorProps { actions: any; kernel?: string; @@ -31,6 +41,7 @@ interface IKernelSelectorProps { default_kernel?: string; kernel_selection?: ImmutableMap; kernels_by_name?: OrderedMap>; + kernels_by_language?: OrderedMap>; closestKernel?: TKernel; } @@ -99,11 +110,15 @@ export class KernelSelector extends Component< return ; } - kernel_name(name: string): string { + 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("display_name", name); + return k.get(attr, name); } render_suggested_link(cocalc) { @@ -121,6 +136,30 @@ export class KernelSelector extends Component< } } + render_kernel_button( + name: string, + size: string = "default", + 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 = ; + } + // TODO do other languages have icons? + } + return ( + + ); + } + render_suggested() { if ( this.props.kernel_selection == null || @@ -145,12 +184,8 @@ export class KernelSelector extends Component< entries.push( - - - - + {this.render_kernel_button(name)} +
{this.render_suggested_link(cocalc)}
@@ -160,10 +195,10 @@ export class KernelSelector extends Component< if (entries.length == 0) return; return ( - <> +

Suggested kernels

{entries} - +
); } @@ -178,52 +213,54 @@ export class KernelSelector extends Component< return this.render_suggested_link(cocalc); } - render_all() { - if (this.props.kernels_by_name == null) return; + 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_name.mapKeys(name => { - if (name == null) return; + this.props.kernels_by_language.forEach((names, lang) => { + const kernels = names.map(name => + this.render_kernel_button(name, "small", false) + ); all.push( - this.setState({ selected_kernel: name })} - > - {this.kernel_name(name)} - + + + {misc.capitalize(lang)} + + + {kernels} + + ); + return true; }); + return all; + } + + render_all() { + if (this.props.kernels_by_language == null) return; + return ( - <> +

All kernels

- - {all} - - - {this.render_select_button()} - - {this.render_all_selected_link()} - + {this.render_all_langs()} +
); } render_last() { if (this.props.default_kernel == null) return; return ( - <> +

Quick selection

Your most recently selected kernel was:{" "} - + {this.render_kernel_button(this.props.default_kernel)}
- +
); } @@ -236,18 +273,18 @@ export class KernelSelector extends Component< msg = "This notebook has no kernel."; } return ( - <> + {msg} A working kernel is required in order to evaluate the code in the notebook. Based on the programming language you want to work with, you have to select one. - + ); } else { return ( - <> + Select a new kernel. (Currently selected:{" "} {this.kernel_name(this.props.kernel)}) - + ); } } @@ -255,53 +292,60 @@ export class KernelSelector extends Component< render_unknown() { const closestKernel = this.props.closestKernel; if (this.props.kernel_info != null || closestKernel == null) return; - - const closestKernelDisplayName = closestKernel.get("display_name"); const closestKernelName = closestKernel.get("name"); + if (closestKernelName == null) return; return ( -

Kernel unknown

+

Unknown Kernel

- Maybe select {closestKernelDisplayName}{" "} - {closestKernelName} + A similar kernel might be{" "} + {this.render_kernel_button(closestKernelName)}.
); } - render() { - const style: React.CSSProperties = { - padding: "20px 40px", - overflowY: "auto", - overflowX: "hidden", - height: "90%" - }; + render_footer(): Rendered { + return ( + + Note: You can always change the selected kernel later + via the »Kernel« menu enty. + + ); + } + render() { let body: Rendered; if ( this.props.kernels_by_name == null || this.props.kernel_selection == null ) { - body = ; + body = ( + + + + ); } else { body = ( <> - -

{"Select a Kernel"}

-
- {this.render_top()} + {this.render_top()} {this.render_unknown()} - {this.render_last()} - {this.render_suggested()} - {this.render_all()} + {this.render_last()} + {this.render_suggested()} + {this.render_all()} +
+ {this.render_footer()} ); } return ( - + + +

{"Select a Kernel"}

+
{body} ); diff --git a/src/smc-webapp/jupyter/store.ts b/src/smc-webapp/jupyter/store.ts index 70f8b35bf7..ef8587a321 100644 --- a/src/smc-webapp/jupyter/store.ts +++ b/src/smc-webapp/jupyter/store.ts @@ -6,7 +6,13 @@ declare const localStorage: any; const misc = require("smc-util/misc"); import { Store } from "../app-framework"; -import { Set, Map as ImmutableMap, OrderedMap } 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, TKernel } from "./util"; @@ -61,6 +67,7 @@ export interface JupyterStoreState { show_kernel_selector: boolean; kernel_selection?: ImmutableMap; kernels_by_name?: OrderedMap>; + kernels_by_language?: OrderedMap>; default_kernel?: string; closestKernel?: TKernel; } @@ -344,19 +351,45 @@ export class JupyterStore extends Store { return ImmutableMap(data); }; - get_kernels_by_name = ( + get_kernels_by_name_or_language = ( kernels: Kernels - ): OrderedMap> => { - const data: any = {}; + ): [ + 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"); - if (name != null) data[name] = entry; - }); - return OrderedMap>(data).sortBy( - (v, k) => { - return v.get("display_name", v.get("name", k)).toLowerCase(); + 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) => {