boff

package
v2.9.10 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package boff holds the block-based rendering machinery used by backoffice pages. A page is a slice of Block; each block renders to a fragment of HTML, and the blocks are concatenated top to bottom into a page shell. This is the generic core, free of any particular page's models, so it can back a history ledger, an overview list, or any other backoffice page.

Index

Constants

View Source
const (
	RoleAdmin = "admin"
	RoleWrite = "write"
	RoleRead  = "read"
)
View Source
const PageParam = "page"

PageParam is the query parameter the overview pager pages with.

Variables

This section is empty.

Functions

func GateSlice

func GateSlice[T HasRequiredRole](rc RenderContext, values []T) []T

GateSlice returns the entries rc's viewer may see, dropping the rest. Returns nil when nothing survives, so an empty block renders nothing.

func MayPerform

func MayPerform(viewer *jwt.Identity, required Role) bool

MayPerform reports whether viewer satisfies required. An empty required is ungated; no viewer denies everything gated.

ponytail: a role qualified with a foreign audience is denied unless the viewer was issued for that audience. Cross-audience display via the advisory Actor-Roles header is only worth building when a page actually mixes services.

func MustTemplatesFromFS

func MustTemplatesFromFS(fsys fs.FS) *template.Template

MustTemplatesFromFS is TemplatesFromFS for a package-level var: it panics on a parse error, since an embedded template that fails to parse is a build-time bug, not a runtime condition.

func Render

func Render(w io.Writer, cfg RenderConfig) error

Render renders cfg.Blocks and writes the full page shell to w, using the built-in shell. Each block renders with a RenderContext carrying cfg.Viewer, so blocks gate themselves. Use RenderWithShell to supply your own shell template (from Templates or TemplatesFromFS) carrying extra funcs or sub-templates.

func RenderOverview

func RenderOverview(w io.Writer, title string, headers []string, rows []OverviewRow) error

RenderOverview writes a standalone clickable table; each row links to Link.

func RenderOverviewWithConfig

func RenderOverviewWithConfig(w io.Writer, cfg OverviewConfig) error

RenderOverviewWithConfig is RenderOverview with the optional display elements (filter form, scope note).

func RenderTemplate

func RenderTemplate(rc RenderContext, tmpl *template.Template, name string, data any) (template.HTML, error)

RenderTemplate executes the named (sub-)template of tmpl with data, bound to rc, and returns its HTML. It is the plumbing behind TemplateBlock and the way to render a template from your own Block: the template can render a child Block inline via {{ . | render }}, which executes against the same rc. tmpl is cloned per call so the binding never leaks into a shared template, which also means tmpl itself is never executed and stays reusable.

func RenderWithShell

func RenderWithShell(w io.Writer, tpl *template.Template, cfg RenderConfig) error

RenderWithShell is Render with an explicit shell template, from Templates or TemplatesFromFS. tpl is never executed directly - RenderTemplate executes a render-bound clone so tpl stays pristine and reusable.

func Templates

func Templates() *template.Template

Templates returns a fresh, empty page template carrying only the shared default funcs (formatTime, add, formatMoney, render). Parse your own block sub-templates into it, then pass it to RenderWithShell. It is a fresh template every call, so callers never share (or accidentally execute) a common base.

func TemplatesFromFS

func TemplatesFromFS(fsys fs.FS) (*template.Template, error)

TemplatesFromFS is Templates with every .gohtml file in fsys parsed in, for a page that keeps its block templates in an embedded FS. It walks fsys, so the files may sit at any depth (a templates/ subdirectory, say) without the caller naming a glob. The parsed definitions and the shared funcs are all available to RenderWithShell.

func ViewerOf

func ViewerOf(ctx context.Context, override *jwt.Identity) *jwt.Identity

ViewerOf returns the identity used for gating: the explicit override, else the verified identity of the request, else nil.

Types

type Action

type Action struct {
	Description    string // e.g. "Cancel item Sword-Pack"
	ButtonText     string // e.g. "Cancel"
	Endpoint       string // e.g. "/orders/backoffice/v1/orders/123/items/1/cancel"
	ConfirmMessage string // optional confirmation prompt; empty = submit immediately
	StatusText     string // non-empty = show label instead of button

	// true renders a link to Endpoint instead of a form, for an action that only
	// navigates. Ignores ConfirmMessage.
	Link bool

	// RequiredRole gates the whole action: "write" or "admin" means that role on
	// this service's own audience, "payment-service:admin" names another
	// audience. Empty means always shown. A viewer who may not perform the action
	// does not see it at all - a disabled button still tells a read-only user
	// which endpoint to curl.
	RequiredRole Role
}

