From cd873764196a92868f7b47d0e1dcd55c8e33f5c7 Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Fri, 7 Jul 2023 13:35:56 +0200 Subject: [PATCH 01/36] add gridstack.js to create app dashboard pages --- assets/css/js_interop.css | 10 +++ assets/js/hooks/gridstack.js | 18 +++++ assets/js/hooks/index.js | 2 + assets/package-lock.json | 21 ++++++ assets/package.json | 1 + lib/livebook_web/live/session_live.ex | 70 +++++++++++++++++++ .../live/session_live/app_info_component.ex | 6 ++ .../session_live/app_settings_component.ex | 4 +- 8 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 assets/js/hooks/gridstack.js diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index a056fb7dbfc..f225fbdd21d 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -269,6 +269,16 @@ solely client-side operations. @apply text-gray-50 bg-gray-700; } +[data-el-session][data-js-side-panel-content="app-info"] + [data-el-notebook] { + @apply hidden; + } + +[data-el-session]:not([data-js-side-panel-content="app-info"]) + [data-el-app-dashboard] { + @apply hidden; + } + [data-el-session][data-js-side-panel-content="app-info"] [data-el-app-indicator] { @apply border-gray-700; diff --git a/assets/js/hooks/gridstack.js b/assets/js/hooks/gridstack.js new file mode 100644 index 00000000000..002d112e601 --- /dev/null +++ b/assets/js/hooks/gridstack.js @@ -0,0 +1,18 @@ +import "gridstack/dist/gridstack.min.css"; +import { GridStack } from "gridstack"; + +/** + * A hook for creating app dashboard. + */ +const Gridstack = { + mounted() { + const options = { + acceptWidgets: true, + float: true + }; + this.grid = GridStack.init(options, ".grid-stack"); + GridStack.setupDragIn("div[data-el-side-panel] .grid-stack-item", { appendTo: "body" }); + }, +}; + +export default Gridstack; diff --git a/assets/js/hooks/index.js b/assets/js/hooks/index.js index f960ce98e75..6292f1d9b06 100644 --- a/assets/js/hooks/index.js +++ b/assets/js/hooks/index.js @@ -6,6 +6,7 @@ import Dropzone from "./dropzone"; import EditorSettings from "./editor_settings"; import EmojiPicker from "./emoji_picker"; import FocusOnUpdate from "./focus_on_update"; +import Gridstack from "./gridstack"; import Headline from "./headline"; import Highlight from "./highlight"; import ImageInput from "./image_input"; @@ -31,6 +32,7 @@ export default { EditorSettings, EmojiPicker, FocusOnUpdate, + Gridstack, Headline, Highlight, ImageInput, diff --git a/assets/package-lock.json b/assets/package-lock.json index 1c84d3dbf2a..f6b56f15158 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -10,6 +10,7 @@ "@fontsource/red-hat-text": "^5.0.1", "@picmo/popup-picker": "^5.7.6", "crypto-js": "^4.0.0", + "gridstack": "^8.3.0", "hast-util-to-text": "^3.1.1", "hyperlist": "^1.0.0", "jest": "^29.1.2", @@ -4783,6 +4784,21 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, + "node_modules/gridstack": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/gridstack/-/gridstack-8.3.0.tgz", + "integrity": "sha512-RcL2xskAYKOpakvpSwHdKheG7C7YgNY7777C5m+T1JMjSgcmEc3qPBM573l0NuyjMz4Errx1/3p+rMgUfF4+mw==", + "funding": [ + { + "type": "paypal", + "url": "https://www.paypal.me/alaind831" + }, + { + "type": "venmo", + "url": "https://www.venmo.com/adumesny" + } + ] + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -13177,6 +13193,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, + "gridstack": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/gridstack/-/gridstack-8.3.0.tgz", + "integrity": "sha512-RcL2xskAYKOpakvpSwHdKheG7C7YgNY7777C5m+T1JMjSgcmEc3qPBM573l0NuyjMz4Errx1/3p+rMgUfF4+mw==" + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", diff --git a/assets/package.json b/assets/package.json index 8946d2f493e..9eea3687253 100644 --- a/assets/package.json +++ b/assets/package.json @@ -14,6 +14,7 @@ "@fontsource/red-hat-text": "^5.0.1", "@picmo/popup-picker": "^5.7.6", "crypto-js": "^4.0.0", + "gridstack": "^8.3.0", "hast-util-to-text": "^3.1.1", "hyperlist": "^1.0.0", "jest": "^29.1.2", diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index 2bdc3c9ae6a..c7878527fdc 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -231,6 +231,7 @@ defmodule LivebookWeb.SessionLive do session={@session} settings={@data_view.app_settings} app={@app} + output_views={@data_view.output_views} deployed_app_slug={@data_view.deployed_app_slug} /> @@ -238,6 +239,13 @@ defmodule LivebookWeb.SessionLive do <.runtime_info data_view={@data_view} session={@session} /> +
+
+
+
Item 1
+
+
+
+ <% else %> + <% end %> <% end %> diff --git a/lib/livebook_web/live/session_live/app_settings_component.ex b/lib/livebook_web/live/session_live/app_settings_component.ex index 1b11edf8ba9..52409c85cea 100644 --- a/lib/livebook_web/live/session_live/app_settings_component.ex +++ b/lib/livebook_web/live/session_live/app_settings_component.ex @@ -121,7 +121,7 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do +
+
+
+ """ + end + def render(assigns) when assigns.app_authenticated? do ~H"""
@@ -202,6 +212,12 @@ defmodule LivebookWeb.AppSessionLive do {:noreply, socket} end + def handle_event("load_grid", _params, socket) do + IO.inspect(socket.assigns.data_view.output_layout) + socket = push_event(socket, "load_grid", socket.assigns.data_view.output_layout) + {:noreply, socket} + end + @impl true def handle_info({:operation, operation}, socket) do {:noreply, handle_operation(socket, operation)} @@ -300,6 +316,8 @@ defmodule LivebookWeb.AppSessionLive do cell_id: cell_id } ), + output_layout: data.notebook.app_settings.output_layout, + output_type: data.notebook.app_settings.output_type, app_status: data.app_data.status, show_source: data.notebook.app_settings.show_source, slug: data.notebook.app_settings.slug, @@ -321,6 +339,7 @@ defmodule LivebookWeb.AppSessionLive do end defp filter_outputs(outputs, :all), do: outputs + defp filter_outputs(outputs, :layout), do: outputs defp filter_outputs(outputs, :rich), do: rich_outputs(outputs) defp rich_outputs(outputs) do diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index c7878527fdc..fd63334ff20 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -231,7 +231,7 @@ defmodule LivebookWeb.SessionLive do session={@session} settings={@data_view.app_settings} app={@app} - output_views={@data_view.output_views} + output_blocks={@data_view.output_blocks} deployed_app_slug={@data_view.deployed_app_slug} />
@@ -1479,6 +1479,14 @@ defmodule LivebookWeb.SessionLive do {:noreply, socket} end + def handle_event("saved_grid_layout", data, socket) do + # TODO: tidy up + socket = put_in(socket.private.data.notebook.app_settings.output_layout, data) + + Session.set_app_settings(socket.assigns.session.pid, socket.private.data.notebook.app_settings) + {:noreply, socket} + end + @impl true def handle_info({:operation, operation}, socket) do {:noreply, handle_operation(socket, operation)} @@ -2252,15 +2260,7 @@ defmodule LivebookWeb.SessionLive do # have to traverse the whole template tree and no diff is sent to the client. defp data_to_view(data) do %{ - output_views: - for( - {cell_id, output} <- visible_outputs(data.notebook), - do: %{ - output: output, - input_values: input_values_for_output(output, data), - cell_id: cell_id - } - ), + output_blocks: output_blocks(data.notebook), file: data.file, persist_outputs: data.notebook.persist_outputs, autosave_interval_s: data.notebook.autosave_interval_s, @@ -2554,56 +2554,11 @@ defmodule LivebookWeb.SessionLive do defp app_status_color(%{execution: :error}), do: "bg-red-400" defp app_status_color(%{execution: :interrupted}), do: "bg-gray-400" - defp input_values_for_output(output, data) do - input_ids = for attrs <- Cell.find_inputs_in_output(output), do: attrs.id - Map.take(data.input_values, input_ids) - end - - defp visible_outputs(notebook) do + defp output_blocks(notebook) do for section <- Enum.reverse(notebook.sections), cell <- Enum.reverse(section.cells), + output_id <- Enum.map(cell.outputs, &(elem(&1, 0))), Cell.evaluable?(cell), - output <- filter_outputs(cell.outputs, notebook.app_settings.output_type), - do: {cell.id, output} - end - - defp filter_outputs(outputs, :all), do: outputs - defp filter_outputs(outputs, :rich), do: rich_outputs(outputs) - - defp rich_outputs(outputs) do - for output <- outputs, output = filter_output(output), do: output - end - - defp filter_output({idx, output}) - when elem(output, 0) in [:plain_text, :markdown, :image, :js, :control, :input], - do: {idx, output} - - defp filter_output({idx, {:tabs, outputs, metadata}}) do - outputs_with_labels = - for {output, label} <- Enum.zip(outputs, metadata.labels), - output = filter_output(output), - do: {output, label} - - {outputs, labels} = Enum.unzip(outputs_with_labels) - - {idx, {:tabs, outputs, %{metadata | labels: labels}}} - end - - defp filter_output({idx, {:grid, outputs, metadata}}) do - outputs = rich_outputs(outputs) - - if outputs != [] do - {idx, {:grid, outputs, metadata}} - end - end - - defp filter_output({idx, {:frame, outputs, metadata}}) do - outputs = rich_outputs(outputs) - {idx, {:frame, outputs, metadata}} + do: "#{cell.id}_#{output_id}" end - - defp filter_output({idx, {:error, _message, {:interrupt, _, _}} = output}), - do: {idx, output} - - defp filter_output(_output), do: nil end diff --git a/lib/livebook_web/live/session_live/app_info_component.ex b/lib/livebook_web/live/session_live/app_info_component.ex index ff9945dfac7..ac3f46ed7ab 100644 --- a/lib/livebook_web/live/session_live/app_info_component.ex +++ b/lib/livebook_web/live/session_live/app_info_component.ex @@ -146,8 +146,8 @@ defmodule LivebookWeb.SessionLive.AppInfoComponent do
<% else %> <% end %> diff --git a/lib/livebook_web/live/session_live/app_settings_component.ex b/lib/livebook_web/live/session_live/app_settings_component.ex index 52409c85cea..9a9cce0c099 100644 --- a/lib/livebook_web/live/session_live/app_settings_component.ex +++ b/lib/livebook_web/live/session_live/app_settings_component.ex @@ -80,7 +80,7 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do <.checkbox_field field={f[:output_type]} label="Only render rich outputs" - checked_value="rich" + checked_value="layout" unchecked_value="all" help={ ~S''' From c83c4e6b1257babb939044d85b749a6dcc687fec Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Sat, 8 Jul 2023 17:20:07 +0200 Subject: [PATCH 03/36] WIP --- assets/js/hooks/gridstack.js | 37 ++++++++++--- lib/livebook_web/live/gridstack_component.ex | 53 +++++++++++++++++++ lib/livebook_web/live/session_live.ex | 35 ++++++++---- .../live/session_live/app_info_component.ex | 14 +++-- .../session_live/app_settings_component.ex | 1 + 5 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 lib/livebook_web/live/gridstack_component.ex diff --git a/assets/js/hooks/gridstack.js b/assets/js/hooks/gridstack.js index 9d41f5ef3a4..38a6bfc14c0 100644 --- a/assets/js/hooks/gridstack.js +++ b/assets/js/hooks/gridstack.js @@ -1,4 +1,4 @@ -import { getAttributeOrDefault, getAttributeOrThrow } from "../lib/attribute"; +import { getAttributeOrThrow, parseInteger } from "../lib/attribute"; import "gridstack/dist/gridstack.min.css"; import { GridStack } from "gridstack"; @@ -7,30 +7,51 @@ import { GridStack } from "gridstack"; */ const Gridstack = { mounted() { + const self = this; + this.props = this.getProps(); + + console.log(this.props); const options = { acceptWidgets: true, - float: true + styleInHead: true, + float: true, }; - this.grid = GridStack.init(options, ".grid-stack"); - GridStack.setupDragIn("div[data-el-side-panel] .grid-stack-item", { appendTo: "body" }); - let parent = this; + this.grid = GridStack.init(options, this.el); this.grid.on("change", function(event, items) { - parent.saveLayout(); + let new_items = items.reduce((acc, item) => { + acc[item.id] = { + x_pos: item.x, + y_pos: item.y, + width: item.w, + height: item.h + }; + return acc; + }, {}); + self.pushEventTo(self.props.phxTarget, "items_changed", new_items); }); + this.handleEvent( `load_grid`, ({ layout: layout }) => { if (layout) { console.log(layout); - parent.loadLayout(layout); + self.loadLayout(layout); } } ); }, + updated() { + this.props = this.getProps(); + }, + getProps() { + return { + phxTarget: getAttributeOrThrow(this.el, "data-phx-target", parseInteger), + }; + }, saveLayout() { const layout = this.grid.save(); - console.log("Test"); + console.log("Layout saved"); console.log(layout); this.pushEvent("saved_grid_layout", { layout: layout }); }, diff --git a/lib/livebook_web/live/gridstack_component.ex b/lib/livebook_web/live/gridstack_component.ex new file mode 100644 index 00000000000..f3dfbef71bf --- /dev/null +++ b/lib/livebook_web/live/gridstack_component.ex @@ -0,0 +1,53 @@ +defmodule LivebookWeb.GridstackComponent do + use LivebookWeb, :live_component + + # The component expects: + # + # * `:grid_id` - id of the grid + # * `:columns` - the number of columns for the grid + + @impl true + def update(assigns, socket) do + socket = + socket + |> assign( + id: assigns.id, + columns: assigns.columns, + cell_height: assigns.columns || "auto" + ) + |> stream(:grid_components, assigns.grid_components) + {:ok, socket} + end + + @impl true + def render(assigns) do + ~H""" +
+
+
<%= item.content %>
+
+
+ """ + end + + @impl true + def handle_event("items_changed", params, socket) do + params = + for {key, %{"x_pos" => x_pos, "y_pos" => y_pos, "width" => width, "height" => height}} <- + params, + into: %{} do + {key, %{x_pos: x_pos, y_pos: y_pos, width: width, height: height}} + end + + IO.inspect(params, label: "params") + {:noreply, socket} + end +end diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index fd63334ff20..685a933a838 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -231,7 +231,7 @@ defmodule LivebookWeb.SessionLive do session={@session} settings={@data_view.app_settings} app={@app} - output_blocks={@data_view.output_blocks} + output_blocks={@data_view.output_blocks} deployed_app_slug={@data_view.deployed_app_slug} /> @@ -239,12 +239,19 @@ defmodule LivebookWeb.SessionLive do <.runtime_info data_view={@data_view} session={@session} /> -
-
-
-
Item 1
-
-
+
+ <.live_component + module={LivebookWeb.GridstackComponent} + id="dashboard" + columns={12} + grid_components={[ + %{id: "test", x_pos: 4, y_pos: 1, width: 2, height: 1, content: "HELLO"} + ]} + />
@@ -1483,7 +1490,11 @@ defmodule LivebookWeb.SessionLive do # TODO: tidy up socket = put_in(socket.private.data.notebook.app_settings.output_layout, data) - Session.set_app_settings(socket.assigns.session.pid, socket.private.data.notebook.app_settings) + Session.set_app_settings( + socket.assigns.session.pid, + socket.private.data.notebook.app_settings + ) + {:noreply, socket} end @@ -2557,8 +2568,10 @@ defmodule LivebookWeb.SessionLive do defp output_blocks(notebook) do for section <- Enum.reverse(notebook.sections), cell <- Enum.reverse(section.cells), - output_id <- Enum.map(cell.outputs, &(elem(&1, 0))), - Cell.evaluable?(cell), - do: "#{cell.id}_#{output_id}" + output_id <- Enum.map(cell.outputs, &elem(&1, 0)), + Cell.evaluable?(cell) do + %{id: output_id, x_pos: 0, y_pos: output_id, width: 1, height: 1, content: "#{cell.id}_#{output_id}"} + end + |> Enum.reverse() end end diff --git a/lib/livebook_web/live/session_live/app_info_component.ex b/lib/livebook_web/live/session_live/app_info_component.ex index ac3f46ed7ab..b24076e7ff4 100644 --- a/lib/livebook_web/live/session_live/app_info_component.ex +++ b/lib/livebook_web/live/session_live/app_info_component.ex @@ -145,11 +145,15 @@ defmodule LivebookWeb.SessionLive.AppInfoComponent do
<% else %> - +
+ <.live_component + module={LivebookWeb.GridstackComponent} + id="output_blocks" + columns={1} + grid_components={@output_blocks} + + /> +
<% end %> <% end %> diff --git a/lib/livebook_web/live/session_live/app_settings_component.ex b/lib/livebook_web/live/session_live/app_settings_component.ex index 9a9cce0c099..f7a73c74ada 100644 --- a/lib/livebook_web/live/session_live/app_settings_component.ex +++ b/lib/livebook_web/live/session_live/app_settings_component.ex @@ -135,6 +135,7 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do """ end + @impl true def handle_event("validate", %{"_target" => ["reset"]}, socket) do From 2526763c9d4885a7407a564dfc42281028d9831f Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Mon, 10 Jul 2023 13:34:56 +0200 Subject: [PATCH 04/36] WIP --- assets/css/js_interop.css | 2 +- assets/js/hooks/gridstack.js | 58 ++++++++++++++---- assets/js/hooks/gridstack_static.js | 35 +++++++++++ assets/js/hooks/index.js | 2 + assets/js/hooks/js_view.js | 3 + lib/livebook/notebook/app_settings.ex | 4 +- lib/livebook_web/helpers.ex | 44 +++++++++++++ lib/livebook_web/live/app_session_live.ex | 61 +++---------------- lib/livebook_web/live/gridstack_component.ex | 55 +++++++++++------ lib/livebook_web/live/session_live.ex | 49 +++++++++------ .../live/session_live/app_info_component.ex | 10 --- .../session_live/app_settings_component.ex | 2 +- 12 files changed, 209 insertions(+), 116 deletions(-) create mode 100644 assets/js/hooks/gridstack_static.js diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index f225fbdd21d..8a627702faa 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -270,7 +270,7 @@ solely client-side operations. } [data-el-session][data-js-side-panel-content="app-info"] - [data-el-notebook] { + [data-el-notebook-content] { @apply hidden; } diff --git a/assets/js/hooks/gridstack.js b/assets/js/hooks/gridstack.js index 38a6bfc14c0..ba87870c9e1 100644 --- a/assets/js/hooks/gridstack.js +++ b/assets/js/hooks/gridstack.js @@ -1,4 +1,5 @@ import { getAttributeOrThrow, parseInteger } from "../lib/attribute"; +import { globalPubSub } from "../lib/pub_sub"; import "gridstack/dist/gridstack.min.css"; import { GridStack } from "gridstack"; @@ -7,16 +8,27 @@ import { GridStack } from "gridstack"; */ const Gridstack = { mounted() { + console.log("Gridstack mounted"); const self = this; - this.props = this.getProps(); - console.log(this.props); - const options = { - acceptWidgets: true, + this.visible = false; + + this.options = { + //acceptWidgets: true, + //removeable: true, styleInHead: true, float: true, + resizable: { handles: "all" }, + margin: 0, + cellHeight: "4rem", }; - this.grid = GridStack.init(options, this.el); + + this.grid = GridStack.init(this.options, this.el); + + document.querySelector("[data-el-app-info-toggle]").addEventListener("click", function(event) { + self.visible = !self.visible; + self.toggleMountOutputs(); + }); this.grid.on("change", function(event, items) { let new_items = items.reduce((acc, item) => { @@ -28,7 +40,8 @@ const Gridstack = { }; return acc; }, {}); - self.pushEventTo(self.props.phxTarget, "items_changed", new_items); + self.pushEvent("items_changed", new_items); + globalPubSub.broadcast("js_views", { type: "reposition" }); }); this.handleEvent( @@ -40,14 +53,37 @@ const Gridstack = { } } ); + + this.handleEvent("save_layout", function() { + self.saveLayout(); + }); }, updated() { - this.props = this.getProps(); + console.log("Gridstack updated"); }, - getProps() { - return { - phxTarget: getAttributeOrThrow(this.el, "data-phx-target", parseInteger), - }; + toggleMountOutputs() { + if (this.visible) { + const outputs = document.querySelectorAll("[data-el-output]"); + console.log(outputs); + for (let output of outputs) { + // safe origin location for unmounting + output._origin || (output._origin = output.parentNode); + const output_id = Number(output.id.split("-")[2]); + const item_content = this.el.querySelector(`[gs-id="${output_id}"] .grid-stack-item-content`); + if (item_content) { + item_content.prepend(output); + } + } + } else { + const output_containers = this.el.querySelectorAll(`.grid-stack-item .grid-stack-item-content`); + for (let container of output_containers) { + const output = container.firstChild; + if (output) { + output._origin && output._origin.prepend(output); + } + } + } + globalPubSub.broadcast("js_views", { type: "reposition" }); }, saveLayout() { const layout = this.grid.save(); diff --git a/assets/js/hooks/gridstack_static.js b/assets/js/hooks/gridstack_static.js new file mode 100644 index 00000000000..7829432b389 --- /dev/null +++ b/assets/js/hooks/gridstack_static.js @@ -0,0 +1,35 @@ +import "gridstack/dist/gridstack.min.css"; +import { GridStack } from "gridstack"; + +/** + * A hook for creating app dashboard. + */ +const GridstackStatic = { + mounted() { + const self = this; + + const options = { + staticGrid: true, + float: true, + margin: 0, + cellHeight: "4rem", + }; + + this.grid = GridStack.init(options, this.el); + + this.handleEvent( + `load_grid`, + ({ layout: layout }) => { + if (layout) { + console.log(layout); + self.loadLayout(layout); + } + } + ); + }, + loadLayout(layout) { + this.grid.load(layout); + } +}; + +export default GridstackStatic; diff --git a/assets/js/hooks/index.js b/assets/js/hooks/index.js index 6292f1d9b06..3e524376165 100644 --- a/assets/js/hooks/index.js +++ b/assets/js/hooks/index.js @@ -7,6 +7,7 @@ import EditorSettings from "./editor_settings"; import EmojiPicker from "./emoji_picker"; import FocusOnUpdate from "./focus_on_update"; import Gridstack from "./gridstack"; +import GridstackStatic from "./gridstack_static"; import Headline from "./headline"; import Highlight from "./highlight"; import ImageInput from "./image_input"; @@ -33,6 +34,7 @@ export default { EmojiPicker, FocusOnUpdate, Gridstack, + GridstackStatic, Headline, Highlight, ImageInput, diff --git a/assets/js/hooks/js_view.js b/assets/js/hooks/js_view.js index bd4f47cae3f..db3b262da69 100644 --- a/assets/js/hooks/js_view.js +++ b/assets/js/hooks/js_view.js @@ -308,6 +308,9 @@ const JSView = { repositionIframe() { const { iframe, iframePlaceholder } = this; const notebookEl = document.querySelector(`[data-el-notebook]`); + console.log(notebookEl, "NOTEOBOK"); + console.log(iframePlaceholder, "Placeholder"); + console.log(iframe, "Iframe"); if (isElementHidden(iframePlaceholder)) { // When the placeholder is hidden, we hide the iframe as well diff --git a/lib/livebook/notebook/app_settings.ex b/lib/livebook/notebook/app_settings.ex index 3d4b45b4dfa..a96956c069a 100644 --- a/lib/livebook/notebook/app_settings.ex +++ b/lib/livebook/notebook/app_settings.ex @@ -19,7 +19,7 @@ defmodule Livebook.Notebook.AppSettings do } @type access_type :: :public | :protected - @type output_type :: :all | :rich | :layout + @type output_type :: :all | :rich | :dashboard @primary_key false embedded_schema do @@ -31,7 +31,7 @@ defmodule Livebook.Notebook.AppSettings do field :access_type, Ecto.Enum, values: [:public, :protected] field :password, :string field :show_source, :boolean - field :output_type, Ecto.Enum, values: [:all, :rich, :layout] + field :output_type, Ecto.Enum, values: [:all, :rich, :dashboard] field :output_layout, :map end diff --git a/lib/livebook_web/helpers.ex b/lib/livebook_web/helpers.ex index 80738b73263..c9ff8b6b4f2 100644 --- a/lib/livebook_web/helpers.ex +++ b/lib/livebook_web/helpers.ex @@ -82,4 +82,48 @@ defmodule LivebookWeb.Helpers do def format_datetime_relatively(date) do date |> DateTime.to_naive() |> Livebook.Utils.Time.time_ago_in_words() end + + @doc """ + """ + @spec filter_outputs(list(any()), atom()) :: list(any()) + def filter_outputs(outputs, :all), do: outputs + def filter_outputs(outputs, :rich), do: rich_outputs(outputs) + def filter_outputs(outputs, :dashboard), do: rich_outputs(outputs) + + defp rich_outputs(outputs) do + for output <- outputs, output = filter_output(output), do: output + end + + defp filter_output({idx, output}) + when elem(output, 0) in [:plain_text, :markdown, :image, :js, :control, :input], + do: {idx, output} + + defp filter_output({idx, {:tabs, outputs, metadata}}) do + outputs_with_labels = + for {output, label} <- Enum.zip(outputs, metadata.labels), + output = filter_output(output), + do: {output, label} + + {outputs, labels} = Enum.unzip(outputs_with_labels) + + {idx, {:tabs, outputs, %{metadata | labels: labels}}} + end + + defp filter_output({idx, {:grid, outputs, metadata}}) do + outputs = rich_outputs(outputs) + + if outputs != [] do + {idx, {:grid, outputs, metadata}} + end + end + + defp filter_output({idx, {:frame, outputs, metadata}}) do + outputs = rich_outputs(outputs) + {idx, {:frame, outputs, metadata}} + end + + defp filter_output({idx, {:error, _message, {:interrupt, _, _}} = output}), + do: {idx, output} + + defp filter_output(_output), do: nil end diff --git a/lib/livebook_web/live/app_session_live.ex b/lib/livebook_web/live/app_session_live.ex index a154ce272ce..dd1d823f211 100644 --- a/lib/livebook_web/live/app_session_live.ex +++ b/lib/livebook_web/live/app_session_live.ex @@ -31,6 +31,8 @@ defmodule LivebookWeb.AppSessionLive do {data, nil} end + data_view = data_to_view(data) + {:ok, socket |> assign( @@ -38,9 +40,11 @@ defmodule LivebookWeb.AppSessionLive do session: session, page_title: get_page_title(data.notebook.name), client_id: client_id, - data_view: data_to_view(data) + data_view: data_view ) - |> assign_private(data: data)} + |> assign_private(data: data) + |> push_event("load_grid", data_view.output_layout) + } else {:ok, assign(socket, @@ -94,12 +98,10 @@ defmodule LivebookWeb.AppSessionLive do """ end - def render(%{data_view: %{output_type: :layout}} = assigns) when assigns.app_authenticated? do + def render(%{data_view: %{output_type: :dashboard}} = assigns) when assigns.app_authenticated? do ~H""" -
<%= inspect(@data_view.output_layout) %>
-
-
+
""" end @@ -212,12 +214,6 @@ defmodule LivebookWeb.AppSessionLive do {:noreply, socket} end - def handle_event("load_grid", _params, socket) do - IO.inspect(socket.assigns.data_view.output_layout) - socket = push_event(socket, "load_grid", socket.assigns.data_view.output_layout) - {:noreply, socket} - end - @impl true def handle_info({:operation, operation}, socket) do {:noreply, handle_operation(socket, operation)} @@ -338,47 +334,6 @@ defmodule LivebookWeb.AppSessionLive do do: {cell.id, output} end - defp filter_outputs(outputs, :all), do: outputs - defp filter_outputs(outputs, :layout), do: outputs - defp filter_outputs(outputs, :rich), do: rich_outputs(outputs) - - defp rich_outputs(outputs) do - for output <- outputs, output = filter_output(output), do: output - end - - defp filter_output({idx, output}) - when elem(output, 0) in [:plain_text, :markdown, :image, :js, :control, :input], - do: {idx, output} - - defp filter_output({idx, {:tabs, outputs, metadata}}) do - outputs_with_labels = - for {output, label} <- Enum.zip(outputs, metadata.labels), - output = filter_output(output), - do: {output, label} - - {outputs, labels} = Enum.unzip(outputs_with_labels) - - {idx, {:tabs, outputs, %{metadata | labels: labels}}} - end - - defp filter_output({idx, {:grid, outputs, metadata}}) do - outputs = rich_outputs(outputs) - - if outputs != [] do - {idx, {:grid, outputs, metadata}} - end - end - - defp filter_output({idx, {:frame, outputs, metadata}}) do - outputs = rich_outputs(outputs) - {idx, {:frame, outputs, metadata}} - end - - defp filter_output({idx, {:error, _message, {:interrupt, _, _}} = output}), - do: {idx, output} - - defp filter_output(_output), do: nil - defp show_app_status?(%{execution: :executed, lifecycle: :active}), do: false defp show_app_status?(_status), do: true end diff --git a/lib/livebook_web/live/gridstack_component.ex b/lib/livebook_web/live/gridstack_component.ex index f3dfbef71bf..85f96af9131 100644 --- a/lib/livebook_web/live/gridstack_component.ex +++ b/lib/livebook_web/live/gridstack_component.ex @@ -3,36 +3,39 @@ defmodule LivebookWeb.GridstackComponent do # The component expects: # - # * `:grid_id` - id of the grid - # * `:columns` - the number of columns for the grid + # * `:output_blocks` - TODO @impl true def update(assigns, socket) do socket = socket - |> assign( - id: assigns.id, - columns: assigns.columns, - cell_height: assigns.columns || "auto" - ) - |> stream(:grid_components, assigns.grid_components) + |> assign(output_blocks: assigns.output_blocks) {:ok, socket} end @impl true def render(assigns) do ~H""" -
+
-
<%= item.content %>
+
+
+
+
+
""" @@ -40,14 +43,28 @@ defmodule LivebookWeb.GridstackComponent do @impl true def handle_event("items_changed", params, socket) do + IO.inspect(params, label: "params") + params = for {key, %{"x_pos" => x_pos, "y_pos" => y_pos, "width" => width, "height" => height}} <- params, into: %{} do {key, %{x_pos: x_pos, y_pos: y_pos, width: width, height: height}} end - - IO.inspect(params, label: "params") {:noreply, socket} end + + @impl true + def handle_event("insert", %{"id" => id, "x" => x, "y" => y, "w" => w, "h" => h} = params, socket) do + IO.inspect(params, label: "INSERT") + IO.inspect("HELLO", label: "INSERT") + {:noreply, stream_insert(socket, :grid_components, %{id: id, x_pos: x, y_pos: y, width: w, height: h}, at: 0)} + end + + @impl true + def handle_event("delete", %{"id" => dom_id} = params , socket) do + IO.inspect("HELLO", label: "DELELTE") + IO.inspect(params, label: "DELELTE") + {:noreply, stream_delete_by_dom_id(socket, :grid_components, dom_id)} + end end diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index 685a933a838..a84f83fe1bc 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -231,7 +231,6 @@ defmodule LivebookWeb.SessionLive do session={@session} settings={@data_view.app_settings} app={@app} - output_blocks={@data_view.output_blocks} deployed_app_slug={@data_view.deployed_app_slug} />
@@ -239,20 +238,6 @@ defmodule LivebookWeb.SessionLive do <.runtime_info data_view={@data_view} session={@session} />
-
- <.live_component - module={LivebookWeb.GridstackComponent} - id="dashboard" - columns={12} - grid_components={[ - %{id: "test", x_pos: 4, y_pos: 1, width: 2, height: 1, content: "HELLO"} - ]} - /> -
+
+ <.live_component + module={LivebookWeb.GridstackComponent} + id="app-dashboard" + output_blocks={@data_view.output_blocks} + /> +
Livebook.Session.deploy_app(socket.assigns.session.pid) socket @@ -1498,6 +1491,18 @@ defmodule LivebookWeb.SessionLive do {:noreply, socket} end + def handle_event("items_changed", params, socket) do + IO.inspect(params, label: "params") + + params = + for {key, %{"x_pos" => x_pos, "y_pos" => y_pos, "width" => width, "height" => height}} <- + params, + into: %{} do + {key, %{x_pos: x_pos, y_pos: y_pos, width: width, height: height}} + end + {:noreply, socket} + end + @impl true def handle_info({:operation, operation}, socket) do {:noreply, handle_operation(socket, operation)} @@ -2271,7 +2276,6 @@ defmodule LivebookWeb.SessionLive do # have to traverse the whole template tree and no diff is sent to the client. defp data_to_view(data) do %{ - output_blocks: output_blocks(data.notebook), file: data.file, persist_outputs: data.notebook.persist_outputs, autosave_interval_s: data.notebook.autosave_interval_s, @@ -2303,6 +2307,7 @@ defmodule LivebookWeb.SessionLive do secrets: data.secrets, hub: Livebook.Hubs.fetch_hub!(data.notebook.hub_id), hub_secrets: data.hub_secrets, + output_blocks: output_blocks(data.notebook), file_entries: Enum.sort_by(data.notebook.file_entries, & &1.name), app_settings: data.notebook.app_settings, deployed_app_slug: data.deployed_app_slug @@ -2568,9 +2573,15 @@ defmodule LivebookWeb.SessionLive do defp output_blocks(notebook) do for section <- Enum.reverse(notebook.sections), cell <- Enum.reverse(section.cells), - output_id <- Enum.map(cell.outputs, &elem(&1, 0)), - Cell.evaluable?(cell) do - %{id: output_id, x_pos: 0, y_pos: output_id, width: 1, height: 1, content: "#{cell.id}_#{output_id}"} + Cell.evaluable?(cell), + output <- filter_outputs(cell.outputs, :dashboard) do + %{ + id: elem(output, 0), + x_pos: 0, + y_pos: 1, + width: 3, + height: 1 + } end |> Enum.reverse() end diff --git a/lib/livebook_web/live/session_live/app_info_component.ex b/lib/livebook_web/live/session_live/app_info_component.ex index b24076e7ff4..61e4171a14c 100644 --- a/lib/livebook_web/live/session_live/app_info_component.ex +++ b/lib/livebook_web/live/session_live/app_info_component.ex @@ -144,16 +144,6 @@ defmodule LivebookWeb.SessionLive.AppInfoComponent do
- <% else %> -
- <.live_component - module={LivebookWeb.GridstackComponent} - id="output_blocks" - columns={1} - grid_components={@output_blocks} - - /> -
<% end %> <% end %> diff --git a/lib/livebook_web/live/session_live/app_settings_component.ex b/lib/livebook_web/live/session_live/app_settings_component.ex index f7a73c74ada..b7e7d501b3b 100644 --- a/lib/livebook_web/live/session_live/app_settings_component.ex +++ b/lib/livebook_web/live/session_live/app_settings_component.ex @@ -80,7 +80,7 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do <.checkbox_field field={f[:output_type]} label="Only render rich outputs" - checked_value="layout" + checked_value="dashboard" unchecked_value="all" help={ ~S''' From a10fbdd96acd2bf83c8429b39b30044e905ddda8 Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Mon, 10 Jul 2023 20:38:36 +0200 Subject: [PATCH 05/36] WIP --- assets/js/hooks/gridstack.js | 50 +++++++-------- assets/js/hooks/gridstack_static.js | 26 +++++--- assets/js/hooks/js_view.js | 3 - lib/livebook_web/live/app_session_live.ex | 22 ++++++- lib/livebook_web/live/gridstack_component.ex | 66 +++++++++----------- lib/livebook_web/live/session_live.ex | 31 ++------- 6 files changed, 95 insertions(+), 103 deletions(-) diff --git a/assets/js/hooks/gridstack.js b/assets/js/hooks/gridstack.js index ba87870c9e1..ea52c3c141a 100644 --- a/assets/js/hooks/gridstack.js +++ b/assets/js/hooks/gridstack.js @@ -8,12 +8,13 @@ import { GridStack } from "gridstack"; */ const Gridstack = { mounted() { + this.props = this.getProps(); console.log("Gridstack mounted"); const self = this; this.visible = false; - this.options = { + const options = { //acceptWidgets: true, //removeable: true, styleInHead: true, @@ -23,7 +24,7 @@ const Gridstack = { cellHeight: "4rem", }; - this.grid = GridStack.init(this.options, this.el); + this.grid = GridStack.init(options, this.el); document.querySelector("[data-el-app-info-toggle]").addEventListener("click", function(event) { self.visible = !self.visible; @@ -31,6 +32,7 @@ const Gridstack = { }); this.grid.on("change", function(event, items) { + console.log(items); let new_items = items.reduce((acc, item) => { acc[item.id] = { x_pos: item.x, @@ -40,35 +42,37 @@ const Gridstack = { }; return acc; }, {}); - self.pushEvent("items_changed", new_items); - globalPubSub.broadcast("js_views", { type: "reposition" }); + self.pushEventTo(self.props.phxTarget, "items_changed", new_items); + self.repositionIframe(); }); - this.handleEvent( - `load_grid`, - ({ layout: layout }) => { - if (layout) { - console.log(layout); - self.loadLayout(layout); - } - } - ); + this.grid.on("drag", function(event, item) { + self.repositionIframe(); + }); this.handleEvent("save_layout", function() { - self.saveLayout(); + const layout = self.grid.save(false, false); + console.log("Layout saved"); + console.log(layout); + self.pushEventTo(self.props.phxTarget, "new_app_layout", { layout: layout }); }); }, updated() { + this.props = this.getProps(); console.log("Gridstack updated"); }, + getProps() { + return { + phxTarget: getAttributeOrThrow(this.el, "data-phx-target", parseInteger), + }; + }, toggleMountOutputs() { if (this.visible) { - const outputs = document.querySelectorAll("[data-el-output]"); - console.log(outputs); + const outputs = document.querySelectorAll("[data-el-outputs-container]"); for (let output of outputs) { // safe origin location for unmounting output._origin || (output._origin = output.parentNode); - const output_id = Number(output.id.split("-")[2]); + const output_id = output.id.split("-")[1]; const item_content = this.el.querySelector(`[gs-id="${output_id}"] .grid-stack-item-content`); if (item_content) { item_content.prepend(output); @@ -83,16 +87,10 @@ const Gridstack = { } } } - globalPubSub.broadcast("js_views", { type: "reposition" }); + this.repositionIframe(); }, - saveLayout() { - const layout = this.grid.save(); - console.log("Layout saved"); - console.log(layout); - this.pushEvent("saved_grid_layout", { layout: layout }); - }, - loadLayout(layout) { - this.grid.load(layout); + repositionIframe() { + globalPubSub.broadcast("js_views", { type: "reposition" }); } }; diff --git a/assets/js/hooks/gridstack_static.js b/assets/js/hooks/gridstack_static.js index 7829432b389..fdcf1d289f1 100644 --- a/assets/js/hooks/gridstack_static.js +++ b/assets/js/hooks/gridstack_static.js @@ -17,18 +17,26 @@ const GridstackStatic = { this.grid = GridStack.init(options, this.el); - this.handleEvent( - `load_grid`, - ({ layout: layout }) => { - if (layout) { - console.log(layout); - self.loadLayout(layout); - } + this.handleEvent("load_layout", function({ layout }) { + if (layout) { + console.log("layout", layout); + self.grid.load(layout); + self.mountOutputs(); } - ); + }); }, loadLayout(layout) { - this.grid.load(layout); + }, + mountOutputs() { + const outputs = document.querySelectorAll("[data-el-output]"); + for (let output of outputs) { + const item_content = this.el.querySelector(`[gs-id="${output.parentNode.id}"] .grid-stack-item-content`); + console.log("output", output); + console.log("content", item_content); + if (item_content) { + item_content.prepend(output); + } + } } }; diff --git a/assets/js/hooks/js_view.js b/assets/js/hooks/js_view.js index db3b262da69..bd4f47cae3f 100644 --- a/assets/js/hooks/js_view.js +++ b/assets/js/hooks/js_view.js @@ -308,9 +308,6 @@ const JSView = { repositionIframe() { const { iframe, iframePlaceholder } = this; const notebookEl = document.querySelector(`[data-el-notebook]`); - console.log(notebookEl, "NOTEOBOK"); - console.log(iframePlaceholder, "Placeholder"); - console.log(iframe, "Iframe"); if (isElementHidden(iframePlaceholder)) { // When the placeholder is hidden, we hide the iframe as well diff --git a/lib/livebook_web/live/app_session_live.ex b/lib/livebook_web/live/app_session_live.ex index dd1d823f211..bcd05b6e2ac 100644 --- a/lib/livebook_web/live/app_session_live.ex +++ b/lib/livebook_web/live/app_session_live.ex @@ -32,6 +32,7 @@ defmodule LivebookWeb.AppSessionLive do end data_view = data_to_view(data) + IO.inspect(data_view.output_layout) {:ok, socket @@ -43,7 +44,7 @@ defmodule LivebookWeb.AppSessionLive do data_view: data_view ) |> assign_private(data: data) - |> push_event("load_grid", data_view.output_layout) + |> push_event("load_layout", %{layout: data_view.output_layout}) } else {:ok, @@ -100,8 +101,22 @@ defmodule LivebookWeb.AppSessionLive do def render(%{data_view: %{output_type: :dashboard}} = assigns) when assigns.app_authenticated? do ~H""" -
-
+
+
+
+
+
+ +
+
""" end @@ -301,6 +316,7 @@ defmodule LivebookWeb.AppSessionLive do end defp data_to_view(data) do + IO.inspect(data, label: "DATA") %{ notebook_name: data.notebook.name, output_views: diff --git a/lib/livebook_web/live/gridstack_component.ex b/lib/livebook_web/live/gridstack_component.ex index 85f96af9131..0abcbda4c42 100644 --- a/lib/livebook_web/live/gridstack_component.ex +++ b/lib/livebook_web/live/gridstack_component.ex @@ -1,40 +1,34 @@ defmodule LivebookWeb.GridstackComponent do use LivebookWeb, :live_component + alias Livebook.Session + # The component expects: # # * `:output_blocks` - TODO - @impl true - def update(assigns, socket) do - socket = - socket - |> assign(output_blocks: assigns.output_blocks) - {:ok, socket} - end - @impl true def render(assigns) do ~H""" -
+
-
-
-
-
+
+
@@ -54,17 +48,17 @@ defmodule LivebookWeb.GridstackComponent do {:noreply, socket} end - @impl true - def handle_event("insert", %{"id" => id, "x" => x, "y" => y, "w" => w, "h" => h} = params, socket) do - IO.inspect(params, label: "INSERT") - IO.inspect("HELLO", label: "INSERT") - {:noreply, stream_insert(socket, :grid_components, %{id: id, x_pos: x, y_pos: y, width: w, height: h}, at: 0)} - end + def handle_event("new_app_layout", %{"layout" => layout} = data, socket) do + # TODO: tidy up + IO.inspect(data) + socket = put_in(socket.assigns.app_settings.output_layout, layout) - @impl true - def handle_event("delete", %{"id" => dom_id} = params , socket) do - IO.inspect("HELLO", label: "DELELTE") - IO.inspect(params, label: "DELELTE") - {:noreply, stream_delete_by_dom_id(socket, :grid_components, dom_id)} + Session.set_app_settings( + socket.assigns.session.pid, + socket.assigns.app_settings + ) + + {:noreply, socket} end + end diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index a84f83fe1bc..f77161e9385 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -249,11 +249,13 @@ defmodule LivebookWeb.SessionLive do runtime={@data_view.runtime} global_status={@data_view.global_status} /> -
+
<.live_component module={LivebookWeb.GridstackComponent} id="app-dashboard" output_blocks={@data_view.output_blocks} + session={@session} + app_settings={@data_view.app_settings} />
x_pos, "y_pos" => y_pos, "width" => width, "height" => height}} <- - params, - into: %{} do - {key, %{x_pos: x_pos, y_pos: y_pos, width: width, height: height}} - end - {:noreply, socket} - end - @impl true def handle_info({:operation, operation}, socket) do {:noreply, handle_operation(socket, operation)} @@ -2575,8 +2553,9 @@ defmodule LivebookWeb.SessionLive do cell <- Enum.reverse(section.cells), Cell.evaluable?(cell), output <- filter_outputs(cell.outputs, :dashboard) do + dbg() %{ - id: elem(output, 0), + id: cell.id, x_pos: 0, y_pos: 1, width: 3, From e17bb5b438f50a74d197846795a3d5dda9723736 Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Mon, 10 Jul 2023 22:59:52 +0200 Subject: [PATCH 06/36] WIP --- assets/js/hooks/gridstack.js | 19 +++----- lib/livebook_web/live/gridstack_component.ex | 50 +++++++++++++------- lib/livebook_web/live/session_live.ex | 9 ++-- 3 files changed, 43 insertions(+), 35 deletions(-) diff --git a/assets/js/hooks/gridstack.js b/assets/js/hooks/gridstack.js index ea52c3c141a..c46e9534378 100644 --- a/assets/js/hooks/gridstack.js +++ b/assets/js/hooks/gridstack.js @@ -32,22 +32,17 @@ const Gridstack = { }); this.grid.on("change", function(event, items) { - console.log(items); - let new_items = items.reduce((acc, item) => { - acc[item.id] = { - x_pos: item.x, - y_pos: item.y, - width: item.w, - height: item.h - }; - return acc; - }, {}); + const new_items = items.map(function({ id, x, y, w, h }) { + return { id: id, x: x, y: y, w: w, h: h }; + }); + console.log("items_changed", new_items); self.pushEventTo(self.props.phxTarget, "items_changed", new_items); self.repositionIframe(); }); this.grid.on("drag", function(event, item) { - self.repositionIframe(); + // TODO update iframe position when dragging + //self.repositionIframe(); }); this.handleEvent("save_layout", function() { @@ -59,7 +54,7 @@ const Gridstack = { }, updated() { this.props = this.getProps(); - console.log("Gridstack updated"); + console.log("Gridstack updated", this.grid); }, getProps() { return { diff --git a/lib/livebook_web/live/gridstack_component.ex b/lib/livebook_web/live/gridstack_component.ex index 0abcbda4c42..0c5467aef1b 100644 --- a/lib/livebook_web/live/gridstack_component.ex +++ b/lib/livebook_web/live/gridstack_component.ex @@ -7,28 +7,42 @@ defmodule LivebookWeb.GridstackComponent do # # * `:output_blocks` - TODO + @impl true + def update(assigns, socket) do + IO.inspect(assigns, label: "UPDATED") + socket = + socket + |> assign( + output_blocks: assigns.output_blocks, + session: assigns.session, + app_settings: assigns.app_settings + ) + {:ok, socket} + end + @impl true def render(assigns) do ~H""" -
+
-
-
+
+
+
+
@@ -45,7 +59,7 @@ defmodule LivebookWeb.GridstackComponent do into: %{} do {key, %{x_pos: x_pos, y_pos: y_pos, width: width, height: height}} end - {:noreply, socket} + {:noreply, assign(socket, output_blocks: params)} end def handle_event("new_app_layout", %{"layout" => layout} = data, socket) do diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index f77161e9385..b14096a865c 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -2553,13 +2553,12 @@ defmodule LivebookWeb.SessionLive do cell <- Enum.reverse(section.cells), Cell.evaluable?(cell), output <- filter_outputs(cell.outputs, :dashboard) do - dbg() %{ id: cell.id, - x_pos: 0, - y_pos: 1, - width: 3, - height: 1 + x: 0, + y: 1, + w: 3, + h: 1 } end |> Enum.reverse() From eeebf8db70271a7c25bc5c4b2b0e7cad319be048 Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Tue, 11 Jul 2023 07:56:41 +0200 Subject: [PATCH 07/36] WIP --- assets/css/js_interop.css | 8 +-- assets/js/hooks/gridstack.js | 47 ++++++++-------- assets/js/hooks/gridstack_static.js | 10 ++-- lib/livebook_web/live/app_session_live.ex | 8 ++- lib/livebook_web/live/gridstack_component.ex | 53 ++++++++++--------- lib/livebook_web/live/session_live.ex | 13 ++--- .../session_live/app_settings_component.ex | 30 +++++------ 7 files changed, 84 insertions(+), 85 deletions(-) diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index 8a627702faa..fb5cf6d507f 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -271,13 +271,13 @@ solely client-side operations. [data-el-session][data-js-side-panel-content="app-info"] [data-el-notebook-content] { - @apply hidden; - } + @apply hidden; +} [data-el-session]:not([data-js-side-panel-content="app-info"]) [data-el-app-dashboard] { - @apply hidden; - } + @apply hidden; +} [data-el-session][data-js-side-panel-content="app-info"] [data-el-app-indicator] { diff --git a/assets/js/hooks/gridstack.js b/assets/js/hooks/gridstack.js index c46e9534378..99cf828db5a 100644 --- a/assets/js/hooks/gridstack.js +++ b/assets/js/hooks/gridstack.js @@ -26,31 +26,32 @@ const Gridstack = { this.grid = GridStack.init(options, this.el); - document.querySelector("[data-el-app-info-toggle]").addEventListener("click", function(event) { - self.visible = !self.visible; - self.toggleMountOutputs(); - }); - - this.grid.on("change", function(event, items) { - const new_items = items.map(function({ id, x, y, w, h }) { - return { id: id, x: x, y: y, w: w, h: h }; + document + .querySelector("[data-el-app-info-toggle]") + .addEventListener("click", function (event) { + self.visible = !self.visible; + self.toggleMountOutputs(); }); - console.log("items_changed", new_items); + + this.grid.on("change", function (event, items) { + console.log(items); + let new_items = items.reduce((acc, item) => { + acc[item.id] = { + x: item.x, + y: item.y, + w: item.w, + h: item.h, + }; + return acc; + }, {}); self.pushEventTo(self.props.phxTarget, "items_changed", new_items); self.repositionIframe(); }); - this.grid.on("drag", function(event, item) { + this.grid.on("drag", function (event, item) { // TODO update iframe position when dragging //self.repositionIframe(); }); - - this.handleEvent("save_layout", function() { - const layout = self.grid.save(false, false); - console.log("Layout saved"); - console.log(layout); - self.pushEventTo(self.props.phxTarget, "new_app_layout", { layout: layout }); - }); }, updated() { this.props = this.getProps(); @@ -68,17 +69,21 @@ const Gridstack = { // safe origin location for unmounting output._origin || (output._origin = output.parentNode); const output_id = output.id.split("-")[1]; - const item_content = this.el.querySelector(`[gs-id="${output_id}"] .grid-stack-item-content`); + const item_content = this.el.querySelector( + `[gs-id="${output_id}"] .grid-stack-item-content` + ); if (item_content) { item_content.prepend(output); } } } else { - const output_containers = this.el.querySelectorAll(`.grid-stack-item .grid-stack-item-content`); + const output_containers = this.el.querySelectorAll( + `.grid-stack-item .grid-stack-item-content` + ); for (let container of output_containers) { const output = container.firstChild; if (output) { - output._origin && output._origin.prepend(output); + output._origin && output._origin.appendChild(output); } } } @@ -86,7 +91,7 @@ const Gridstack = { }, repositionIframe() { globalPubSub.broadcast("js_views", { type: "reposition" }); - } + }, }; export default Gridstack; diff --git a/assets/js/hooks/gridstack_static.js b/assets/js/hooks/gridstack_static.js index fdcf1d289f1..104257534b2 100644 --- a/assets/js/hooks/gridstack_static.js +++ b/assets/js/hooks/gridstack_static.js @@ -17,7 +17,7 @@ const GridstackStatic = { this.grid = GridStack.init(options, this.el); - this.handleEvent("load_layout", function({ layout }) { + this.handleEvent("load_layout", function ({ layout }) { if (layout) { console.log("layout", layout); self.grid.load(layout); @@ -25,19 +25,19 @@ const GridstackStatic = { } }); }, - loadLayout(layout) { - }, mountOutputs() { const outputs = document.querySelectorAll("[data-el-output]"); for (let output of outputs) { - const item_content = this.el.querySelector(`[gs-id="${output.parentNode.id}"] .grid-stack-item-content`); + const item_content = this.el.querySelector( + `[gs-id="${output.parentNode.id}"] .grid-stack-item-content` + ); console.log("output", output); console.log("content", item_content); if (item_content) { item_content.prepend(output); } } - } + }, }; export default GridstackStatic; diff --git a/lib/livebook_web/live/app_session_live.ex b/lib/livebook_web/live/app_session_live.ex index bcd05b6e2ac..fa67eb273ee 100644 --- a/lib/livebook_web/live/app_session_live.ex +++ b/lib/livebook_web/live/app_session_live.ex @@ -32,7 +32,6 @@ defmodule LivebookWeb.AppSessionLive do end data_view = data_to_view(data) - IO.inspect(data_view.output_layout) {:ok, socket @@ -44,8 +43,7 @@ defmodule LivebookWeb.AppSessionLive do data_view: data_view ) |> assign_private(data: data) - |> push_event("load_layout", %{layout: data_view.output_layout}) - } + |> push_event("load_layout", %{layout: data_view.output_layout})} else {:ok, assign(socket, @@ -99,7 +97,8 @@ defmodule LivebookWeb.AppSessionLive do """ end - def render(%{data_view: %{output_type: :dashboard}} = assigns) when assigns.app_authenticated? do + def render(%{data_view: %{output_type: :dashboard}} = assigns) + when assigns.app_authenticated? do ~H"""
@@ -316,7 +315,6 @@ defmodule LivebookWeb.AppSessionLive do end defp data_to_view(data) do - IO.inspect(data, label: "DATA") %{ notebook_name: data.notebook.name, output_views: diff --git a/lib/livebook_web/live/gridstack_component.ex b/lib/livebook_web/live/gridstack_component.ex index 0c5467aef1b..c51d7d34ada 100644 --- a/lib/livebook_web/live/gridstack_component.ex +++ b/lib/livebook_web/live/gridstack_component.ex @@ -8,20 +8,31 @@ defmodule LivebookWeb.GridstackComponent do # * `:output_blocks` - TODO @impl true - def update(assigns, socket) do - IO.inspect(assigns, label: "UPDATED") + def update(%{app_settings: app_settings, output_blocks: output_blocks} = assigns, socket) do socket = socket |> assign( - output_blocks: assigns.output_blocks, session: assigns.session, - app_settings: assigns.app_settings + output_blocks: restore_layout(app_settings.output_layout, output_blocks), + app_settings: app_settings ) + {:ok, socket} end + defp restore_layout(output_layout, output_blocks) do + restored_layout = + Enum.into(output_layout, %{}, fn map -> + {id, rest} = Map.pop(map, "id") + {id, rest} + end) + + Map.merge(output_blocks, restored_layout) + end + @impl true def render(assigns) do + # TODO remove phx-update="ignore". Currently gridstack crashes without it ~H"""
@@ -51,28 +62,20 @@ defmodule LivebookWeb.GridstackComponent do @impl true def handle_event("items_changed", params, socket) do - IO.inspect(params, label: "params") + updated = Map.merge(socket.assigns.output_blocks, params) - params = - for {key, %{"x_pos" => x_pos, "y_pos" => y_pos, "width" => width, "height" => height}} <- - params, - into: %{} do - {key, %{x_pos: x_pos, y_pos: y_pos, width: width, height: height}} + output_layout = + for {id, rest} <- updated do + Map.put(rest, "id", id) end - {:noreply, assign(socket, output_blocks: params)} - end - def handle_event("new_app_layout", %{"layout" => layout} = data, socket) do - # TODO: tidy up - IO.inspect(data) - socket = put_in(socket.assigns.app_settings.output_layout, layout) + app_settings = %{socket.assigns.app_settings | output_layout: output_layout} Session.set_app_settings( socket.assigns.session.pid, - socket.assigns.app_settings + app_settings ) - {:noreply, socket} + {:noreply, assign(socket, output_blocks: updated, app_settings: app_settings)} end - end diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index b14096a865c..8e30b877e93 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -1344,7 +1344,6 @@ defmodule LivebookWeb.SessionLive do end def handle_event("deploy_app", %{}, socket) do - socket = push_event(socket, "save_layout", %{}) on_confirm = fn socket -> Livebook.Session.deploy_app(socket.assigns.session.pid) socket @@ -2552,15 +2551,9 @@ defmodule LivebookWeb.SessionLive do for section <- Enum.reverse(notebook.sections), cell <- Enum.reverse(section.cells), Cell.evaluable?(cell), - output <- filter_outputs(cell.outputs, :dashboard) do - %{ - id: cell.id, - x: 0, - y: 1, - w: 3, - h: 1 - } + output <- filter_outputs(cell.outputs, :dashboard), + into: %{} do + {cell.id, %{"w" => 3, "h" => 6}} end - |> Enum.reverse() end end diff --git a/lib/livebook_web/live/session_live/app_settings_component.ex b/lib/livebook_web/live/session_live/app_settings_component.ex index b7e7d501b3b..96930e15a03 100644 --- a/lib/livebook_web/live/session_live/app_settings_component.ex +++ b/lib/livebook_web/live/session_live/app_settings_component.ex @@ -67,26 +67,27 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do ''' } /> - <.checkbox_field - field={f[:show_source]} - label="Show source" + <.select_field + field={f[:output_type]} + label="Output type" + options={[ + {"All", :all}, + {"Rich", :rich}, + {"Dashboard", :dashboard} + ]} help={ ~S''' - When enabled, it makes notebook source - accessible in the app menu. + TODO ''' } /> <.checkbox_field - field={f[:output_type]} - label="Only render rich outputs" - checked_value="dashboard" - unchecked_value="all" + field={f[:show_source]} + label="Show source" help={ ~S''' - When enabled, hides simple outputs - and only shows rich elements, such - as inputs, frames, tables, etc. + When enabled, it makes notebook source + accessible in the app menu. ''' } /> @@ -124,8 +125,8 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do phx-click={JS.patch(~p"/sessions/#{@session.id}")} disabled={not @changeset.valid?} > - <.remix_icon icon="rocket-line" class="align-middle mr-1" /> - Deploy + <.remix_icon icon="save-line" class="align-middle mr-1" /> + Save
-
-
- -
- <.live_component - module={LivebookWeb.GridstackComponent} - id="app-dashboard" - output_blocks={@data_view.output_blocks} - session={@session} - app_settings={@data_view.app_settings} +
+
+
+ -
-
-
-
-
-

<%= @data_view.notebook_name %>

+
+
+
+
+

<%= @data_view.notebook_name %>

+
+ + <.menu id="session-menu"> + <:toggle> + + + <.menu_item> + <.link patch={~p"/sessions/#{@session.id}/export/livemd"} role="menuitem"> + <.remix_icon icon="download-2-line" /> + Export + + + <.menu_item> + + + <.menu_item> + + + <.menu_item> + + <.remix_icon icon="dashboard-2-line" /> + See on Dashboard + + + <.menu_item variant={:danger}> + + +
- - <.menu id="session-menu"> - <:toggle> - - - <.menu_item> - <.link patch={~p"/sessions/#{@session.id}/export/livemd"} role="menuitem"> - <.remix_icon icon="download-2-line" /> - Export - - - <.menu_item> - - - <.menu_item> - - - <.menu_item> - - <.remix_icon icon="dashboard-2-line" /> - See on Dashboard - - - <.menu_item variant={:danger}> - - - -
-
- <.menu position={:bottom_left} id="notebook-hub-menu"> - <:toggle> -
- in - <%= @data_view.hub.hub_emoji %> - <%= @data_view.hub.hub_name %> - <.remix_icon icon="arrow-down-s-line" class="invisible group-hover:visible" /> -
- - <.menu_item :for={hub <- @saved_hubs}> - - - <.menu_item> - <.link navigate={~p"/hub"} aria-label="Add Hub" role="menuitem"> - <.remix_icon icon="add-line" class="align-middle mr-1" /> Add Hub - - - - -
- <%= cond do %> - <% @data_view.file == nil -> %> - - - - <% @data_view.file in @starred_files -> %> - - - - <% true -> %> - - - - <% end %> +
+ <.menu position={:bottom_left} id="notebook-hub-menu"> + <:toggle> +
+ in + <%= @data_view.hub.hub_emoji %> + <%= @data_view.hub.hub_name %> + <.remix_icon icon="arrow-down-s-line" class="invisible group-hover:visible" /> +
+ + <.menu_item :for={hub <- @saved_hubs}> + + + <.menu_item> + <.link navigate={~p"/hub"} aria-label="Add Hub" role="menuitem"> + <.remix_icon icon="add-line" class="align-middle mr-1" /> Add Hub + + + + +
+ <%= cond do %> + <% @data_view.file == nil -> %> + + + + <% @data_view.file in @starred_files -> %> + + + + <% true -> %> + + + + <% end %> +
-
-
- <.live_component - module={LivebookWeb.SessionLive.CellComponent} - id={@data_view.setup_cell_view.id} - session_id={@session.id} - session_pid={@session.pid} - client_id={@client_id} - runtime={@data_view.runtime} - installing?={@data_view.installing?} - allowed_uri_schemes={@allowed_uri_schemes} - cell_view={@data_view.setup_cell_view} - /> -
-
-
- +
+ <.live_component + module={LivebookWeb.SessionLive.CellComponent} + id={@data_view.setup_cell_view.id} + session_id={@session.id} + session_pid={@session.pid} + client_id={@client_id} + runtime={@data_view.runtime} + installing?={@data_view.installing?} + allowed_uri_schemes={@allowed_uri_schemes} + cell_view={@data_view.setup_cell_view} + /> +
+
+
+ +
+ <.live_component + :for={{section_view, index} <- Enum.with_index(@data_view.section_views)} + module={LivebookWeb.SessionLive.SectionComponent} + id={section_view.id} + index={index} + session_id={@session.id} + session_pid={@session.pid} + client_id={@client_id} + runtime={@data_view.runtime} + smart_cell_definitions={@data_view.smart_cell_definitions} + code_block_definitions={@data_view.code_block_definitions} + installing?={@data_view.installing?} + allowed_uri_schemes={@allowed_uri_schemes} + section_view={section_view} + default_language={@data_view.default_language} + /> +
- <.live_component - :for={{section_view, index} <- Enum.with_index(@data_view.section_views)} - module={LivebookWeb.SessionLive.SectionComponent} - id={section_view.id} - index={index} - session_id={@session.id} - session_pid={@session.pid} - client_id={@client_id} - runtime={@data_view.runtime} - smart_cell_definitions={@data_view.smart_cell_definitions} - code_block_definitions={@data_view.code_block_definitions} - installing?={@data_view.installing?} - allowed_uri_schemes={@allowed_uri_schemes} - section_view={section_view} - default_language={@data_view.default_language} - /> -
+
+ <%= live_render(@socket, LivebookWeb.SessionLive.CanvasLive, + id: "canvas", + session: %{ + "session" => @session, + "runtime" => @data_view.runtime, + "client_id" => @client_id, + "canvas_settings" => @data_view.canvas_settings + } + ) %> +
@@ -1087,6 +1095,11 @@ defmodule LivebookWeb.SessionLive do {:noreply, insert_cell_below(socket, params)} end + def handle_event("move_output_to_canvas", %{"cell_id" => cell_id}, socket) do + Session.move_output_to_canvas(socket.assigns.session.pid, cell_id) + {:noreply, socket} + end + def handle_event("delete_cell", %{"cell_id" => cell_id}, socket) do on_confirm = fn socket -> Session.delete_cell(socket.assigns.session.pid, cell_id) @@ -2287,7 +2300,8 @@ defmodule LivebookWeb.SessionLive do output_blocks: output_blocks(data.notebook), file_entries: Enum.sort_by(data.notebook.file_entries, & &1.name), app_settings: data.notebook.app_settings, - deployed_app_slug: data.deployed_app_slug + deployed_app_slug: data.deployed_app_slug, + canvas_settings: data.notebook.canvas_settings } end @@ -2402,6 +2416,7 @@ defmodule LivebookWeb.SessionLive do defp eval_info_to_view(cell, eval_info, data) do %{ outputs: cell.outputs, + output_location: cell.output_location, validity: eval_info.validity, status: eval_info.status, errored: eval_info.errored, diff --git a/lib/livebook_web/live/session_live/canvas_live.ex b/lib/livebook_web/live/session_live/canvas_live.ex new file mode 100644 index 00000000000..3747756cb55 --- /dev/null +++ b/lib/livebook_web/live/session_live/canvas_live.ex @@ -0,0 +1,71 @@ +defmodule LivebookWeb.SessionLive.CanvasLive do + use LivebookWeb, :live_view + + @impl true + def mount( + _params, + %{ + "session" => session, + "runtime" => runtime, + "client_id" => client_id, + "canvas_settings" => canvas_settings + }, + socket + ) do + socket = + socket + |> assign( + session: session, + runtime: runtime, + client_id: client_id, + canvas_settings: canvas_settings + ) + + {:ok, socket} + end + + @impl true + def render(assigns) do + ~H""" +
+
+
+
+
+ +
+
+
+
+
+ """ + end + + @impl true + def handle_event("items_changed", params, socket) do + # Session.update_canvas_layout(socket.assigns.session.pid, app_settings) + + {:noreply, socket} + end +end diff --git a/lib/livebook_web/live/session_live/cell_component.ex b/lib/livebook_web/live/session_live/cell_component.ex index 0ee73937c56..4118b19bc18 100644 --- a/lib/livebook_web/live/session_live/cell_component.ex +++ b/lib/livebook_web/live/session_live/cell_component.ex @@ -82,6 +82,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do <.move_cell_up_button cell_id={@cell_view.id} /> <.move_cell_down_button cell_id={@cell_view.id} /> <.delete_cell_button cell_id={@cell_view.id} /> + <.move_output_to_canvas_button cell_id={@cell_view.id} /> <.cell_body> @@ -98,6 +99,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<.evaluation_outputs + :if={@output_location == :notebook} cell_view={@cell_view} session_id={@session_id} session_pid={@session_pid} @@ -176,6 +178,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do <.move_cell_up_button cell_id={@cell_view.id} /> <.move_cell_down_button cell_id={@cell_view.id} /> <.delete_cell_button cell_id={@cell_view.id} /> + <.move_output_to_canvas_button cell_id={@cell_view.id} /> <.cell_body> @@ -476,6 +479,20 @@ defmodule LivebookWeb.SessionLive.CellComponent do """ end + defp move_output_to_canvas_button(assigns) do + ~H""" + + + + """ + end + defp cell_link_button(assigns) do ~H""" diff --git a/lib/livebook_web/live/session_live/indicators_component.ex b/lib/livebook_web/live/session_live/indicators_component.ex index eb41f4eb56f..7b5f1e44a13 100644 --- a/lib/livebook_web/live/session_live/indicators_component.ex +++ b/lib/livebook_web/live/session_live/indicators_component.ex @@ -73,6 +73,12 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do <.remix_icon icon="layout-5-line" class="text-xl text-green-bright-400" /> + <.menu_item> + + <.menu_item>
-
-
- <.canvas_popout session_id={@session_id} /> +
+
<.view_indicator /> <.persistence_indicator file={@file} @@ -50,25 +46,56 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do <.insert_mode_indicator />
+
+
+ <.canvas_close_button /> + <.canvas_popout_button /> + <.canvas_popin_button /> +
+
""" end - defp canvas_popout(assigns) do + defp canvas_close_button(assigns) do + ~H""" + + + + """ + end + + defp canvas_popout_button(assigns) do ~H""" - <.link - href={~p"/sessions/#{@session_id}/canvas"} + + + """ + end + + defp canvas_popin_button(assigns) do + ~H""" + + """ end From e0473e6f28efb1562da14b8822581814863cbe42 Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Wed, 12 Jul 2023 09:42:33 +0200 Subject: [PATCH 16/36] hide move to canvas button when output is already on canvas --- assets/css/js_interop.css | 2 ++ lib/livebook_web/live/session_live/cell_component.ex | 1 + 2 files changed, 3 insertions(+) diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index ee744734587..a5bfe1b338c 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -291,6 +291,8 @@ solely client-side operations. [data-el-session]:not([data-js-view="canvas"]) [data-el-canvas], [data-el-session]:not([data-js-view^="canvas"]) [data-el-move-output-to-canvas-button], +[data-el-cell][data-el-js-cell-output-location-canvas] + [data-el-move-output-to-canvas-button], [data-el-session]:not([data-js-view="canvas-popped-out"]) [data-el-canvas-popin-button], [data-el-session]:not([data-js-view="canvas"]) [data-el-canvas-popout-button], diff --git a/lib/livebook_web/live/session_live/cell_component.ex b/lib/livebook_web/live/session_live/cell_component.ex index 9c17a7fe10d..36058056a99 100644 --- a/lib/livebook_web/live/session_live/cell_component.ex +++ b/lib/livebook_web/live/session_live/cell_component.ex @@ -9,6 +9,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
Date: Wed, 12 Jul 2023 10:59:56 +0200 Subject: [PATCH 17/36] Add move output to notebook button --- assets/css/js_interop.css | 4 ++-- assets/js/hooks/session.js | 8 ++++++- lib/livebook/notebook.ex | 22 +++++++++++++++++++ lib/livebook/session.ex | 14 ++++++++++++ lib/livebook/session/data.ex | 19 ++++++++++++++++ lib/livebook_web/live/session_live.ex | 5 +++++ .../live/session_live/cell_component.ex | 21 +++++++++++++++++- 7 files changed, 89 insertions(+), 4 deletions(-) diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index a5bfe1b338c..b20e08677c0 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -289,10 +289,10 @@ solely client-side operations. /* === Views === */ [data-el-session]:not([data-js-view="canvas"]) [data-el-canvas], -[data-el-session]:not([data-js-view^="canvas"]) - [data-el-move-output-to-canvas-button], [data-el-cell][data-el-js-cell-output-location-canvas] [data-el-move-output-to-canvas-button], +[data-el-cell]:not([data-el-js-cell-output-location-canvas]) + [data-el-move-output-to-notebook-button], [data-el-session]:not([data-js-view="canvas-popped-out"]) [data-el-canvas-popin-button], [data-el-session]:not([data-js-view="canvas"]) [data-el-canvas-popout-button], diff --git a/assets/js/hooks/session.js b/assets/js/hooks/session.js index 69e446dbe8f..218316eac6f 100644 --- a/assets/js/hooks/session.js +++ b/assets/js/hooks/session.js @@ -93,6 +93,12 @@ const Session = { document.addEventListener("focus", this._handleDocumentFocus, true); document.addEventListener("click", this._handleDocumentClick); + document.addEventListener("canvas:show", (event) => { + if (this.el.getAttribute("data-js-view") != "canvas-popped-out") { + this.el.setAttribute("data-js-view", "canvas"); + } + }); + this.getElement("sections-list").addEventListener("click", (event) => { this.handleSectionsListClick(event); this.handleCellIndicatorsClick(event); @@ -674,7 +680,7 @@ const Session = { "_blank", "toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, copyhistory=yes, width=600, height=600" ); - window.addEventListener("message", function (event) { + window.addEventListener("message", function(event) { if (event.data === "closing") { self.el.setAttribute("data-js-view", "canvas"); } diff --git a/lib/livebook/notebook.ex b/lib/livebook/notebook.ex index 06901e78c35..877a08f5040 100644 --- a/lib/livebook/notebook.ex +++ b/lib/livebook/notebook.ex @@ -126,6 +126,28 @@ defmodule Livebook.Notebook do end) end + @doc """ + TODO + """ + @spec move_output_to_notebook(t(), Cell.t(), Section.t()) :: t() + def move_output_to_notebook(notebook, cell, section) do + notebook + |> update_in( + [ + Access.key(:sections), + access_by_id(section.id), + Access.key(:cells), + access_by_id(cell.id) + ], + fn cell -> + %{cell | output_location: :notebook} + end + ) + |> update_in([Access.key(:canvas_settings), Access.key(:items)], fn items -> + Map.delete(items, cell.id) + end) + end + @doc """ Returns the default value of `persist_outputs`. """ diff --git a/lib/livebook/session.ex b/lib/livebook/session.ex index f96383be98f..23ca6ce7999 100644 --- a/lib/livebook/session.ex +++ b/lib/livebook/session.ex @@ -310,6 +310,14 @@ defmodule Livebook.Session do GenServer.cast(pid, {:move_output_to_canvas, self(), cell_id}) end + @doc """ + TODO + """ + @spec move_output_to_notebook(pid(), Cell.id()) :: :ok + def move_output_to_notebook(pid, cell_id) do + GenServer.cast(pid, {:move_output_to_notebook, self(), cell_id}) + end + @doc """ Sends section insertion request to the server. """ @@ -976,6 +984,12 @@ defmodule Livebook.Session do {:noreply, handle_operation(state, operation)} end + def handle_cast({:move_output_to_notebook, client_pid, cell_id}, state) do + client_id = client_id(state, client_pid) + operation = {:move_output_to_notebook, client_id, cell_id} + {:noreply, handle_operation(state, operation)} + end + def handle_cast({:insert_section, client_pid, index}, state) do client_id = client_id(state, client_pid) # Include new id in the operation, so it's reproducible diff --git a/lib/livebook/session/data.ex b/lib/livebook/session/data.ex index 6419810861c..a9ed444913b 100644 --- a/lib/livebook/session/data.ex +++ b/lib/livebook/session/data.ex @@ -172,6 +172,7 @@ defmodule Livebook.Session.Data do @type operation :: {:set_notebook_attributes, client_id(), map()} | {:move_output_to_canvas, client_id(), Cell.id()} + | {:move_output_to_notebook, client_id(), Cell.id()} | {:insert_section, client_id(), index(), Section.id()} | {:insert_section_into, client_id(), Section.id(), index(), Section.id()} | {:set_section_parent, client_id(), Section.id(), parent_id :: Section.id()} @@ -388,6 +389,19 @@ defmodule Livebook.Session.Data do end end + def apply_operation(data, {:move_output_to_notebook, _client_id, cell_id}) do + with {:ok, cell, section} <- Notebook.fetch_cell_and_section(data.notebook, cell_id), + false <- Cell.setup?(cell) do + data + |> with_actions() + |> move_output_to_notebook(cell, section) + |> set_dirty() + |> wrap_ok() + else + _ -> :error + end + end + def apply_operation(data, {:insert_section, _client_id, index, id}) do section = %{Section.new() | id: id} @@ -986,6 +1000,11 @@ defmodule Livebook.Session.Data do |> set!(notebook: Notebook.move_output_to_canvas(data.notebook, cell, section)) end + defp move_output_to_notebook({data, _} = data_actions, cell, section) do + data_actions + |> set!(notebook: Notebook.move_output_to_notebook(data.notebook, cell, section)) + end + defp insert_section({data, _} = data_actions, index, section) do data_actions |> set!( diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index 0d0aac42ef8..02c2d0dc909 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -1100,6 +1100,11 @@ defmodule LivebookWeb.SessionLive do {:noreply, socket} end + def handle_event("move_output_to_notebook", %{"cell_id" => cell_id}, socket) do + Session.move_output_to_notebook(socket.assigns.session.pid, cell_id) + {:noreply, socket} + end + def handle_event("delete_cell", %{"cell_id" => cell_id}, socket) do on_confirm = fn socket -> Session.delete_cell(socket.assigns.session.pid, cell_id) diff --git a/lib/livebook_web/live/session_live/cell_component.ex b/lib/livebook_web/live/session_live/cell_component.ex index 36058056a99..29b6b2a77aa 100644 --- a/lib/livebook_web/live/session_live/cell_component.ex +++ b/lib/livebook_web/live/session_live/cell_component.ex @@ -84,6 +84,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do <.move_cell_down_button cell_id={@cell_view.id} /> <.delete_cell_button cell_id={@cell_view.id} /> <.move_output_to_canvas_button cell_id={@cell_view.id} /> + <.move_output_to_notebook_button cell_id={@cell_view.id} /> <.cell_body> @@ -488,7 +489,11 @@ defmodule LivebookWeb.SessionLive.CellComponent do @@ -496,6 +501,20 @@ defmodule LivebookWeb.SessionLive.CellComponent do """ end + defp move_output_to_notebook_button(assigns) do + ~H""" + + + + """ + end + defp cell_link_button(assigns) do ~H""" From 8a3e33c9a0cd4e522ec5cd677c9d046edf13c64c Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Wed, 12 Jul 2023 11:17:44 +0200 Subject: [PATCH 18/36] hide canvas options in sub menu --- assets/css/js_interop.css | 1 + assets/js/hooks/session.js | 2 +- .../live/session_live/indicators_component.ex | 76 ++++++++----------- 3 files changed, 35 insertions(+), 44 deletions(-) diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index b20e08677c0..7da6b08bfbb 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -293,6 +293,7 @@ solely client-side operations. [data-el-move-output-to-canvas-button], [data-el-cell]:not([data-el-js-cell-output-location-canvas]) [data-el-move-output-to-notebook-button], +[data-el-session]:not([data-js-view^="canvas"]) [data-el-canvas-menu], [data-el-session]:not([data-js-view="canvas-popped-out"]) [data-el-canvas-popin-button], [data-el-session]:not([data-js-view="canvas"]) [data-el-canvas-popout-button], diff --git a/assets/js/hooks/session.js b/assets/js/hooks/session.js index 218316eac6f..5f1a3e890da 100644 --- a/assets/js/hooks/session.js +++ b/assets/js/hooks/session.js @@ -680,7 +680,7 @@ const Session = { "_blank", "toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, copyhistory=yes, width=600, height=600" ); - window.addEventListener("message", function(event) { + window.addEventListener("message", function (event) { if (event.data === "closing") { self.el.setAttribute("data-js-view", "canvas"); } diff --git a/lib/livebook_web/live/session_live/indicators_component.ex b/lib/livebook_web/live/session_live/indicators_component.ex index 24644517947..cae7e11d302 100644 --- a/lib/livebook_web/live/session_live/indicators_component.ex +++ b/lib/livebook_web/live/session_live/indicators_component.ex @@ -46,57 +46,47 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do <.insert_mode_indicator />
-
+
- <.canvas_close_button /> - <.canvas_popout_button /> - <.canvas_popin_button /> + <.canvas_indicator />
""" end - defp canvas_close_button(assigns) do + defp canvas_indicator(assigns) do ~H""" - - - - """ - end - - defp canvas_popout_button(assigns) do - ~H""" - - - - """ - end - - defp canvas_popin_button(assigns) do - ~H""" - - - +
+ <.menu id="canvas-menu" position={:bottom_right}> + <:toggle> + + + <.menu_item> + + + <.menu_item> + + + <.menu_item> + + + +
""" end From 3370fd64d94d41f6662fcfebbbf2cb54d32769a3 Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Wed, 12 Jul 2023 13:34:55 +0200 Subject: [PATCH 19/36] Implement suggested view mode UI --- assets/css/js_interop.css | 21 ++++++++++++++++++- assets/js/hooks/session.js | 21 ++++++++++++++++++- .../live/session_live/indicators_component.ex | 16 ++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index 7da6b08bfbb..41ddb8f480e 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -288,6 +288,16 @@ solely client-side operations. /* === Views === */ +[data-el-session]:not([data-js-view]) [data-el-view-turn-off-button] { + @apply hidden; +} + +[data-el-session][data-js-view] [data-el-view-toggle="canvas"], +[data-el-session][data-js-view] [data-el-view-toggle="code-zen"], +[data-el-session][data-js-view] [data-el-view-toggle="presentation"] { + @apply pointer-events-none; +} + [data-el-session]:not([data-js-view="canvas"]) [data-el-canvas], [data-el-cell][data-el-js-cell-output-location-canvas] [data-el-move-output-to-canvas-button], @@ -305,10 +315,19 @@ solely client-side operations. @apply fixed bottom-[0.4rem] right-1/2; } -[data-el-session][data-js-view^="canvas"] [data-el-view-toggle="canvas"] { +[data-el-session][data-js-view^="canvas"] [data-el-view-toggle="canvas"], +[data-el-session][data-js-view^="canvas"] + [data-el-view-canvas-poppedout-button] { @apply text-green-bright-400; } +[data-el-session]:not([data-js-view="canvas-popped-out"]) + [data-el-view-canvas-poppedout-button], +[data-el-session][data-js-view="canvas-popped-out"] + [data-el-view-toggle="canvas"] { + @apply hidden; +} + [data-el-session][data-js-view="code-zen"] [data-el-section-headline], [data-el-session][data-js-view="code-zen"] [data-el-section-subheadline], [data-el-session][data-js-view="code-zen"] diff --git a/assets/js/hooks/session.js b/assets/js/hooks/session.js index 5f1a3e890da..57aa6da9bb7 100644 --- a/assets/js/hooks/session.js +++ b/assets/js/hooks/session.js @@ -156,6 +156,20 @@ const Session = { this.handleViewsClick(event); }); + this.getElement("view-turn-off-button").addEventListener( + "click", + (event) => { + this.handleViewTrunOffClick(); + } + ); + + this.getElement("view-canvas-poppedout-button").addEventListener( + "click", + (event) => { + this.handleCanvasPopinClick(); + } + ); + this.getElement("section-toggle-collapse-all-button").addEventListener( "click", (event) => this.toggleCollapseAllSections() @@ -689,7 +703,7 @@ const Session = { }, handleCanvasPopinClick() { - this.canvasWindow.close(); + this.canvasWindow && this.canvasWindow.close(); this.el.setAttribute("data-js-view", "canvas"); }, @@ -1047,6 +1061,11 @@ const Session = { } }, + handleViewTrunOffClick() { + this.canvasWindow && this.canvasWindow.close(); + this.el.removeAttribute("data-js-view"); + }, + toggleCollapseSection() { if (this.focusedId) { const sectionId = this.getSectionIdByFocusableId(this.focusedId); diff --git a/lib/livebook_web/live/session_live/indicators_component.ex b/lib/livebook_web/live/session_live/indicators_component.ex index cae7e11d302..f8cc45bbfcd 100644 --- a/lib/livebook_web/live/session_live/indicators_component.ex +++ b/lib/livebook_web/live/session_live/indicators_component.ex @@ -116,6 +116,12 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do Canvas + <.menu_item> + + <.menu_item> + <.menu_item> + +
""" From 8b70d5e6128ba0880f29645f52a54b57bf95decd Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Wed, 12 Jul 2023 14:18:49 +0200 Subject: [PATCH 20/36] fix nasty popped out window bug --- assets/js/hooks/session.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/assets/js/hooks/session.js b/assets/js/hooks/session.js index 57aa6da9bb7..a9bcea22fbd 100644 --- a/assets/js/hooks/session.js +++ b/assets/js/hooks/session.js @@ -76,6 +76,8 @@ const Session = { this.followedClientId = null; this.canvasWindow = null; + this.handleCanvasWindowClosed = this.handleCanvasWindowClosed.bind(this); + setFavicon(this.faviconForEvaluationStatus(this.props.globalStatus)); this.updateSectionListHighlight(); @@ -683,27 +685,22 @@ const Session = { }, handleCanvasCloseClick() { - this.canvasWindow && this.canvasWindow.close(); + this.closeCanvasWindow(); this.el.removeAttribute("data-js-view"); }, handleCanvasPopoutClick() { - const self = this; this.canvasWindow = window.open( window.location.pathname + "/canvas", "_blank", "toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, copyhistory=yes, width=600, height=600" ); - window.addEventListener("message", function (event) { - if (event.data === "closing") { - self.el.setAttribute("data-js-view", "canvas"); - } - }); + window.addEventListener("message", this.handleCanvasWindowClosed); this.el.setAttribute("data-js-view", "canvas-popped-out"); }, handleCanvasPopinClick() { - this.canvasWindow && this.canvasWindow.close(); + this.closeCanvasWindow(); this.el.setAttribute("data-js-view", "canvas"); }, @@ -1062,7 +1059,7 @@ const Session = { }, handleViewTrunOffClick() { - this.canvasWindow && this.canvasWindow.close(); + this.closeCanvasWindow(); this.el.removeAttribute("data-js-view"); }, @@ -1406,6 +1403,15 @@ const Session = { isViewPresentation() { return this.view === "presentation"; }, + closeCanvasWindow() { + window.removeEventListener("message", this.handleCanvasWindowClosed); + this.canvasWindow && this.canvasWindow.close(); + }, + handleCanvasWindowClosed(event) { + if (event.data === "closing") { + this.el.setAttribute("data-js-view", "canvas"); + } + }, }; /** From 643984165ffe4a4343ad30e551bce9139f76ae04 Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Mon, 17 Jul 2023 08:15:36 +0200 Subject: [PATCH 21/36] Apply suggestions - set view button green when canvas is selected - show canvas settings in popped out window --- assets/css/js_interop.css | 12 +++++- assets/js/hooks/gridstack.js | 32 +++++++++++++-- assets/js/hooks/session.js | 30 ++++++++------ .../live/session_live/canvas_live.ex | 38 ++++++++++++++++++ .../live/session_live/indicators_component.ex | 40 ------------------- 5 files changed, 94 insertions(+), 58 deletions(-) diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index 41ddb8f480e..8c96784e377 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -369,12 +369,20 @@ solely client-side operations. @apply text-green-bright-400; } -[data-el-session]:is([data-js-view="code-zen"], [data-js-view="presentation"]) +[data-el-session]:is( + [data-js-view^="canvas"], + [data-js-view="code-zen"], + [data-js-view="presentation"] + ) [data-el-views-disabled] { @apply hidden; } -[data-el-session]:not([data-js-view="code-zen"], [data-js-view="presentation"]) +[data-el-session]:not( + [data-js-view=^="canvas"], + [data-js-view="code-zen"], + [data-js-view="presentation"] + ) [data-el-views-enabled] { @apply hidden; } diff --git a/assets/js/hooks/gridstack.js b/assets/js/hooks/gridstack.js index c7ff41c0ffe..193f1eb3637 100644 --- a/assets/js/hooks/gridstack.js +++ b/assets/js/hooks/gridstack.js @@ -13,9 +13,16 @@ const Gridstack = { this.visible = false; if (this.props.externWindow) { - window.addEventListener("beforeunload", function (event) { - window.opener.postMessage("closing", "*"); - }); + this.handleBeforeUnloadEvent = this.handleBeforeUnloadEvent.bind(this); + window.addEventListener("beforeunload", this.handleBeforeUnloadEvent); + this.getElement("canvas-close-button").addEventListener( + "click", + (event) => this.handleCanvasCloseClick() + ); + this.getElement("canvas-popin-button").addEventListener( + "click", + (event) => this.handleCanvasPopinClick() + ); } const options = { @@ -62,6 +69,25 @@ const Gridstack = { repositionIframe() { globalPubSub.broadcast("js_views", { type: "reposition" }); }, + handleBeforeUnloadEvent(event) { + this.sendToParent("popin"); + }, + handleCanvasCloseClick() { + window.removeEventListener("beforeunload", this.handleBeforeUnloadEvent); + this.sendToParent("close"); + window.close(); + }, + handleCanvasPopinClick() { + window.removeEventListener("beforeunload", this.handleBeforeUnloadEvent); + this.sendToParent("popin"); + window.close(); + }, + getElement(name) { + return document.querySelector(`[data-el-${name}]`); + }, + sendToParent(message) { + window.opener.postMessage(message, window.location.origin); + }, }; export default Gridstack; diff --git a/assets/js/hooks/session.js b/assets/js/hooks/session.js index a9bcea22fbd..1b877ca169a 100644 --- a/assets/js/hooks/session.js +++ b/assets/js/hooks/session.js @@ -76,7 +76,7 @@ const Session = { this.followedClientId = null; this.canvasWindow = null; - this.handleCanvasWindowClosed = this.handleCanvasWindowClosed.bind(this); + this.handleCanvasWindowMessage = this.handleCanvasWindowMessage.bind(this); setFavicon(this.faviconForEvaluationStatus(this.props.globalStatus)); @@ -150,10 +150,6 @@ const Session = { this.handleCanvasPopoutClick() ); - this.getElement("canvas-popin-button").addEventListener("click", (event) => - this.handleCanvasPopinClick() - ); - this.getElement("views").addEventListener("click", (event) => { this.handleViewsClick(event); }); @@ -168,7 +164,7 @@ const Session = { this.getElement("view-canvas-poppedout-button").addEventListener( "click", (event) => { - this.handleCanvasPopinClick(); + this.handleViewCanvasPoppedoutClick(); } ); @@ -693,13 +689,13 @@ const Session = { this.canvasWindow = window.open( window.location.pathname + "/canvas", "_blank", - "toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, copyhistory=yes, width=600, height=600" + "toolbar=no, location=no, directories=no, titlebar=no, toolbar=0, status=no, menubar=no, scrollbars=yes, resizable=yes, copyhistory=yes, width=600, height=600" ); - window.addEventListener("message", this.handleCanvasWindowClosed); + window.addEventListener("message", this.handleCanvasWindowMessage); this.el.setAttribute("data-js-view", "canvas-popped-out"); }, - handleCanvasPopinClick() { + handleViewCanvasPoppedoutClick() { this.closeCanvasWindow(); this.el.setAttribute("data-js-view", "canvas"); }, @@ -1404,12 +1400,20 @@ const Session = { return this.view === "presentation"; }, closeCanvasWindow() { - window.removeEventListener("message", this.handleCanvasWindowClosed); + window.removeEventListener("message", this.handleCanvasWindowMessage); this.canvasWindow && this.canvasWindow.close(); }, - handleCanvasWindowClosed(event) { - if (event.data === "closing") { - this.el.setAttribute("data-js-view", "canvas"); + handleCanvasWindowMessage(event) { + if (event.origin != window.location.origin) return; + switch (event.data) { + case "popin": + this.el.setAttribute("data-js-view", "canvas"); + break; + case "close": + this.el.removeAttribute("data-js-view"); + break; + default: + console.log("Got unkown message: ", event); } }, }; diff --git a/lib/livebook_web/live/session_live/canvas_live.ex b/lib/livebook_web/live/session_live/canvas_live.ex index 35baad20b1a..b036077c5e4 100644 --- a/lib/livebook_web/live/session_live/canvas_live.ex +++ b/lib/livebook_web/live/session_live/canvas_live.ex @@ -52,6 +52,7 @@ defmodule LivebookWeb.SessionLive.CanvasLive do ~H"""
+ <.canvas_settings_button extern_window={@extern_window} />
+
+ <.menu id="canvas-menu" position={:bottom_right}> + <:toggle> + + + <.menu_item :if={not @extern_window}> + + + <.menu_item :if={@extern_window}> + + + <.menu_item> + + + +
+
+ """ + end + @impl true def handle_event("items_changed", params, socket) do # Session.update_canvas_layout(socket.assigns.session.pid, app_settings) diff --git a/lib/livebook_web/live/session_live/indicators_component.ex b/lib/livebook_web/live/session_live/indicators_component.ex index f8cc45bbfcd..6747df65cd6 100644 --- a/lib/livebook_web/live/session_live/indicators_component.ex +++ b/lib/livebook_web/live/session_live/indicators_component.ex @@ -46,46 +46,6 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do <.insert_mode_indicator />
-
-
- <.canvas_indicator /> -
-
-
- """ - end - - defp canvas_indicator(assigns) do - ~H""" -
- <.menu id="canvas-menu" position={:bottom_right}> - <:toggle> - - - <.menu_item> - - - <.menu_item> - - - <.menu_item> - - -
""" end From 91e956d2df2e6959610e19a0f164189981442d22 Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Mon, 17 Jul 2023 08:32:18 +0200 Subject: [PATCH 22/36] Fix click twice to enable canvas mode bug --- assets/css/js_interop.css | 2 +- assets/js/hooks/session.js | 24 ++++++++----------- .../live/session_live/indicators_component.ex | 2 +- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index 8c96784e377..744b372f628 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -288,7 +288,7 @@ solely client-side operations. /* === Views === */ -[data-el-session]:not([data-js-view]) [data-el-view-turn-off-button] { +[data-el-session]:not([data-js-view]) [data-el-view-deactivate-button] { @apply hidden; } diff --git a/assets/js/hooks/session.js b/assets/js/hooks/session.js index 1b877ca169a..448e0358342 100644 --- a/assets/js/hooks/session.js +++ b/assets/js/hooks/session.js @@ -154,10 +154,10 @@ const Session = { this.handleViewsClick(event); }); - this.getElement("view-turn-off-button").addEventListener( + this.getElement("view-deactivate-button").addEventListener( "click", (event) => { - this.handleViewTrunOffClick(); + this.handleViewDeactivateClick(); } ); @@ -448,9 +448,9 @@ const Session = { } else if (keyBuffer.tryMatch(["M"])) { !this.isViewCodeZen() && this.insertCellAboveFocused("markdown"); } else if (keyBuffer.tryMatch(["v", "z"])) { - this.toggleView("code-zen"); + this.activateView("code-zen"); } else if (keyBuffer.tryMatch(["v", "p"])) { - this.toggleView("presentation"); + this.activateView("presentation"); } else if (keyBuffer.tryMatch(["c"])) { !this.isViewCodeZen() && this.toggleCollapseSection(); } else if (keyBuffer.tryMatch(["C"])) { @@ -1025,18 +1025,13 @@ const Session = { if (button) { const view = button.getAttribute("data-el-view-toggle"); - this.toggleView(view); + this.activateView(view); } }, - toggleView(view) { - if (this.view === view) { - this.view = null; - this.el.removeAttribute("data-js-view"); - } else { - this.view = view; - this.el.setAttribute("data-js-view", view); - } + activateView(view) { + this.view = view; + this.el.setAttribute("data-js-view", view); // If nothing is focused, use the first cell in the viewport const focusedId = this.focusedId || this.nearbyFocusableId(null, 0); @@ -1054,8 +1049,9 @@ const Session = { } }, - handleViewTrunOffClick() { + handleViewDeactivateClick() { this.closeCanvasWindow(); + this.view = null; this.el.removeAttribute("data-js-view"); }, diff --git a/lib/livebook_web/live/session_live/indicators_component.ex b/lib/livebook_web/live/session_live/indicators_component.ex index 6747df65cd6..e180d81ac75 100644 --- a/lib/livebook_web/live/session_live/indicators_component.ex +++ b/lib/livebook_web/live/session_live/indicators_component.ex @@ -98,7 +98,7 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
- <%= live_render(@socket, LivebookWeb.SessionLive.CanvasLive, - id: "canvas", - session: %{ - "session" => @session, - "client_id" => @client_id, - "canvas_settings" => @data_view.canvas_settings - } - ) %> + <.live_component + module={LivebookWeb.SessionLive.CanvasComponent} + id="canvas" + canvas_layout={@data_view.canvas_layout} + session={@session} + client_id={@client_id} + /> +
+
+ <.menu id="canvas-menu" position={:bottom_right}> + <:toggle> + + + <.menu_item> + + + <.menu_item> + + + +
+
@@ -1618,10 +1642,6 @@ defmodule LivebookWeb.SessionLive do {:noreply, assign(socket, :app, app)} end - def handle_info({:canvas_pid, pid}, socket) do - {:noreply, assign(socket, canvas_pid: pid)} - end - def handle_info(_message, socket), do: {:noreply, socket} defp handle_relative_path(socket, path, requested_url) do @@ -1965,16 +1985,6 @@ defmodule LivebookWeb.SessionLive do end end - defp after_operation( - %{assigns: %{canvas_pid: pid}} = socket, - _prev_socket, - {:move_output_to_canvas, _client_id, _cell_id} - ) - when not is_nil(pid) do - send(pid, {:new_data, socket.private.data.notebook.canvas_settings}) - socket - end - defp after_operation(socket, _prev_socket, _operation), do: socket defp handle_actions(socket, actions) do @@ -2319,7 +2329,7 @@ defmodule LivebookWeb.SessionLive do file_entries: Enum.sort_by(data.notebook.file_entries, & &1.name), app_settings: data.notebook.app_settings, deployed_app_slug: data.deployed_app_slug, - canvas_settings: data.notebook.canvas_settings + canvas_layout: canvas_outputs(data.notebook) } end @@ -2434,7 +2444,7 @@ defmodule LivebookWeb.SessionLive do defp eval_info_to_view(cell, eval_info, data) do %{ outputs: cell.outputs, - output_location: cell.output_location, + output_location: cell.output_location && :canvas, validity: eval_info.validity, status: eval_info.status, errored: eval_info.errored, diff --git a/lib/livebook_web/live/session_live/app_settings_component.ex b/lib/livebook_web/live/session_live/app_settings_component.ex index 951b297c59a..fca8b543449 100644 --- a/lib/livebook_web/live/session_live/app_settings_component.ex +++ b/lib/livebook_web/live/session_live/app_settings_component.ex @@ -71,9 +71,9 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do field={f[:output_type]} label="Output type" options={[ - {"All", :all}, - {"Rich", :rich}, - {"Dashboard", :dashboard} + {"All outputs", :all}, + {"Rich outputs only", :rich}, + {"Canvas", :canvas} ]} help={ ~S''' diff --git a/lib/livebook_web/live/session_live/canvas_component.ex b/lib/livebook_web/live/session_live/canvas_component.ex new file mode 100644 index 00000000000..e62c43ba87f --- /dev/null +++ b/lib/livebook_web/live/session_live/canvas_component.ex @@ -0,0 +1,58 @@ +defmodule LivebookWeb.SessionLive.CanvasComponent do + use LivebookWeb, :live_component + + alias Livebook.{Session, Sessions} + + @impl true + def mount(socket) do + {:ok, socket} + end + + @impl true + def update(assigns, socket) do + socket = + socket + |> assign( + session: assigns.session, + client_id: assigns.client_id, + canvas_layout: assigns.canvas_layout + ) + |> push_event("init", %{payload: assigns.canvas_layout}) + + {:ok, socket} + end + + @impl true + def render(assigns) do + ~H""" +
+
+
+ """ + end + + @impl true + def handle_event("items_changed", params, socket) do + items = params |> Enum.map(fn {k, v} -> {k, convert(v)} end) |> Enum.into(%{}) + Session.update_canvas(socket.assigns.session.pid, items) + {:noreply, socket} + end + + # TODO rename/optimize + defp convert(value) when is_map(value) do + Enum.into(value, %{}, fn {k, v} -> {String.to_existing_atom(k), v} end) + end + + defp convert(value), do: value + + @impl true + def handle_info({:new_output, cell_id}, socket) do + {:noreply, push_event(socket, "added", %{payload: "TODO"})} + end +end diff --git a/lib/livebook_web/live/session_live/canvas_live.ex b/lib/livebook_web/live/session_live/canvas_live.ex deleted file mode 100644 index b036077c5e4..00000000000 --- a/lib/livebook_web/live/session_live/canvas_live.ex +++ /dev/null @@ -1,145 +0,0 @@ -defmodule LivebookWeb.SessionLive.CanvasLive do - use LivebookWeb, :live_view - - alias Livebook.{Session, Sessions} - - @impl true - def mount( - _params, - %{ - "session" => session, - "client_id" => client_id, - "canvas_settings" => canvas_settings - }, - socket - ) do - socket = - socket - |> assign( - session: session, - client_id: client_id, - canvas_settings: canvas_settings, - extern_window: false - ) - - if connected?(socket) do - send(socket.parent_pid, {:canvas_pid, self()}) - end - - {:ok, socket} - end - - def mount(%{"id" => session_id}, _session, socket) do - {:ok, session} = Sessions.fetch_session(session_id) - - {data, client_id} = - Session.register_client(session.pid, self(), socket.assigns.current_user) - - socket = - socket - |> assign( - session: session, - client_id: client_id, - canvas_settings: data.notebook.canvas_settings, - extern_window: true - ) - - {:ok, socket} - end - - @impl true - def render(assigns) do - ~H""" -
-
- <.canvas_settings_button extern_window={@extern_window} /> -
-
-
-
- -
-
-
-
-
- """ - end - - defp canvas_settings_button(assigns) do - ~H""" -
-
- <.menu id="canvas-menu" position={:bottom_right}> - <:toggle> - - - <.menu_item :if={not @extern_window}> - - - <.menu_item :if={@extern_window}> - - - <.menu_item> - - - -
-
- """ - end - - @impl true - def handle_event("items_changed", params, socket) do - # Session.update_canvas_layout(socket.assigns.session.pid, app_settings) - - {:noreply, socket} - end - - @impl true - def handle_info({:new_data, canvas_settings}, socket) do - {:noreply, assign(socket, canvas_settings: canvas_settings)} - end -end diff --git a/lib/livebook_web/live/session_live/cell_component.ex b/lib/livebook_web/live/session_live/cell_component.ex index 29b6b2a77aa..06bf14e77ca 100644 --- a/lib/livebook_web/live/session_live/cell_component.ex +++ b/lib/livebook_web/live/session_live/cell_component.ex @@ -101,7 +101,6 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<.evaluation_outputs - :if={@output_location == :notebook} cell_view={@cell_view} session_id={@session_id} session_pid={@session_pid} @@ -196,7 +195,6 @@ defmodule LivebookWeb.SessionLive.CellComponent do js_view={@cell_view.js_view} session_id={@session_id} client_id={@client_id} - output_location={:notebook} /> <.cell_editor :if={@cell_view.editor} @@ -253,7 +251,6 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<.evaluation_outputs - :if={@output_location == :notebook} cell_view={@cell_view} session_id={@session_id} session_pid={@session_pid} @@ -663,7 +660,6 @@ defmodule LivebookWeb.SessionLive.CellComponent do session_pid={@session_pid} client_id={@client_id} cell_id={@cell_view.id} - output_location={:notebook} input_values={@cell_view.eval.input_values} />
diff --git a/lib/livebook_web/live/session_live/popout_window_live.ex b/lib/livebook_web/live/session_live/popout_window_live.ex new file mode 100644 index 00000000000..b84d937d2fb --- /dev/null +++ b/lib/livebook_web/live/session_live/popout_window_live.ex @@ -0,0 +1,75 @@ +defmodule LivebookWeb.SessionLive.PopoutWindowLive do + use LivebookWeb, :live_view + + alias Livebook.{Session, Sessions} + + @impl true + def mount(%{"id" => session_id}, _session, socket) do + {:ok, session} = Sessions.fetch_session(session_id) + + data = Session.get_data(session.pid) + + {:ok, + socket + |> assign( + session: session, + # TODO + client_id: "", + data_view: data_to_view(data) + ) + |> assign_private(data: data)} + end + + defp assign_private(socket, assigns) do + Enum.reduce(assigns, socket, fn {key, value}, socket -> + put_in(socket.private[key], value) + end) + end + + defp data_to_view(data) do + %{ + canvas_layout: canvas_outputs(data.notebook) + } + end + + @impl true + def render(assigns) do + ~H""" +
+ <.live_component + module={LivebookWeb.SessionLive.CanvasComponent} + id="canvas" + canvas_layout={@data_view.canvas_layout} + session={@session} + client_id={@client_id} + /> +
+
+ <.menu id="canvas-menu" position={:bottom_right}> + <:toggle> + + + <.menu_item> + + + <.menu_item> + + + +
+
+
+ """ + end +end diff --git a/lib/livebook_web/router.ex b/lib/livebook_web/router.ex index 9e85e0d71ad..c69ceb39c0a 100644 --- a/lib/livebook_web/router.ex +++ b/lib/livebook_web/router.ex @@ -81,7 +81,7 @@ defmodule LivebookWeb.Router do live "/hub/:id/secrets/edit/:secret_name", Hub.EditLive, :edit_secret, as: :hub live "/sessions/:id", SessionLive, :page - live "/sessions/:id/canvas", SessionLive.CanvasLive, :page + live "/sessions/:id/popout-window", SessionLive.PopoutWindowLive live "/sessions/:id/shortcuts", SessionLive, :shortcuts live "/sessions/:id/secrets", SessionLive, :secrets live "/sessions/:id/settings/runtime", SessionLive, :runtime_settings From e10655393806d562e53b6d80c9ce199f90d44705 Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Thu, 20 Jul 2023 07:50:49 +0200 Subject: [PATCH 29/36] Add gridstack to popped out window --- assets/css/js_interop.css | 2 -- assets/js/hooks/canvas.js | 7 ++++ lib/livebook_web/helpers.ex | 7 +++- .../live/session_live/popout_window_live.ex | 32 +++++++++++++++++-- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index c444d079ea7..744b372f628 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -299,8 +299,6 @@ solely client-side operations. } [data-el-session]:not([data-js-view="canvas"]) [data-el-canvas], -[data-el-cell][data-el-js-cell-output-location-canvas] - [data-el-outputs-container], [data-el-cell][data-el-js-cell-output-location-canvas] [data-el-move-output-to-canvas-button], [data-el-cell]:not([data-el-js-cell-output-location-canvas]) diff --git a/assets/js/hooks/canvas.js b/assets/js/hooks/canvas.js index f2b48196511..589d8f1741c 100644 --- a/assets/js/hooks/canvas.js +++ b/assets/js/hooks/canvas.js @@ -20,6 +20,7 @@ const Canvas = { resizable: { handles: "all" }, margin: 0, cellHeight: "4rem", + //handle: ".drag-handle", }; this.grid = GridStack.init(options, this.el); @@ -34,6 +35,12 @@ const Canvas = { }); this.grid.on("added", (event, items) => { + items.forEach((item) => { + const output_id = `[id^=outputs-${item.id}]`; + const output_el = document.querySelector(output_id); + output_el._notebookLocation = output_el.parentNode; + item.el.firstChild.appendChild(output_el); + }); console.log("ADDED", items); }); diff --git a/lib/livebook_web/helpers.ex b/lib/livebook_web/helpers.ex index f95e0764ec7..72dbc1c7ec2 100644 --- a/lib/livebook_web/helpers.ex +++ b/lib/livebook_web/helpers.ex @@ -86,6 +86,10 @@ defmodule LivebookWeb.Helpers do date |> DateTime.to_naive() |> Livebook.Utils.Time.time_ago_in_words() end + @doc """ + TODO + """ + @spec canvas_outputs(Notebook.t()) :: list({Cell.id(), map()}) def canvas_outputs(notebook) do for {cell, section} <- Notebook.cells_with_section(notebook), Cell.evaluable?(cell), @@ -93,7 +97,8 @@ defmodule LivebookWeb.Helpers do into: %{} do content = "#{section.name}\n#{cell.id}" - {cell.id, Map.put(cell.output_location, :content, content)} + # {cell.id, Map.put(cell.output_location, :content, content)} + {cell.id, cell.output_location} end end end diff --git a/lib/livebook_web/live/session_live/popout_window_live.ex b/lib/livebook_web/live/session_live/popout_window_live.ex index b84d937d2fb..ac9ac277617 100644 --- a/lib/livebook_web/live/session_live/popout_window_live.ex +++ b/lib/livebook_web/live/session_live/popout_window_live.ex @@ -1,7 +1,8 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do use LivebookWeb, :live_view - alias Livebook.{Session, Sessions} + alias Livebook.{Notebook, Session, Sessions} + alias Livebook.Notebook.Cell @impl true def mount(%{"id" => session_id}, _session, socket) do @@ -28,7 +29,18 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do defp data_to_view(data) do %{ - canvas_layout: canvas_outputs(data.notebook) + canvas_layout: canvas_outputs(data.notebook), + output_views: + for {cell, _section} <- Notebook.cells_with_section(data.notebook), + Cell.evaluable?(cell), + cell.id != "setup" do + %{ + outputs: cell.outputs, + # input_values: input_values_for_output(cell.outputs, data), + input_values: %{}, + cell_id: cell.id + } + end } end @@ -36,6 +48,20 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do def render(assigns) do ~H"""
+
+ +
<.live_component module={LivebookWeb.SessionLive.CanvasComponent} id="canvas" @@ -56,7 +82,7 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do <.menu_item> From 59a66f10205b48cea241df5738ef78eccb5bf3a1 Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Thu, 20 Jul 2023 11:34:13 +0200 Subject: [PATCH 30/36] Add show canvas options to app settings --- assets/css/js_interop.css | 8 +++++++ assets/js/hooks/canvas.js | 2 +- assets/js/hooks/js_view.js | 5 +---- assets/js/hooks/popout_window.js | 2 -- assets/js/hooks/session.js | 22 +++++++++++++++---- lib/livebook/notebook/cell/smart.ex | 3 +-- lib/livebook_web/live/js_view_component.ex | 1 - lib/livebook_web/live/session_live.ex | 4 ++-- .../session_live/app_settings_component.ex | 16 ++++++++++++++ .../live/session_live/canvas_component.ex | 5 ----- .../live/session_live/cell_component.ex | 3 ++- .../live/session_live/popout_window_live.ex | 1 + 12 files changed, 50 insertions(+), 22 deletions(-) diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index 744b372f628..11e3e8a68ff 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -286,6 +286,14 @@ solely client-side operations. @apply hidden; } +[data-el-session][data-js-view^="canvas"] ~ #app-settings-modal [data-el-app-settings-enable-canvas-button] { + @apply hidden; +} + +[data-el-session]:not([data-js-view="canvas-popped-out"]) ~ #app-settings-modal [data-el-app-settings-popin-canvas-button] { + @apply hidden; +} + /* === Views === */ [data-el-session]:not([data-js-view]) [data-el-view-deactivate-button] { diff --git a/assets/js/hooks/canvas.js b/assets/js/hooks/canvas.js index 589d8f1741c..1288b5cf698 100644 --- a/assets/js/hooks/canvas.js +++ b/assets/js/hooks/canvas.js @@ -56,7 +56,7 @@ const Canvas = { return acc; }, {}); self.pushEventTo(self.props.phxTarget, "items_changed", new_items); - self.repositionIframe(); + //self.repositionIframe(); }); this.grid.on("removed", (event, items) => { diff --git a/assets/js/hooks/js_view.js b/assets/js/hooks/js_view.js index b8b8fefeba4..bd4f47cae3f 100644 --- a/assets/js/hooks/js_view.js +++ b/assets/js/hooks/js_view.js @@ -194,7 +194,6 @@ const JSView = { ), iframeUrl: getAttributeOrDefault(this.el, "data-iframe-url", null), timeoutMessage: getAttributeOrThrow(this.el, "data-timeout-message"), - outputLocation: getAttributeOrThrow(this.el, "data-output-location"), }; }, @@ -328,9 +327,7 @@ const JSView = { }, loadIframe() { - const iframesEl = document.querySelector( - `[data-el-js-view-iframes]#js-view-iframes-${this.props.outputLocation}` - ); + const iframesEl = document.querySelector(`[data-el-js-view-iframes]`); initializeIframeSource( this.iframe, this.props.iframePort, diff --git a/assets/js/hooks/popout_window.js b/assets/js/hooks/popout_window.js index 716259afd62..a9c730e7b45 100644 --- a/assets/js/hooks/popout_window.js +++ b/assets/js/hooks/popout_window.js @@ -29,12 +29,10 @@ const PopoutWindow = { handleCanvasCloseClick() { window.removeEventListener("beforeunload", this.handleBeforeUnloadEvent); this.sendToParent("canvas_close_clicked"); - window.close(); }, handleCanvasPopinClick() { window.removeEventListener("beforeunload", this.handleBeforeUnloadEvent); this.sendToParent("canvas_popin_clicked"); - window.close(); }, getElement(name) { return document.querySelector(`[data-el-${name}]`); diff --git a/assets/js/hooks/session.js b/assets/js/hooks/session.js index 297bed52738..dce16cf1d24 100644 --- a/assets/js/hooks/session.js +++ b/assets/js/hooks/session.js @@ -95,12 +95,20 @@ const Session = { document.addEventListener("focus", this._handleDocumentFocus, true); document.addEventListener("click", this._handleDocumentClick); - document.addEventListener("canvas:show", (event) => { + this.el.addEventListener("canvas:show", (event) => { if (this.el.getAttribute("data-js-view") != "canvas-popped-out") { this.el.setAttribute("data-js-view", "canvas"); } }); + this.el.addEventListener("canvas:popin", (event) => { + this.handleCanvasPopinClick(); + }); + + this.el.addEventListener("canvas:enable", (event) => { + this.el.setAttribute("data-js-view", "canvas"); + }); + this.getElement("sections-list").addEventListener("click", (event) => { this.handleSectionsListClick(event); this.handleCellIndicatorsClick(event); @@ -164,7 +172,7 @@ const Session = { this.getElement("view-canvas-poppedout-button").addEventListener( "click", (event) => { - this.handleViewCanvasPoppedoutClick(); + this.handleCanvasPopinClick(); } ); @@ -695,7 +703,7 @@ const Session = { this.el.setAttribute("data-js-view", "canvas-popped-out"); }, - handleViewCanvasPoppedoutClick() { + handleCanvasPopinClick() { this.closeCanvasWindow(); this.el.setAttribute("data-js-view", "canvas"); }, @@ -1390,6 +1398,10 @@ const Session = { return this.el.querySelector(`[data-el-${name}]`); }, + isViewCanvas() { + return this.view.startsWith("canvas"); + }, + isViewCodeZen() { return this.view === "code-zen"; }, @@ -1397,16 +1409,18 @@ const Session = { isViewPresentation() { return this.view === "presentation"; }, + closeCanvasWindow() { window.removeEventListener("message", this.handleCanvasWindowMessage); this.canvasWindow && this.canvasWindow.close(); this.canvasWindow = null; }, + handleCanvasWindowMessage(event) { if (event.origin != window.location.origin) return; switch (event.data) { case "canvas_popin_clicked": - this.el.setAttribute("data-js-view", "canvas"); + this.handleCanvasPopinClick(); break; case "canvas_close_clicked": this.handleCanvasCloseClick(); diff --git a/lib/livebook/notebook/cell/smart.ex b/lib/livebook/notebook/cell/smart.ex index 301046f1b5c..30cf5569405 100644 --- a/lib/livebook/notebook/cell/smart.ex +++ b/lib/livebook/notebook/cell/smart.ex @@ -13,8 +13,7 @@ defmodule Livebook.Notebook.Cell.Smart do source: String.t() | :__pruned__, chunks: Livebook.Runtime.chunks() | nil, outputs: list(Cell.indexed_output()), - # TODO - output_location: nil, + output_location: Cell.canvas_sprite() | nil, kind: String.t() | nil, attrs: attrs() | :__pruned__, js_view: Livebook.Runtime.js_view() | nil, diff --git a/lib/livebook_web/live/js_view_component.ex b/lib/livebook_web/live/js_view_component.ex index 9fce44cbe27..e853d3cf9e7 100644 --- a/lib/livebook_web/live/js_view_component.ex +++ b/lib/livebook_web/live/js_view_component.ex @@ -25,7 +25,6 @@ defmodule LivebookWeb.JSViewComponent do data-iframe-local-port={LivebookWeb.IframeEndpoint.port()} data-iframe-url={Livebook.Config.iframe_url()} data-timeout-message={@timeout_message} - data-output-location={@output_location} >
""" diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index d942f453602..209a4b0e976 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -240,7 +240,7 @@ defmodule LivebookWeb.SessionLive do
-
+
+
+ <.link + data-el-app-settings-enable-canvas-button + phx-click={JS.dispatch("canvas:enable", to: "[data-el-session]")} + > + <.remix_icon icon="arrow-right-line" class="text-sm" /> + Enable the Canvas view + + <.link + data-el-app-settings-popin-canvas-button + phx-click={JS.dispatch("canvas:popin", to: "[data-el-session]")} + > + <.remix_icon icon="arrow-right-line" /> + Bring Canvas to front + +
<.checkbox_field field={f[:show_source]} label="Show source" diff --git a/lib/livebook_web/live/session_live/canvas_component.ex b/lib/livebook_web/live/session_live/canvas_component.ex index e62c43ba87f..85ccf3f8cbd 100644 --- a/lib/livebook_web/live/session_live/canvas_component.ex +++ b/lib/livebook_web/live/session_live/canvas_component.ex @@ -50,9 +50,4 @@ defmodule LivebookWeb.SessionLive.CanvasComponent do end defp convert(value), do: value - - @impl true - def handle_info({:new_output, cell_id}, socket) do - {:noreply, push_event(socket, "added", %{payload: "TODO"})} - end end diff --git a/lib/livebook_web/live/session_live/cell_component.ex b/lib/livebook_web/live/session_live/cell_component.ex index 06bf14e77ca..ca001908f9a 100644 --- a/lib/livebook_web/live/session_live/cell_component.ex +++ b/lib/livebook_web/live/session_live/cell_component.ex @@ -9,7 +9,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<.delete_cell_button cell_id={@cell_view.id} /> <.move_output_to_canvas_button cell_id={@cell_view.id} /> + <.move_output_to_notebook_button cell_id={@cell_view.id} /> <.cell_body> diff --git a/lib/livebook_web/live/session_live/popout_window_live.ex b/lib/livebook_web/live/session_live/popout_window_live.ex index ac9ac277617..be6756ff9e5 100644 --- a/lib/livebook_web/live/session_live/popout_window_live.ex +++ b/lib/livebook_web/live/session_live/popout_window_live.ex @@ -48,6 +48,7 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do def render(assigns) do ~H"""
+
Date: Thu, 20 Jul 2023 12:48:27 +0200 Subject: [PATCH 31/36] Add canvas deployment --- assets/css/js_interop.css | 8 +- assets/js/hooks/app_canvas.js | 36 +++++++ assets/js/hooks/canvas.js | 6 +- assets/js/hooks/gridstack_static.js | 40 -------- assets/js/hooks/index.js | 4 +- lib/livebook_web/live/app_session_live.ex | 20 ++-- .../live/session_live/popout_window_live.ex | 96 ++++++++++--------- 7 files changed, 111 insertions(+), 99 deletions(-) create mode 100644 assets/js/hooks/app_canvas.js delete mode 100644 assets/js/hooks/gridstack_static.js diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index 11e3e8a68ff..783554307e6 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -286,11 +286,15 @@ solely client-side operations. @apply hidden; } -[data-el-session][data-js-view^="canvas"] ~ #app-settings-modal [data-el-app-settings-enable-canvas-button] { +[data-el-session][data-js-view^="canvas"] + ~ #app-settings-modal + [data-el-app-settings-enable-canvas-button] { @apply hidden; } -[data-el-session]:not([data-js-view="canvas-popped-out"]) ~ #app-settings-modal [data-el-app-settings-popin-canvas-button] { +[data-el-session]:not([data-js-view="canvas-popped-out"]) + ~ #app-settings-modal + [data-el-app-settings-popin-canvas-button] { @apply hidden; } diff --git a/assets/js/hooks/app_canvas.js b/assets/js/hooks/app_canvas.js new file mode 100644 index 00000000000..0f72887e63c --- /dev/null +++ b/assets/js/hooks/app_canvas.js @@ -0,0 +1,36 @@ +import "gridstack/dist/gridstack.min.css"; +import { GridStack } from "gridstack"; + +/** + * A hook for creating app dashboard. + */ +const AppCanvas = { + mounted() { + const self = this; + + const options = { + staticGrid: true, + float: true, + margin: 0, + cellHeight: "4rem", + }; + + this.grid = GridStack.init(options, this.el); + + this.handleEvent("init", ({ payload }) => { + const grid_items = Object.entries(payload).map(([id, value]) => ({ + id, + ...value, + })); + this.grid.load(grid_items); + console.log(this.grid.el.children); + Array.from(this.grid.el.children).forEach((item) => { + const output_id = `[id^=outputs-${item.id}]`; + const output_el = document.querySelector(output_id); + item.firstChild.appendChild(output_el); + }); + }); + }, +}; + +export default AppCanvas; diff --git a/assets/js/hooks/canvas.js b/assets/js/hooks/canvas.js index 1288b5cf698..7637ef7a4e7 100644 --- a/assets/js/hooks/canvas.js +++ b/assets/js/hooks/canvas.js @@ -44,7 +44,7 @@ const Canvas = { console.log("ADDED", items); }); - this.grid.on("change", function(event, items) { + this.grid.on("change", function (event, items) { console.log("ITEMS changed: ", items); let new_items = items.reduce((acc, item) => { acc[item.id] = { @@ -56,14 +56,14 @@ const Canvas = { return acc; }, {}); self.pushEventTo(self.props.phxTarget, "items_changed", new_items); - //self.repositionIframe(); + self.repositionIframe(); }); this.grid.on("removed", (event, items) => { console.log("REMOVED", event); }); - this.grid.on("drag", function(event, item) { + this.grid.on("drag", function (event, item) { // TODO update iframe position when dragging //self.repositionIframe(); }); diff --git a/assets/js/hooks/gridstack_static.js b/assets/js/hooks/gridstack_static.js deleted file mode 100644 index 9a8a3298ec4..00000000000 --- a/assets/js/hooks/gridstack_static.js +++ /dev/null @@ -1,40 +0,0 @@ -import "gridstack/dist/gridstack.min.css"; -import { GridStack } from "gridstack"; - -/** - * A hook for creating app dashboard. - */ -const GridstackStatic = { - mounted() { - const self = this; - - const options = { - staticGrid: true, - float: true, - margin: 0, - cellHeight: "4rem", - }; - - this.grid = GridStack.init(options, this.el); - - this.handleEvent("load_layout", function ({ layout }) { - if (layout) { - self.grid.load(layout); - self.mountOutputs(); - } - }); - }, - mountOutputs() { - const outputs = document.querySelectorAll("[data-el-output]"); - for (let output of outputs) { - const item_content = this.el.querySelector( - `[gs-id="${output.parentNode.id}"] .grid-stack-item-content` - ); - if (item_content) { - item_content.prepend(output); - } - } - }, -}; - -export default GridstackStatic; diff --git a/assets/js/hooks/index.js b/assets/js/hooks/index.js index ba647dbe095..0548d431d52 100644 --- a/assets/js/hooks/index.js +++ b/assets/js/hooks/index.js @@ -1,4 +1,5 @@ import AppAuth from "./app_auth"; +import AppCanvas from "./app_canvas"; import AudioInput from "./audio_input"; import Canvas from "./canvas"; import Cell from "./cell"; @@ -7,7 +8,6 @@ import Dropzone from "./dropzone"; import EditorSettings from "./editor_settings"; import EmojiPicker from "./emoji_picker"; import FocusOnUpdate from "./focus_on_update"; -import GridstackStatic from "./gridstack_static"; import Headline from "./headline"; import Highlight from "./highlight"; import ImageInput from "./image_input"; @@ -27,6 +27,7 @@ import VirtualizedLines from "./virtualized_lines"; export default { AppAuth, + AppCanvas, AudioInput, Canvas, Cell, @@ -35,7 +36,6 @@ export default { EditorSettings, EmojiPicker, FocusOnUpdate, - GridstackStatic, Headline, Highlight, ImageInput, diff --git a/lib/livebook_web/live/app_session_live.ex b/lib/livebook_web/live/app_session_live.ex index 6f5869c0fc1..db746580f9e 100644 --- a/lib/livebook_web/live/app_session_live.ex +++ b/lib/livebook_web/live/app_session_live.ex @@ -31,6 +31,8 @@ defmodule LivebookWeb.AppSessionLive do {data, nil} end + data_view = data_to_view(data) + {:ok, socket |> assign( @@ -38,9 +40,10 @@ defmodule LivebookWeb.AppSessionLive do session: session, page_title: get_page_title(data.notebook.name), client_id: client_id, - data_view: data_to_view(data) + data_view: data_view ) - |> assign_private(data: data)} + |> assign_private(data: data) + |> push_event("init", %{payload: data_view.canvas_layout})} else {:ok, assign(socket, @@ -94,14 +97,16 @@ defmodule LivebookWeb.AppSessionLive do """ end - def render(%{data_view: %{output_type: :dashboard}} = assigns) + def render(%{data_view: %{output_type: :canvas}} = assigns) when assigns.app_authenticated? do ~H"""
-
-
-
+
+
+
""" @@ -316,6 +322,7 @@ defmodule LivebookWeb.AppSessionLive do defp data_to_view(data) do %{ notebook_name: data.notebook.name, + canvas_layout: canvas_outputs(data.notebook), output_views: for( {cell_id, output} <- visible_outputs(data.notebook), @@ -349,6 +356,7 @@ defmodule LivebookWeb.AppSessionLive do defp filter_outputs(outputs, :all), do: outputs defp filter_outputs(outputs, :rich), do: rich_outputs(outputs) + defp filter_outputs(outputs, :canvas), do: outputs defp rich_outputs(outputs) do for output <- outputs, output = filter_output(output), do: output diff --git a/lib/livebook_web/live/session_live/popout_window_live.ex b/lib/livebook_web/live/session_live/popout_window_live.ex index be6756ff9e5..bb267693d48 100644 --- a/lib/livebook_web/live/session_live/popout_window_live.ex +++ b/lib/livebook_web/live/session_live/popout_window_live.ex @@ -48,52 +48,56 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do def render(assigns) do ~H"""
-
-
- -
- <.live_component - module={LivebookWeb.SessionLive.CanvasComponent} - id="canvas" - canvas_layout={@data_view.canvas_layout} - session={@session} - client_id={@client_id} - /> -
-
- <.menu id="canvas-menu" position={:bottom_right}> - <:toggle> - - - <.menu_item> - - - <.menu_item> - - - +
+
+
+
+ +
+ <.live_component + module={LivebookWeb.SessionLive.CanvasComponent} + id="canvas" + canvas_layout={@data_view.canvas_layout} + session={@session} + client_id={@client_id} + /> +
+
+ <.menu id="canvas-menu" position={:bottom_right}> + <:toggle> + + + <.menu_item> + + + <.menu_item> + + + +
+
From 0859c43856c3f6b2b3bff2c0c74e2ade36307e6f Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Thu, 20 Jul 2023 12:52:37 +0200 Subject: [PATCH 32/36] Remove canvas settings --- lib/livebook/notebook.ex | 2 +- lib/livebook/notebook/canvas_settings.ex | 34 ------------------------ 2 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 lib/livebook/notebook/canvas_settings.ex diff --git a/lib/livebook/notebook.ex b/lib/livebook/notebook.ex index 12dcb7d22bb..abafcf16809 100644 --- a/lib/livebook/notebook.ex +++ b/lib/livebook/notebook.ex @@ -30,7 +30,7 @@ defmodule Livebook.Notebook do :teams_enabled ] - alias Livebook.Notebook.{Section, Cell, AppSettings, CanvasSettings} + alias Livebook.Notebook.{Section, Cell, AppSettings} alias Livebook.Utils.Graph import Livebook.Utils, only: [access_by_id: 1] diff --git a/lib/livebook/notebook/canvas_settings.ex b/lib/livebook/notebook/canvas_settings.ex deleted file mode 100644 index 06e47bf66fc..00000000000 --- a/lib/livebook/notebook/canvas_settings.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule Livebook.Notebook.CanvasSettings do - @moduledoc false - - # Data structure representing a canvas of the notebook. - - defstruct [:items] - - alias Livebook.Utils - alias Livebook.Notebook.Cell - - @type id :: Utils.id() - - @type t :: %__MODULE__{ - items: %{id() => item()} - } - - @type item :: %{ - x: non_neg_integer(), - y: non_neg_integer(), - w: non_neg_integer(), - h: non_neg_integer(), - outputs: list(Cell.indexed_output()) - } - - @doc """ - Returns a blank canvas. - """ - @spec new() :: t() - def new() do - %__MODULE__{ - items: %{} - } - end -end From d30b795693044f230242e493eeef7bea01a7879a Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Thu, 20 Jul 2023 14:12:27 +0200 Subject: [PATCH 33/36] Get session updates in popout window --- assets/js/hooks/canvas.js | 18 +++------- lib/livebook/notebook/cell.ex | 2 +- lib/livebook/notebook/cell/code.ex | 2 +- lib/livebook/notebook/cell/smart.ex | 2 +- .../live/session_live/canvas_component.ex | 2 +- .../live/session_live/popout_window_live.ex | 35 +++++++++++++++++++ 6 files changed, 44 insertions(+), 17 deletions(-) diff --git a/assets/js/hooks/canvas.js b/assets/js/hooks/canvas.js index 7637ef7a4e7..c89bc290b41 100644 --- a/assets/js/hooks/canvas.js +++ b/assets/js/hooks/canvas.js @@ -13,35 +13,27 @@ const Canvas = { const self = this; const options = { - //acceptWidgets: true, - //removeable: true, styleInHead: true, float: true, resizable: { handles: "all" }, margin: 0, cellHeight: "4rem", - //handle: ".drag-handle", }; this.grid = GridStack.init(options, this.el); - this.handleEvent("init", ({ payload }) => { + this.handleEvent("reload", ({ payload }) => { const grid_items = Object.entries(payload).map(([id, value]) => ({ id, ...value, })); - console.log("LAYOUT", grid_items); this.grid.load(grid_items); - }); - - this.grid.on("added", (event, items) => { - items.forEach((item) => { - const output_id = `[id^=outputs-${item.id}]`; + Array.from(this.grid.el.children).forEach((item) => { + console.log(item.attributes); + const output_id = `[id^=outputs-${item.attributes["gs-id"].value}]`; const output_el = document.querySelector(output_id); - output_el._notebookLocation = output_el.parentNode; - item.el.firstChild.appendChild(output_el); + item.firstChild.appendChild(output_el); }); - console.log("ADDED", items); }); this.grid.on("change", function (event, items) { diff --git a/lib/livebook/notebook/cell.ex b/lib/livebook/notebook/cell.ex index 02a3fea8186..504827e1212 100644 --- a/lib/livebook/notebook/cell.ex +++ b/lib/livebook/notebook/cell.ex @@ -18,7 +18,7 @@ defmodule Livebook.Notebook.Cell do @type indexed_output :: {non_neg_integer(), Livebook.Runtime.output()} - @type canvas_sprite :: %{ + @type canvas_location :: %{ x: non_neg_integer(), y: non_neg_integer(), w: non_neg_integer(), diff --git a/lib/livebook/notebook/cell/code.ex b/lib/livebook/notebook/cell/code.ex index b320774a22b..ef8b74fec9e 100644 --- a/lib/livebook/notebook/cell/code.ex +++ b/lib/livebook/notebook/cell/code.ex @@ -24,7 +24,7 @@ defmodule Livebook.Notebook.Cell.Code do id: Cell.id(), source: String.t() | :__pruned__, outputs: list(Cell.indexed_output()), - output_location: Cell.canvas_sprite() | nil, + output_location: Cell.canvas_location() | nil, language: :elixir | :erlang, disable_formatting: boolean(), reevaluate_automatically: boolean(), diff --git a/lib/livebook/notebook/cell/smart.ex b/lib/livebook/notebook/cell/smart.ex index 30cf5569405..e8a505b63fe 100644 --- a/lib/livebook/notebook/cell/smart.ex +++ b/lib/livebook/notebook/cell/smart.ex @@ -13,7 +13,7 @@ defmodule Livebook.Notebook.Cell.Smart do source: String.t() | :__pruned__, chunks: Livebook.Runtime.chunks() | nil, outputs: list(Cell.indexed_output()), - output_location: Cell.canvas_sprite() | nil, + output_location: Cell.canvas_location() | nil, kind: String.t() | nil, attrs: attrs() | :__pruned__, js_view: Livebook.Runtime.js_view() | nil, diff --git a/lib/livebook_web/live/session_live/canvas_component.ex b/lib/livebook_web/live/session_live/canvas_component.ex index 85ccf3f8cbd..e812524c60e 100644 --- a/lib/livebook_web/live/session_live/canvas_component.ex +++ b/lib/livebook_web/live/session_live/canvas_component.ex @@ -17,7 +17,7 @@ defmodule LivebookWeb.SessionLive.CanvasComponent do client_id: assigns.client_id, canvas_layout: assigns.canvas_layout ) - |> push_event("init", %{payload: assigns.canvas_layout}) + |> push_event("reload", %{payload: assigns.canvas_layout}) {:ok, socket} end diff --git a/lib/livebook_web/live/session_live/popout_window_live.ex b/lib/livebook_web/live/session_live/popout_window_live.ex index bb267693d48..f27f3be30e7 100644 --- a/lib/livebook_web/live/session_live/popout_window_live.ex +++ b/lib/livebook_web/live/session_live/popout_window_live.ex @@ -10,6 +10,8 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do data = Session.get_data(session.pid) + Session.subscribe(session_id) + {:ok, socket |> assign( @@ -103,4 +105,37 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do
""" end + + @impl true + def handle_info({:operation, operation}, socket) do + {:noreply, handle_operation(socket, operation)} + end + + @impl true + def handle_info(message, socket) do + IO.inspect(message, label: "Not implemented for Popout Window") + {:noreply, socket} + end + + defp handle_operation(socket, operation) do + case Session.Data.apply_operation(socket.private.data, operation) do + {:ok, data, actions} -> + socket + |> assign_private(data: data) + |> assign( + data_view: + update_data_view(socket.assigns.data_view, socket.private.data, data, operation) + ) + + :error -> + socket + end + end + + defp update_data_view(data_view, prev_data, data, operation) do + case operation do + _ -> + data_to_view(data) + end + end end From ff8b5312a36632f2c7ba13182f9dce99d5eff765 Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Thu, 20 Jul 2023 17:23:37 +0200 Subject: [PATCH 34/36] Some cleanup --- lib/livebook_web/helpers.ex | 4 +--- lib/livebook_web/live/app_session_live.ex | 14 ++------------ .../live/session_live/canvas_component.ex | 3 ++- .../live/session_live/popout_window_live.ex | 6 +++--- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/lib/livebook_web/helpers.ex b/lib/livebook_web/helpers.ex index 72dbc1c7ec2..62be6fa3187 100644 --- a/lib/livebook_web/helpers.ex +++ b/lib/livebook_web/helpers.ex @@ -91,13 +91,11 @@ defmodule LivebookWeb.Helpers do """ @spec canvas_outputs(Notebook.t()) :: list({Cell.id(), map()}) def canvas_outputs(notebook) do - for {cell, section} <- Notebook.cells_with_section(notebook), + for {cell, _section} <- Notebook.cells_with_section(notebook), Cell.evaluable?(cell), cell.output_location != nil, into: %{} do - content = "#{section.name}\n#{cell.id}" - # {cell.id, Map.put(cell.output_location, :content, content)} {cell.id, cell.output_location} end end diff --git a/lib/livebook_web/live/app_session_live.ex b/lib/livebook_web/live/app_session_live.ex index db746580f9e..a4da5e9237f 100644 --- a/lib/livebook_web/live/app_session_live.ex +++ b/lib/livebook_web/live/app_session_live.ex @@ -114,7 +114,6 @@ defmodule LivebookWeb.AppSessionLive do session_pid={@session.pid} client_id={@client_id} cell_id={output_view.cell_id} - output_location={:canvas} input_values={output_view.input_values} />
@@ -162,7 +161,7 @@ defmodule LivebookWeb.AppSessionLive do
-
+

<%= @data_view.notebook_name %> @@ -183,7 +182,6 @@ defmodule LivebookWeb.AppSessionLive do session_pid={@session.pid} client_id={@client_id} cell_id={output_view.cell_id} - output_location={:notebook} input_values={output_view.input_values} />

@@ -349,8 +347,7 @@ defmodule LivebookWeb.AppSessionLive do for section <- Enum.reverse(notebook.sections), cell <- Enum.reverse(section.cells), Cell.evaluable?(cell), - output <- - filter_outputs(cell.outputs, notebook.app_settings.output_type), + output <- filter_outputs(cell.outputs, notebook.app_settings.output_type), do: {cell.id, output} end @@ -395,13 +392,6 @@ defmodule LivebookWeb.AppSessionLive do defp filter_output(_output), do: nil - defp get_dashboard_layout(dashboard_items) do - for {id, rest} <- dashboard_items do - rest = %{"x" => rest[:x], "y" => rest[:y], "w" => rest[:w], "h" => rest[:h]} - Map.put(rest, "id", id) - end - end - defp show_app_status?(%{execution: :executed, lifecycle: :active}), do: false defp show_app_status?(_status), do: true end diff --git a/lib/livebook_web/live/session_live/canvas_component.ex b/lib/livebook_web/live/session_live/canvas_component.ex index e812524c60e..01a7f89efd9 100644 --- a/lib/livebook_web/live/session_live/canvas_component.ex +++ b/lib/livebook_web/live/session_live/canvas_component.ex @@ -1,7 +1,7 @@ defmodule LivebookWeb.SessionLive.CanvasComponent do use LivebookWeb, :live_component - alias Livebook.{Session, Sessions} + alias Livebook.Session @impl true def mount(socket) do @@ -40,6 +40,7 @@ defmodule LivebookWeb.SessionLive.CanvasComponent do @impl true def handle_event("items_changed", params, socket) do items = params |> Enum.map(fn {k, v} -> {k, convert(v)} end) |> Enum.into(%{}) + IO.inspect(items, label: "ITEMS CHANGED") Session.update_canvas(socket.assigns.session.pid, items) {:noreply, socket} end diff --git a/lib/livebook_web/live/session_live/popout_window_live.ex b/lib/livebook_web/live/session_live/popout_window_live.ex index f27f3be30e7..e5b706c5f79 100644 --- a/lib/livebook_web/live/session_live/popout_window_live.ex +++ b/lib/livebook_web/live/session_live/popout_window_live.ex @@ -119,12 +119,12 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do defp handle_operation(socket, operation) do case Session.Data.apply_operation(socket.private.data, operation) do - {:ok, data, actions} -> + {:ok, data, _actions} -> socket |> assign_private(data: data) |> assign( data_view: - update_data_view(socket.assigns.data_view, socket.private.data, data, operation) + update_data_view(data, operation) ) :error -> @@ -132,7 +132,7 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do end end - defp update_data_view(data_view, prev_data, data, operation) do + defp update_data_view(data, operation) do case operation do _ -> data_to_view(data) From 992e9fd52a9f348898454932a1e0a41c4f874d16 Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Thu, 20 Jul 2023 20:04:59 +0200 Subject: [PATCH 35/36] Add client_id to popout window & handle /popout-window navigation --- lib/livebook_web/helpers.ex | 1 - .../live/session_live/popout_window_live.ex | 84 +++++++++++-------- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/lib/livebook_web/helpers.ex b/lib/livebook_web/helpers.ex index 62be6fa3187..a4161d565c4 100644 --- a/lib/livebook_web/helpers.ex +++ b/lib/livebook_web/helpers.ex @@ -95,7 +95,6 @@ defmodule LivebookWeb.Helpers do Cell.evaluable?(cell), cell.output_location != nil, into: %{} do - {cell.id, cell.output_location} end end diff --git a/lib/livebook_web/live/session_live/popout_window_live.ex b/lib/livebook_web/live/session_live/popout_window_live.ex index e5b706c5f79..9f0cd202f63 100644 --- a/lib/livebook_web/live/session_live/popout_window_live.ex +++ b/lib/livebook_web/live/session_live/popout_window_live.ex @@ -5,22 +5,43 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do alias Livebook.Notebook.Cell @impl true - def mount(%{"id" => session_id}, _session, socket) do - {:ok, session} = Sessions.fetch_session(session_id) + def mount(%{"id" => session_id, "type" => type}, _session, socket) do + case Sessions.fetch_session(session_id) do + {:ok, %{pid: session_pid}} -> + {data, client_id} = + if connected?(socket) do + {data, client_id} = + Session.register_client(session_pid, self(), socket.assigns.current_user) + + Session.subscribe(session_id) + + {data, client_id} + else + data = Session.get_data(session_pid) + {data, nil} + end - data = Session.get_data(session.pid) + session = Session.get_by_pid(session_pid) - Session.subscribe(session_id) + {:ok, + socket + |> assign( + session: session, + client_id: client_id, + type: type, + data_view: data_to_view(data) + ) + |> assign_private(data: data)} + + :error -> + # TODO: handle this error correclty + {:ok, redirect(socket, to: ~p"/")} + end + end - {:ok, - socket - |> assign( - session: session, - # TODO - client_id: "", - data_view: data_to_view(data) - ) - |> assign_private(data: data)} + @impl true + def mount(%{"id" => session_id}, _session, socket) do + {:ok, redirect(socket, to: ~p"/sessions/#{session_id}")} end defp assign_private(socket, assigns) do @@ -47,7 +68,7 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do end @impl true - def render(assigns) do + def render(%{type: "canvas"} = assigns) do ~H"""
@@ -108,7 +129,18 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do @impl true def handle_info({:operation, operation}, socket) do - {:noreply, handle_operation(socket, operation)} + socket = + case Session.Data.apply_operation(socket.private.data, operation) do + {:ok, data, _actions} -> + socket + |> assign_private(data: data) + |> assign(data_to_view(data)) + + :error -> + socket + end + + {:noreply, socket} end @impl true @@ -116,26 +148,4 @@ defmodule LivebookWeb.SessionLive.PopoutWindowLive do IO.inspect(message, label: "Not implemented for Popout Window") {:noreply, socket} end - - defp handle_operation(socket, operation) do - case Session.Data.apply_operation(socket.private.data, operation) do - {:ok, data, _actions} -> - socket - |> assign_private(data: data) - |> assign( - data_view: - update_data_view(data, operation) - ) - - :error -> - socket - end - end - - defp update_data_view(data, operation) do - case operation do - _ -> - data_to_view(data) - end - end end From ade354fd392fdb4140fbd57ac261a4f8be68ee39 Mon Sep 17 00:00:00 2001 From: Jannik Becher Date: Fri, 21 Jul 2023 10:15:37 +0200 Subject: [PATCH 36/36] Fix not rendering markdown --- assets/css/js_interop.css | 4 ---- .../live/session_live/cell_component.ex | 15 ++++++++++----- .../live/session_live/section_component.ex | 1 - 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index 783554307e6..df4a98e3e55 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -311,10 +311,6 @@ solely client-side operations. } [data-el-session]:not([data-js-view="canvas"]) [data-el-canvas], -[data-el-cell][data-el-js-cell-output-location-canvas] - [data-el-move-output-to-canvas-button], -[data-el-cell]:not([data-el-js-cell-output-location-canvas]) - [data-el-move-output-to-notebook-button], [data-el-session]:not([data-js-view^="canvas"]) [data-el-canvas-menu], [data-el-session]:not([data-js-view="canvas-popped-out"]) [data-el-canvas-popin-button], diff --git a/lib/livebook_web/live/session_live/cell_component.ex b/lib/livebook_web/live/session_live/cell_component.ex index ca001908f9a..dae82896759 100644 --- a/lib/livebook_web/live/session_live/cell_component.ex +++ b/lib/livebook_web/live/session_live/cell_component.ex @@ -9,7 +9,6 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<.move_cell_down_button cell_id={@cell_view.id} /> <.delete_cell_button cell_id={@cell_view.id} /> - <.move_output_to_canvas_button cell_id={@cell_view.id} /> - <.move_output_to_notebook_button cell_id={@cell_view.id} /> + <%= if @cell_view.eval.output_location do %> + <.move_output_to_notebook_button cell_id={@cell_view.id} /> + <% else %> + <.move_output_to_canvas_button cell_id={@cell_view.id} /> + <% end %> <.cell_body> @@ -179,8 +181,11 @@ defmodule LivebookWeb.SessionLive.CellComponent do <.move_cell_up_button cell_id={@cell_view.id} /> <.move_cell_down_button cell_id={@cell_view.id} /> <.delete_cell_button cell_id={@cell_view.id} /> - <.move_output_to_canvas_button cell_id={@cell_view.id} /> - <.move_output_to_notebook_button cell_id={@cell_view.id} /> + <%= if @cell_view.eval.output_location do %> + <.move_output_to_notebook_button cell_id={@cell_view.id} /> + <% else %> + <.move_output_to_canvas_button cell_id={@cell_view.id} /> + <% end %> <.cell_body> diff --git a/lib/livebook_web/live/session_live/section_component.ex b/lib/livebook_web/live/session_live/section_component.ex index dc51c22b8d1..93ed231e631 100644 --- a/lib/livebook_web/live/session_live/section_component.ex +++ b/lib/livebook_web/live/session_live/section_component.ex @@ -183,7 +183,6 @@ defmodule LivebookWeb.SessionLive.SectionComponent do installing?={@installing?} allowed_uri_schemes={@allowed_uri_schemes} cell_view={cell_view} - output_location={cell_view.eval.output_location} /> <.live_component module={LivebookWeb.SessionLive.InsertButtonsComponent}