Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion module/AUDIT_METRICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ never the SDK binding input. Regenerate the export before generating the SDK.

## Unknown and partial results

Empty queries have no points and render **No data yet**. Numeric sums with no
Empty queries have no points and render as no data, in the form the widget's
visualization has for it (see *How a resolved series is drawn*). Numeric sums with no
numeric observations omit their value, just like averages/percentiles; an emitted
zero stays zero. `samples[alias]` reports observations before reduction (so
repeated logical IDs are not mistaken for missing telemetry). Fewer samples than
Expand All @@ -212,6 +213,44 @@ The host DSL also withholds totals after top-N truncation. Authorization/RPC
errors remain errors, never empty successful results. Existing consumers of the
SDK must handle `total: null` when adopting this contract.

## How a resolved series is drawn

Every dashboard built from these series — the host's own pages, a solution's
declared dashboard, and a solution remote that mounts the kit itself — is drawn
by one renderer, `<Dashboard>` from `@codefly-dev/ui/dashboard`, with the kit's
**metric tier**. There is no second, plainer rendering: a consuming solution
does not have to ask which half of the kit its dashboard will get.

| Widget | Drawn as | Empty series | `coverage: "partial"` | `total: null` |
| --- | --- | --- | --- | --- |
| `number` | `StatTile`: the series total, in the widget's `format` and `unit`; a sparkline and a delta against the previous bucket when the metric is bucketed over **time** | `—` with a **No data** badge, never `0` | `—` with a **Partial data** badge | `—` with a **No total** badge when coverage is complete |
| `line` / `area` / `bar` | the kit's metric chart over a `ChartSeries` — axes from the appearance palette, a crosshair readout, a legend for two or more series, and every exact value in a visually-hidden data table | the chart's own **No data** plot, plus a **No data** badge on the card | the observed points still plot, with a **Partial data** badge on the card | nothing: a series widget shows no total |
| `table` | key/value rows | **No data yet.** | a **Partial data** badge on the card | — |

Two of those cells are the contract, not presentation. A `null` total renders as
a dash and never as `0`, because "cannot say" and "nothing happened" are
different answers and only one of them is in the data; and a series with no
points is no data rather than a zero, because the aggregate omits a bucket
instead of emitting an empty one. A sparkline and a bucket delta are drawn
**only** for a time-bucketed series: a line through counts grouped by event type
or region would assert an order, and a delta between two of them a change over
time, that a categorical grouping does not have.