Action is one row in the actions table on a backoffice page. When StatusText is non-empty the row renders a static label instead of a button (e.g. "Cancelled"); otherwise a plain HTML form that POSTs to Endpoint.

The page ships no JavaScript: the form is a form, and a ConfirmMessage renders a Bootstrap modal driven by Bootstrap's own JS. Endpoint must therefore be a URL the browser can resolve - a service behind backoffice builds it from the base path backoffice sends with the fragment request, not from its own internal path.

func (Action) GatingRole

func (a Action) GatingRole() Role

GatingRole implements HasRequiredRole.

type ActionsBlock

type ActionsBlock []Action

ActionsBlock is an Action list rendered as the actions table. It gates itself at render time: the actions a viewer may not perform are dropped. Renders nothing when empty.

The slice is the block: boff.ActionsBlock{...} or a boff.ActionsBlock(actions) conversion both give you a Block.

func (ActionsBlock) Render

func (b ActionsBlock) Render(rc RenderContext) (template.HTML, error)

type Block

type Block interface {
	Render(rc RenderContext) (template.HTML, error)
}

Block is one renderable section of a page. Blocks are rendered top to bottom in the order given, so a page is just a slice of them. Providing your own Block is how you extend a page beyond its built-in sections without touching the page shell.

Render receives a RenderContext carrying the viewing identity and the shell template. A block gates itself here - it consults rc.May and emits only what the viewer may see. Because the context is passed on every call, a container block hands the same rc to its children, so gating and template resolution compose to any nesting depth.

A block is free to produce any HTML it likes. An empty block returns no bytes and is simply skipped.

func Blocks

func Blocks(children ...Block) Block

Blocks renders a sequence of child blocks and concatenates their output into one HTML fragment, so a container block can hold other blocks - and so a page turns its whole block slice into markup. Empty children (those that render to nothing) are skipped, and the same rc is passed to each, so gating reaches nested blocks unchanged.

func FiltersBlock

func FiltersBlock(filters []OverviewFilter) Block

FiltersBlock renders the GET filter form. Renders nothing when filters is empty.

func Gate

func Gate(required Role, block Block) Block

Gate wraps a block so it renders only when the viewer satisfies required, in the notation of Action.RequiredRole. A denied viewer sees nothing - the whole wrapped block vanishes. This is the coarse counterpart to the fine-grained, per-item gating SummaryBlock and ActionsBlock do: reach for it to hide an entire section (a card, a whole custom block) behind one role.

func PagerBlock

func PagerBlock(m PagerModel) Block

PagerBlock renders the pagination nav. Renders nothing when there is no previous or next page.

func ScopeNoteBlock

func ScopeNoteBlock(note string) Block

ScopeNoteBlock renders the one-line scope note. Renders nothing when empty.

func TableBlock

func TableBlock(headers []string, rows []OverviewRow) Block

TableBlock renders the clickable rows table. Always renders (shows a "No records." row when rows is empty).

type BlockFunc

type BlockFunc func(rc RenderContext) (template.HTML, error)

BlockFunc adapts a plain function to a Block, so a one-off block needs no named type.

func (BlockFunc) Render

func (f BlockFunc) Render(rc RenderContext) (template.HTML, error)

type CardBlock

type CardBlock struct {
	Title    string
	Subtitle string
	Body     Block
	// Raised adds a small drop shadow (Bootstrap shadow-sm) to lift the card off
	// the page.
	Raised bool
}

CardBlock wraps a body block in a Bootstrap card with a title and optional subtitle. The body is itself a Block, rendered inline by the card template via the render func (RenderTemplate), with the same rc - so gating and nested blocks compose inside a card like anywhere else.

func (CardBlock) Render

func (b CardBlock) Render(rc RenderContext) (template.HTML, error)

type DefaultOverviewBlocks

type DefaultOverviewBlocks struct {
	Header    Block
	Filters   Block
	ScopeNote Block
	Pager     Block
	Table     Block
}

DefaultOverviewBlocks are the built-in blocks an overview page renders out of the box, handed to an OverviewConfig.Blocks callback so custom layouts can reuse them. Pager is shared by the top and bottom pager.

