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
- func GateSlice[T HasRequiredRole](rc RenderContext, values []T) []T
- func MayPerform(viewer *jwt.Identity, required Role) bool
- func MustTemplatesFromFS(fsys fs.FS) *template.Template
- func Render(w io.Writer, cfg RenderConfig) error
- func RenderOverview(w io.Writer, title string, headers []string, rows []OverviewRow) error
- func RenderOverviewWithConfig(w io.Writer, cfg OverviewConfig) error
- func RenderTemplate(rc RenderContext, tmpl *template.Template, name string, data any) (template.HTML, error)
- func RenderWithShell(w io.Writer, tpl *template.Template, cfg RenderConfig) error
- func Templates() *template.Template
- func TemplatesFromFS(fsys fs.FS) (*template.Template, error)
- func ViewerOf(ctx context.Context, override *jwt.Identity) *jwt.Identity
- type Action
- type ActionsBlock
- type Block
- type BlockFunc
- type CardBlock
- type DefaultOverviewBlocks
- type FilterOption
- type HTMLBlock
- type HasRequiredRole
- type HeaderBlock
- type NavBlock
- type NavLink
- type OverviewCell
- type OverviewConfig
- type OverviewFilter
- type OverviewRow
- type PagerModel
- type RenderConfig
- type RenderContext
- type Role
- type SummaryBlock
- type SummaryItem
- type TemplateBlock
Constants ¶
const ( RoleAdmin = "admin" RoleWrite = "write" RoleRead = "read" )
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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.
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 ¶
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 ¶
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 ¶
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.
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.
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 ¶
FilterOption is one entry of an OverviewFilter dropdown.
type HTMLBlock ¶
HTMLBlock is a block of pre-rendered HTML, for callers that want to drop in arbitrary markup without a template.
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 ¶
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 ¶
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.
type NavLink ¶
type NavLink struct {
// 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 (NavLink) GatingRole ¶
GatingRole implements HasRequiredRole.
type OverviewCell ¶
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 ¶
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 ¶
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.
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 ¶
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)