Skip to content

Commit

Permalink
Refactor Projects Dashboard & Fix Last Activity Bug (#2606)
Browse files Browse the repository at this point in the history
* Add arcade resources, unload video when modal is closed
* Move Arcade implementation to live_component
* Pass phx-target to component
* Assign projects for picker
* Cherry pick projects last activity fix
  • Loading branch information
elias-ba authored Oct 25, 2024
1 parent 94b7bfc commit 3fa40f9
Show file tree
Hide file tree
Showing 9 changed files with 730 additions and 402 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ and this project adheres to

### Fixed

- Refactor projects dashboard page and fix bug on last activity column
[#2593](https://github.com/OpenFn/lightning/issues/2593)

## [v2.9.11] - 2024-10-23

### Added
Expand Down
44 changes: 44 additions & 0 deletions lib/lightning/projects.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,50 @@ defmodule Lightning.Projects do

require Logger

defmodule ProjectOverviewRow do
@moduledoc """
Represents a summarized view of a project for a user, used in the project overview table.
"""
defstruct [
:id,
:name,
:role,
:workflows_count,
:collaborators_count,
:last_activity
]
end

def get_projects_overview(%User{id: user_id}, opts \\ []) do
order_by = Keyword.get(opts, :order_by, {:asc, :name})

from(p in Project,
join: pu in assoc(p, :project_users),
left_join: w in assoc(p, :workflows),
left_join: pu_all in assoc(p, :project_users),
where: pu.user_id == ^user_id and is_nil(w.deleted_at),
group_by: [p.id, pu.role],
select: %ProjectOverviewRow{
id: p.id,
name: p.name,
role: pu.role,
workflows_count: count(w.id, :distinct),
collaborators_count: count(pu_all.user_id, :distinct),
last_activity: max(w.updated_at)
},
order_by: ^dynamic_order_by(order_by)
)
|> Repo.all()
end

defp dynamic_order_by({direction, :name}) do
{direction, dynamic([p, _pu, _w, _pu_all], field(p, :name))}
end

defp dynamic_order_by({direction, :last_activity}) do
{direction, dynamic([_p, _pu, w, _pu_all], max(w.updated_at))}
end

@doc """
Perform, when called with %{"type" => "purge_deleted"}
will find projects that are ready for permanent deletion and purge them.
Expand Down
30 changes: 27 additions & 3 deletions lib/lightning_web/live/components/modal.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule LightningWeb.Components.Modal do
attr :id, :string, required: true
attr :show, :boolean, default: false
attr :with_frame, :boolean, default: true
attr :target, :any, default: nil
attr :position, :string, default: "fixed inset-0"
attr :width, :string, default: "max-w-3xl"
attr :close_on_click_away, :boolean, default: true
Expand All @@ -34,7 +35,7 @@ defmodule LightningWeb.Components.Modal do
<div
id={@id}
phx-mounted={@show && show_modal(@id)}
phx-on-close={hide_modal(@id)}
phx-on-close={hide_modal(@on_close, @id)}
phx-hook="ModalHook"
class={"#{@position} z-50 hidden"}
{@rest}
Expand All @@ -43,6 +44,11 @@ defmodule LightningWeb.Components.Modal do
id={"#{@id}-bg"}
class="fixed inset-0 bg-black bg-opacity-60 transition-opacity"
aria-hidden="true"
phx-click={
@close_on_click_away &&
hide_modal(@on_close, @id)
|> push_modal_closed(@id, @target)
}
/>
<div
class="fixed inset-0 overflow-y-auto sm:py-2"
Expand All @@ -57,9 +63,17 @@ defmodule LightningWeb.Components.Modal do
<.focus_wrap
id={"#{@id}-container"}
phx-mounted={@show && show_modal(@on_open, @id)}
phx-window-keydown={@close_on_keydown && hide_modal(@on_close, @id)}
phx-window-keydown={
@close_on_keydown &&
hide_modal(@on_close, @id)
|> push_modal_closed(@id, @target)
}
phx-key="escape"
phx-click-away={@close_on_click_away && hide_modal(@on_close, @id)}
phx-click-away={
@close_on_click_away &&
hide_modal(@on_close, @id)
|> push_modal_closed(@id, @target)
}
class={[
"hidden relative rounded-xl transition",
@with_frame &&
Expand Down Expand Up @@ -150,4 +164,14 @@ defmodule LightningWeb.Components.Modal do
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|> JS.pop_focus()
end

defp push_modal_closed(js, modal_id, nil) do
js
|> JS.push("modal_closed", value: %{id: modal_id})
end

defp push_modal_closed(js, modal_id, target) do
js
|> JS.push("modal_closed", value: %{id: modal_id}, target: target)
end
end
134 changes: 113 additions & 21 deletions lib/lightning_web/live/dashboard_live/components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,102 @@ defmodule LightningWeb.DashboardLive.Components do

import PetalComponents.Table

alias Lightning.Accounts.User
alias Lightning.Projects.Project
def welcome_banner(assigns) do
~H"""
<div class="pb-6">
<div class="flex justify-between items-center pt-6">
<h1 class="text-2xl font-medium">
Good day, <%= @user.first_name %>!
</h1>
<button
phx-click="toggle-welcome-banner"
phx-target={@target}
class="text-gray-500 focus:outline-none"
>
<span class="text-lg">
<.icon name={"hero-chevron-#{if @collapsed, do: "down", else: "up"}"} />
</span>
</button>
</div>
<div
id="welcome-banner-content"
class={[
"hover:overflow-visible transition-all duration-500 ease-in-out overflow-hidden",
banner_content_classes(@collapsed)
]}
>
<p class="mb-6 mt-4">
Here are some resources to help you get started with OpenFn
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<%= for resource <- @resources do %>
<.resource_card resource={resource} target={@target} />
<% end %>
</div>
</div>
<hr class="border-t border-gray-300 mt-4" />
<.arcade_modal
:if={@selected_resource}
resource={@selected_resource}
target={@id}
/>
</div>
"""
end

defp resource_card(assigns) do
~H"""
<button
type="button"
phx-click="select-arcade-resource"
phx-target={@target}
phx-value-resource={@resource.id}
class="relative flex items-end h-[150px] bg-gradient-to-r from-blue-400 to-purple-500 text-white rounded-lg shadow-sm hover:shadow-md transition-shadow duration-300 p-4 text-left"
>
<h2 class="text-lg font-semibold absolute bottom-4 left-4">
<%= @resource.title %>
</h2>
</button>
"""
end

defp arcade_modal(assigns) do
~H"""
<div class="text-xs">
<.modal
id={"arcade-modal-#{@resource.id}"}
target={"##{@target}"}
with_frame={false}
show={true}
width="w-5/6"
>
<div class="relative h-0 w-full pb-[60%]">
<iframe
src={@resource.link}
title={@resource.title}
frameborder="0"
loading="lazy"
webkitallowfullscreen
mozallowfullscreen
allowfullscreen
allow="clipboard-write"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; color-scheme: light;"
>
</iframe>
</div>
</.modal>
</div>
"""
end

defp project_role(
%User{id: user_id} = _user,
%Project{project_users: project_users} = _project
) do
project_users
|> Enum.find(fn pu -> pu.user_id == user_id end)
|> Map.get(:role)
|> Atom.to_string()
|> String.capitalize()
defp banner_content_classes(collapsed) do
case collapsed do
true -> "max-h-0"
false -> "max-h-[500px]"
nil -> "max-h-[500px]"
end
end

defp table_title(assigns) do
Expand All @@ -36,7 +120,7 @@ defmodule LightningWeb.DashboardLive.Components do
projects_count: assigns.projects |> Enum.count(),
empty?: assigns.projects |> Enum.empty?(),
name_sort_icon: next_sort_icon[assigns.name_direction],
activity_sort_icon: next_sort_icon[assigns.activity_direction]
last_activity_sort_icon: next_sort_icon[assigns.last_activity_direction]
)

~H"""
Expand All @@ -57,6 +141,7 @@ defmodule LightningWeb.DashboardLive.Components do
<span
phx-click="sort"
phx-value-by="name"
phx-target={@target}
class="cursor-pointer align-middle ml-2 flex-none rounded text-gray-400 group-hover:visible group-focus:visible"
>
<.icon name={@name_sort_icon} />
Expand All @@ -71,10 +156,11 @@ defmodule LightningWeb.DashboardLive.Components do
Last Activity
<span
phx-click="sort"
phx-value-by="activity"
phx-value-by="last_activity"
phx-target={@target}
class="cursor-pointer align-middle ml-2 flex-none rounded text-gray-400 group-hover:visible group-focus:visible"
>
<.icon name={@activity_sort_icon} />
<.icon name={@last_activity_sort_icon} />
</span>
</div>
</.th>
Expand All @@ -95,24 +181,30 @@ defmodule LightningWeb.DashboardLive.Components do
</.link>
</.td>
<.td class="break-words max-w-[25rem]">
<%= project_role(@user, project) %>
<%= project.role
|> Atom.to_string()
|> String.capitalize() %>
</.td>
<.td class="break-words max-w-[10rem]">
<%= length(project.workflows) %>
<%= project.workflows_count %>
</.td>
<.td class="break-words max-w-[5rem]">
<.link
class="link"
href={~p"/projects/#{project.id}/settings#collaboration"}
>
<%= length(project.project_users) %>
<%= project.collaborators_count %>
</.link>
</.td>
<.td>
<%= Lightning.Helpers.format_date(
project.updated_at,
"%d/%b/%Y %H:%M:%S"
) %>
<%= if project.last_activity do %>
<%= Lightning.Helpers.format_date(
project.last_activity,
"%d/%b/%Y %H:%M:%S"
) %>
<% else %>
No activity
<% end %>
</.td>
<.td class="text-right">
<.link
Expand Down
Loading

0 comments on commit 3fa40f9

Please sign in to comment.