diff --git a/assets/css/app.scss b/assets/css/app.scss index 5c2c9b5ca..f7775db94 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -1,5 +1,60 @@ /* This file is for your main application css. */ @import "./phoenix.css"; +@import "../node_modules/nprogress/nprogress.css"; + +/* LiveView specific classes for your customizations */ +.phx-no-feedback.invalid-feedback, +.phx-no-feedback .invalid-feedback { + display: none; +} + +.phx-click-loading { + opacity: 0.5; + transition: opacity 1s ease-out; +} + +.phx-disconnected{ + cursor: wait; +} +.phx-disconnected *{ + pointer-events: none; +} + +.phx-modal { + opacity: 1!important; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgb(0,0,0); + background-color: rgba(0,0,0,0.4); +} + +.phx-modal-content { + background-color: #fefefe; + margin: 15% auto; + padding: 20px; + border: 1px solid #888; + width: 80%; +} + +.phx-modal-close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.phx-modal-close:hover, +.phx-modal-close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + /* Alerts and form errors */ .alert { diff --git a/assets/js/app.js b/assets/js/app.js index af0059649..dfc0f9759 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -13,3 +13,23 @@ import "../css/app.scss" // import socket from "./socket" // import "phoenix_html" +import {Socket} from "phoenix" +import NProgress from "nprogress" +import {LiveSocket} from "phoenix_live_view" + +let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") +let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) + +// Show progress bar on live navigation and form submits +window.addEventListener("phx:page-loading-start", info => NProgress.start()) +window.addEventListener("phx:page-loading-stop", info => NProgress.done()) + +// connect if there are any LiveViews on the page +liveSocket.connect() + +// expose liveSocket on window for web console debug logs and latency simulation: +// >> liveSocket.enableDebug() +// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session +// >> liveSocket.disableLatencySim() +window.liveSocket = liveSocket + diff --git a/assets/js/socket.js b/assets/js/socket.js deleted file mode 100644 index 09929abcf..000000000 --- a/assets/js/socket.js +++ /dev/null @@ -1,63 +0,0 @@ -// NOTE: The contents of this file will only be executed if -// you uncomment its entry in "assets/js/app.js". - -// To use Phoenix channels, the first step is to import Socket, -// and connect at the socket path in "lib/web/endpoint.ex". -// -// Pass the token on params as below. Or remove it -// from the params if you are not using authentication. -import {Socket} from "phoenix" - -let socket = new Socket("/socket", {params: {token: window.userToken}}) - -// When you connect, you'll often need to authenticate the client. -// For example, imagine you have an authentication plug, `MyAuth`, -// which authenticates the session and assigns a `:current_user`. -// If the current user exists you can assign the user's token in -// the connection for use in the layout. -// -// In your "lib/web/router.ex": -// -// pipeline :browser do -// ... -// plug MyAuth -// plug :put_user_token -// end -// -// defp put_user_token(conn, _) do -// if current_user = conn.assigns[:current_user] do -// token = Phoenix.Token.sign(conn, "user socket", current_user.id) -// assign(conn, :user_token, token) -// else -// conn -// end -// end -// -// Now you need to pass this token to JavaScript. You can do so -// inside a script tag in "lib/web/templates/layout/app.html.eex": -// -// -// -// You will need to verify the user token in the "connect/3" function -// in "lib/web/channels/user_socket.ex": -// -// def connect(%{"token" => token}, socket, _connect_info) do -// # max_age: 1209600 is equivalent to two weeks in seconds -// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do -// {:ok, user_id} -> -// {:ok, assign(socket, :user, user_id)} -// {:error, reason} -> -// :error -// end -// end -// -// Finally, connect to the socket: -socket.connect() - -// Now that you are connected, you can join channels with a topic: -let channel = socket.channel("topic:subtopic", {}) -channel.join() - .receive("ok", resp => { console.log("Joined successfully", resp) }) - .receive("error", resp => { console.log("Unable to join", resp) }) - -export default socket diff --git a/assets/package-lock.json b/assets/package-lock.json index 7a844cef1..4357a42f3 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -5138,6 +5138,11 @@ "set-blocking": "~2.0.0" } }, + "nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha1-y480xTIT2JVyP8urkH6UIq28r7E=" + }, "nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", @@ -5471,6 +5476,9 @@ "phoenix_html": { "version": "file:../deps/phoenix_html" }, + "phoenix_live_view": { + "version": "file:../deps/phoenix_live_view" + }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", diff --git a/assets/package.json b/assets/package.json index 170b4d539..aa41dee12 100644 --- a/assets/package.json +++ b/assets/package.json @@ -8,7 +8,9 @@ }, "dependencies": { "phoenix": "file:../deps/phoenix", - "phoenix_html": "file:../deps/phoenix_html" + "phoenix_html": "file:../deps/phoenix_html", + "phoenix_live_view": "file:../deps/phoenix_live_view", + "nprogress": "^0.2.0" }, "devDependencies": { "@babel/core": "^7.0.0", diff --git a/config/config.exs b/config/config.exs index 0f63e81e9..41e7cd6d7 100644 --- a/config/config.exs +++ b/config/config.exs @@ -10,10 +10,10 @@ use Mix.Config # Configures the endpoint config :live_book, LiveBookWeb.Endpoint, url: [host: "localhost"], - secret_key_base: "xDSK5nu5cynBegaOd+XedDm30Z4iuRnesn/x0MRAv6uxFx+NYiFTNU1gpfq4IkQE", + secret_key_base: "9hHHeOiAA8wrivUfuS//jQMurHxoMYUtF788BQMx2KO7mYUE8rVrGGG09djBNQq7", render_errors: [view: LiveBookWeb.ErrorView, accepts: ~w(html json), layout: false], pubsub_server: LiveBook.PubSub, - live_view: [signing_salt: "1Z+NpfzU"] + live_view: [signing_salt: "mAPgPEM4"] # Configures Elixir's Logger config :logger, :console, diff --git a/lib/live_book_web.ex b/lib/live_book_web.ex index b68d2ad72..23abffb4a 100644 --- a/lib/live_book_web.ex +++ b/lib/live_book_web.ex @@ -41,12 +41,30 @@ defmodule LiveBookWeb do end end + def live_view do + quote do + use Phoenix.LiveView, + layout: {LiveBookWeb.LayoutView, "live.html"} + + unquote(view_helpers()) + end + end + + def live_component do + quote do + use Phoenix.LiveComponent + + unquote(view_helpers()) + end + end + def router do quote do use Phoenix.Router import Plug.Conn import Phoenix.Controller + import Phoenix.LiveView.Router end end @@ -61,6 +79,9 @@ defmodule LiveBookWeb do # Use all HTML functionality (forms, tags, etc) use Phoenix.HTML + # Import LiveView helpers (live_render, live_component, live_patch, etc) + import Phoenix.LiveView.Helpers + # Import basic rendering functionality (render, render_layout, etc) import Phoenix.View diff --git a/lib/live_book_web/endpoint.ex b/lib/live_book_web/endpoint.ex index a39a7daa3..a08036570 100644 --- a/lib/live_book_web/endpoint.ex +++ b/lib/live_book_web/endpoint.ex @@ -7,9 +7,12 @@ defmodule LiveBookWeb.Endpoint do @session_options [ store: :cookie, key: "_live_book_key", - signing_salt: "LZr3S4e9" + signing_salt: "SqUy8vWM" ] + socket "/live", Phoenix.LiveView.Socket, + websocket: [connect_info: [session: @session_options]] + # Serve at "/" the static files from "priv/static" directory. # # You should set gzip to true if you are running phx.digest diff --git a/lib/live_book_web/live/page_live.ex b/lib/live_book_web/live/page_live.ex new file mode 100644 index 000000000..2905f3c3b --- /dev/null +++ b/lib/live_book_web/live/page_live.ex @@ -0,0 +1,39 @@ +defmodule LiveBookWeb.PageLive do + use LiveBookWeb, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, query: "", results: %{})} + end + + @impl true + def handle_event("suggest", %{"q" => query}, socket) do + {:noreply, assign(socket, results: search(query), query: query)} + end + + @impl true + def handle_event("search", %{"q" => query}, socket) do + case search(query) do + %{^query => vsn} -> + {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")} + + _ -> + {:noreply, + socket + |> put_flash(:error, "No dependencies found matching \"#{query}\"") + |> assign(results: %{}, query: query)} + end + end + + defp search(query) do + if not LiveBookWeb.Endpoint.config(:code_reloader) do + raise "action disabled when not in development" + end + + for {app, desc, vsn} <- Application.started_applications(), + app = to_string(app), + String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"), + into: %{}, + do: {app, vsn} + end +end diff --git a/lib/live_book_web/live/page_live.html.leex b/lib/live_book_web/live/page_live.html.leex new file mode 100644 index 000000000..f116c9cb2 --- /dev/null +++ b/lib/live_book_web/live/page_live.html.leex @@ -0,0 +1,48 @@ +
+

Welcome to Phoenix!

+

Peace of mind from prototype to production

+ +
+ + + <%= for {app, _vsn} <- @results do %> + + <% end %> + + +
+
+ +
+
+

Resources

+ +
+
+

Help

+ +
+
diff --git a/lib/live_book_web/router.ex b/lib/live_book_web/router.ex index 78d74e655..b5d333aaa 100644 --- a/lib/live_book_web/router.ex +++ b/lib/live_book_web/router.ex @@ -4,7 +4,8 @@ defmodule LiveBookWeb.Router do pipeline :browser do plug :accepts, ["html"] plug :fetch_session - plug :fetch_flash + plug :fetch_live_flash + plug :put_root_layout, {LiveBookWeb.LayoutView, :root} plug :protect_from_forgery plug :put_secure_browser_headers end @@ -16,7 +17,7 @@ defmodule LiveBookWeb.Router do scope "/", LiveBookWeb do pipe_through :browser - get "/", PageController, :index + live "/", PageLive, :index end # Other scopes may use custom stacks. diff --git a/lib/live_book_web/templates/layout/app.html.eex b/lib/live_book_web/templates/layout/app.html.eex index 22235a675..09ffdadd3 100644 --- a/lib/live_book_web/templates/layout/app.html.eex +++ b/lib/live_book_web/templates/layout/app.html.eex @@ -1,31 +1,5 @@ - - - - - - - LiveBook · Phoenix Framework - "/> - - - -
-
- - -
-
-
- - - <%= @inner_content %> -
- - +
+ + + <%= @inner_content %> +
diff --git a/lib/live_book_web/templates/layout/live.html.leex b/lib/live_book_web/templates/layout/live.html.leex new file mode 100644 index 000000000..8dcded594 --- /dev/null +++ b/lib/live_book_web/templates/layout/live.html.leex @@ -0,0 +1,11 @@ +
+ + + + + <%= @inner_content %> +
diff --git a/lib/live_book_web/templates/layout/root.html.leex b/lib/live_book_web/templates/layout/root.html.leex new file mode 100644 index 000000000..d873fc458 --- /dev/null +++ b/lib/live_book_web/templates/layout/root.html.leex @@ -0,0 +1,28 @@ + + + + + + + <%= csrf_meta_tag() %> + <%= live_title_tag assigns[:page_title] || "LiveBook", suffix: " · Phoenix Framework" %> + "/> + + + +
+
+ + +
+
+ <%= @inner_content %> + + diff --git a/mix.exs b/mix.exs index 9eff2eb55..e407de340 100644 --- a/mix.exs +++ b/mix.exs @@ -34,6 +34,8 @@ defmodule LiveBook.MixProject do defp deps do [ {:phoenix, "~> 1.5.7"}, + {:phoenix_live_view, "~> 0.15.0"}, + {:floki, ">= 0.27.0", only: :test}, {:phoenix_html, "~> 2.11"}, {:phoenix_live_reload, "~> 1.2", only: :dev}, {:telemetry_metrics, "~> 0.4"}, diff --git a/mix.lock b/mix.lock index 9640166d0..31d2a80ec 100644 --- a/mix.lock +++ b/mix.lock @@ -3,11 +3,14 @@ "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "floki": {:hex, :floki, "0.29.0", "b1710d8c93a2f860dc2d7adc390dd808dc2fb8f78ee562304457b75f4c640881", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "008585ce64b9f74c07d32958ec9866f4b8a124bf4da1e2941b28e41384edaaad"}, + "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, "mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"}, "phoenix": {:hex, :phoenix, "1.5.7", "2923bb3af924f184459fe4fa4b100bd25fa6468e69b2803dfae82698269aa5e0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "774cd64417c5a3788414fdbb2be2eb9bcd0c048d9e6ad11a0c1fd67b7c0d0978"}, "phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.0", "f35f61c3f959c9a01b36defaa1f0624edd55b87e236b606664a556d6f72fd2e7", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "02c1007ae393f2b76ec61c1a869b1e617179877984678babde131d716f95b582"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.15.3", "70c7917e5c421e32d1a1c8ddf8123378bb741748cd8091eb9d557fb4be92a94f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cabcfb6738419a08600009219a5f0d861de97507fc1232121e1d5221aba849bd"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"}, "plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"}, diff --git a/test/live_book_web/live/page_live_test.exs b/test/live_book_web/live/page_live_test.exs new file mode 100644 index 000000000..1b20156ee --- /dev/null +++ b/test/live_book_web/live/page_live_test.exs @@ -0,0 +1,11 @@ +defmodule LiveBookWeb.PageLiveTest do + use LiveBookWeb.ConnCase + + import Phoenix.LiveViewTest + + test "disconnected and connected render", %{conn: conn} do + {:ok, page_live, disconnected_html} = live(conn, "/") + assert disconnected_html =~ "Welcome to Phoenix!" + assert render(page_live) =~ "Welcome to Phoenix!" + end +end