Skip to content

Web dashboard for the FunWithFlags Elixir package

License

Notifications You must be signed in to change notification settings

liveflow-io/fun_with_flags_ui

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

292 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FunWithFlags.UI

Mix Tests Code Quality
Hex.pm

A Web dashboard for the FunWithFlags Elixir package.

How to run

FunWithFlags.UI is just a plug and it can be run in a number of ways. It's primarily meant to be embedded in a host Plug application, either Phoenix or another Plug app.

Mounted in Phoenix

The router plug can be mounted inside the Phoenix router with Phoenix.Router.forward/4.

defmodule MyPhoenixAppWeb.Router do
  use MyPhoenixAppWeb, :router

  pipeline :mounted_apps do
    plug :accepts, ["html"]
    plug :put_secure_browser_headers
  end

  scope path: "/feature-flags" do
    pipe_through :mounted_apps
    forward "/", FunWithFlags.UI.Router, namespace: "feature-flags"
  end
end

Note: There is no need to add :protect_from_forgery to the :mounted_apps pipeline because this package already implements CSRF protection. In order to enable it, your host application must use the Plug.Session plug, which is usually configured in the endpoint module in Phoenix.

Mounted in another Plug application

Since it's just a plug, it can also be mounted into any other Plug application using Plug.Router.forward/2.

defmodule Another.App do
  use Plug.Router
  forward "/feature-flags", to: FunWithFlags.UI.Router, init_opts: [namespace: "feature-flags"]
end

Note: If your plug router uses Plug.CSRFProtection, FunWithFlags.UI.Router should be added before your CSRF protection plug because it already implements its own CSRF protection. If you declare FunWithFlags.UI.Router after, your CSRF plug will likely block GET requests for the JS assets of the dashboard.

Standalone

Again, because it's just a plug, it can be run standalone.

If you clone the repository, the library comes with two convenience functions to accomplish this:

# Simple, let Cowboy sort out the supervision tree:
{:ok, pid} = FunWithFlags.UI.run_standalone()

# Uses some explicit supervision configuration:
{:ok, pid} = FunWithFlags.UI.run_supervised()

These functions come in handy for local development, and are not necessary when embedding the Plug into a host application.

Please note that even though the FunWithFlags.UI module implements the Application behavior and comes with a proper start/2 callback, this is not enabled by design and, in fact, the Mixfile doesn't declare an entry module.

If you really need to run it standalone in a reliable manner, you are encouraged to write your own supervision setup.

Security

For obvious reasons, you don't want to make this web control panel publicly accessible.

The library itself doesn't provide any auth functionality because, as a Plug, it is easier to wrap it into the authentication and authorization logic of the host application.

The easiest thing to do is to protect it with HTTP Basic Auth, provided by Plug itself.

For example, in Phoenix:

defmodule MyPhoenixAppWeb.Router do
  use MyPhoenixAppWeb, :router
+ import Plug.BasicAuth

  pipeline :mounted_apps do
    plug :accepts, ["html"]
    plug :put_secure_browser_headers
+   plug :basic_auth, username: "foo", password: "bar"
  end

  scope path: "/feature-flags" do
    pipe_through :mounted_apps
    forward "/", FunWithFlags.UI.Router, namespace: "feature-flags"
  end
end

Configuration Options

You can configure which gate types are available in the UI:

# config/config.exs
config :fun_with_flags_ui,
  # Disable percentage gates (default: true)
  percentage_gates: false,

  # Disable actor gates (default: true)
  actor_gates: true,

  # Disable group gates (default: true)
  group_gates: true

When a gate type is disabled, its section will not appear in the UI and users will not be able to create gates of that type.

Actor and Group Search

The dashboard supports searchable autocomplete for actors and groups. This allows you to search for users by name or email when adding actor gates, and search for organizations by name when adding group gates.

To enable this feature, configure your search callbacks in your application config:

# config/config.exs
config :fun_with_flags_ui,
  # Search callbacks (for autocomplete when adding new gates)
  actor_search: {MyApp.FlagHelpers, :search_users},
  group_search: {MyApp.FlagHelpers, :search_organizations},

  # Label lookup callbacks (for displaying human-readable names)
  actor_labels: {MyApp.FlagHelpers, :get_user_labels},
  group_labels: {MyApp.FlagHelpers, :get_org_labels}

Implementing Search Callbacks

Search functions receive a query string and should return a list of maps with :id and :label keys:

defmodule MyApp.FlagHelpers do
  @doc """
  Search for users by name or email.
  The `id` should be the actor ID used by FunWithFlags.
  The `label` is what will be displayed in the search results.
  """
  def search_users(query) do
    users = MyApp.Users.search_by_name_or_email(query)
    
    Enum.map(users, fn user ->
      %{
        id: "user:#{user.id}",
        label: "#{user.name} (#{user.email})"
      }
    end)
  end

  @doc """
  Search for organizations by name.
  The `id` should be the group name used by FunWithFlags.
  The `label` is what will be displayed in the search results.
  """
  def search_organizations(query) do
    orgs = MyApp.Organizations.search_by_name(query)
    
    Enum.map(orgs, fn org ->
      %{
        id: org.slug,
        label: org.name
      }
    end)
  end
end

Implementing Label Lookup Callbacks

Label functions receive a list of IDs and should return a map of ID => label. This is called once per page load with all visible IDs, enabling efficient batch lookups:

defmodule MyApp.FlagHelpers do
  @doc """
  Look up user labels by actor IDs.
  Returns a map of actor_id => human-readable label.
  """
  def get_user_labels(actor_ids) do
    # Extract user IDs from actor IDs like "user:uuid"
    user_ids =
      actor_ids
      |> Enum.map(fn id ->
        case String.split(id, ":", parts: 2) do
          ["user", user_id] -> user_id
          _ -> nil
        end
      end)
      |> Enum.reject(&is_nil/1)

    users = MyApp.Users.get_by_ids(user_ids)

    Map.new(users, fn user ->
      {"user:#{user.id}", "#{user.name} (#{user.email})"}
    end)
  end

  @doc """
  Look up organization labels by group names (slugs).
  Returns a map of group_name => human-readable label.
  """
  def get_org_labels(group_names) do
    orgs = MyApp.Organizations.get_by_slugs(group_names)
    Map.new(orgs, fn org -> {org.slug, org.name} end)
  end
end

You can also use anonymous functions:

config :fun_with_flags_ui,
  actor_search: fn query ->
    # Your search logic here
    [%{id: "user:123", label: "John Doe"}]
  end,
  actor_labels: fn actor_ids ->
    # Your batch lookup logic here
    %{"user:123" => "John Doe (john@example.com)"}
  end

When search is configured, the actor and group input fields will show a search dropdown as you type. If search is not configured, the inputs work as before with manual entry.

When label lookup is configured, existing actor and group gates will display human-readable labels alongside their IDs.

Caveats

While the base fun_with_flags library is quite relaxed in terms of valid flag names, group names and actor identifers, this web dashboard extension applies some more restrictive rules. The reason is that all fun_with_flags cares about is that some flag and group names can be represented as an Elixir Atom, while actor IDs are just strings. Since you can use that API in your code, the library will only check that the parameters have the right type.

Things change on the web, however. Think about the binary "Ook? Ook!". In code, it can be accepted as a valid flag name:

{:ok, true} = FunWithFlags.enable(:"Ook? Ook!", for_group: :"weird, huh?")

On the web, however, the question mark makes working with URLs a bit tricky: in http://localhost:8080/flags/Ook?%20Ook!, the flag name will be Ook and the rest will be a query string.

For this reason this library enforces some stricter rules when creating flags and groups. Blank values are not allowed, ? neither, and flag names must match /^w+$/.

Installation

The package can be installed by adding fun_with_flags_ui to your list of dependencies in mix.exs.
It requires fun_with_flags, see its installation documentation for more details.

def deps do
  [{:fun_with_flags_ui, "~> 1.1"}]
end

About

Web dashboard for the FunWithFlags Elixir package

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • Elixir 72.0%
  • HTML 20.0%
  • JavaScript 7.0%
  • CSS 1.0%