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 HighlightJSON(payload string) string
- 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 UpdateNavigation(c *echo.Context, links []NavLink)
- func ViewerOf(ctx context.Context, override *jwt.Identity) *jwt.Identity
- type Action
- type ActionsBlock
- type Block
- func Blocks(children ...Block) Block
- func FilterableTableBlock(filters []OverviewFilter, headers []string, rows []OverviewRow) Block
- func FiltersBlock(filters []OverviewFilter) Block
- func Gate(required Role, block Block) Block
- func PagerBlock(m PagerModel) Block
- func ScopeNoteBlock(note string) Block
- func SummaryCard(title string, items []SummaryItem) Block
- func TableBlock(headers []string, rows []OverviewRow) Block
- type BlockFunc
- type Breadcrumb
- type BreadcrumbsBlock
- type CardBlock
- type DefaultOverviewBlocks
- type FilterOption
- type HTMLBlock
- type HasRequiredRole
- type HeaderBlock
- type NavLink
- type NavigationBlock
- type OverviewCell
- type OverviewConfig
- type OverviewFilter
- type OverviewRow
- type PagerModel
- type PanelBlock
- 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 HighlightJSON ¶ added in v2.10.2
HighlightJSON colourises an already-indented JSON payload with Bootstrap text-colour utility classes, escaping it first. Those classes come from the stylesheet the page already has - server-side because a client-side highlighter would be dropped together with the <head> when the page is embedded as a backoffice fragment. Shared by every block that shows a JSON payload verbatim (a ledger record, a summary row) so the rendering stays consistent across backoffice pages.
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.
func UpdateNavigation ¶ added in v2.10.2
UpdateNavigation writes links to the navigation cookie as the persistent top-level navigation, replacing whatever was stored before.
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
ConfirmText string // label of the modal's confirm button; empty = "OK"
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 FilterableTableBlock ¶ added in v2.10.2
func FilterableTableBlock(filters []OverviewFilter, headers []string, rows []OverviewRow) Block
FilterableTableBlock renders the filter form and the results table together in one card, the form separated from the table by a divider. This is what RenderOverviewWithConfig uses by default. Renders the table even when filters is empty (the form section is simply omitted).
func FiltersBlock ¶
func FiltersBlock(filters []OverviewFilter) Block
FiltersBlock renders the GET filter form on its own, outside any card. Reach for FilterableTableBlock instead when the filters belong above a results table - the common case, and the default overview layout. 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 SummaryCard ¶ added in v2.10.2
func SummaryCard(title string, items []SummaryItem) Block
SummaryCard is SummaryBlock under a card title, for a page that shows more than one summary section (e.g. account data next to profile data) and needs each one labeled. It gates and renders nothing exactly like SummaryBlock.
func TableBlock ¶
func TableBlock(headers []string, rows []OverviewRow) Block
TableBlock renders the clickable rows table in its own card. Always renders (shows a "No records." row when rows is empty). Use this for a table with no filter form (e.g. a fixed recent-N list); use FilterableTableBlock when the table has a filter form above it, so both sit in one card.
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 Breadcrumb ¶ added in v2.10.2
Breadcrumb is a single entry in the cross-service navigation trail.
Key is a stable logical identifier for the resource shown on the page, e.g. "player:abcdef" or "order:123". It is used to look up an entry across services, independent of the actual URL structure.
Label and Path are only known to (and only ever set by) the service that owns the resource identified by Key. Other services may reference the same Key without knowing Label or Path.
func BreadcrumbLocal ¶ added in v2.10.2
func BreadcrumbLocal(key, label, path string) Breadcrumb
BreadcrumbLocal creates a Breadcrumb for a resource owned by the calling service, with an authoritative label and path.
func BreadcrumbRemote ¶ added in v2.10.2
func BreadcrumbRemote(key string) Breadcrumb
BreadcrumbRemote creates a Breadcrumb for an ancestor resource whose label and path are not known to the calling service. It will be rendered without a link unless a cached entry with the same Key is already known.
func BreadcrumbRemoteWithLabel ¶ added in v2.10.2
func BreadcrumbRemoteWithLabel(key, label string) Breadcrumb
BreadcrumbRemoteWithLabel creates a Breadcrumb for an ancestor resource whose label is known to the calling service, but whose path is not. It will be rendered without a link unless a cached entry with the same Key is already known. BreadcrumbRemote should be prefered.
func ReconcileBreadcrumbs ¶ added in v2.10.2
func ReconcileBreadcrumbs(c *echo.Context, entries ...Breadcrumb) []Breadcrumb
ReconcileBreadcrumbs loads the current breadcrumb cookie, reconciles it with the given breadcrumb chain (see BreadcrumbCookie.Reconcile), saves the result back to the cookie, and returns the reconciled breadcrumbs.
type BreadcrumbsBlock ¶ added in v2.10.2
type BreadcrumbsBlock []Breadcrumb
BreadcrumbsBlock renders the breadcrumb trail as a bootstrap breadcrumb.
func (BreadcrumbsBlock) Render ¶ added in v2.10.2
func (b BreadcrumbsBlock) 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.
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. Filters is the form on its own (not part of All()) for a custom layout that wants it apart from Table, which by default already renders the filters and the table together.
func (DefaultOverviewBlocks) All ¶
func (d DefaultOverviewBlocks) All() []Block
All returns the default blocks in their default order (header, scope note, pager, filters+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 NavLink ¶
type NavLink struct {
// current request path. It is never persisted in the cookie.
IsActive bool `json:"-"`
}
NavLink is a persistent, top-level navigation link that, unlike a Breadcrumb, is not part of the current drill-down trail. It is stored alongside the breadcrumb trail so it survives across services too.
func LoadNavigation ¶ added in v2.10.2
LoadNavigation returns the persistent top-level navigation links stored in the navigation cookie. It returns nil if the cookie is missing or could not be decoded.
type NavigationBlock ¶ added in v2.10.2
type NavigationBlock []NavLink
NavigationBlock renders the persistent navigation links as a bootstrap nav bar.
func (NavigationBlock) Render ¶ added in v2.10.2
func (b NavigationBlock) Render(rc RenderContext) (template.HTML, error)
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: a HeaderBlock, ScopeNoteBlock, a PagerBlock, a
// FilterableTableBlock (filters and table in one card) 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
// CellTones renders individual cells as a bootstrap badge in that tone
// (index-aligned with Cells, empty entry = plain text) - for a state column an
// operator scans down rather than reads, e.g. a status. A cell link wins over
// a tone.
CellTones []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 PanelBlock ¶ added in v2.10.2
type PanelBlock struct {
Children []Block
// Raised adds a small drop shadow (Bootstrap shadow-sm) to lift the card off
// the page.
Raised bool
}
PanelBlock wraps child blocks in a bare Bootstrap card frame, one that adds no card-body padding of its own - unlike CardBlock, whose Body always sits in a padded card-body. Reach for PanelBlock when a card holds sections with different padding needs (e.g. a padded filter form above a table that must sit flush against the card's edges); each child renders itself and is responsible for its own spacing. A child that renders to nothing (Skip, or an empty Blocks) simply contributes nothing.
func (PanelBlock) Render ¶ added in v2.10.2
func (b PanelBlock) Render(rc RenderContext) (template.HTML, error)
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. Use SummaryCard instead for a titled card.
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
// JSON, when non-empty, renders Value as a collapsible <details> whose body is
// this payload, highlighted like a ledger record - for a row that summarises a
// structure the operator sometimes needs verbatim (an order item as stored).
JSON string
// Tone, when non-empty, renders Value as a bootstrap badge in that tone
// ("success", "danger", "warning", "info", "secondary", ...) - for a state
// value an operator scans for rather than reads, e.g. an order status. Ignored
// for a JSON row.
Tone string
}
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.
func (SummaryItem) JSONHTML ¶ added in v2.10.2
func (i SummaryItem) JSONHTML() template.HTML
JSONHTML is SummaryItem.JSON colourised, same treatment as a record payload.
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 Skip 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)