How to render a separate top-level LiveView component for a specific user role?

Hi,

We have two different user types - a Clinician and a Patient, and both have a dashboard under the same /dashboard route.

As those are two completely different user roles, their dashboards have entirely different UI controls and present different data sets. So just rendering separate templates won’t cut it.

Differentiating between those two use cases is not a problem in a classic controller - just pattern match on the user’s role field in a controller action and then do your thing. But it seems to be a bit harder with LV.

Right now, we are using live_render and having a problem with catching phx:page-loading-stop

defmodule DashboardController do

   def show(%{assigns: %{current_user: %{role: :patient}}} = conn, params) do
     live_render(
       conn,
       PatientDashboardLive
     )
   end

   def show(%{assigns: %{current_user: %{role: :clinician}}} = conn, params) do
     live_render(
       conn,
       ClinicianDashboardLive
     )
   end
 end

@chrismccord wrote in Slack:

you forgo a lot with live_render in the controller
you basically almost never want to do that
live navigation is not supported in such cases, so the page lifecycle is not in our control in that case

So I wonder what’s the idiomatic approach to handle this scenario with LiveView?

Thanks!

I guess if you really want them under the same route, I would make it one LiveView and maybe just pattern match the sections that differ in components.

For example if you have a navbar that changes with roles.

You can make something like:

<nav>
  <div>MyApp Logo</div>
  ...
  <.nav_links user={@current_user} />
  <a href...>Logout</a>
</nav>

And then have something like this in your LV

defp nav_links(%{user: %{role: :patient}} = assigns) do
  some heex here
end

defp nav_links(%{user: %{role: :clinician}} = assigns) do
  some heex here
end

My overall suggestion is to nest your routes under /clinician/dashboard and patient/dashboard but if that is not possible, you may consider writing the bulk of the business logic in two separate LiveComponents and render those based on the user role. Here’s a basic example (I wrote this in the comment box, not guaranteed to compile):

# dashboard_live.ex
defmodule MyAppWeb.DashboardLive do
  use MyAppWeb, :live_view

  alias MyAppWeb.PatientDashboardLiveComponent
  alias MyAppWeb.ClinicianDashboardLiveComponent

  @impl true
  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  @impl true
  def render(assigns) do
  ~H"""
  <%= if @user.role == :patient do %>
    <.live_component module={PatientDashboardLiveComponent}
      id="patient-dashboard"
      user={@user}
     />
  <% end %>

  <% if @user.role == :clinician do %>
  <.live_component module={ClinicianDashboardLiveComponent}
    id="clinician-dashboard"
    user={@user}
  />
  <% end %>
  """
  end
end

Advantages of this approach: Using the @myself assign, the components can handle their own internal state and events (e.g. phx-click)

Drawbacks: LiveComponents share the PID of their parent LiveView. Any handle_info/PubSub/message passing between processes will need to be handled by the parent LiveView. It could get messy depending on how much of that you have in your app.

Make sure you are familiar with the Security considerations of the LiveView model — Phoenix LiveView v0.17.11 - Every action will need to be checked for the proper access roles if these two dashboards are indeed sharing a LiveView.

Again I recommend you separate them with HTTP redirects if people try to access the old /dashboard route but that may be a decision that is out of your hands. If that is the case I recommend the LiveComponent approach

3 Likes

I know this doesn’t answer your question, but I pretty strongly believe you should be nesting your routes to both 1) make it extremely clear to the user whether they’re acting as a patient or a clinician, and 2) create a sharp separation of concerns on the backend between clinician and patient logic.

At a very minimum, stick clinitians under some additional route and give patients “/dashboard” if you want a “nicer” URL for the larger user-group. But my wife, for instance, is a clinitian that is also a patient of the same organization – even if this isn’t a use-case you need to account for now, it’s one that will need to be addressed in the future. In all likelihood, clinitians and patients shouldn’t even be a part of the same user table, since HIPAA still applies to employees who are patients…

I don’t meant to unnecessarily open a can of worms here, but I guess I’d rather bring this sort of stuff up now in case it saves you a large amount of trouble later.


Edit: Reading this over and want to make a more concrete recommendation. Basically: If you’re dealing with sensitive/medical information, you may want to strongly reconsider serving patients and clinitians from the same application. This may very well be one of those cases where a bit of “over”-engineering really saves you a lot of headache in the future (i.e. a data service that serves both your clinitian application and your patient application). I don’t want to pretend that I’m some kind of expert – this should be a solved problem – but serving both from the same endpoint seems like asking for trouble.

3 Likes

Thanks for the prompt answers, guys.
I argued against splitting pages on the routes level because it feels like a hack.
In our case a user can be either a patient or a clinician. There is near zero chance for those two to intersect in real life, and even if they do, that invites another account.
So having a single ‘/dashboard’ seems to be pretty justified just because of that.
Additionally, if we will have two separate URLs, it will spread role/permissions checks all over the place rather than just in one single place (and having a plug for that feels like too much glue for a “simple” condition that can be solved by pattern-matching in a classic controller).

Next, two different URLs violate the principle of the least surprise from how I see it.
I imagine a new developer coming in and looking at two different routes that don’t make sense from the business perspective and can hear “WTF” in his head.
Next thirty minutes, he is trying to wrap all that wiring and make sense of it to understand that it was put in place just to circumvent technical limitations and then regrets for taking the offer till the rest of the day, haha :joy:

First of all I want to warn that I don’t know anything yet about LiveView, and therefore important aspects of the problem posed here could completely escape me. If you allow me anyway, that’s the way (maybe naive) I see it.

When I read this I feel like I would go even for two different web apps with a third context core app that would provide/persist all necessary data.

I feel it rather like a radical but clear separation of concerns. It could even make it easy to find specific code.

If user roles are going to grow in the future, I would even group them by similarity then having for example the same UI for all admin like users etc.

Maybe you can have a subdomain per type of UI/app then ?

admin.site/dashboard ------> admin/superadmin/etc.
staff.site/dashboard ------> clinician/etc.
site/dashboard ------------> default/patient/etc.

I think this kind of urls won’t bother anyone.

This maybe a lot of work but if the project grow in complexity, it could pay off.