๐ช MishkaGervaz
A comprehensive, declarative UI library for the Ash Framework โ define admin tables, forms, and data-driven interfaces entirely through DSL. โจ
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?
- Highlights
- Installation
- Quick start
- Customization & overrides
- Architecture
- Compatibility
- Documentation
- Status & roadmap
- Contributing
- Funding & sponsorship
- License
๐ญ 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
- ๐จ Columns by atom or module โ built-ins for
:text,:number,:boolean,:date,:datetime,:enum,:tags,:money,:url,:image,:json,:uuid,:array, plus a registry that accepts any custom column module. - ๐ Filters as first-class entities โ text, select, multi-select, date range, number range, boolean, relation (with search / load-more / static modes), with predicate operators (
contains,equals,gt,lt,between, โฆ). - ๐ Pagination โ numbered, load-more, infinite-scroll. Configurable page size, page-size options, max page size.
- โ๏ธ Sorting โ declarative, multi-column, deep-link-friendly.
- ๐ URL sync โ page state (filters, sort, page, search) round-trips through the URL so refresh and copy-paste-link both work.
- โก Real-time โ wire
pubsuband rows update live without manual subscriptions. - ๐ฆ Bulk actions โ
:destroy,:unarchive,:permanent_destroy, plus your own per-resource handlers. - ๐ฏ Row actions โ custom buttons / links per row, with master / tenant gating.
- ๐๏ธ Archive support โ soft-delete column, restore action, master-vs-tenant action mapping.
- โจ Auto-detect from Ash attributes โ
auto_columns truebuilds a sensible default column set so you can opt in incrementally.
๐ Forms
- ๐งฉ Field types โ
:text,:textarea,:password,:select,:multi_select,:checkbox,:toggle,:date,:datetime,:range,:number,:hidden,:file,:upload,:relation,:json,:nested,:array_of_maps,:string_list,:combobox, plus arbitrary custom modules. - ๐ช Layout modes โ
:standard,:wizard(sequential steps),:tabs(free navigation). - ๐ Groups โ visually section fields with optional collapsibles.
- โ
Validation โ driven by Ash actions;
phx-changevalidation surfaces field-level errors automatically. - ๐ช Lifecycle hooks โ
on_init,on_validate,before_save,after_save,on_cancel,on_change, plus per-field JS hooks. - ๐ฌ Notices โ info / warning / error / success banners with positions, group anchoring, step targeting, dismiss, and visibility predicates.
- ๐ฉ Header / footer chrome โ title, description, content, icon, class, and dynamic show/hide.
- ๐ Uploads โ drop-zone or button styles; multi-file; auto-namespaced names so multiple form components on one page never collide; existing-files list + delete.
- ๐ Relations โ static (load all), search (autocomplete), search-multi, load-more pagination; with
display_field,value_field,search_field,min_chars,debounce, customload fnfor tenant filtering. - ๐ชบ Constrained-map nested fields โ array-of-maps without changing your DB shape; add / remove rows; per-sub-field validation.
- ๐ Per-mode access control โ
restricted: truefor master-only fields, function predicates for fine-grained gating, per-action:create/:updaterules. - ๐ฅ Master / tenant action tuples โ
read {:master_get, :read}style; the same DSL drives different Ash actions depending on the user.
๐ Cross-cutting
- ๐จ UI adapter โ pluggable component layer. Tailwind adapter ships in; swap in your own to render against any design system.
- ๐ง Override surface โ every state builder, event handler, data loader, template, and adapter is
defoverridable. Replace just one piece, all of them, or wire it via the DSL (state do field MyMod end). - ๐ i18n โ Gettext baked in; every label resolves through
Gettextso translations land in the right places. - ๐งช Fully tested core โ verifiers, transformers, sub-handlers, and helpers each have direct unit tests on top of integration tests; over 3,600 tests on the suite at the time of writing.
๐ Installation
Add to your mix.exs:
def deps do
[
{:mishka_gervaz, "~> 0.0.1-alpha.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)
- ๐ง State โ single struct per LiveComponent, partitioned into
static(config, never re-renders) and dynamic (form, errors, current_step, โฆ). Sub-builders for fields, groups, steps, presentation, access โ eachdefoverridable. - ๐ DataLoader โ async record loading, AshPhoenix.Form construction, relation option loading, hook execution. Sub-builders:
RecordLoader,RelationLoader,TenantResolver,HookRunner. - ๐ก Events โ dispatch table for every
phx-event the component sees. Sub-handlers: sanitization, validation, submit, step navigation, uploads, relation search, hooks. - ๐ฌ Renderer โ thin bridge between LiveComponent and Templates; passes the static / dynamic split through so LiveView's diffing engine can skip work.
- ๐จ UI adapter โ the leaf layer that turns "render a button / a select / a stepper" into actual markup. Swap to retheme without touching the rest.
๐ Compatibility
| Dependency | Required version |
|---|---|
| Elixir | ~> 1.17 |
| Ash | ~> 3.0 |
| AshPhoenix | ~> 2.3 |
| Phoenix LiveView | ~> 1.0 (optional) |
| Spark | ~> 2.6 |
| Gettext | ~> 1.0 |
| Jason | ~> 1.0 |
๐ Documentation
- ๐ API docs โ hexdocs.pm/mishka_gervaz (published with each release).
- ๐งญ Guides โ every public module ends its
@moduledocwith a "See also" cross-link to its siblings, so navigation through the codebase stays close to the runtime call graph. - ๐ฌ Reference resources โ the test fixtures under
test/support/resources/show every DSL feature in working form.
๐ฃ๏ธ Status & roadmap
| Area | Status |
|---|---|
| Table DSL + LiveView | ๐ก Alpha โ feature-complete; API may change |
| Form DSL + LiveView | ๐ก Alpha โ feature-complete; API may change |
| Tailwind UI adapter | ๐ก Alpha |
| Multi-tenancy & access gates | ๐ข Stable in scope |
| Test coverage | ๐ข 3,600+ tests, growing |
| GuardedStruct integration | ๐ต Planned for the field-types layer |
| Docs site | ๐ต Planned |
Breaking changes will be flagged in the CHANGELOG.
๐ค 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:
- โ
mix testโ full suite green - โ
mix formatโ formatter passes - โ
mix dialyzerโ type analysis clean (where applicable)
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:
โ 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.