Dashboard

PermissionsMatrix

Roles across, permissions down, a checkbox at each: a form sends the pairs ticked, a column box ticks a whole role.

#dashboard#admin#security#forms#table#interactive

Demo

Preview
PermissionOwner5 of 5EditorViewer
Content
Read
WriteCreate and edit pages
Publish
Team
Invite people
Billing
See invoicesAmounts and cards

Tick a box, or a role's All box.

PermissionOwnerEditor
Read
Write

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/permissions_matrix.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/permissions_matrix.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/permissions_matrix.qwe
let permissions_matrix_demo_roles = [{ key: "owner", label: "Owner", locked: true }, { key: "editor", label: "Editor" }, { key: "viewer", label: "Viewer" }]let permissions_matrix_demo_perms = [    { key: "read", label: "Read", group: "Content" },    { key: "write", label: "Write", group: "Content", hint: "Create and edit pages" },    { key: "publish", label: "Publish", group: "Content" },    { key: "invite", label: "Invite people", group: "Team" },    { key: "billing", label: "See invoices", group: "Billing", hint: "Amounts and cards" },]component PermissionsMatrixDemoLive() {    state last = null    <div.demo-stack>        <PermissionsMatrix roles={permissions_matrix_demo_roles} permissions={permissions_matrix_demo_perms}                           granted={["owner:read", "owner:write", "owner:publish", "owner:invite", "owner:billing", "editor:read", "editor:write", "editor:publish", "viewer:read"]}                           on_change={(r, p, on) => { last = `${r} ${on ? "gets" : "loses"} ${p}` }}/>        <p.demo-note>{last == null ? "Tick a box, or a role's All box." : `Last change: ${last}.`}</p>    </div>}view PermissionsMatrixDemo() =    <div.demo-stack>        <PermissionsMatrixDemoLive/>        <form method="post" action="#permissions_matrix" id="permissions-matrix-demo-form">            <PermissionsMatrix name="grant" label="Project roles" roles={take(permissions_matrix_demo_roles, 2) | map((r) => { key: r.key, label: r.label })}                               permissions={take(permissions_matrix_demo_perms, 2) | map((p) => { key: p.key, label: p.label })} granted={["owner:read"]}/>        </form>    </div>

Source

