๐Ÿช„ MishkaGervaz

A comprehensive, declarative UI library for the Ash Framework โ€” define admin tables, forms, and data-driven interfaces entirely through DSL. โœจ

Hex.pm Hex Downloads License GitHub Sponsors Buy Me a Coffee


๐Ÿ“– Table of contents


๐Ÿ’ญ Why MishkaGervaz?

Building admin UIs around an Ash resource is repetitive: list views, filters, sorting, pagination, edit forms, validation, multi-step wizards, file uploads, master / tenant access control. Each surface calls the same building blocks in slightly different ways.

MishkaGervaz collapses that into a DSL. Declare what your admin surface looks like โ€” fields, columns, filters, steps, uploads, access rules โ€” and the library builds the LiveView, wires events, runs queries, handles form state, and renders through a swappable UI adapter. Every component, behaviour, and adapter is overridable; nothing is hidden.

mishka_gervaz do
  table do
    identity do
      name :posts
      route "/admin/dashboard/blog/posts"
    end

    columns do
      column :title do
        sortable true
        label fn -> dgettext("blog", "Title") end
      end

      column :status do
        label fn -> dgettext("blog", "Status") end
      end
    end

    filters do
      filter :search, :text, fields: [:title, :slug]
      filter :status, :select do
        options [{"Published", :published}, {"Draft", :draft}]
      end
    end
  end

  form do
    fields do
      field :title do
        required true
        ui do
          label fn -> dgettext("blog", "Title") end
        end
      end

      field :status do
        options [{"Draft", :draft}, {"Published", :published}]
      end
    end
  end
end

That's the whole admin surface for a resource. Add a route, render the LiveComponent, and you have a working list page with create / edit forms, filters, sort, master / tenant access gates, and PubSub-powered real-time updates. ๐Ÿš€


โœจ Highlights

๐Ÿ“Š Tables

๐Ÿ“ Forms

๐ŸŒ Cross-cutting


๐Ÿš€ Installation

Add to your mix.exs:

def deps do
  [
    {:mishka_gervaz, "~> 0.0.1"},
    {:ash, "~> 3.0"},
    {:ash_phoenix, "~> 2.3"},
    {:phoenix_live_view, "~> 1.0"}
  ]
end

Fetch and compile:

mix deps.get
mix compile

Add the extension to your domain โ€” set defaults that every resource in the domain inherits:

defmodule MyApp.Blog do
  use Ash.Domain,
    extensions: [MishkaGervaz.Domain]

  mishka_gervaz do
    table do
      actor_key :current_user
      ui_adapter MishkaGervaz.UIAdapters.Tailwind

      pagination do
        type :numbered
        page_size 20
        page_size_options [20, 50, 100]
      end

      realtime do
        pubsub MyApp.PubSub
      end

      actions do
        read {:master_read, :read}
        get {:master_get, :read}
        destroy {:master_destroy, :destroy}
      end

      archive do
        read_action {:master_archived, :archived}
        get_action {:master_get_archived, :get_archived}
        restore_action {:master_unarchive, :unarchive}
        destroy_action {:master_permanent_destroy, :permanent_destroy}
      end
    end

    form do
      actions do
        create {:master_create, :create}
        update {:master_update, :update}
        read {:master_get, :read}
      end

      submit do
        create label: fn -> dgettext("blog", "Create") end
        update label: fn -> dgettext("blog", "Save Changes") end
        cancel label: fn -> dgettext("blog", "Cancel") end
        position :bottom
      end
    end
  end

  resources do
    resource MyApp.Blog.Post
  end
end

Then add the resource extension:

defmodule MyApp.Blog.Post do
  use Ash.Resource,
    domain: MyApp.Blog,
    extensions: [MishkaGervaz.Resource]

  # ... your attributes / actions / relationships
end

๐ŸŽฏ Quick start

๐Ÿ“Š A table

mishka_gervaz do
  table do
    identity do
      name :posts
      route "/admin/dashboard/blog/posts"
    end

    source do
      preload do
        always [:site, :tag_count, :comment_count]
      end
    end

    columns do
      column :title do
        sortable true

        render fn value ->
          assigns = %{title: value}
          ~H"<span class=\"font-semibold\">{@title}</span>"
        end
      end

      column :site_id do
        static true
        requires [:site_id, :site]
        label fn -> dgettext("blog", "Site") end
      end

      column :status do
        sortable true
        sort_field [:status]
      end

      column :inserted_at do
        sortable true
      end
    end

    filters do
      filter :search, :text, fields: [:title, :slug, :excerpt]

      filter :site_id, :relation do
        mode :search_multi
        display_field :name
        search_field :name
        restricted true
      end

      filter :status, :select do
        options [
          {"Published", :published},
          {"Draft", :draft},
          {"Archived", :archived}
        ]
      end
    end

    row_actions do
      action :edit do
        type :edit
        visible :active

        ui do
          icon "hero-pencil-square"
          class "text-blue-600 hover:bg-blue-50"
        end
      end

      action :archive do
        type :destroy
        confirm "Archive this post?"
        visible :active
      end

      action :unarchive do
        type :unarchive
        confirm "Restore this post?"
        visible :archived
      end
    end

    bulk_actions do
      action :archive, type: :destroy, confirm: "Archive selected posts?"

      action :unarchive,
        type: :unarchive,
        confirm: "Restore selected posts?",
        visible: :archived
    end

    url_sync do
      mode :bidirectional
      params [:filters, :sort, :page, :search]
      prefix "posts"
    end
  end
end

๐ŸŽฌ Mount it:

