Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Live Data Source #84

Merged
merged 32 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
80821a0
First draft
APB9785 Oct 6, 2023
e947f81
Assigns form page
APB9785 Oct 20, 2023
6fd337d
Fixes
APB9785 Oct 27, 2023
0e6834f
Merge branch 'main' into apb/data-source
APB9785 Oct 27, 2023
60fa96e
remove new
APB9785 Oct 27, 2023
b7e0a9f
Merge branch 'main' into apb/data-source
APB9785 Nov 17, 2023
5ee28be
Show available variables for elixir code
APB9785 Nov 17, 2023
51137a3
Checkpoint
APB9785 Dec 9, 2023
1a216be
Save state
APB9785 Dec 11, 2023
68f88d1
Fix Assigns liveview
APB9785 Jan 5, 2024
60f3a3a
Remove old defaults from new path attrs
APB9785 Jan 6, 2024
dcfd4c2
Improve UX
APB9785 Jan 11, 2024
e6f58a6
Strip leading slash in path (with todo)
APB9785 Jan 22, 2024
9b9b275
assign key placeholder
APB9785 Feb 2, 2024
de67c4d
Merge branch 'main' into apb/data-source
APB9785 Feb 2, 2024
51d50e3
Sanitize assign key
APB9785 Feb 2, 2024
981bf1f
Shared pagination/sorting/search functionality (#102)
leandrocp Feb 6, 2024
94c4ae9
Update live_monaco_editor
leandrocp Feb 8, 2024
263b841
Add site selector to switch between available sites in the cluster (#…
alexandrexaviersm Feb 9, 2024
19f0b7a
UX (#106)
leandrocp Feb 12, 2024
30deb3e
Revert toggle sidebar
leandrocp Feb 13, 2024
604458b
small change to page title
leandrocp Feb 13, 2024
48f78f7
update beacon
leandrocp Feb 13, 2024
b22f9ed
Merge branch 'main' into apb/data-source
leandrocp Feb 13, 2024
7fe190f
unlock unused deps
leandrocp Feb 13, 2024
2d6b8f0
allow paths starting with /
leandrocp Feb 13, 2024
74300d2
fix edit existing path test
leandrocp Feb 14, 2024
0fb9e2b
mix format
leandrocp Feb 14, 2024
786e1a2
remove assigns_test
leandrocp Feb 14, 2024
83f6117
update beacon
leandrocp Feb 14, 2024
422e621
assigns test
leandrocp Feb 14, 2024
a7cf51c
guard against missing assign
leandrocp Feb 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions lib/beacon/live_admin/content.ex
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,56 @@ defmodule Beacon.LiveAdmin.Content do
def valid_error_statuses(site) do
call(site, Beacon.Content.ErrorPage, :valid_statuses, [])
end

def live_data_assign_formats(site) do
call(site, Beacon.Content, :live_data_assign_formats, [])
end

def change_live_data_path(site, live_data, attrs \\ %{}) do
call(site, Beacon.Content, :change_live_data_path, [live_data, attrs])
end

def change_live_data_assign(site, live_data_assign, attrs \\ %{}) do
call(site, Beacon.Content, :change_live_data_assign, [live_data_assign, attrs])
end

def create_live_data(site, attrs) do
call(site, Beacon.Content, :create_live_data, [attrs])
end

def create_assign_for_live_data(site, live_data, attrs) do
call(site, Beacon.Content, :create_assign_for_live_data, [live_data, attrs])
end

def get_live_data(site, path) do
call(site, Beacon.Content, :get_live_data, [site, path])
end

def live_data_for_site(site) do
call(site, Beacon.Content, :live_data_for_site, [site])
end

def live_data_paths_for_site(site) do
call(site, Beacon.Content, :live_data_paths_for_site, [site])
end

def live_data_paths_for_site(site, opts) do
call(site, Beacon.Content, :live_data_paths_for_site, [site, opts])
end

def update_live_data_path(site, live_data, attrs) do
call(site, Beacon.Content, :update_live_data_path, [live_data, attrs])
end

def update_live_data_assign(site, live_data_assign, attrs) do
call(site, Beacon.Content, :update_live_data_assign, [live_data_assign, attrs])
end

def delete_live_data(site, live_data) do
call(site, Beacon.Content, :delete_live_data, [live_data])
end

def delete_live_data_assign(site, live_data_assign) do
call(site, Beacon.Content, :delete_live_data_assign, [live_data_assign])
end
end
7 changes: 7 additions & 0 deletions lib/beacon/live_admin/live/home_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ defmodule Beacon.LiveAdmin.HomeLive do
Pages
</.link>

<.link
href={Beacon.LiveAdmin.Router.beacon_live_admin_path(@socket, site, "/live_data")}
class="whitespace-nowrap text-sm leading-5 py-3.5 font-bold tracking-widest text-center uppercase bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-blue-200 active:bg-blue-800 px-6 text-gray-50"
>
Live Data
</.link>

<.link
href={Beacon.LiveAdmin.Router.beacon_live_admin_path(@socket, site, "/error_pages")}
class="whitespace-nowrap text-sm leading-5 py-3.5 font-bold tracking-widest text-center uppercase bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-blue-200 active:bg-blue-800 px-6 text-gray-50"
Expand Down
320 changes: 320 additions & 0 deletions lib/beacon/live_admin/live/live_data_editor_live/assigns.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
defmodule Beacon.LiveAdmin.LiveDataEditorLive.Assigns do
@moduledoc false
use Beacon.LiveAdmin.PageBuilder

alias Beacon.LiveAdmin.Content

def menu_link("/live_data", :assigns), do: {:submenu, "Live Data"}
def menu_link(_, _), do: :skip

def handle_params(params, _url, socket) do
path = URI.decode_www_form(params["path"])

socket =
socket
|> assign(live_data: Content.get_live_data(socket.assigns.beacon_page.site, path))
|> assign(unsaved_changes: false)
|> assign(show_nav_modal: false)
|> assign(show_create_modal: false)
|> assign(show_delete_modal: false)
|> assign(new_assign_form: to_form(%{"key" => ""}))
|> assign(page_title: "Live Data")
|> assign_selected(params["key"])
|> assign_form()

{:noreply, socket}
end

def handle_event("select-" <> key, _, socket) do
%{live_data: %{site: site, path: path}} = socket.assigns

path =
beacon_live_admin_path(
socket,
site,
"/live_data/#{sanitize(path)}/#{sanitize(key)}"
)

if socket.assigns.unsaved_changes do
{:noreply, assign(socket, show_nav_modal: true, confirm_nav_path: path)}
else
{:noreply, push_redirect(socket, to: path)}
end
end

def handle_event("live_data_assign_editor_lost_focus", %{"value" => value}, socket) do
%{selected: selected, live_data: %{site: site}, form: form} = socket.assigns

changeset =
site
|> Content.change_live_data_assign(selected, %{
"value" => value,
"key" => form.params["key"] || Map.fetch!(form.data, :key),
"format" => form.params["format"] || Map.fetch!(form.data, :format)
})
|> Map.put(:action, :validate)

socket =
socket
|> assign(form: to_form(changeset))
|> assign(changed_value: value)
|> assign(unsaved_changes: !(changeset.changes == %{}))

{:noreply, socket}
end

def handle_event("validate", %{"live_data_assign" => params}, socket) do
%{selected: selected, live_data: %{site: site}} = socket.assigns

changeset =
site
|> Content.change_live_data_assign(selected, params)
|> Map.put(:action, :validate)

socket =
socket
|> assign(form: to_form(changeset))
|> assign(unsaved_changes: !(changeset.changes == %{}))

{:noreply, socket}
end

def handle_event("set_value", %{"value" => value}, socket) do
%{selected: selected, beacon_page: %{site: site}, form: form} = socket.assigns

params = Map.merge(form.params, %{"value" => value})
changeset = Content.change_live_data_assign(site, selected, params)

socket =
socket
|> assign_form(changeset)
|> assign(unsaved_changes: !(changeset.changes == %{}))

{:noreply, socket}
end

def handle_event("save_changes", %{"live_data_assign" => params}, socket) do
%{selected: selected, live_data: %{site: site, path: live_data_path}} = socket.assigns

attrs = %{key: params["key"], value: params["value"], format: params["format"]}

socket =
case Content.update_live_data_assign(site, selected, attrs) do
{:ok, live_data_assign} ->
path =
beacon_live_admin_path(
socket,
site,
"/live_data/#{sanitize(live_data_path)}/#{sanitize(live_data_assign.key)}"
)

socket
|> assign(live_data: Content.get_live_data(site, live_data_path))
|> assign_form()
|> assign(unsaved_changes: false)
|> push_patch(to: path)

{:error, changeset} ->
changeset = Map.put(changeset, :action, :update)
assign(socket, form: to_form(changeset))
end

{:noreply, socket}
end

def handle_event("show_create_modal", _params, socket) do
{:noreply, assign(socket, show_create_modal: true)}
end

def handle_event("submit_new", params, socket) do
%{live_data: %{site: site} = live_data, selected: selected} = socket.assigns
selected = selected || %{key: nil}

attrs = %{key: params["key"], value: "Your value here", format: :text}
# TODO: handle errors
{:ok, updated_live_data} = Content.create_assign_for_live_data(site, live_data, attrs)

socket =
socket
|> assign(live_data: updated_live_data)
|> assign_selected(selected.key)

{:noreply, assign(socket, show_create_modal: false)}
end

def handle_event("delete", _, socket) do
{:noreply, assign(socket, show_delete_modal: true)}
end

def handle_event("delete_confirm", _, socket) do
%{selected: selected, live_data: %{site: site, path: path}} = socket.assigns

{:ok, _} = Content.delete_live_data_assign(site, selected)

{:noreply,
push_redirect(socket,
to: beacon_live_admin_path(socket, site, "/live_data/#{sanitize(path)}")
)}
end

def handle_event("create_cancel", _, socket) do
{:noreply, assign(socket, show_create_modal: false)}
end

def handle_event("delete_cancel", _, socket) do
{:noreply, assign(socket, show_delete_modal: false)}
end

def handle_event("stay_here", _params, socket) do
{:noreply, assign(socket, show_nav_modal: false, confirm_nav_path: nil)}
end

def handle_event("discard_changes", _params, socket) do
{:noreply, push_redirect(socket, to: socket.assigns.confirm_nav_path)}
end

def render(assigns) do
~H"""
<div>
<.header>
<%= @page_title %>
<:actions>
<.button type="button" phx-click="show_create_modal">New Live Data Assign</.button>
</:actions>
</.header>
<.main_content class="h-[calc(100vh_-_223px)]">
<.modal :if={@show_nav_modal} id="confirm-nav" on_cancel={JS.push("stay_here")} show>
<p>You've made unsaved changes to this assign!</p>
<p>Navigating to another assign without saving will cause these changes to be lost.</p>
<.button type="button" phx-click="stay_here">
Stay here
</.button>
<.button type="button" phx-click="discard_changes">
Discard changes
</.button>
</.modal>

<.modal :if={@show_delete_modal} id="confirm-delete" on_cancel={JS.push("delete_cancel")} show>
<p class="mb-2">Are you sure you want to delete this assign?</p>
<div class="flex justify-end w-full gap-4 mt-10">
<.button id="delete-confirm" type="button" phx-click="delete_confirm">
Delete
</.button>
<.button type="button" phx-click="delete_cancel">
Cancel
</.button>
</div>
</.modal>

<.modal :if={@show_create_modal} id="create-modal" on_cancel={JS.push("create_cancel")} show>
<p class="text-2xl font-bold mb-12">New Assign</p>
<.form id="new-assign-form" for={@new_assign_form} phx-submit="submit_new">
<.input type="text" field={@new_assign_form[:key]} placeholder="assign_key" />
<div class="flex mt-8 gap-x-[20px]">
<.button type="submit">Create</.button>
<.button type="button" phx-click={JS.push("create_cancel")}>Cancel</.button>
</div>
</.form>
</.modal>

<div class="grid items-start grid-cols-1 grid-rows-1 mx-auto gap-x-8 gap-y-8 lg:mx-0 lg:max-w-none lg:grid-cols-3">
<div class="h-full lg:overflow-y-auto pb-4 lg:h-[calc(100vh_-_239px)]">
<div class="text-xl flex gap-x-6">
<div>Path:</div>
<div><%= @live_data.path %></div>
</div>
<.table :if={@selected} id="assigns" rows={@live_data.assigns} row_click={fn assign -> "select-#{assign.key}" end}>
<:col :let={assign} label="assign">
@<%= assign.key %>
</:col>
</.table>
</div>

<div :if={@form} class="w-full col-span-2">
<.form :let={f} id="edit-assign-form" for={@form} class="flex items-end gap-4" phx-change="validate" phx-submit="save_changes">
<.input label="Key" field={f[:key]} type="text" />
<.input label="Format" field={f[:format]} type="select" options={["elixir", "text"]} />

<.input type="hidden" field={f[:value]} name="live_data_assign[value]" id="live_data_assign-form_value" value={Phoenix.HTML.Form.input_value(f, :value)} />

<.button phx-disable-with="Saving..." class="ml-auto">Save Changes</.button>
<.button type="button" phx-click="delete" class="">Delete</.button>
</.form>
<div :if={@form[:format].value in [:elixir, "elixir"]} class="mt-4 flex gap-x-4">
<div>Variables available:</div>
<div><%= variables_available(@live_data.path) %></div>
</div>
<%= template_error(@form[:value]) %>
<div class="w-full mt-10 space-y-8">
<div class="py-6 rounded-[1.25rem] bg-[#0D1829] [&_.monaco-editor-background]:!bg-[#0D1829] [&_.margin]:!bg-[#0D1829]">
<LiveMonacoEditor.code_editor
path="live_data_assign"
class="col-span-full lg:col-span-2"
value={@selected.value}
change="set_value"
opts={Map.merge(LiveMonacoEditor.default_opts(), %{"language" => "elixir"})}
/>
</div>
</div>
</div>
</div>
</.main_content>
</div>
"""
end

defp assign_selected(socket, nil) do
case socket.assigns.live_data.assigns do
[] ->
assign(socket, selected: nil, changed_value: "")

[live_data_assign | _] ->
assign(socket, selected: live_data_assign, changed_value: live_data_assign.value)
end
end

defp assign_selected(socket, key) do
key = URI.decode_www_form(key)
selected = Enum.find(socket.assigns.live_data.assigns, &(&1.key == key))

if selected do
assign(socket, selected: selected, changed_value: selected.value)
else
path = beacon_live_admin_path(socket, socket.assigns.beacon_page.site, "/live_data")

socket
|> assign(selected: nil)
|> push_navigate(to: path, replace: true)
end
end

defp assign_form(socket) do
form =
case socket.assigns do
%{selected: nil} ->
nil

%{selected: selected, live_data: %{site: site}} ->
site
|> Content.change_live_data_assign(selected)
|> to_form()
end

assign(socket, form: form)
end

defp assign_form(socket, changeset) do
assign(socket, :form, to_form(changeset))
end

defp sanitize(path_or_key), do: URI.encode_www_form(path_or_key)

defp variables_available(path) do
path
|> String.split("/", trim: true)
|> Enum.filter(&String.starts_with?(&1, ":"))
|> Enum.map(fn ":" <> param -> param end)
|> Kernel.++(["params"])
|> Enum.join(" ")
end
end
Loading
Loading