func (DefaultOverviewBlocks) All

func (d DefaultOverviewBlocks) All() []Block

All returns the default blocks in their default order (header, filters, scope note, pager, table, pager).

type FilterOption

type FilterOption struct {
	Value string
	Label string
}

FilterOption is one entry of an OverviewFilter dropdown.

type HTMLBlock

type HTMLBlock template.HTML

HTMLBlock is a block of pre-rendered HTML, for callers that want to drop in arbitrary markup without a template.

func (HTMLBlock) Render

func (b HTMLBlock) Render(RenderContext) (template.HTML, error)

type HasRequiredRole

type HasRequiredRole interface {
	GatingRole() Role
}

HasRequiredRole is implemented by anything a viewer can be gated against - an Action, a NavLink. GateSlice uses it to drop the entries a viewer may not see, so a new gated element type only has to report its role, not its own filter.

type HeaderBlock

type HeaderBlock struct {
	Title        string
	Subtitle     string
	ErrorMessage string
}

HeaderBlock is the page heading: an h1 title, an optional monospace subtitle, and an optional error alert. It was the shell's built-in header; as a block a page places it wherever it likes (or drops it), for full control of the page layout.

func (HeaderBlock) Render

func (b HeaderBlock) Render(rc RenderContext) (template.HTML, error)
type NavBlock []NavLink

NavBlock is a NavLink list rendered as a <nav> bar. It gates itself at render time: the links a viewer may not follow are dropped. Renders nothing when empty.

The slice is the block: boff.NavBlock{...} or a boff.NavBlock(links) conversion both give you a Block.

func (b NavBlock) Render(rc RenderContext) (template.HTML, error)
type NavLink struct {
	Label  string // e.g. "Orders"
	Href   string // e.g. "/orders/backoffice/v1/orders"
	Active bool   // true renders the current page's entry as active

	// RequiredRole gates the link, in the same notation as Action.RequiredRole. A
	// viewer who lacks the role does not see the entry at all - a nav link is a
	// pointer to a page, and pointing a read-only user at a page they cannot open
	// is noise.
	RequiredRole Role
}

NavLink is one entry of a NavBlock: a labelled link in the page's navigation bar. Active marks the current page, so the nav can highlight where the viewer is.

func (l NavLink) GatingRole() Role

GatingRole implements HasRequiredRole.

type OverviewCell

type OverviewCell struct {
	Text string
	Link string
}

OverviewCell is one table cell with its optional own link.

type OverviewConfig

type OverviewConfig struct {
	Title     string
	Headers   []string
	Rows      []OverviewRow
	Filters   []OverviewFilter
	ScopeNote string

	// Page is the 1-based page number shown; 0 and 1 both mean the first page.
	// HasNext tells the pager that another page exists - the caller knows this by
	// loading one row more than it displays, so no count query is needed.
	Page    int
	HasNext bool
	// TotalPages enables the jump to the last page. Leave it 0 when counting the
	// whole result set is not worth a second query; the pager then only walks.
	TotalPages int

	// Blocks overrides the sections rendered on the page. When nil the page uses
	// its default layout: FiltersBlock, ScopeNoteBlock, a PagerBlock, TableBlock
	// and a trailing PagerBlock built from the fields above. When set, the
	// callback receives those default blocks and returns the blocks to render in
	// order - so a caller can reorder them, drop one, or splice its own Block in.
	Blocks func(defaults DefaultOverviewBlocks) []Block
}

OverviewConfig bundles everything the overview page renders.

type OverviewFilter

type OverviewFilter struct {
	Label       string
	Name        string
	Value       string
	Placeholder string
	// Type is the HTML input type; empty means "text". "date" gets the browser's
	// own date picker, no JavaScript needed.
	Type string
	// Options, when non-empty, renders a <select> instead of an input. The empty
	// value must be part of the list to allow "no filter".
	Options []FilterOption
	// Hidden keeps the value in the form and in the pagination links without
	// showing a control - for a filter the page receives from elsewhere (a deep
	// link carrying a player id) rather than one the viewer types.
	Hidden bool
}

OverviewFilter is one field of the overview filter form. Value is the currently applied value, echoed back so the form stays sticky.

func (OverviewFilter) InputType

func (f OverviewFilter) InputType() string

InputType is the type attribute of a text-ish filter input.

type OverviewRow