A host composes its own features around that renderer — `<Dashboard>`'s slots
take header actions, a layout (the host's solution dashboard passes a
drag-to-reorder grid using the kit's shared layout), a per-widget renderer for a host that resolves each
widget in its own query, and an empty state. A consumer re-implementing any part
of the drawing is refused by a gate: `src/components/__tests__/kit.test.ts` in
the frontend.

The declaration can add dashboard `columns`, widget `span`, and named
`sections` with their own heading and column count. The defaults need none of
these: adjacent scalar widgets form compact four-column rows, series use two
columns, and cards keep their natural height. A stack stays single-column.
Section and widget order come from the declaration until a viewer customizes
within a section. See [the layout contract](docs/plugin-manifest-schema.md#dashboard-layout)
for responsive spans, persistence and the compatibility limit of legacy hosts.

## Independent validation

Run `go test -race ./pkg/infra/auditmetricstest` from accounts/code for pure
Expand Down
53 changes: 52 additions & 1 deletion module/docs/plugin-manifest-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,65 @@ has three node kinds:
one metric. These metric-bound widgets are distinct from `ui.widgets`, which
are presentation slots contributed to host surfaces; the two never mix.

### Dashboard layout

Layout fields are **additive**. Existing declarations need no edits. A grid
packs consecutive `number` widgets into four-column rows and series into
two-column rows, collapsing to one column on narrow screens. Runs retain widget
order; a scalar is never moved ahead of a preceding chart. Cards align at their
natural height, so a short or empty card does not stretch to its neighbour.
`stack` keeps one widget per row and ignores column/span hints.

- `Dashboard.columns?: 1 | 2 | 3 | 4` overrides the default for both kinds of row.
- `MetricWidget.span?: 1 | 2 | 3 | 4` applies to scalars and series, clamped to
the containing row's columns and the current responsive breakpoint.
- `Dashboard.sections?: { id, title, description?, columns? }[]` declares named
bands in section order. Each widget may name one with `section`; the id must
exist. A section inherits the dashboard column count unless it overrides it.
Widgets without a section appear first; within each section, widget order is
preserved. Empty sections draw nothing.

For example, an existing dashboard can add:

```json
{
"id": "overview",
"layout": "grid",
"columns": 2,
"sections": [
{ "id": "headline", "title": "Headline numbers", "columns": 4 },
{ "id": "trends", "title": "Over time" }
],
"widgets": [
{ "id": "total", "metric": "total_events", "visualization": "number", "section": "headline" },
{ "id": "trend", "metric": "events_per_day", "visualization": "line", "section": "trends", "span": 2 }
]
}
```

`runDashboard` and `fromDashboardData` carry these fields to the kit unchanged.
The host's sortable view uses the same geometry. Dragging swaps widget IDs;
keyboard arrows move one place within the widget's declared section. A viewer's
preference does not change declared section membership.

**Independent rollout:** hosts with this validator ignore unknown presentation
fields on dashboards, sections and widgets. Unknown metric/query fields still
fail validation. Previously released hosts used exact-key validation and will
reject the new fields: upgrade those hosts before adding layout fields, or keep
sending the unchanged declaration to them. Additive types cannot retrofit an
already-deployed validator.

On a registered solution's page the host renders every declared dashboard in a
**Dashboard** tab beside the solution's own **App** tab, never stacked above
the solution; the solution stays mounted while the dashboard is open. A
solution that declares no dashboard gets no tab bar. Each viewer can arrange a
dashboard for themselves: reorder its declared widgets, remove
them, and re-add the ones they removed. The declared widgets, in declared order,
are where every viewer starts, and a widget declared later reaches viewers who
already rearranged. A viewer's arrangement is a preference (ADR 0007): it never
already rearranged. Saved layouts use widget IDs, not positions, and record the
IDs known at save time. An unchanged saved default follows a new declaration;
a customized order keeps surviving IDs, appends newly declared IDs once, and
keeps intentionally removed widgets hidden. A viewer's arrangement is a preference (ADR 0007): it never
adds a metric the dashboard does not draw, so a metric declared only as a
derived metric's input stays off the page.

Expand Down
34 changes: 34 additions & 0 deletions module/services/frontend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,40 @@ whether `/s/{id}` renders, and it stays installation-blind.
observation speaks for the present is the host's to decide, and a copy of that
window here would drift from it.

## Drawing a solution's declared dashboard

`src/solutions/SolutionDashboard.tsx` **mounts** the shared kit's
`<Dashboard>` (`@codefly-dev/ui/dashboard`); it draws nothing itself. Which
tier each widget and each coverage state is drawn with is the module's
[AUDIT_METRICS.md](../../AUDIT_METRICS.md) ("How a resolved series is drawn") —
one renderer for the host's pages, a declared solution dashboard and a solution
remote alike.

What this file owns is only what is the host's, composed through that
renderer's slots:

- **the audit queries**, one per widget, so a widget whose metric errors fails
alone instead of blanking its siblings. Each lives in its own component
(`SolutionWidget`) that resolves the series and then mounts the kit's
`DashboardWidget`, because React fixes the number of hooks a component may
call and a renderer taking the whole view cannot hold a varying number of
queries;
- **the viewer's arrangement** — reorder by drag or arrow keys, remove, add
back, saved per viewer and organization (`dashboard-layout.ts`), handed to
the renderer's `layout` slot as the kit's `SortableGrid`;
- **the ⓘ** that says what a tile counts, from which audit events, over what
range and who caused the newest ones (`metric-info.ts`, the principal
directory, the recent-events drill-down), handed to each widget as its
header actions.

Re-implementing any part of the drawing is refused by
`src/components/__tests__/kit.test.ts`: a declaration taking a name
`@codefly-dev/ui` exports must render the kit's component of that name or carry
a reviewed entry, `class-variance-authority` is not a dependency of this app so
a kit-shaped primitive cannot be built here, and the two chart surfaces
(`src/components/charts/chart.tsx`,
`src/features/dashboard/ui/charts/line-chart.tsx`) stay re-exports.

## Loading a remote, and its CSP

`/s/[solutionId]` (`src/app/(dashboard)/s/[solutionId]/page.tsx`) loads the
Expand Down
4 changes: 2 additions & 2 deletions module/services/frontend/code/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/dev/types/root-params.d.ts";
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
17 changes: 8 additions & 9 deletions module/services/frontend/code/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions module/services/frontend/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
"@bufbuild/protobuf": "^2.11.0",
"@codefly-dev/saas-plugin-contract": "2.4.2",
"@codefly-dev/saas-plugin-react": "0.4.3",
"@codefly-dev/saas-sdk": "0.3.17",
"@codefly-dev/saas-ui": "0.12.6",
"@codefly-dev/ui": "0.12.6",
"@codefly/saas-plugin-manifest": "0.1.1",
"@codefly-dev/saas-sdk": "0.3.18",
"@codefly-dev/saas-ui": "0.12.7",
"@codefly-dev/ui": "0.12.7",
"@codefly/saas-plugin-manifest": "0.1.2",
"@codefly/saas-settings": "0.1.0",
"@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-web": "^2.1.1",
Expand All @@ -57,7 +57,6 @@
"@simplewebauthn/browser": "^13.3.0",
"@tanstack/react-query": "^5.60.0",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"codefly": "https://github.com/codefly-dev/sdk-js/archive/9f3bb78682a0c2b9a70631d35c2ae0094c281ac5.tar.gz",
"js-yaml": "^4.3.2",
Expand Down
26 changes: 25 additions & 1 deletion module/services/frontend/code/packages/codefly-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ and the v1 subset — is [CATALOG.md](./CATALOG.md).
(`Banner`, `EmptyState`, `ErrorState`) and overlays (`Dialog`, `AlertDialog`, `Notice`, `Tooltip`,
`DropdownMenu`); `dashboard` is `<Dashboard>`, charts, `fromDashboardData`; `chat`
is `<Chat>`. React only: no plugin runtime, no host context.
`<Dashboard>` paints with the **metric tier** — `StatTile` in compact rows for scalar
widgets, `metric-chart`'s line/area/bar over `ChartSeries` for series ones —
which is the tier the host's own operations pages use, so a solution's
dashboard and the host's own are the same picture. `DashboardWidget` is that
renderer for one widget, exported for a consumer that resolves each widget in
its own query; `slots` take header actions, a layout, a per-widget renderer
and an empty state, so a consumer composes its own features around the
renderer rather than beside it. What each widget and each coverage state
draws is tabulated in the module's `AUDIT_METRICS.md`.
This is the surface a solution fe-remote consumes. `<Chat>` is fed by
`@codefly-dev/saas-sdk`'s `useChatStream` — the hook owns the SSE/WS transport, the
component stays pure, the same split as `runDashboard` → `<Dashboard>`.
Expand Down Expand Up @@ -106,7 +115,7 @@ solution measured where it had to reach past the kit.
| `@codefly-dev/ui/plugin-host/ui` | Client UI adapters (`PluginErrorBoundary`) |
| `@codefly-dev/ui/skin` | `resolveSkin`, skin types |
| `@codefly-dev/ui/layout` | `Card`/`Section`/`Tabs` + shadcn primitives (React-only) |
| `@codefly-dev/ui/dashboard` | `Dashboard`, charts, `fromDashboardData` (React-only) |
| `@codefly-dev/ui/dashboard` | `Dashboard`, `DashboardWidget`, metric tiles + charts, `fromDashboardData` (React-only) |
| `@codefly-dev/ui/chat` | `Chat` (React-only) |
| `@codefly-dev/ui/content` | `Content`, `Markdown`, `JsonView`, `CodeBlock`, `TextBlock` (React-only) |
| `@codefly-dev/ui/board` | `Board` (React-only) |
Expand Down Expand Up @@ -633,3 +642,18 @@ Version 0.12.3 refreshes the default preview palette from appearance contract 2.
Version 0.12.4 also makes destructive/danger Badge text mix 30% toward the active
foreground, retaining its status hue while meeting small-text contrast on its
tinted background in the tested default and supplied light/dark skins.

### Declared dashboard geometry

`DashboardView.sections` names bands with `id`, `title`, optional `description`
and `columns`; a widget's `section` refers to one. Ungrouped widgets appear first,
then sections in declared order. Consecutive scalars default to four columns,
series to two, with natural-height cards. `columns` overrides both defaults and
`span` applies to every widget, clamped responsively. `stack` ignores both hints.
`fromDashboardData` preserves these fields from the SDK.

The `layout` slot receives the tiles and a `renderLayout(nodesById)` callback.
Pass that callback to `SortableGrid.renderLayout` to retain exactly the default
geometry under one drag context, including drops across scalar/chart bands.
`SortableGrid.canSwap` can constrain drops to declared sections. Keep widget IDs
stable across declaration changes; never persist positional indexes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codefly-dev/ui",
"version": "0.12.6",
"version": "0.12.7",
"description": "The shared, versioned Codefly SaaS frontend kit: the plugin host and the tokens-as-data skin mechanism, standalone so the host and Module-Federation remotes import and dedupe one copy.",
"repository": {
"type": "git",
Expand Down
Loading
Loading