ui/permissions_matrix.qwe
// PermissionsMatrix: roles across the top, permissions down the side, a checkbox// where each meets. Each checkbox is named `name` with "role:permission" as its// value, so a plain <form> sends the ones ticked. A role's header has a checkbox// that ticks or clears its whole column.////   <PermissionsMatrix name="grant"//       roles={[{ key: "owner", label: "Owner", locked: true }, { key: "editor", label: "Editor" }, { key: "viewer", label: "Viewer" }]}//       permissions={[{ key: "read", label: "Read", group: "Content" }, { key: "write", label: "Write", group: "Content" },//                     { key: "billing", label: "See invoices", group: "Billing", hint: "Amounts and cards" }]}//       granted={["owner:read", "owner:write", "owner:billing", "editor:read", "editor:write", "viewer:read"]}/>//   <PermissionsMatrix roles={roles} permissions={perms} granted={given} on_change={(role, perm, on) => { ... }}/>//// roles: a list of { key, label, locked? }: a locked role (the owner) shows its// ticks but cannot be changed. permissions: a list of { key, label, hint?, group? };// permissions with the same group sit under a heading row. granted: the// "role:permission" pairs ticked to start with. name: the checkboxes' name// ("permissions"). label: the table's name ("Permissions"). on_change(role,// permission, granted): inside one of your components, called for each change,// with the keys and whether it is now ticked (once per cell when a column is// ticked or cleared). The count ticked per role shows under its name.import ui/theme.qwe use ui_clslet permissions_matrix_css = css`.ui-perms { overflow-x: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface);  font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-perms table { width: 100%; border-collapse: collapse; }.ui-perms th, .ui-perms td { padding: 10px 12px; border-bottom: 1px solid var(--ui-border); text-align: center; vertical-align: middle; }.ui-perms tbody tr:last-child td { border-bottom: 0; }.ui-perms th:first-child, .ui-perms td:first-child { text-align: left; }.ui-perms thead th { background: var(--ui-subtle); font-size: 13px; font-weight: 600; }.ui-perms-role { display: grid; gap: 4px; justify-items: center; }.ui-perms-count { color: var(--ui-muted); font-size: 12px; font-weight: 400; font-variant-numeric: tabular-nums; }.ui-perms-group th { padding: 8px 12px; background: var(--ui-subtle); color: var(--ui-muted); font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; text-align: left; }.ui-perms-label { font-weight: 500; }.ui-perms-hint { display: block; color: var(--ui-muted); font-size: 12px; font-weight: 400; }.ui-perms input[type="checkbox"] { width: 18px; height: 18px; margin: 0; accent-color: var(--ui-accent); vertical-align: middle; cursor: pointer; }.ui-perms input[type="checkbox"]:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-perms input[type="checkbox"]:disabled { cursor: not-allowed; opacity: .6; }.ui-perms-locked { color: var(--ui-muted); }.ui-perms-locked svg { width: 12px; height: 12px; vertical-align: -1px; margin-left: 3px; }.ui-perms tbody tr:hover td { background: color-mix(in srgb, var(--ui-subtle) 60%, transparent); }`component PermissionsMatrix(roles: list, permissions: list, granted: list = [], name: str = "permissions",                            label: str = "Permissions", on_change = null, class: str? = null) {    style global permissions_matrix_css    state on = granted    let pair = (r: str, p: str) => `${r}:${p}`    let has = (r: str, p: str) => contains(on, pair(r, p))    let flip = (r: str, p: str, to: bool) => {        if to && !has(r, p) { on = on | push(pair(r, p)) }        if !to && has(r, p) { on = on | filter((x) => x != pair(r, p)) }        on_change && on_change(r, p, to)    }    let permissions_matrix_ticked = (r: str) => permissions | filter((p) => has(r, p.key)) | len    let groups = permissions | map((p) => p.group) | unique    <div class={ui_cls(["ui-perms", class])}>        <table aria-label={label}>            <thead><tr>                <th scope="col">Permission</th>                {for r in roles => {                    let n = permissions_matrix_ticked(r.key)                    let every_one = n == len(permissions)                    <th scope="col" key={r.key}>                        <span.ui-perms-role>                            <span>{r.label}{r.locked == true && <span.ui-perms-locked aria-label="locked" title="Cannot be changed">                                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>                            </span>}</span>                            {r.locked == true                                ? <span.ui-perms-count>{n} of {len(permissions)}</span>                                : <label.ui-perms-count>                                    <input type="checkbox" form="ui-perms-none" aria-label={`All permissions for ${r.label}`} checked={every_one}                                           @mount={(el) => { el.indeterminate = n > 0 && !every_one }}                                           @change={(e) => { for p in permissions => flip(r.key, p.key, e.checked) }}/>                                    {" "}{n} of {len(permissions)}                                </label>}                        </span>                    </th>                }}            </tr></thead>            <tbody>                {for g in groups => [                    g != null && <tr.ui-perms-group key={`group-${g}`}><th scope="colgroup" colspan={len(roles) + 1}>{g}</th></tr>,                    for p in permissions | filter((x) => x.group == g) => <tr key={p.key}>                        <th scope="row">                            <span.ui-perms-label>{p.label}</span>                            {p.hint != null && <span.ui-perms-hint>{p.hint}</span>}                        </th>                        {for r in roles => <td key={r.key}>                            <input type="checkbox" name={name} value={pair(r.key, p.key)} checked={has(r.key, p.key)}                                   disabled={r.locked == true} aria-label={`${p.label} for ${r.label}`}                                   @change={(e) => { flip(r.key, p.key, e.checked) }}/>                        </td>}                    </tr>,                ]}            </tbody>        </table>    </div>}
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On PermissionsMatrix: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.