๐Ÿช„ 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


Warning

๐Ÿšง Status โ€” alpha. APIs are still evolving and the library is not yet recommended for production. Track progress on GitHub and the CHANGELOG.


๐Ÿ“– 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.