<.live_component
  module={MishkaGervaz.Table.Web.Live}
  id="posts-table"
  resource={MyApp.Blog.Post}
  current_user={@current_user}
/>

๐Ÿ“ A form

mishka_gervaz do
  form do
    identity do
      name :post_form
    end

    source do
      preload do
        master [:master_collections, :master_tags]
        tenant [:tenant_collections, :tenant_tags]
      end
    end

    fields do
      field :title do
        required true

        ui do
          label fn -> dgettext("blog", "Title") end
          placeholder "Post title"
        end
      end

      field :status do
        options [
          {"Draft", :draft},
          {"Published", :published},
          {"Scheduled", :scheduled}
        ]
      end

      field :language, :combobox do
        options fn ->
          MyApp.Repo.distinct_languages()
          |> Enum.map(fn lang -> {String.upcase(lang), lang} end)
        end

        ui do
          label fn -> dgettext("blog", "Language") end
          placeholder "en, fa, ar, ..."
        end
      end

      field :site_id, :relation do
        mode :search
        display_field :name
        search_field :name
        restricted true
        show_on :create

        ui do
          label fn -> dgettext("blog", "Site") end
        end
      end

      field :tag_ids, :relation do
        virtual true
        resource MyApp.Blog.Tag
        mode :search_multi
        display_field :name
        search_field :name
        depends_on :site_id

        load fn query, state ->
          site_id =
            if state.master_user?,
              do: Map.get(state.field_values, :site_id),
              else: state.current_user.site_id

          Ash.Query.filter_input(query, %{site_id: site_id})
        end
      end

      field :body, :textarea do
        required true

        ui do
          label fn -> dgettext("blog", "Body") end
        end
      end
    end

    groups do
      group :basic_info do
        fields [:title, :status, :language]

        ui do
          label fn -> dgettext("blog", "Basics") end
        end
      end

      group :relationships do
        fields [:site_id, :tag_ids]

        ui do
          label fn -> dgettext("blog", "Relationships") end
        end
      end

      group :content do
        fields [:body]

        ui do
          label fn -> dgettext("blog", "Content") end
        end
      end
    end

    uploads do
      upload :cover do
        accept "image/*"
        max_file_size 5_000_000
      end
    end
  end
end

๐ŸŽฌ Mount it:

<.live_component
  module={MishkaGervaz.Form.Web.Live}
  id="post-form"
  resource={MyApp.Blog.Post}
  current_user={@current_user}
  record_id={@post_id}
/>

๐Ÿชœ A wizard or tabbed multi-step form is a one-block change:

layout do
  mode :wizard            # or :tabs

  step :basics do
    groups [:basic_info]
  end

  step :metadata do
    groups [:relationships]
  end

  step :content do
    groups [:content]
  end

  step :review do
    summary true
  end
end

๐Ÿ”ง Customization & overrides

Three layers, each independent.

1๏ธโƒฃ Per-callback override via use

defmodule MyApp.Form.SubmitHandler do
  use MishkaGervaz.Form.Web.Events.SubmitHandler

  def transform_params(state, params) do
    params
    |> super(state)
    |> Map.put("ingested_at", DateTime.utc_now())
  end
end

super falls through to the default. Every callback in every sub-builder is defoverridable.

2๏ธโƒฃ Wire your override via DSL

mishka_gervaz do
  form do
    events do
      submit MyApp.Form.SubmitHandler
      validation MyApp.Form.ValidationHandler
    end

    state do
      field MyApp.Form.FieldBuilder
    end

    data_loader do
      relation MyApp.Form.RelationLoader
    end
  end
end

The DSL config is read at runtime by the orchestrator โ€” no recompiling the macro tree.

3๏ธโƒฃ Replace an entire subsystem module

mishka_gervaz do
  form do
    events MyApp.CustomFormEvents
    state module: MyApp.CustomState
  end
end

๐Ÿ“š See the moduledocs of MishkaGervaz.Form.Web.State, MishkaGervaz.Form.Web.Events, MishkaGervaz.Form.Web.DataLoader, and the table-side counterparts for the full override surface.


๐Ÿ—๏ธ Architecture

                +----------------------------+
                | Phoenix.LiveComponent      |
                | (Form.Web.Live /           |
                |  Table.Web.Live)           |
                +--------------+-------------+
                               |
       +-----------+-----------+-----------+-----------+
       |           |           |           |           |
       v           v           v           v           v
   +-------+   +-------+   +-------+   +-------+   +-------+
   | State |   | Events|   |DataLdr|   |Render |   |Adapter|
   +-------+   +-------+   +-------+   +-------+   +-------+
       |           |           |           |           |
       v           v           v           v           v
   sub-builders  sub-handlers  sub-builders templates  components
   (5)           (7)           (4)          (Standard) (Tailwind / yours)

๐Ÿ“š Documentation


๐Ÿค Contributing

Issues, PRs, and design discussions are welcome.

git clone https://github.com/mishka-group/mishka_gervaz.git
cd mishka_gervaz
mix deps.get
mix test

Before opening a PR:

For larger feature work, please open an issue first so we can align on the design. ๐Ÿ’ฌ


๐Ÿ’– Funding & sponsorship

MishkaGervaz is open-source software developed by Mishka Group. If your team or company benefits from this work, please consider supporting continued development:

GitHub Sponsors ย ย ย  Buy Me a Coffee

โ˜• Donate / sponsor: github.com/sponsors/mishka-group ยท buymeacoffee.com/mishkagroup

Sponsorship directly funds maintenance, new features, and documentation. Thank you. ๐Ÿ’š


๐Ÿ“œ License

Apache License 2.0 โ€” see LICENSE.

Copyright ยฉ Mishka Group and contributors.