diff --git a/src/packages/frontend/components/icon.tsx b/src/packages/frontend/components/icon.tsx index 1fd6705436..fe58077824 100644 --- a/src/packages/frontend/components/icon.tsx +++ b/src/packages/frontend/components/icon.tsx @@ -296,6 +296,7 @@ const IconSpec = { fire: FireOutlined, firefox: { IconFont: "firefox" }, flash: ThunderboltOutlined, + "floppy-o": SaveOutlined, // used by matplotlib widget "flow-chart": { IconFont: "flow-chart" }, folder: FolderOutlined, "folder-open": FolderOpenOutlined, @@ -649,10 +650,23 @@ if (typeof $ != "undefined") { // @ts-ignore const that = $(this); for (const elt of that.find(".fa")) { + let style: CSS | undefined = undefined; + if (elt.className.includes("fa-fw")) { + // could break if subset of some other icon name, but doesn't seem to be. + // this is "fixed width" in font awesome: + style = { width: "1.28571429em", textAlign: "center" }; + } for (const cls of elt.className.split(/\s+/)) { + if (cls == "fa-fw") { + continue; + } if (cls.startsWith("fa-")) { ReactDOM.render( - <Icon name={cls.slice(3)} spin={cls == "fa-cocalc-ring"} />, + <Icon + style={style} + name={cls.slice(3)} + spin={cls == "fa-cocalc-ring"} + />, elt ); break; diff --git a/src/packages/frontend/jupyter/project-actions.ts b/src/packages/frontend/jupyter/project-actions.ts index ee4a3d5dea..1983c83fd8 100644 --- a/src/packages/frontend/jupyter/project-actions.ts +++ b/src/packages/frontend/jupyter/project-actions.ts @@ -124,7 +124,7 @@ export class JupyterActions extends JupyterActions0 { 5; cells = cells.toJS(); } - dbg(`cells at manage_init = ${JSON.stringify(cells)}`); + //dbg(`cells at manage_init = ${JSON.stringify(cells)}`); this.sync_exec_state = underscore.debounce(this.sync_exec_state, 2000); this._throttled_ensure_positions_are_unique = underscore.debounce( @@ -400,13 +400,13 @@ export class JupyterActions extends JupyterActions0 { // Only one client -- the project itself -- will run this code. manager_on_cell_change = (id: any, new_cell: any, old_cell: any) => { const dbg = this.dbg(`manager_on_cell_change(id='${id}')`); - dbg( - `new_cell='${misc.to_json( - new_cell != null ? new_cell.toJS() : undefined - )}',old_cell='${misc.to_json( - old_cell != null ? old_cell.toJS() : undefined - )}')` - ); +// dbg( +// `new_cell='${misc.to_json( +// new_cell != null ? new_cell.toJS() : undefined +// )}',old_cell='${misc.to_json( +// old_cell != null ? old_cell.toJS() : undefined +// )}')` +// ); if ( (new_cell != null ? new_cell.get("state") : undefined) === "start" && @@ -1173,6 +1173,13 @@ export class JupyterActions extends JupyterActions0 { data ); */ + } else if (type == "message_to_kernel") { + const content = + this.syncdb.ipywidgets_state.getLastMessageToKernel(model_id); + this.jupyter_kernel.send_comm_message_to_kernel(misc.uuid(), model_id, { + method: "custom", + content, + }); } else { throw Error(`invalid synctable state -- unknown type '${type}'`); } @@ -1183,7 +1190,9 @@ export class JupyterActions extends JupyterActions0 { const dbg = this.dbg("process_comm_message_from_kernel"); // serializing the full message could cause enormous load on the server, since // the mesg may contain large buffers. Only do for low level debugging! - // dbg(mesg); // EXTREME DANGER! + // EXTREME DANGER! + const TYPESCRIPT_ERROR_ON_PURPOSE_TO_REMIND_ME = false; + dbg(JSON.stringify(mesg)); // This should be safe: dbg(JSON.stringify(mesg.header)); if (this.syncdb.ipywidgets_state == null) { diff --git a/src/packages/frontend/jupyter/widgets/manager.ts b/src/packages/frontend/jupyter/widgets/manager.ts index d2ca3392dd..6c1762e320 100644 --- a/src/packages/frontend/jupyter/widgets/manager.ts +++ b/src/packages/frontend/jupyter/widgets/manager.ts @@ -69,6 +69,7 @@ export class WidgetManager extends base.ManagerBase<HTMLElement> { } this.setWidgetModelIdState = setWidgetModelIdState; this.init_ipywidgets_state(); + window.x = this; } private async init_ipywidgets_state(): Promise<void> { @@ -126,6 +127,9 @@ export class WidgetManager extends base.ManagerBase<HTMLElement> { case "message": this.handle_table_model_message_change(model_id); break; + case "message_to_kernel": + // only kernel handles this. + break; default: throw Error(`unknown state type '${type}'`); } @@ -262,11 +266,34 @@ export class WidgetManager extends base.ManagerBase<HTMLElement> { model_id: string ): Promise<void> { const message = this.ipywidgets_state.get_message(model_id); + console.log("message = ", message, size(message)); if (size(message) == 0) return; // temporary until we have delete functionality // console.log("handle_table_model_message_change", message); - const model = await this.get_model(model_id); - if (model == null) return; - model.trigger("msg:custom", message); + let d = 50; + for (let i = 0; i < 100; i++) { + const model = await this.get_model(model_id); + if (model != null) { + model.trigger("msg:custom", message); + return; + } + await delay(d); + d *= 1.2; + } + console.log( + "handle_table_model_message_change: unable to deliver message since model doesn't exist.", + { message, model_id } + ); + } + + private async handle_custom_message_from_model_to_kernel( + model_id: string, + message: object + ): Promise<void> { + console.log("handle_custom_message_from_model_to_kernel", { + model_id, + message, + }); + this.ipywidgets_state.sendMessageToKernel(model_id, message); } async deserialize_state( @@ -467,6 +494,16 @@ export class WidgetManager extends base.ManagerBase<HTMLElement> { // Start listening to model changes. model.on("change", this.handle_model_change.bind(this)); + + model.on("msg:custom", (message) => { + this.handle_custom_message_from_model_to_kernel(model_id, message); + }); + + // DEBUG For low level debugging, we can listen to all events from the model: + // model.on("all", (...evt) => { + // console.log("received event from model", evt); + // }); + this.setWidgetModelIdState(model_id, ""); // console.log("create_new-model - FINISHED", { model_id, serialized_state }); } @@ -482,6 +519,7 @@ export class WidgetManager extends base.ManagerBase<HTMLElement> { _data?: any, _metadata?: any ): Promise<Comm> { + console.log("TODO: _create_comm"); const comm = new Comm( target_name, model_id, @@ -495,7 +533,7 @@ export class WidgetManager extends base.ManagerBase<HTMLElement> { model_id: string, data: any ): string { - // console.log("TODO: process_comm_message_from_browser", model_id, data); + console.log("TODO: process_comm_message_from_browser", model_id, data); if (data == null) { throw Error("data must not be null"); } @@ -586,8 +624,7 @@ export class WidgetManager extends base.ManagerBase<HTMLElement> { // NOTE: I completely rewrote the entire k3d widget interface... module = await import("./k3d"); } else if (moduleName === "jupyter-matplotlib") { - //module = await import("jupyter-matplotlib"); - throw Error(`custom widgets: ${moduleName} not installed`); + module = await import("jupyter-matplotlib"); } else if (moduleName === "jupyter-threejs") { //module = await import("jupyter-threejs"); throw Error(`custom widgets: ${moduleName} not installed`); diff --git a/src/packages/frontend/package-lock.json b/src/packages/frontend/package-lock.json index 48d4e96485..213f6ef45e 100644 --- a/src/packages/frontend/package-lock.json +++ b/src/packages/frontend/package-lock.json @@ -21,11 +21,11 @@ "@ant-design/icons": "^4.7.0", "@cocalc/assets": "^1.7.1", "@cocalc/cdn": "^1.12.0", - "@cocalc/hub": "^1.75.0", + "@cocalc/hub": "^1.76.3", "@cocalc/local-storage-lru": "^2.1.1", "@cocalc/project": "^1.27.4", "@cocalc/sync": "^0.8.2", - "@cocalc/util": "^1.53.0", + "@cocalc/util": "^1.55.0", "@jupyter-widgets/base": "^4.1.0", "@jupyter-widgets/controls": "^3.1.0", "@jupyter-widgets/output": "^4.1.0", @@ -74,6 +74,7 @@ "js-cookie": "^2.2.1", "json-stable-stringify": "^1.0.1", "jsonic": "^1.0.1", + "jupyter-matplotlib": "^0.11.1", "k3d": "^2.14.1", "katex": "^0.15.0", "langs": "^2.0.0", @@ -197,7 +198,7 @@ }, "../hub": { "name": "@cocalc/hub", - "version": "1.75.0", + "version": "1.76.3", "license": "SEE LICENSE.md", "workspaces": [ "./", @@ -216,10 +217,10 @@ "@cocalc/backend": "^1.17.3", "@cocalc/cdn": "^1.12.0", "@cocalc/database": "^0.24.0", - "@cocalc/frontend": "^1.69.0", - "@cocalc/next": "^0.61.0", - "@cocalc/server": "^0.33.0", - "@cocalc/static": "^1.103.0", + "@cocalc/frontend": "^1.70.3", + "@cocalc/next": "^0.62.2", + "@cocalc/server": "^0.33.1", + "@cocalc/static": "^1.104.2", "@cocalc/util": "^1.53.0", "@passport-next/passport-google-oauth2": "^1.0.0", "@passport-next/passport-oauth2": "^2.1.1", @@ -426,7 +427,7 @@ }, "../util": { "name": "@cocalc/util", - "version": "1.53.0", + "version": "1.55.0", "license": "SEE LICENSE.md", "workspaces": [ "." @@ -6803,6 +6804,21 @@ "verror": "1.10.0" } }, + "node_modules/jupyter-matplotlib": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/jupyter-matplotlib/-/jupyter-matplotlib-0.11.1.tgz", + "integrity": "sha512-grurKdGmbIz8i15PlsXuRsoPJIaWO6mAqCJ/775IlVTWpDvY8vor5uzD23U8ODSRUOIkDJJk5eH5tGHbCvkawQ==", + "dependencies": { + "@jupyter-widgets/base": "^2 || ^3 || ^4.0.0", + "@types/node": "^14.14.35", + "lodash": "^4.17.21" + } + }, + "node_modules/jupyter-matplotlib/node_modules/@types/node": { + "version": "14.18.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.21.tgz", + "integrity": "sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q==" + }, "node_modules/k3d": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/k3d/-/k3d-2.14.1.tgz", @@ -13382,6 +13398,7 @@ "js-cookie": "^2.2.1", "json-stable-stringify": "^1.0.1", "jsonic": "^1.0.1", + "jupyter-matplotlib": "*", "k3d": "^2.14.1", "katex": "^0.15.0", "langs": "^2.0.0", @@ -13424,7 +13441,7 @@ "react-redux": "^7.2.0", "react-sortable-hoc": "^2.0.0", "react-timeago": "^6.2.1", - "react-virtualized-auto-sizer": "1.0.6", + "react-virtualized-auto-sizer": "^1.0.6", "react-virtuoso": "^2.13.3", "redux": "^4.0.1", "requirejs": "^2.3.6", @@ -14655,10 +14672,10 @@ "@cocalc/backend": "^1.17.3", "@cocalc/cdn": "^1.12.0", "@cocalc/database": "^0.24.0", - "@cocalc/frontend": "^1.69.0", - "@cocalc/next": "^0.61.0", - "@cocalc/server": "^0.33.0", - "@cocalc/static": "^1.103.0", + "@cocalc/frontend": "^1.70.3", + "@cocalc/next": "^0.62.2", + "@cocalc/server": "^0.33.1", + "@cocalc/static": "^1.104.2", "@cocalc/util": "^1.53.0", "@passport-next/passport-google-oauth2": "^1.0.0", "@passport-next/passport-oauth2": "^2.1.1", @@ -18490,6 +18507,23 @@ "verror": "1.10.0" } }, + "jupyter-matplotlib": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/jupyter-matplotlib/-/jupyter-matplotlib-0.11.1.tgz", + "integrity": "sha512-grurKdGmbIz8i15PlsXuRsoPJIaWO6mAqCJ/775IlVTWpDvY8vor5uzD23U8ODSRUOIkDJJk5eH5tGHbCvkawQ==", + "requires": { + "@jupyter-widgets/base": "^2 || ^3 || ^4.0.0", + "@types/node": "^14.14.35", + "lodash": "^4.17.21" + }, + "dependencies": { + "@types/node": { + "version": "14.18.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.21.tgz", + "integrity": "sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q==" + } + } + }, "k3d": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/k3d/-/k3d-2.14.1.tgz", @@ -22564,10 +22598,10 @@ "@cocalc/backend": "^1.17.3", "@cocalc/cdn": "^1.12.0", "@cocalc/database": "^0.24.0", - "@cocalc/frontend": "^1.69.0", - "@cocalc/next": "^0.61.0", - "@cocalc/server": "^0.33.0", - "@cocalc/static": "^1.103.0", + "@cocalc/frontend": "^1.70.3", + "@cocalc/next": "^0.62.2", + "@cocalc/server": "^0.33.1", + "@cocalc/static": "^1.104.2", "@cocalc/util": "^1.53.0", "@passport-next/passport-google-oauth2": "^1.0.0", "@passport-next/passport-oauth2": "^2.1.1", @@ -26399,6 +26433,23 @@ "verror": "1.10.0" } }, + "jupyter-matplotlib": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/jupyter-matplotlib/-/jupyter-matplotlib-0.11.1.tgz", + "integrity": "sha512-grurKdGmbIz8i15PlsXuRsoPJIaWO6mAqCJ/775IlVTWpDvY8vor5uzD23U8ODSRUOIkDJJk5eH5tGHbCvkawQ==", + "requires": { + "@jupyter-widgets/base": "^2 || ^3 || ^4.0.0", + "@types/node": "^14.14.35", + "lodash": "^4.17.21" + }, + "dependencies": { + "@types/node": { + "version": "14.18.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.21.tgz", + "integrity": "sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q==" + } + } + }, "k3d": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/k3d/-/k3d-2.14.1.tgz", diff --git a/src/packages/frontend/package.json b/src/packages/frontend/package.json index 91a505b94d..0f851af27c 100644 --- a/src/packages/frontend/package.json +++ b/src/packages/frontend/package.json @@ -96,6 +96,7 @@ "js-cookie": "^2.2.1", "json-stable-stringify": "^1.0.1", "jsonic": "^1.0.1", + "jupyter-matplotlib": "^0.11.1", "k3d": "^2.14.1", "katex": "^0.15.0", "langs": "^2.0.0", @@ -207,4 +208,4 @@ "bugs": { "url": "https://github.com/sagemathinc/cocalc/issues" } -} \ No newline at end of file +} diff --git a/src/packages/project/client.coffee b/src/packages/project/client.coffee index 7eef55bcd3..19589ac1b6 100644 --- a/src/packages/project/client.coffee +++ b/src/packages/project/client.coffee @@ -102,7 +102,7 @@ class exports.Client extends EventEmitter kucalc.init(@) # use to define a logging function that is cleanly used internally - dbg: (f, trunc=1000) => + dbg: (f, trunc=10000) => if DEBUG and @_winston return (m...) => switch m.length diff --git a/src/packages/project/package.json b/src/packages/project/package.json index b6b85fd8f9..94cc6b707c 100644 --- a/src/packages/project/package.json +++ b/src/packages/project/package.json @@ -91,7 +91,8 @@ }, "scripts": { "start": "NODE_OPTIONS='--trace-warnings --unhandled-rejections=strict --enable-source-maps' npx cocalc-project", - "build": "npx tsc && npx coffee -m -c -o dist/ .", + "build": "npx tsc && npm run coffee", + "coffee": "npx coffee -m -c -o dist/ .", "tsc": "npx tsc --watch --pretty --preserveWatchOutput" }, "author": "SageMath, Inc.", diff --git a/src/packages/static/webpack.config.js b/src/packages/static/webpack.config.js index 26dbd8ce94..d5e6472ca8 100644 --- a/src/packages/static/webpack.config.js +++ b/src/packages/static/webpack.config.js @@ -159,6 +159,12 @@ module.exports = { module: { rules: require("./src/module-rules")(PRODMODE), }, + ignoreWarnings: [ + { + // The .map files are missing from jupyter-matplotlib, which causes several warnings. + module: /.*jupyter-matplotlib\/.*/, + }, + ], resolve: { alias: { // @cocalc/frontend alias so we can write `require("@cocalc/frontend/...")` diff --git a/src/packages/sync/editor/generic/ipywidgets-state.ts b/src/packages/sync/editor/generic/ipywidgets-state.ts index 1af302826c..ea600a4a09 100644 --- a/src/packages/sync/editor/generic/ipywidgets-state.ts +++ b/src/packages/sync/editor/generic/ipywidgets-state.ts @@ -28,6 +28,13 @@ type State = "init" | "ready" | "closed"; type Value = { [key: string]: any }; +type TableType = + | "value" + | "state" + | "buffers" + | "message" // message to browser + | "message_to_kernel"; + // When there is no activity for this much time, them we // do some garbage collection. This is only done in the // backend project, and not by frontend browser clients. @@ -326,7 +333,7 @@ export class IpywidgetsState extends EventEmitter { // Do any setting of the underlying table through this function. public set( model_id: string, - type: "value" | "state" | "buffers" | "message", + type: TableType, data: any, fire_change_event: boolean = true ): void { @@ -350,7 +357,7 @@ export class IpywidgetsState extends EventEmitter { // already set, but overwrite // when they change. merge = "deep"; - } else if (type == "message") { + } else if (type == "message" || type == 'message_to_kernel') { merge = "none"; } else { merge = "deep"; @@ -770,6 +777,14 @@ export class IpywidgetsState extends EventEmitter { this.set(model_id, "message", {}, fire_change_event); } + public sendMessageToKernel(model_id: string, message: object) { + this.set(model_id, "message_to_kernel", message, true); + } + + public getLastMessageToKernel(model_id): object { + return this.get(model_id, "message_to_kernel")?.toJS(); + } + public get_message(model_id: string) { return this.get(model_id, "message")?.toJS(); }