Packages

Mishka Gervaz is a comprehensive, declarative UI library for the Ash ecosystem โ€” define tables, forms, and data-driven interfaces entirely through DSL, with built-in sorting, filtering, real-time updates, and extensible templates.

Current section

Files

Jump to
mishka_gervaz README.md
Raw

README.md

<div align="center">
# ๐Ÿช„ MishkaGervaz
**A comprehensive, declarative UI library for the [Ash Framework](https://ash-hq.org/) โ€” define admin tables, forms, and data-driven interfaces entirely through DSL.** โœจ
[![Hex.pm](https://img.shields.io/hexpm/v/mishka_gervaz.svg?style=flat-square)](https://hex.pm/packages/mishka_gervaz)
[![Hex Downloads](https://img.shields.io/hexpm/dt/mishka_gervaz.svg?style=flat-square)](https://hex.pm/packages/mishka_gervaz)
[![License](https://img.shields.io/hexpm/l/mishka_gervaz.svg?style=flat-square)](https://github.com/mishka-group/mishka_gervaz/blob/master/LICENSE)
[![GitHub Sponsors](https://img.shields.io/badge/Sponsor-mishka--group-ea4aaa?style=flat-square&logo=github)](https://github.com/sponsors/mishka-group)
[![Buy Me a Coffee](https://img.shields.io/badge/Buy_Me_a_Coffee-mishkagroup-ffdd00?style=flat-square&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/mishkagroup)
</div>
---
> [!WARNING]
> **๐Ÿšง Status โ€” alpha.** APIs are still evolving and the library is not yet recommended for production.
> Track progress on [GitHub](https://github.com/mishka-group/mishka_gervaz) and the [CHANGELOG](https://github.com/mishka-group/mishka_gervaz/blob/master/CHANGELOG.md).
---
## ๐Ÿ“– Table of contents
- [Why MishkaGervaz?](#-why-mishkagervaz)
- [Highlights](#-highlights)
- [Installation](#-installation)
- [Quick start](#-quick-start)
- [A table](#-a-table)
- [A form](#-a-form)
- [Customization & overrides](#-customization--overrides)
- [Architecture](#-architecture)
- [Compatibility](#-compatibility)
- [Documentation](#-documentation)
- [Status & roadmap](#-status--roadmap)
- [Contributing](#-contributing)
- [Funding & sponsorship](#-funding--sponsorship)
- [License](#-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.
```elixir
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 `pubsub` and 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 true` builds 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-change` validation 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`, custom `load fn` for 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: true` for master-only fields, function predicates for fine-grained gating, per-action `:create` / `:update` rules.
- ๐Ÿ‘ฅ **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 `Gettext` so 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`:
```elixir
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:
```sh
mix deps.get
mix compile
```
Add the extension to your domain โ€” set defaults that every resource in the domain inherits:
```elixir
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:
```elixir
defmodule MyApp.Blog.Post do
use Ash.Resource,
domain: MyApp.Blog,
extensions: [MishkaGervaz.Resource]
# ... your attributes / actions / relationships
end
```
---
## ๐ŸŽฏ Quick start
### ๐Ÿ“Š A table
```elixir
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:
```heex
<.live_component
module={MishkaGervaz.Table.Web.Live}
id="posts-table"
resource={MyApp.Blog.Post}
current_user={@current_user}
/>
```
### ๐Ÿ“ A form
```elixir
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:
```heex
<.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:
```elixir
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`
```elixir
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
```elixir
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
```elixir
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 โ€” each `defoverridable`.
- ๐Ÿšš **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](https://hexdocs.pm/mishka_gervaz) (published with each release).
- ๐Ÿงญ **Guides** โ€” every public module ends its `@moduledoc` with 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](https://github.com/mishka-group/mishka_gervaz/blob/master/CHANGELOG.md).
---
## ๐Ÿค Contributing
Issues, PRs, and design discussions are welcome.
```sh
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](https://github.com/mishka-group). If your team or company benefits from this work, please consider supporting continued development:
<div align="center">
[![GitHub Sponsors](https://img.shields.io/badge/GitHub_Sponsors-mishka--group-ea4aaa?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/mishka-group)
&nbsp;&nbsp;&nbsp;
[![Buy Me a Coffee](https://img.shields.io/badge/Buy_Me_a_Coffee-mishkagroup-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/mishkagroup)
**โ˜• Donate / sponsor:**
[github.com/sponsors/mishka-group](https://github.com/sponsors/mishka-group) ยท [buymeacoffee.com/mishkagroup](https://www.buymeacoffee.com/mishkagroup)
</div>
Sponsorship directly funds maintenance, new features, and documentation. Thank you. ๐Ÿ’š
---
## ๐Ÿ“œ License
Apache License 2.0 โ€” see [`LICENSE`](LICENSE).
Copyright ยฉ Mishka Group and contributors.