type OverviewRow struct {
	Link      string
	Cells     []string
	CellLinks []string
}

OverviewRow is one list entry; Cells aligns with the overview Headers.

Link makes the whole row navigate to one detail page. CellLinks instead links individual cells (index-aligned with Cells, empty entry = plain text), which is what a table with several targets needs - an id column pointing at this service's detail page, a foreign id pointing at the owning service. A cell link is rendered as a visible link; a row link stays inconspicuous.

func (OverviewRow) CellAt

func (r OverviewRow) CellAt(i int) OverviewCell

CellAt pairs a cell with its own link, so the template does not have to index two slices in parallel.

type PagerModel

type PagerModel struct {
	Page       int
	TotalPages int
	FirstLink  string
	PrevLink   string
	NextLink   string
	LastLink   string
}

PagerModel is the pagination state a PagerBlock renders. Both pagers (above and below the table) render the same model. The Link fields are empty when there is no such page.

type RenderConfig

type RenderConfig struct {
	Title  string
	Viewer *jwt.Identity
	Blocks []Block
}

RenderConfig is the caller-supplied config for the shell. Title feeds the document <title>; the visible page heading is a HeaderBlock a page adds to Blocks. Blocks are rendered into the shell body in order.

Viewer is the identity the page's gated blocks filter themselves against. It is put into the RenderContext each block renders with. Leave it nil to fail closed - every gated element is then hidden. Use ViewerOf to derive it from the request context.

type RenderContext

type RenderContext struct {
	// Viewer is the identity gated blocks filter themselves against. Nil means
	// fail closed: May reports false for everything gated.
	Viewer *jwt.Identity
}

RenderContext carries everything a Block needs while it renders - currently the viewing identity to gate against. It is passed to every Block.Render, including nested ones: a container block hands its own rc straight to its children, so gating reaches any depth.

It is a struct (not a bare *jwt.Identity) so more render-time values can be propagated later - a base path for URL building, a nonce, feature flags - without churning the Block interface again.

func (RenderContext) May

func (rc RenderContext) May(required Role) bool

May reports whether the context's viewer satisfies required, in the notation of Action.RequiredRole ("write", "admin", or "audience:role"). An empty required is ungated and always allowed; a nil viewer denies everything gated.

This is the one check both Go blocks and the "may" template function share, so a block's Render and its template agree on who may see what. Reuse it when you write your own block instead of reaching for jwt directly.

type Role

type Role string

Role names the permission a gated element requires, in the notation the whole package shares: "write" or "admin" means that role on this service's own audience; "payment-service:admin" names a role on a foreign audience. The empty Role is ungated - always shown.

func RoleOf

func RoleOf(audience, role string) Role

type SummaryBlock

type SummaryBlock []SummaryItem

SummaryBlock is a SummaryItem list rendered as the current-state summary card. It gates itself at render time: the links a viewer may not follow are demoted to plain values. Renders nothing when empty.

The slice is the block: boff.SummaryBlock{...} or a boff.SummaryBlock(items) conversion both give you a Block.

func (SummaryBlock) Render

func (b SummaryBlock) Render(rc RenderContext) (template.HTML, error)

type SummaryItem

type SummaryItem struct {
	Label string
	Value string
	// Link, when non-empty, renders Value as an <a href> to this URL instead of
	// plain text - e.g. a cross-service backoffice link to the page that owns the
	// referenced entity (a payment or draw history page behind the api-gateway).
	Link string

	// RequiredRole gates the Link only, in the same notation as
	// Action.RequiredRole. A denied item still shows Label and Value, just not as
	// an anchor: the value itself is not the secret, the page behind it is.
	RequiredRole Role
}

SummaryItem is one label/value row shown above the page content, describing the current state of the tracked object. Ordered slice (not a map) so the page renders stably.

type TemplateBlock

type TemplateBlock struct {
	Name     string
	Model    any
	Empty    bool
	Template *template.Template
}

TemplateBlock renders a named (sub-)template with a model. It is the common shape of the built-in blocks and the easiest way to add your own: give it the Name of a template and a Model to execute it with.

Template must contain a definition for Name. The built-in blocks set it to the package shell, so they resolve the sub-templates defined alongside the shell. Set Empty to render nothing, which is how a section vanishes when it has no content.

func (TemplateBlock) Render

func (b TemplateBlock) Render(rc RenderContext) (template.HTML, error)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL