diff --git a/policy-site/README.md b/policy-site/README.md index bbb5908c..21a4e7c8 100644 --- a/policy-site/README.md +++ b/policy-site/README.md @@ -48,6 +48,8 @@ Without that one the page has a plain ground. syntax. - `src/ui/snippet.ts`: read-only JSON, highlighted by walking the parsed value. Its text is exactly `JSON.stringify(value, null, 2)`. +- `src/ui/policy-review.ts`: a policy at a glance, as the list and the + examples show it: the record's fields, and the document's body highlighted. - `src/ui/policy-editor.ts`: the policy editor. The record's own fields are form fields, and the engine select sets the document's `$type`. `src/ui/policy-document.ts` turns a record into those fields and back, diff --git a/policy-site/src/ui/examples.ts b/policy-site/src/ui/examples.ts index 8971702e..496ae45a 100644 --- a/policy-site/src/ui/examples.ts +++ b/policy-site/src/ui/examples.ts @@ -3,6 +3,7 @@ import type { Example } from "../checks.ts"; import { h, replaceChildren } from "../dom.ts"; import type { PolicySyntax } from "./policy-syntax.ts"; +import { facts, policyBody, policyFacts } from "./policy-review.ts"; import { snippet } from "./snippet.ts"; // Examples name their repository with the first DID, inside AT-URIs, and a @@ -40,15 +41,15 @@ export function renderExamples( "li", { class: "card" }, h("h3", {}, example.title), - h( - "dl", - {}, - h("dt", {}, "collection"), - h("dd", { class: "mono" }, example.collection), - h("dt", {}, "rkey"), - h("dd", { class: "mono" }, example.rkey), - ), - snippet(example.record, syntax), + ...(example.collection === "bot.did.policy" + ? [facts(policyFacts(example.record, example.rkey)), policyBody(example.record, syntax)] + : [ + facts([ + ["collection", example.collection], + ["rkey", example.rkey], + ]), + snippet(example.record, syntax), + ]), h( "div", { class: "buttons" }, diff --git a/policy-site/src/ui/policy-review.ts b/policy-site/src/ui/policy-review.ts new file mode 100644 index 00000000..451e4679 --- /dev/null +++ b/policy-site/src/ui/policy-review.ts @@ -0,0 +1,33 @@ +// A policy at a glance: its record's own fields as a short list, and its +// document's body as highlighted JSON. The policy list and the examples both +// show a policy this way. + +import { h } from "../dom.ts"; +import { documentBody } from "./policy-document.ts"; +import { VALUE, type PolicySyntax } from "./policy-syntax.ts"; +import { snippet } from "./snippet.ts"; + +const text = (value: unknown): string => (typeof value === "string" ? value : ""); +const list = (value: unknown): string => (Array.isArray(value) ? value.map(String).join(", ") : ""); + +/** The record's own fields, as `[term, value]` pairs. */ +export function policyFacts(record: Record, rkey: string): [string, string][] { + return [ + ["name", text(record.name)], + ["rkey", rkey], + ["actions", list(record.actions)], + ["engine", text(documentBody(record.document).type)], + ["createdAt", text(record.createdAt)], + ]; +} + +/** A `
` of `[term, value]` pairs. */ +export function facts(pairs: [string, string][]): HTMLElement { + return h("dl", {}, ...pairs.flatMap(([term, value]) => [h("dt", {}, term), h("dd", { class: "mono" }, value)])); +} + +/** The document's body, highlighted under its engine's grammar. */ +export function policyBody(record: Record, syntax: PolicySyntax): HTMLElement { + const { type, whole, value } = documentBody(record.document); + return snippet(value, syntax, whole ? VALUE : syntax.bodyRole(type)); +} diff --git a/policy-site/src/ui/records.ts b/policy-site/src/ui/records.ts index 3985e085..a539d930 100644 --- a/policy-site/src/ui/records.ts +++ b/policy-site/src/ui/records.ts @@ -3,6 +3,8 @@ import type { Checks, Problem } from "../checks.ts"; import { h, replaceChildren } from "../dom.ts"; import type { StoredRecord } from "../pds.ts"; +import { PolicySyntax } from "./policy-syntax.ts"; +import { facts, policyBody, policyFacts } from "./policy-review.ts"; import { hasErrors, problemList, summary } from "./problems.ts"; const text = (value: unknown): string => (typeof value === "string" ? value : ""); @@ -11,21 +13,19 @@ const list = (value: unknown): string => (Array.isArray(value) ? value.map(Strin function card( stored: StoredRecord, title: string, - facts: [string, string][], + pairs: [string, string][], problems: Problem[], onEdit: (stored: StoredRecord) => void, + body: HTMLElement | null = null, ): HTMLElement { return h( "li", { class: "card" }, h("h3", {}, title), - h( - "dl", - {}, - ...facts.flatMap(([term, value]) => [h("dt", {}, term), h("dd", { class: "mono" }, value)]), - ), + facts(pairs), h("p", { class: hasErrors(problems) ? "status error" : "status ok" }, `Check: ${summary(problems)}`), problems.length ? problemList(problems) : null, + body, h("button", { attrs: { type: "button" }, on: { click: () => onEdit(stored) } }, "Edit"), ); } @@ -36,24 +36,19 @@ export function renderPolicies( checks: Checks, onEdit: (stored: StoredRecord) => void, ): void { + const syntax = new PolicySyntax(checks.grammars); replaceChildren( into, - ...records.map((stored) => { - const { value } = stored; - const document = value.document as { $type?: unknown } | undefined; - return card( + ...records.map((stored) => + card( stored, - text(value.name) || stored.rkey, - [ - ["rkey", stored.rkey], - ["actions", list(value.actions)], - ["engine", text(document?.$type)], - ["uri", stored.uri], - ], - checks.checkPolicy(value).problems, + text(stored.value.name) || stored.rkey, + [...policyFacts(stored.value, stored.rkey), ["uri", stored.uri]], + checks.checkPolicy(stored.value).problems, onEdit, - ); - }), + policyBody(stored.value, syntax), + ), + ), ); }