Every view is data-bound (core: Doc/GUI/DataBinding → Templates first, data later). A node page
in this pack used to subscribe to GetMeshNodeStream() on its hub, wait, and only then build its
controls out of the record. Until the owning hub answered the page was a spinner, and each emission
rebuilt the whole tree. Each converted page now has three parts.
The shape
| Part | What it is |
|---|---|
| Template | BuildTemplate… (pure) returns the whole tree. Every view in it is a control, and every value is a JsonPointerReference into a projection record. A section the record may lack is in the tree anyway; its style is bound and is display: none; when the projection says it is absent. The tree's shape never depends on the data. |
| Projection | Project(…) (pure) turns the node's content into that record (ApiTokenView, ReleaseView, NotificationView). It holds the decisions the builders used to make inline: which sections show, the status label and colour, the toggle's label, the viewer-zone instants. The area returns stream.Select(Project).Bind(_ => template, ViewId), so the framework feeds /data/{ViewId} live. |
| Slots | Two platform pieces still take a LOADED node. The header is MeshNodeLayoutAreas.BuildHeader. The property form is OverviewLayoutArea.BuildPropertyOverview, which picks its fields from the content's runtime type. Each renders in a nested area (NodePageSlots.HeaderArea = PageHeader, NodePageSlots.PropertiesArea = PageProperties) with SpinnerType.Skeleton, so the page around it is already there. When core offers path-bound forms of the two, NodePageSlots is the one place to swap them in. |
A page gated on Read (GroupMembership) keeps its gate as DATA. The access-denied notice and the
body are both in the template. NodePageSlots.ReadGated binds their styles to the viewer's effective
permission.
The bound style hides; it does not withhold. Every viewer receives the body, so a read-gated body holds nothing of the node: only static controls and references to slots. The node's content is served by the slots, and each is an area of its own that a client can ask for with nothing but the node's address. Two things keep it from a viewer without Read:
- The platform refuses the subscription.
SubscribeRequestrequiresPermission.Readon the node, so the viewer is answered "Access denied: user '…' lacks Read permission on '…'" for the page and for each slot. Measured byNodePageSlotsReadGateTeston a mesh with row-level security and no blanket grant. - The slots decide Read themselves.
ReadOnlyHeader,EditableHeaderandPropertiesbuild their piece throughNodePageSlots.ForReader, which returns an empty stack and never calls the builder when the viewer's effective permissions lack Read. This is the decision the old Overview made before it built anything, kept where the content is now built. - Until the node has loaded,
Propertiesemits nothing (NodePageSlots.LoadedForReaderanswers null and the slot holds it back), so the skeleton covers the load. An empty stack there would replace the skeleton with a blank.
Node content in the page
- Instants. A stored instant is UTC and is shown in the viewer's zone (
NodePageSlots.Instant). The zone is read once, on the render turn (NodePageSlots.ViewerZone), and handed to the projection. The projection runs on later emissions of the node stream, where the ambient access context is no longer the viewer's, so reading the zone there shows UTC to everyone. - Links. The release page shows file keys, the NodeType path and the compile-activity path as
markdown links. They are node content, so
ReleaseLayoutAreas.Linkescapes both halves: the text reads as literal characters and the destination cannot close its angle brackets early. - Hrefs. Every node href in these pages (release links and history links, a notification's
target, a membership card) is built by
NodePageSlots.InMeshHref. A path is node content and may begin with/,\or whitespace;"/" + "/evil.example"would be the protocol-relative//evil.example, a link out of the mesh.InMeshHrefstrips that leading run, so the result is always a root-relative path (https://evil.examplebecomes/https://evil.example).
Until the projection arrives, the bound fields draw the platform's loading shape (LoadingShape). A node that has not loaded projects to "every section hidden". A node that loaded without the expected content projects to the notice the page always showed.
Converted
| Page | Areas |
|---|---|
| API token | Overview, Thumbnail |
| Release | Overview |
| Notification | Overview, Thumbnail (a node without a notification falls back to the path-only platform card) |
| GroupMembership | Overview (header and property form in their slots) |
The text these pages used to carry inline now lives in the module-owned table NodeChromeTexts
(English and German). Strings that were already catalog keys stay catalog keys.
Pinned by
src/MeshWeaver.Graph.Views.Test/NodeChromeTemplatesTest.cs checks these things:
- Every template is static, and every pointer in it names a property of its projection.
- The header and property-form slots are skeleton slots.
- The projections make the decisions they replaced.
NodeChromeProjectionsTest: a hostile file key or path renders, through the platform's markdown pipeline, as the text of its own link and nothing else; an API token node that has not loaded hides every section, and only a loaded node without a token shows the notice; stored instants render in the viewer's zone across both DST changes and the date rollover;ForReaderbuilds nothing without Read;LoadedForReaderemits nothing until the node has loaded; a node path beginning with/,//,\or a scheme stays an in-mesh href on every page.NodePageSlotsReadGateTest(live, row-level security, no blanket grant): a viewer without Read who asks for the GroupMembership Overview,PageHeaderorPagePropertiesis refused and receives nothing of the membership, and one who asks for a Notification's read-onlyPageHeaderreceives nothing of the node. The node's admin draws each slot through the same subscriptions.- Live: the rendered Notification Overview's first control is the template bound to
/data/notificationView, and its projection follows an edit of the node. The GroupMembership Overview publishes its read gate, and both slots draw.
Negative control: adding one deferred view to a template fails EveryViewIsStatic.
Deploy
These views are compiled into the pack's assembly, so a new activation picks them up. A node hub
that is already running keeps the old views until it is disposed. After the roll, recycle the
NodeTypes ApiToken, Release, Notification and GroupMembership; the dispose cascades to their
live instances.