From 4dcd0b463258b549a853df2c23e32b4cfe608845 Mon Sep 17 00:00:00 2001 From: robinvrd Date: Thu, 29 Oct 2026 11:23:44 +0100 Subject: [PATCH 1/5] docs: separate app composition from SDK component guidance --- AGENTS.md | 6 ++-- CONTRIBUTING.md | 6 ++-- README.md | 6 ++-- docs/app-authoring.md | 34 ++++++++++++------- docs/composition.md | 27 +++------------ docs/contract.md | 5 +-- docs/hosted-apps.md | 11 +++--- docs/local-data-apps.md | 5 +-- docs/react.md | 9 +++-- docs/stories-and-export.md | 17 +++++++--- docs/ui-quality.md | 3 +- docs/ui.md | 2 +- .../starter-local-data-app/AGENTS.md | 16 ++++----- .../starter-local-data-app/README.md | 6 ++-- 14 files changed, 82 insertions(+), 71 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 440edd3a..e45d7931 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,9 @@ # Build Altertable data apps -Follow [app authoring](docs/app-authoring.md) with the selected starter for a new -app or the current source for an existing app. Open another guide only when the +Use [app authoring](docs/app-authoring.md) for SDK mechanics and available starters. +The calling skill or application brief chooses the use case, content structure, +and starter; examples demonstrate APIs rather than a required app layout. +For an existing app, start from its current source. Open another guide only when the task needs a pattern that source does not cover. Search [REFERENCE.md](REFERENCE.md) for the exact export or prop. ## Contribute to this repository diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2be88014..41c55945 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,8 +60,10 @@ accessible role/name or label queries scoped to the relevant region. Keep browse workflows self-contained in `apps-*.test.ts`. When a change requires new app code or an authoring decision, update the relevant -guide and starter. Keep app authoring in `docs/`; document current usage with one -focused example, `` and `functionName()` notation. +guide and starter. Keep SDK mechanics and shared component guidance in `docs/`; +use-case composition belongs to the authoring skill and selected starter. +Document current usage with one focused example, `` and +`functionName()` notation. Show complete enclosing calls and components; use `// ...` for unrelated setup. Name the app declaration `dataApp`; keep the API's `app` prop name. Leave prop inventories to TypeScript and JSDoc. Generate `REFERENCE.md` with diff --git a/README.md b/README.md index 17363475..60a9030d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) Build interactive data explorations with TypeScript, React, and Altertable. -Compose filters, charts, tables, and narrative with built-in stories and CSV export. +Compose filters, charts, tables, and narrative, with APIs for stories and CSV export. Run in Altertable, MCP Apps hosts, or a local browser. ## Install @@ -16,7 +16,9 @@ Local apps also install `@altertable/data-app-bun`. ## Build an app -Start with [app authoring](docs/app-authoring.md); search +Use [app authoring](docs/app-authoring.md) for technical integration and available +starters. The authoring skill or app brief defines the content for a metric +illustration, Finding document, or standalone app. Search [REFERENCE.md](REFERENCE.md) for exports and props. Read runtime details only when changing execution mode or converting an app: diff --git a/docs/app-authoring.md b/docs/app-authoring.md index f2801a21..eff2c872 100644 --- a/docs/app-authoring.md +++ b/docs/app-authoring.md @@ -1,7 +1,9 @@ # Author a data app -Build an exploration that answers the user's question and a story of its strongest -findings. +Use the SDK to connect queries, operations, views, and widgets. The authoring +skill or app brief determines the content structure for metric illustrations, +Finding documents, and standalone apps. Component examples do not prescribe +which sections or features every app must contain. ## Use release-matched starters @@ -14,19 +16,23 @@ for the requested change. For a new app, use the starter supplied by the authori | Hosted / remote / cloud | `starter-data-app/index.tsx` | | Local | `starter-local-data-app` | -The hosted starter is the canonical query → operation → context → view → dataset -→ widget → story example. Hosted apps stay in one file with public package imports +These are the currently shipped runtime starters. Select a use-case starter when +one is supplied by the authoring workflow; otherwise adapt the shared starter's +sample content to the app brief. The hosted starter demonstrates query → operation +→ context → view → dataset → widget composition, including a story example. +Hosted apps stay in one file with public package imports and a literal `defineDataApp()` declaration. Local apps use the same composition with their existing server/client wiring. Replace fixture data and connectivity screens; edit app-owned files, not installed packages. -## Build the exploration +## Connect data and components 1. **Define the question and scope.** Inspect relevant fields, definitions, time coverage, and missing or incomplete observations. Establish the population, grain, measures, and comparison. Choose a chart that answers the question. -2. **Replace the starter queries and operation.** Keep supplied registry SQL - unchanged; otherwise write queries against inspected data. Declare `params` +2. **Replace the starter queries and operation.** Preserve semantic metric + definitions when extending queries with supported dimensions and filters; + write other queries against inspected data. Declare `params` and result aliases in `projections`. Validate flat operation inputs, execute named queries with `query()`, and derive results from its typed rows. 3. **Update context and bindings.** Describe definitions and coverage in the data @@ -37,16 +43,17 @@ app-owned files, not installed packages. useful filters in `variables`; `` renders them. Use `defineTimeView()` for period-driven analysis and `defineDataView()` otherwise. Split views only for independent requests. Keep protected values such as `orgId` backend-owned. -5. **Compose the exploration.** Lead with the strongest supported finding and - visible scope. Use `` for sections, `` for peers, and bound widgets +5. **Compose the content.** Follow the app brief for section order and narrative. + Use `` for sections, `` for peers, and bound widgets from `@altertable/data-app-react`. Keep static explanations outside loading boundaries. Use app tabs only for distinct questions and widget `views` only for alternate representations of one dataset. Keep default appearance; customize when requested. Use custom UI only when bound components cannot express the requirement. -6. **Adapt the story.** Return one to four supported findings using the same - bindings and displayed input as the exploration. Supply every distinct - displayed dataset to `` for CSV export. +6. **Connect shell features.** See [stories and export](stories-and-export.md) + for the current `DataApp` requirements and how to bind these features to the + displayed result. The app brief determines presentation content and export + coverage; [embedding](embed.md#parent-presentation) describes host-owned framing. Keep credentials, authorization, and enforced query limits in the host/backend. The starter supplies transport and mounting; change them only for an explicit @@ -60,7 +67,8 @@ Run the authoring workflow's validation, then inspect the app: Distinguish measured zero, missing values, and empty results. - Change filters and exercise loading, empty, error, refresh, and stale states. Narrative, scope, story, and export must agree with the displayed result. -- Present the story and check exported columns, raw values, filename, and scope. +- For exposed presentation and export features, check the story, exported + columns, raw values, filename, and scope. - Review app-owned content for clipping and readability at its intended width. Check custom layout or CSS at narrow and wide widths; check custom colors in both themes. diff --git a/docs/composition.md b/docs/composition.md index a6073e87..64d7ea9d 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -1,13 +1,12 @@ -# Compose an exploration +# Compose data components Define the question, population, period, measures, and comparison before choosing widgets. A focused answer may need only a chart and a paragraph. -## Build a sequence of questions +## Choose components -Lead with the strongest supported result and visible scope. Give each section a -question: what happened, when, where it differs, or what supports the finding. -Omit sections that add no evidence. +The authoring skill or app brief determines the content and section order. +Choose components for the information each section needs to convey: | Element | Purpose | | --------- | ------------------------------------------------------ | @@ -33,21 +32,3 @@ averaging subgroup rates. Derive claims from the [displayed result and its input](views.md#preserve-displayed-results). A claim about all channels may stop being valid after selecting one. Observed differences support investigation; they do not establish a cause. - -## Example: where are we losing signups? - -Define funnel entry, completion window, and channel attribution. Compare -populations with equal opportunity to complete and identify incomplete periods. - -| Question | Display | -| ------------------------------------- | -------------------------------------------------------- | -| How many starters complete? | Conversion versus a comparable prior period, with counts | -| When does conversion change? | Trend with incomplete periods identified | -| At which step and for which channels? | Funnel and channel comparisons with counts and rates | -| What supports the comparison? | Table of counts, denominators, and rates | - -Use one view for these related datasets. Let dates select funnel entry dates and -channel filters affect every section. Reuse datasets for charts and tables. -Select one to four supported findings for the [story](stories-and-export.md). -Verify that changing a filter updates every claim and preserves a useful reading -order. diff --git a/docs/contract.md b/docs/contract.md index 9635f3d2..6ff7b62d 100644 --- a/docs/contract.md +++ b/docs/contract.md @@ -33,8 +33,9 @@ ORDER BY group_name LIMIT $limit`, }); ``` -Replace fixture SQL with inspected data. Preserve supplied registry SQL. Keep -operation inputs flat; derive SQL parameters inside `run()`: +Replace fixture SQL with inspected data. Keep query declarations and projections +in sync when adapting SQL to the app's inputs. Preserve semantic metric definitions. +Keep operation inputs flat; derive SQL parameters inside `run()`: ```ts import { parseCount } from '@altertable/data-app/contract'; diff --git a/docs/hosted-apps.md b/docs/hosted-apps.md index b2a3e471..e27dd7b8 100644 --- a/docs/hosted-apps.md +++ b/docs/hosted-apps.md @@ -1,6 +1,8 @@ # Author a data app -Follow the [shared authoring flow](app-authoring.md) with the version-matched `starter-data-app/index.tsx` for a hosted / remote / cloud data app. Keep the app in +Follow the [SDK integration guide](app-authoring.md) with a version-matched hosted +starter selected by the authoring workflow. The currently shipped shared starter +is `starter-data-app/index.tsx`; its sample content is replaceable. Keep the app in one file with public package imports; omit server files, HTML, credentials, and relative or app-alias imports. @@ -13,15 +15,16 @@ interpolation), finite signed numbers, booleans, and null. Parentheses, `as`, an computed keys, references, and callbacks. Static apps use `queries: {}`. Replace sample queries and bindings with inspected source data. Supply `scope` -in `defineDataApp()` and `story` and `datasets` on ``. +in `defineDataApp()`. When using bound ``, follow its current +[story and dataset requirements](stories-and-export.md). Keep the starter's client, style injection, and mounting code. The host supplies the transport; no bootstrap or message routes are needed in app code. ## Convert a local data app -1. Combine the app's operations and parsers, data context, views, story, - CSV export, app declaration, and browser entry into one `index.tsx`, following the +1. Combine the app's operations and parsers, data context, views, any presentation + and export features, app declaration, and browser entry into one `index.tsx`, following the [single-file starter](app-authoring.md#use-release-matched-starters). 2. Replace the HTTP client with `createDataClient({ operations })`, using the operation registry as a value. See [browser-owned operations](client.md#browser-owned-operations-for-bundle-apps) diff --git a/docs/local-data-apps.md b/docs/local-data-apps.md index 08740298..af2e9796 100644 --- a/docs/local-data-apps.md +++ b/docs/local-data-apps.md @@ -2,8 +2,9 @@ Use the local scaffold supplied by your authoring workflow. Its README covers setup and its AGENTS.md identifies app-owned files. -Replace the connectivity screen with the exploration, CSV export, and story from the -[shared authoring flow](app-authoring.md). +Replace the connectivity screen with the content specified by the app brief or +authoring skill, using the [shared SDK patterns](app-authoring.md). Local execution +does not require a particular content structure or presentation story. | Task | Documentation | | ---------------------------- | ---------------------------------------- | diff --git a/docs/react.md b/docs/react.md index adc91aec..692c4fd2 100644 --- a/docs/react.md +++ b/docs/react.md @@ -15,9 +15,12 @@ mountDataApp({ app: dataApp, component: App }); The app supplies `dataApp`, `App`, and a `#root` element. -Keep the starter's mounting and client wiring. Use declared views and bound -widgets for exploration. Use `/ui` for setup/static screens or a custom component -only when bound components cannot express the requirement. +Keep the selected starter's mounting and client wiring. The authoring skill or +app brief selects the starter and content structure; the available runtime +starters are listed in [app authoring](app-authoring.md#use-release-matched-starters). +Use declared views and bound widgets for data-backed content. Use `/ui` for +setup/static screens or a custom component only when bound components cannot +express the requirement. Hosts embedding an app use the separate `@altertable/data-app-embed` package; that integration is outside ordinary app authoring. diff --git a/docs/stories-and-export.md b/docs/stories-and-export.md index 0511311c..db774ddb 100644 --- a/docs/stories-and-export.md +++ b/docs/stories-and-export.md @@ -1,8 +1,14 @@ # Stories and CSV export -Supply `story` and `datasets` on `` from `@altertable/data-app-react`. -Both use the displayed snapshot, including during refresh or failure. -Setup and static screens use `` from `/ui` and may omit both. +The current bound `` from `@altertable/data-app-react` requires a `story` +callback and a nonempty `datasets` list. Both use the displayed snapshot, including +during refresh or failure. An empty story callback leaves a disabled presentation +action; it does not hide the toolbar. Setup and static screens can use `` +from `/ui`, which may omit both. + +These are component contracts. The authoring skill or app brief determines the +intended presentation and export coverage. A presentation story is distinct from +explanatory prose in the app body. ## Present data with stories @@ -41,8 +47,9 @@ Keep the starter's `formatMetric()` import for formatted headlines. ## Export displayed data as CSV Declare [datasets](widgets.md#declare-datasets-and-metrics) once and pass -`datasets={[sampleCounts]}`. Include every distinct displayed dataset -at its displayed grain; reuse a dataset for visuals sharing the same rows. +`datasets={[sampleCounts]}`. This list determines which datasets are available +for export, using their bound rows and columns. Preserve the displayed grain; +reuse a dataset for visuals sharing the same rows. One dataset downloads as CSV; multiple datasets offer individual downloads and **Export all** as ZIP. Filenames use the displayed scope. Accessors must return diff --git a/docs/ui-quality.md b/docs/ui-quality.md index 2714a798..b61bbe90 100644 --- a/docs/ui-quality.md +++ b/docs/ui-quality.md @@ -1,6 +1,7 @@ # UI quality -Lead with a supported finding and visible scope. Explain each chart's purpose, +Make the displayed data and its scope understandable, using context supplied by +the host where available instead of repeating it. Explain each chart's purpose, measures, units, and source evidence. Use package typography and formatters; keep primary values prominent and preserve exact values in records and exports. diff --git a/docs/ui.md b/docs/ui.md index 1ba62da0..afbe0e84 100644 --- a/docs/ui.md +++ b/docs/ui.md @@ -1,7 +1,7 @@ # Direct UI composition Import from `@altertable/data-app-react/ui` for setup/static screens, custom -controls, or a deliberately custom shell. Standard data apps use declared views +controls, or a deliberately custom shell. For data-backed content, prefer declared views and bound widgets from `@altertable/data-app-react`. `` here renders a static screen and accepts optional direct CSV data. diff --git a/packages/data-app-examples/templates/starter-local-data-app/AGENTS.md b/packages/data-app-examples/templates/starter-local-data-app/AGENTS.md index 36d10a96..56d374be 100644 --- a/packages/data-app-examples/templates/starter-local-data-app/AGENTS.md +++ b/packages/data-app-examples/templates/starter-local-data-app/AGENTS.md @@ -4,14 +4,14 @@ Read `node_modules/@altertable/data-app/docs/app-authoring.md` and use this proj as the selected scaffold. If dependencies are unavailable, read the [version-matched recipe](https://github.com/altertable-ai/data-app/blob/v0.74.0/docs/app-authoring.md). -| Task | App-owned files | -| ---------------------------------------- | -------------------------------------------------------- | -| Define queries and inputs | `app.ts`, `src/operations.ts`, optional shared contracts | -| Build the exploration, export, and story | `src/App.tsx` | -| Bind definitions and source evidence | `src/data-context.ts` | -| Change identity and appearance | `app.ts` | -| Inject styles and mount React | `src/main.tsx` | -| Configure local serving | `src/server.ts` | +| Task | App-owned files | +| ------------------------------------------ | -------------------------------------------------------- | +| Define queries and inputs | `app.ts`, `src/operations.ts`, optional shared contracts | +| Compose app content and supported features | `src/App.tsx` | +| Bind definitions and source evidence | `src/data-context.ts` | +| Change identity and appearance | `app.ts` | +| Inject styles and mount React | `src/main.tsx` | +| Configure local serving | `src/server.ts` | See [README.md](README.md) for setup and checks. diff --git a/packages/data-app-examples/templates/starter-local-data-app/README.md b/packages/data-app-examples/templates/starter-local-data-app/README.md index 2ce91259..99ab1800 100644 --- a/packages/data-app-examples/templates/starter-local-data-app/README.md +++ b/packages/data-app-examples/templates/starter-local-data-app/README.md @@ -21,10 +21,10 @@ Open . Alternatively, configure `ALTERTABLE_DATA_PROXY_URL` and `ALTERTABLE_DATA_PROXY_TOKEN` for an existing CLI proxy. Missing credentials stop startup. -## Build the exploration +## Build the app -Replace the connectivity check with bounded operations, views, CSV export, and -story. Change identity, appearance, and queries in `app.ts`. +Replace the connectivity check with bounded operations, views, and the content +specified by the authoring skill or app brief. Change identity, appearance, and queries in `app.ts`. ```fish bun run check From 834d3f1475c8b1d146ab99156e927cebdc11b9ba Mon Sep 17 00:00:00 2001 From: robinvrd Date: Thu, 29 Oct 2026 11:54:36 +0100 Subject: [PATCH 2/5] docs: clarify the empty story return value --- docs/stories-and-export.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/stories-and-export.md b/docs/stories-and-export.md index db774ddb..31d13f27 100644 --- a/docs/stories-and-export.md +++ b/docs/stories-and-export.md @@ -2,8 +2,8 @@ The current bound `` from `@altertable/data-app-react` requires a `story` callback and a nonempty `datasets` list. Both use the displayed snapshot, including -during refresh or failure. An empty story callback leaves a disabled presentation -action; it does not hide the toolbar. Setup and static screens can use `` +during refresh or failure. A story callback returning `[]` leaves a disabled +presentation action; it does not hide the toolbar. Setup and static screens can use `` from `/ui`, which may omit both. These are component contracts. The authoring skill or app brief determines the From c397b51a3cc479e575504e8bb89cc38e6ce239f6 Mon Sep 17 00:00:00 2001 From: Robin <61582293+robinvrd@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:31:27 +0100 Subject: [PATCH 3/5] Apply batched suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Chalifour --- AGENTS.md | 2 +- CONTRIBUTING.md | 2 +- README.md | 3 +-- docs/app-authoring.md | 8 +++----- docs/composition.md | 2 +- docs/contract.md | 2 +- docs/hosted-apps.md | 2 +- docs/react.md | 2 +- docs/stories-and-export.md | 2 +- .../templates/starter-local-data-app/README.md | 2 +- 10 files changed, 12 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e45d7931..890a1cd7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # Build Altertable data apps Use [app authoring](docs/app-authoring.md) for SDK mechanics and available starters. -The calling skill or application brief chooses the use case, content structure, +The calling app brief chooses the use case, content structure, and starter; examples demonstrate APIs rather than a required app layout. For an existing app, start from its current source. Open another guide only when the task needs a pattern that source does not cover. Search [REFERENCE.md](REFERENCE.md) for the exact export or prop. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41c55945..b331370b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,7 +61,7 @@ workflows self-contained in `apps-*.test.ts`. When a change requires new app code or an authoring decision, update the relevant guide and starter. Keep SDK mechanics and shared component guidance in `docs/`; -use-case composition belongs to the authoring skill and selected starter. +use-case composition belongs to the app brief and selected starter. Document current usage with one focused example, `` and `functionName()` notation. Show complete enclosing calls and components; use `// ...` for unrelated setup. diff --git a/README.md b/README.md index 60a9030d..fccba4ea 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,7 @@ Local apps also install `@altertable/data-app-bun`. ## Build an app Use [app authoring](docs/app-authoring.md) for technical integration and available -starters. The authoring skill or app brief defines the content for a metric -illustration, Finding document, or standalone app. Search +starters. The app brief defines the content for the kind of data app. Search [REFERENCE.md](REFERENCE.md) for exports and props. Read runtime details only when changing execution mode or converting an app: diff --git a/docs/app-authoring.md b/docs/app-authoring.md index eff2c872..b13a4f71 100644 --- a/docs/app-authoring.md +++ b/docs/app-authoring.md @@ -1,8 +1,6 @@ # Author a data app -Use the SDK to connect queries, operations, views, and widgets. The authoring -skill or app brief determines the content structure for metric illustrations, -Finding documents, and standalone apps. Component examples do not prescribe +Use the SDK to connect queries, operations, views, and widgets. The app brief determines the content structure for metric illustrations. Component examples do not prescribe which sections or features every app must contain. ## Use release-matched starters @@ -51,9 +49,9 @@ app-owned files, not installed packages. customize when requested. Use custom UI only when bound components cannot express the requirement. 6. **Connect shell features.** See [stories and export](stories-and-export.md) - for the current `DataApp` requirements and how to bind these features to the + for the current `` requirements and how to bind these features to the displayed result. The app brief determines presentation content and export - coverage; [embedding](embed.md#parent-presentation) describes host-owned framing. + coverage. Keep credentials, authorization, and enforced query limits in the host/backend. The starter supplies transport and mounting; change them only for an explicit diff --git a/docs/composition.md b/docs/composition.md index 64d7ea9d..6801caeb 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -5,7 +5,7 @@ widgets. A focused answer may need only a chart and a paragraph. ## Choose components -The authoring skill or app brief determines the content and section order. +The app brief determines the content and section order. Choose components for the information each section needs to convey: | Element | Purpose | diff --git a/docs/contract.md b/docs/contract.md index 6ff7b62d..40a93cdf 100644 --- a/docs/contract.md +++ b/docs/contract.md @@ -34,7 +34,7 @@ ORDER BY group_name LIMIT $limit`, ``` Replace fixture SQL with inspected data. Keep query declarations and projections -in sync when adapting SQL to the app's inputs. Preserve semantic metric definitions. +in sync when adapting SQL to the app's inputs. Preserve metric definitions. Keep operation inputs flat; derive SQL parameters inside `run()`: ```ts diff --git a/docs/hosted-apps.md b/docs/hosted-apps.md index e27dd7b8..2f497521 100644 --- a/docs/hosted-apps.md +++ b/docs/hosted-apps.md @@ -1,7 +1,7 @@ # Author a data app Follow the [SDK integration guide](app-authoring.md) with a version-matched hosted -starter selected by the authoring workflow. The currently shipped shared starter +starter selected by the authoring workflow. The shared starter is `starter-data-app/index.tsx`; its sample content is replaceable. Keep the app in one file with public package imports; omit server files, HTML, credentials, and relative or app-alias imports. diff --git a/docs/react.md b/docs/react.md index 692c4fd2..de16efec 100644 --- a/docs/react.md +++ b/docs/react.md @@ -15,7 +15,7 @@ mountDataApp({ app: dataApp, component: App }); The app supplies `dataApp`, `App`, and a `#root` element. -Keep the selected starter's mounting and client wiring. The authoring skill or +Keep the selected starter's mounting and client wiring. The app brief selects the starter and content structure; the available runtime starters are listed in [app authoring](app-authoring.md#use-release-matched-starters). Use declared views and bound widgets for data-backed content. Use `/ui` for diff --git a/docs/stories-and-export.md b/docs/stories-and-export.md index 31d13f27..4ab4d9aa 100644 --- a/docs/stories-and-export.md +++ b/docs/stories-and-export.md @@ -6,7 +6,7 @@ during refresh or failure. A story callback returning `[]` leaves a disabled presentation action; it does not hide the toolbar. Setup and static screens can use `` from `/ui`, which may omit both. -These are component contracts. The authoring skill or app brief determines the +These are component contracts. The app brief determines the intended presentation and export coverage. A presentation story is distinct from explanatory prose in the app body. diff --git a/packages/data-app-examples/templates/starter-local-data-app/README.md b/packages/data-app-examples/templates/starter-local-data-app/README.md index 99ab1800..4fc455f0 100644 --- a/packages/data-app-examples/templates/starter-local-data-app/README.md +++ b/packages/data-app-examples/templates/starter-local-data-app/README.md @@ -24,7 +24,7 @@ CLI proxy. Missing credentials stop startup. ## Build the app Replace the connectivity check with bounded operations, views, and the content -specified by the authoring skill or app brief. Change identity, appearance, and queries in `app.ts`. +specified by the app brief. Change identity, appearance, and queries in `app.ts`. ```fish bun run check From 70fbe9528979bfab34b02e45297e5397b1037024 Mon Sep 17 00:00:00 2001 From: robinvrd Date: Thu, 29 Oct 2026 15:44:38 +0100 Subject: [PATCH 4/5] docs: address remaining authoring review feedback --- docs/app-authoring.md | 7 +++---- docs/react.md | 2 +- docs/ui-quality.md | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/app-authoring.md b/docs/app-authoring.md index b13a4f71..83db776c 100644 --- a/docs/app-authoring.md +++ b/docs/app-authoring.md @@ -1,6 +1,6 @@ # Author a data app -Use the SDK to connect queries, operations, views, and widgets. The app brief determines the content structure for metric illustrations. Component examples do not prescribe +Use the SDK to connect queries, operations, views, and widgets. The app brief determines the content structure. Component examples do not prescribe which sections or features every app must contain. ## Use release-matched starters @@ -28,9 +28,8 @@ app-owned files, not installed packages. 1. **Define the question and scope.** Inspect relevant fields, definitions, time coverage, and missing or incomplete observations. Establish the population, grain, measures, and comparison. Choose a chart that answers the question. -2. **Replace the starter queries and operation.** Preserve semantic metric - definitions when extending queries with supported dimensions and filters; - write other queries against inspected data. Declare `params` +2. **Replace the starter queries and operation.** Write queries + against inspected data and preserve supplied metric definitions. Declare `params` and result aliases in `projections`. Validate flat operation inputs, execute named queries with `query()`, and derive results from its typed rows. 3. **Update context and bindings.** Describe definitions and coverage in the data diff --git a/docs/react.md b/docs/react.md index de16efec..94d6707d 100644 --- a/docs/react.md +++ b/docs/react.md @@ -15,7 +15,7 @@ mountDataApp({ app: dataApp, component: App }); The app supplies `dataApp`, `App`, and a `#root` element. -Keep the selected starter's mounting and client wiring. The +Keep the selected starter's mounting and client wiring. The app brief selects the starter and content structure; the available runtime starters are listed in [app authoring](app-authoring.md#use-release-matched-starters). Use declared views and bound widgets for data-backed content. Use `/ui` for diff --git a/docs/ui-quality.md b/docs/ui-quality.md index b61bbe90..eec71125 100644 --- a/docs/ui-quality.md +++ b/docs/ui-quality.md @@ -1,8 +1,8 @@ # UI quality -Make the displayed data and its scope understandable, using context supplied by -the host where available instead of repeating it. Explain each chart's purpose, -measures, units, and source evidence. Use package typography and formatters; +Make the main result easy to identify and compare. Make its scope, units, and +supporting evidence explicit, using context supplied by the host where available +instead of repeating it. Explain each chart's purpose and measures. Use package typography and formatters; keep primary values prominent and preserve exact values in records and exports. Compose with ``, ``, and ``; parents own spacing and From 8249bbe0586fb2cecbd12d7774bc73152ff0da88 Mon Sep 17 00:00:00 2001 From: Robin <61582293+robinvrd@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:48:11 +0100 Subject: [PATCH 5/5] feat: add hosted and local metric and finding starters (#128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Metric illustrations and Finding documents currently start from the generic exploration scaffold. Add a focused starter for each use case in both hosted and local execution, and register all four in the examples catalog. The metric starter renders a main chart and a secondary segment breakdown. Selecting a group filters both charts; the group control resets the scope. The Finding starter renders a single-column document with a supporting chart. The metric starter queries a seeded semantic view with `SEMANTIC_VIEW`, including dimension filters. The Finding starter queries the same fictional rows with ordinary SQL and renders inline values, a compact ranking, and an evidence table. Both retain app identity for standalone viewing and defer embedded header presentation to the host. `bun run dev:gallery` seeds the sample data in a pinned Altertable mock image containing DuckDB 2 and `semantic_layer`. Copied apps retain the normal lakehouse connection; no mock or demo creation is added to app runtime code. Make bound `` story and export configuration optional, and add `showToolbar={false}` for apps whose brief does not need page actions. Existing defaults and supplied story/export behavior remain unchanged. Package and release checks cover the new templates. ## Validation - Hosted and local starters were checked against the live mock preview, including semantic filtering and Finding values. Starter-specific browser tests were removed following review; shared SDK regression and starter build checks remain. - Type checks and all three local starter builds pass. - Full `bun run check` passes with pinned Bun 1.4.4 and Node 24.18.0: 1,015 tests across 77 files, package boundaries, type/lint/style/docs checks, generated reference, packed consumers, and all local starter checks. Stacked on #125. Companion: https://github.com/altertable-ai/skills/pull/113 selects these starters by use case and execution mode. Publish the SDK before relying on the new starter paths and props; skill guidance falls back to the installed version's shared starter when needed. ## Screenshots ### Finding starter ![Finding starter](https://github.com/user-attachments/assets/46036d48-71b8-4d55-ae2a-ba8621e9202a) ### Metric starter — filtered to Alpha ![Metric starter filtered to Alpha](https://github.com/user-attachments/assets/22460124-8c7e-4619-9f79-7e7daa6d4e40) --- REFERENCE.md | 7 +- bun.lock | 42 ++++ dev/fixtures/bundle-host.tsx | 2 + dev/server.ts | 31 +++ dev/starter-mock.ts | 14 ++ docs/app-authoring.md | 9 +- docs/local-data-apps.md | 18 +- docs/stories-and-export.md | 12 +- knip.ts | 6 +- package.json | 6 +- packages/data-app-examples/README.md | 12 +- packages/data-app-examples/src/index.ts | 24 ++ .../starter-finding-data-app/LICENSE | 21 ++ .../starter-finding-data-app/index.tsx | 177 ++++++++++++++ .../.bun-version | 1 + .../.node-version | 1 + .../.oxfmtrc.json | 11 + .../.oxlintrc.json | 33 +++ .../starter-local-finding-data-app/AGENTS.md | 18 ++ .../starter-local-finding-data-app/LICENSE | 21 ++ .../starter-local-finding-data-app/README.md | 43 ++++ .../starter-local-finding-data-app/_gitignore | 3 + .../starter-local-finding-data-app/app.ts | 25 ++ .../bunfig.toml | 2 + .../package.json | 36 +++ .../src/App.tsx | 122 ++++++++++ .../src/assets.d.ts | 1 + .../src/data-context.ts | 13 + .../src/favicon.svg | 13 + .../src/index.html | 14 ++ .../src/main.tsx | 8 + .../src/operations.ts | 17 ++ .../src/server.ts | 7 + .../tsconfig.json | 21 ++ .../.bun-version | 1 + .../.node-version | 1 + .../.oxfmtrc.json | 11 + .../.oxlintrc.json | 33 +++ .../starter-local-metric-data-app/AGENTS.md | 18 ++ .../starter-local-metric-data-app/LICENSE | 21 ++ .../starter-local-metric-data-app/README.md | 43 ++++ .../starter-local-metric-data-app/_gitignore | 3 + .../starter-local-metric-data-app/app.ts | 39 +++ .../starter-local-metric-data-app/bunfig.toml | 2 + .../package.json | 36 +++ .../starter-local-metric-data-app/src/App.tsx | 124 ++++++++++ .../src/assets.d.ts | 1 + .../src/data-context.ts | 19 ++ .../src/favicon.svg | 13 + .../src/index.html | 14 ++ .../src/main.tsx | 8 + .../src/operations.ts | 50 ++++ .../src/server.ts | 7 + .../tsconfig.json | 21 ++ .../templates/starter-metric-data-app/LICENSE | 21 ++ .../starter-metric-data-app/index.tsx | 229 ++++++++++++++++++ packages/data-app-react/src/ui/DataApp.tsx | 38 +-- .../data-app-react/src/ui/DataAppFrame.tsx | 127 +++++----- release-please-config.json | 46 ++++ scripts/check-boundaries.ts | 76 +++--- scripts/check-package.ts | 8 + scripts/check-starter.ts | 40 +-- scripts/dev-gallery.ts | 7 +- tests/public-api/apps-export.test.ts | 26 ++ tests/public-api/authoring-entry.types.tsx | 2 +- tests/public-api/fixtures/declared-app.tsx | 7 +- tests/public-api/manifest.test.ts | 10 +- tsconfig.json | 4 +- 68 files changed, 1742 insertions(+), 155 deletions(-) create mode 100644 dev/starter-mock.ts create mode 100644 packages/data-app-examples/templates/starter-finding-data-app/LICENSE create mode 100644 packages/data-app-examples/templates/starter-finding-data-app/index.tsx create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/.bun-version create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/.node-version create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/.oxfmtrc.json create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/.oxlintrc.json create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/AGENTS.md create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/LICENSE create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/README.md create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/_gitignore create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/app.ts create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/bunfig.toml create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/package.json create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/src/App.tsx create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/src/assets.d.ts create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/src/data-context.ts create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/src/favicon.svg create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/src/index.html create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/src/main.tsx create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/src/operations.ts create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/src/server.ts create mode 100644 packages/data-app-examples/templates/starter-local-finding-data-app/tsconfig.json create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/.bun-version create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/.node-version create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/.oxfmtrc.json create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/.oxlintrc.json create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/AGENTS.md create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/LICENSE create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/README.md create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/_gitignore create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/app.ts create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/bunfig.toml create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/package.json create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/src/App.tsx create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/src/assets.d.ts create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/src/data-context.ts create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/src/favicon.svg create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/src/index.html create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/src/main.tsx create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/src/operations.ts create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/src/server.ts create mode 100644 packages/data-app-examples/templates/starter-local-metric-data-app/tsconfig.json create mode 100644 packages/data-app-examples/templates/starter-metric-data-app/LICENSE create mode 100644 packages/data-app-examples/templates/starter-metric-data-app/index.tsx diff --git a/REFERENCE.md b/REFERENCE.md index 65e36a30..53d2bbbf 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -298,6 +298,7 @@ CsvTable.rows: readonly (readonly CsvCell[])[] DataApp (component, @altertable/data-app-react): (props: DataAppProps) — Executes the primary view and owns its controls, inspection, exports, and stories. DataApp.children: ReactNode DataApp.description?: ReactNode — Override the declared subtitle; null hides it. +DataApp.showToolbar?: boolean — Show page actions. Defaults to true; does not hide filters or data states. DataApp.toolbarActions?: ReactNode DataApp.footerActions?: ReactNode DataApp.footer?: ReactNode — Optional source, coverage, or methodology note below the app content. Compose prose and inline components; typography and spacing are provided. @@ -305,13 +306,14 @@ DataApp.showSearch?: boolean — Show declared search controls. Defaults to true DataApp.showFilters?: boolean — Show declared filter controls, including display options. Defaults to true; hiding preserves their values. DataApp.view: DeclaredView & { scope(snapshot: DisplayedSnapshot): string; } DataApp.showSearchSuggestions?: boolean — Show supplied datasets’ record suggestions on the first declared search. Defaults to true. -DataApp.story: (snapshot: DisplayedSnapshot) => readonly (Omit & { evidence: RegisteredBinding; })[] -DataApp.datasets: readonly [ExportDataset, ...ExportDataset[]] +DataApp.story?: ((snapshot: DisplayedSnapshot) => readonly (Omit & { evidence: RegisteredBinding; })[]) — Omit to leave presentation out of the toolbar. +DataApp.datasets?: readonly [ExportDataset, ...ExportDataset[]] — Omit to leave CSV export out of the toolbar. DataApp.details?: readonly DetailContent[] — Detail destinations available to dataset actions and metric record inspection. DataApp (component, @altertable/data-app-react/ui): (props: DataAppProps) — Setup and static screens; fetched data uses the declared-view DataApp. DataApp.children: ReactNode DataApp.dataContext: DataContext DataApp.description?: ReactNode — Override the declared subtitle; null hides it. +DataApp.showToolbar?: boolean — Show page actions. Defaults to true; does not hide filters or data states. DataApp.toolbarActions?: ReactNode DataApp.footerActions?: ReactNode DataApp.footer?: ReactNode — Optional source, coverage, or methodology note below the app content. Compose prose and inline components; typography and spacing are provided. @@ -342,6 +344,7 @@ DataAppProps (type, @altertable/data-app-react/ui) DataAppProps.children: ReactNode DataAppProps.dataContext: DataContext DataAppProps.description?: ReactNode — Override the declared subtitle; null hides it. +DataAppProps.showToolbar?: boolean — Show page actions. Defaults to true; does not hide filters or data states. DataAppProps.toolbarActions?: ReactNode DataAppProps.footerActions?: ReactNode DataAppProps.footer?: ReactNode — Optional source, coverage, or methodology note below the app content. Compose prose and inline components; typography and spacing are provided. diff --git a/bun.lock b/bun.lock index 46128b30..c9b5fd45 100644 --- a/bun.lock +++ b/bun.lock @@ -97,6 +97,44 @@ "typescript": "7.0.2", }, }, + "packages/data-app-examples/templates/starter-local-finding-data-app": { + "name": "data-app-finding-starter", + "dependencies": { + "@altertable/data-app": "0.74.0", + "@altertable/data-app-bun": "0.74.0", + "@altertable/data-app-react": "0.74.0", + "react": "19.3.1", + "react-dom": "19.3.1", + }, + "devDependencies": { + "@types/bun": "1.4.3", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "oxfmt": "0.71.0", + "oxlint": "1.86.0", + "oxlint-tsgolint": "7.0.2003", + "typescript": "7.0.2", + }, + }, + "packages/data-app-examples/templates/starter-local-metric-data-app": { + "name": "data-app-metric-starter", + "dependencies": { + "@altertable/data-app": "0.74.0", + "@altertable/data-app-bun": "0.74.0", + "@altertable/data-app-react": "0.74.0", + "react": "19.3.1", + "react-dom": "19.3.1", + }, + "devDependencies": { + "@types/bun": "1.4.3", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "oxfmt": "0.71.0", + "oxlint": "1.86.0", + "oxlint-tsgolint": "7.0.2003", + "typescript": "7.0.2", + }, + }, "packages/data-app-manifest": { "name": "@altertable/data-app-manifest", "version": "0.74.0", @@ -759,6 +797,10 @@ "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + "data-app-finding-starter": ["data-app-finding-starter@workspace:packages/data-app-examples/templates/starter-local-finding-data-app"], + + "data-app-metric-starter": ["data-app-metric-starter@workspace:packages/data-app-examples/templates/starter-local-metric-data-app"], + "data-app-starter": ["data-app-starter@workspace:packages/data-app-examples/templates/starter-local-data-app"], "dead-or-alive": ["dead-or-alive@1.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "fast-content-type-parse": "^3.0.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-is-element": "^3.0.0", "hast-util-select": "^6.0.0", "html-url-attributes": "^3.0.0", "levenshtein-edit-distance": "^3.0.0", "undici": "^8.0.0", "unist-util-visit": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-WTw0SWjsRlwhEK34dQgiXCwfoVH7C4DcViKYT/K43ShApOsMiQDryup3SJ/Fp7fVsDA/NqeW810oUnuICYYzNA=="], diff --git a/dev/fixtures/bundle-host.tsx b/dev/fixtures/bundle-host.tsx index 32122ec3..418170a9 100644 --- a/dev/fixtures/bundle-host.tsx +++ b/dev/fixtures/bundle-host.tsx @@ -69,6 +69,8 @@ if (isPlayground) document.title = `${isContentSizing ? 'Content sizing' : 'Playground'} · Altertable`; const appPreview = [ '/starter-data-app', + '/starter-metric-data-app', + '/starter-finding-data-app', '/playground', '/content-sizing', ].includes(location.pathname); diff --git a/dev/server.ts b/dev/server.ts index 3818f33b..19b48da5 100644 --- a/dev/server.ts +++ b/dev/server.ts @@ -24,6 +24,10 @@ import styles from '@/dev/fixtures/styles.html'; import { dataApp as starterApp } from '@/packages/data-app-examples/templates/starter-local-data-app/app'; import starterPage from '@/packages/data-app-examples/templates/starter-local-data-app/src/index.html'; import { operations as starterOperations } from '@/packages/data-app-examples/templates/starter-local-data-app/src/operations'; +import findingStarterPage from '@/packages/data-app-examples/templates/starter-local-finding-data-app/src/index.html'; +import { operations as findingStarterOperations } from '@/packages/data-app-examples/templates/starter-local-finding-data-app/src/operations'; +import metricStarterPage from '@/packages/data-app-examples/templates/starter-local-metric-data-app/src/index.html'; +import { operations as metricStarterOperations } from '@/packages/data-app-examples/templates/starter-local-metric-data-app/src/operations'; async function bundle(entry: string) { const result = await Bun.build({ @@ -93,6 +97,12 @@ const hostedApps = { }, }; const hostedApp = hostedApps[isDevelopment ? 'development' : 'test']; +const loadMetricStarterBundle = await createBundleLoader( + '../packages/data-app-examples/templates/starter-metric-data-app/index.tsx' +); +const loadFindingStarterBundle = await createBundleLoader( + '../packages/data-app-examples/templates/starter-finding-data-app/index.tsx' +); const loadHostedAppBundle = await createBundleLoader(hostedApp.entry); const loadPlaygroundBundle = isDevelopment ? loadHostedAppBundle @@ -101,6 +111,7 @@ const serveReloadEvents = isDevelopment && createReloadHandler([ '../dev', + '../packages/data-app-examples/templates', './fixtures/bundle-app.tsx', './fixtures/bridge-frame.ts', ]); @@ -120,6 +131,20 @@ serveLocalApp({ title: starterApp.title, port: port + 2, }); +if (isDevelopment) + serveLocalApp({ + page: metricStarterPage, + operations: metricStarterOperations, + title: 'Local metric starter', + port: port + 3, + }); +if (isDevelopment) + serveLocalApp({ + page: findingStarterPage, + operations: findingStarterOperations, + title: 'Local finding starter', + port: port + 4, + }); const loadFrameBundle = await createBundleLoader('./fixtures/bridge-frame.ts'); Bun.serve({ hostname: '127.0.0.1', @@ -153,6 +178,8 @@ Bun.serve({ '/playground': bundleHost, '/content-sizing': bundleHost, [hostedApp.path]: bundleHost, + '/starter-finding-data-app': bundleHost, + '/starter-metric-data-app': bundleHost, }, async fetch(request, server) { const path = new URL(request.url).pathname; @@ -204,6 +231,10 @@ Bun.serve({ } if (path === '/__test/content-sizing') return new Response(await loadHostedAppBundle()); + if (path === '/__test/starter-metric-data-app') + return new Response(await loadMetricStarterBundle()); + if (path === '/__test/starter-finding-data-app') + return new Response(await loadFindingStarterBundle()); if (path === '/__test/playground') return new Response(await loadPlaygroundBundle()); if (path === `/__test${hostedApp.path}`) diff --git a/dev/starter-mock.ts b/dev/starter-mock.ts new file mode 100644 index 00000000..5c0c2864 --- /dev/null +++ b/dev/starter-mock.ts @@ -0,0 +1,14 @@ +/** Mock image with DuckDB 2 and semantic_layer (altertable-mock PR #31). */ +export const starterMockImage = + 'ghcr.io/altertable-ai/altertable-mock@sha256:fc23b3fc07865b4d469f0451e36ee1f514e321848abdc38cc4a267e8ccfa33d0'; + +/** Fictional evidence shared by the metric and Finding examples. Never run on a user's lakehouse. */ +export const starterSeedStatements = [ + 'CREATE SCHEMA starter_demo', + 'CREATE TABLE starter_demo.sample_counts (group_name VARCHAR, segment_name VARCHAR, sample_count BIGINT)', + "INSERT INTO starter_demo.sample_counts VALUES ('Alpha', 'Online', 1500000), ('Alpha', 'Retail', 700000), ('Beta', 'Partner', 600000)", + `CREATE SEMANTIC VIEW starter_sample_counts + TABLES (samples AS starter_demo.sample_counts) + DIMENSIONS (samples.group_name AS group_name, samples.segment_name AS segment_name) + METRICS (samples.sample_count AS SUM(samples.sample_count))`, +]; diff --git a/docs/app-authoring.md b/docs/app-authoring.md index 83db776c..895af6a7 100644 --- a/docs/app-authoring.md +++ b/docs/app-authoring.md @@ -13,10 +13,13 @@ for the requested change. For a new app, use the starter supplied by the authori | ----------------------- | ------------------------------------------------------------------- | | Hosted / remote / cloud | `starter-data-app/index.tsx` | | Local | `starter-local-data-app` | +| Hosted metric | `starter-metric-data-app/index.tsx` | +| Hosted Finding | `starter-finding-data-app/index.tsx` | +| Local metric | `starter-local-metric-data-app` | +| Local Finding | `starter-local-finding-data-app` | -These are the currently shipped runtime starters. Select a use-case starter when -one is supplied by the authoring workflow; otherwise adapt the shared starter's -sample content to the app brief. The hosted starter demonstrates query → operation +Choose the runtime and use case supplied by the app brief. Each starter is editable +source; replace fixture queries and sample content before publishing. The hosted starter demonstrates query → operation → context → view → dataset → widget composition, including a story example. Hosted apps stay in one file with public package imports and a literal `defineDataApp()` declaration. Local apps use the same composition diff --git a/docs/local-data-apps.md b/docs/local-data-apps.md index af2e9796..7801854f 100644 --- a/docs/local-data-apps.md +++ b/docs/local-data-apps.md @@ -2,8 +2,8 @@ Use the local scaffold supplied by your authoring workflow. Its README covers setup and its AGENTS.md identifies app-owned files. -Replace the connectivity screen with the content specified by the app brief or -authoring skill, using the [shared SDK patterns](app-authoring.md). Local execution +Replace the connectivity screen or fixture examples with the content specified by +the app brief, using the [shared SDK patterns](app-authoring.md). Local execution does not require a particular content structure or presentation story. | Task | Documentation | @@ -13,3 +13,17 @@ does not require a particular content structure or presentation story. | Customize the browser client | [HTTP client](client.md#http-operations) | You can also [convert the local app to a hosted data app](hosted-apps.md#convert-a-local-data-app). + +## Preview the use-case starters + +From the SDK repository, run `bun run dev:gallery` with Docker available. It starts +and seeds the Altertable mock with fictional sample data. + +| Starter | Hosted preview | Local preview | +| ------- | ------------------------------------------------- | ------------------------ | +| Metric | | | +| Finding | | | + +Copied starters use the normal query connection. Replace their example queries +with verified sources in the target lakehouse; the mock and seed belong only to +local development. Never create demo objects in a user's lakehouse. diff --git a/docs/stories-and-export.md b/docs/stories-and-export.md index 4ab4d9aa..2bdfdc05 100644 --- a/docs/stories-and-export.md +++ b/docs/stories-and-export.md @@ -1,10 +1,12 @@ # Stories and CSV export -The current bound `` from `@altertable/data-app-react` requires a `story` -callback and a nonempty `datasets` list. Both use the displayed snapshot, including -during refresh or failure. A story callback returning `[]` leaves a disabled -presentation action; it does not hide the toolbar. Setup and static screens can use `` -from `/ui`, which may omit both. +The bound `` from `@altertable/data-app-react` accepts an optional `story` +callback and optional nonempty `datasets` list. Omit either prop to omit its toolbar +control. Supplied features use the displayed snapshot, including during refresh or +failure. A story callback returning `[]` leaves a disabled presentation action. +Set `showToolbar={false}` to omit page actions together; data loading, error states, +and declared filters remain available. Setup and static screens can use `` +from `/ui`. These are component contracts. The app brief determines the intended presentation and export coverage. A presentation story is distinct from diff --git a/knip.ts b/knip.ts index e62dcca8..b2bddf2f 100644 --- a/knip.ts +++ b/knip.ts @@ -85,10 +85,10 @@ export default { }, 'packages/data-app-examples': { ...packages['packages/data-app-examples'], - entry: ['src/index.ts', 'templates/starter-data-app/index.tsx'], - project: ['src/**/*.ts', 'templates/starter-data-app/*.tsx'], + entry: ['src/index.ts', 'templates/starter-*/index.tsx'], + project: ['src/**/*.ts', 'templates/starter-*/*.tsx'], }, - 'packages/data-app-examples/templates/starter-local-data-app': { + 'packages/data-app-examples/templates/starter-local-*': { entry: ['src/server.ts', 'src/main.tsx'], project: ['src/**/*.{ts,tsx}'], }, diff --git a/package.json b/package.json index ec229a52..8bea84cf 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "workspaces": [ "packages/*", - "packages/data-app-examples/templates/starter-local-data-app" + "packages/data-app-examples/templates/starter-local-*" ], "type": "module", "scripts": { @@ -14,14 +14,14 @@ "dev:file": "bun run scripts/dev-file.ts", "dev:dir": "bun run scripts/dev-dir.ts", "typecheck": "tsc --noEmit && bun run --filter '@altertable/*' typecheck", - "lint": "oxlint --type-aware packages/*/src packages/data-app-examples/templates/starter-data-app tests scripts dev vitest.config.ts knip.ts", + "lint": "oxlint --type-aware packages/*/src packages/data-app-examples/templates/starter-data-app packages/data-app-examples/templates/starter-metric-data-app packages/data-app-examples/templates/starter-finding-data-app tests scripts dev vitest.config.ts knip.ts", "format": "oxfmt .", "format:check": "oxfmt --check .", "test": "vitest run", "check": "bun run build && bun run check:boundaries && bun run typecheck && bun run lint && bun run knip && bun run check:styles && bun run lint:md && bun run check:links && bun run format:check && bun run test && bun run check:reference && bun run check:package && bun run check:starter", "check:package": "bun run scripts/check-package.ts", "check:workflows": "bash scripts/check-workflows.sh", - "lint:fix": "oxlint --type-aware --fix packages/*/src packages/data-app-examples/templates/starter-data-app tests scripts dev vitest.config.ts knip.ts", + "lint:fix": "oxlint --type-aware --fix packages/*/src packages/data-app-examples/templates/starter-data-app packages/data-app-examples/templates/starter-metric-data-app packages/data-app-examples/templates/starter-finding-data-app tests scripts dev vitest.config.ts knip.ts", "check:starter": "bun run scripts/check-starter.ts", "lint:md": "markdownlint-cli2", "check:links": "remark . --ext md --rc-path .remarkrc.json --frail --no-stdout", diff --git a/packages/data-app-examples/README.md b/packages/data-app-examples/README.md index 90162bb5..567a99bf 100644 --- a/packages/data-app-examples/README.md +++ b/packages/data-app-examples/README.md @@ -17,10 +17,14 @@ on this package. ## Starters -| ID | Use | -| ------------------------ | ------------------------------------------------------------------------- | -| `starter-data-app` | [Hosted single-file app](templates/starter-data-app/index.tsx) | -| `starter-local-data-app` | [Local Bun and React project](templates/starter-local-data-app/README.md) | +| ID | Use | +| -------------------------------- | ------------------------------------------------------------------------- | +| `starter-data-app` | [Hosted single-file app](templates/starter-data-app/index.tsx) | +| `starter-local-data-app` | [Local Bun and React project](templates/starter-local-data-app/README.md) | +| `starter-metric-data-app` | [Hosted metric app](templates/starter-metric-data-app/index.tsx) | +| `starter-finding-data-app` | [Hosted finding app](templates/starter-finding-data-app/index.tsx) | +| `starter-local-metric-data-app` | [Local metric app](templates/starter-local-metric-data-app/README.md) | +| `starter-local-finding-data-app` | [Local finding app](templates/starter-local-finding-data-app/README.md) | Pin to the same exact version as `@altertable/data-app`. Authoring guides and API reference ship in that base package. diff --git a/packages/data-app-examples/src/index.ts b/packages/data-app-examples/src/index.ts index 410df6f0..8cecd4b4 100644 --- a/packages/data-app-examples/src/index.ts +++ b/packages/data-app-examples/src/index.ts @@ -18,6 +18,30 @@ export const dataAppExamples = [ description: 'A Bun and React project with server-side lakehouse access.', mode: 'local', }, + { + id: 'starter-metric-data-app', + title: 'Hosted metric illustration', + description: 'A focused metric app for hosted execution.', + mode: 'hosted', + }, + { + id: 'starter-finding-data-app', + title: 'Hosted Finding document', + description: 'A focused finding app for hosted execution.', + mode: 'hosted', + }, + { + id: 'starter-local-metric-data-app', + title: 'Local metric illustration', + description: 'A focused metric app for local execution.', + mode: 'local', + }, + { + id: 'starter-local-finding-data-app', + title: 'Local Finding document', + description: 'A focused finding app for local execution.', + mode: 'local', + }, ] as const; /** Stable identifier for a canonical starter. */ diff --git a/packages/data-app-examples/templates/starter-finding-data-app/LICENSE b/packages/data-app-examples/templates/starter-finding-data-app/LICENSE new file mode 100644 index 00000000..845a62ce --- /dev/null +++ b/packages/data-app-examples/templates/starter-finding-data-app/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Altertable + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/data-app-examples/templates/starter-finding-data-app/index.tsx b/packages/data-app-examples/templates/starter-finding-data-app/index.tsx new file mode 100644 index 00000000..68b8d9bb --- /dev/null +++ b/packages/data-app-examples/templates/starter-finding-data-app/index.tsx @@ -0,0 +1,177 @@ +import { defineDataApp } from '@altertable/data-app'; +import { + createDataContext, + injectDataAppStyles, + mountDataApp, +} from '@altertable/data-app-react'; +import { + createDataHooks, + DataApp, + DataSection, + VisualizationWidget, + DataValue, + TableWidget, + Ranking, + Stack, + TextContent, +} from '@altertable/data-app-react'; +import { createDataClient } from '@altertable/data-app/client'; +import { parseCount } from '@altertable/data-app/contract'; +const dataApp = defineDataApp({ + title: 'Sample finding', + description: + 'Fixture evidence; replace with the Finding and its supporting queries.', + scope: { organization: 'demo', environment: 'sample' }, + appearance: { theme: 'system' }, + queries: { + sampleCountsByGroup: { + projections: { + group_name: { type: 'string' }, + sample_count: { type: 'number' }, + }, + statement: ` +SELECT group_name, SUM(sample_count) AS sample_count +FROM starter_demo.sample_counts +WHERE $groupName = '' OR group_name = $groupName +GROUP BY group_name ORDER BY sample_count DESC LIMIT $limit`, + params: { + groupName: { type: 'string', defaultValue: '' }, + limit: { type: 'number', defaultValue: 10 }, + }, + }, + }, +}); +const operations = { + sampleCountsByGroup: dataApp.defineOperation({ + input: () => ({}), + checks: [{}], + policy: { maxQueryRows: 10, maxDurationMs: 15000 }, + async run({ query }) { + const result = await query('sampleCountsByGroup', {}); + return result.rows.map(row => ({ + groupName: row.group_name, + sampleCount: parseCount(row.sample_count), + })); + }, + }), +}; +const queryNames = operations.sampleCountsByGroup.queryNames; +const sampleDataContext = createDataContext(queryNames)({ + description: + 'Fictional sample volumes grouped from starter_demo.sample_counts. Replace this evidence with the actual Finding queries.', + glossary: { + sampleCount: { + term: 'Sample count', + definition: 'A fixture value: Alpha is 2,200,000 and Beta is 600,000.', + queryNames: [queryNames.sampleCountsByGroup], + }, + }, +}); +const { defineDataView } = createDataHooks(createDataClient({ operations })); +const sampleCountsView = defineDataView({ + dataContext: sampleDataContext, + operation: 'sampleCountsByGroup', + describeInput: () => 'fixture groups', + isEmpty: sampleCounts => sampleCounts.length === 0, + emptyFallback: { + title: 'No matching groups', + description: 'No observations are available for this query.', + }, +}); +const sampleCounts = sampleCountsView.dataset({ + name: 'Sample counts', + select: rows => rows, + rowKey: row => row.groupName, + columns: { + groupName: { label: 'Group', value: row => row.groupName }, + sampleCount: { value: row => row.sampleCount, format: { kind: 'count' } }, + }, + evidence: { + id: 'counts-by-group', + glossaryIds: ['sampleCount'], + }, +}); +const totalSamples = sampleCountsView.metric( + { + id: 'total-samples', + label: 'Total samples', + glossaryId: 'sampleCount', + format: { kind: 'count' }, + }, + rows => ({ current: rows.reduce((sum, row) => sum + row.sampleCount, 0) }) +); +const alphaSamples = sampleCountsView.metric( + { + id: 'alpha-samples', + label: 'Alpha samples', + glossaryId: 'sampleCount', + format: { kind: 'count' }, + }, + rows => { + const alpha = rows.find(row => row.groupName === 'Alpha'); + if (!alpha) + throw new Error('The sample Finding requires evidence for Alpha.'); + return { current: alpha.sampleCount }; + } +); +// This fictional Finding demonstrates prose, inline evidence, and a Markdown-table equivalent. +// Preserve each real Finding's own headings and argument instead of reusing these sections. +const sampleContent = sampleCountsView.content(result => ( + + +

Contribution by group

+

+ Alpha contributed{' '} + of the{' '} + recorded samples. + The group comparison below supports that concentration; it does not + explain its cause. +

+
+ + {rows => ( + ({ + id: row.groupName, + label: row.groupName, + value: row.sampleCount, + }))} + /> + )} + + +

Supporting evidence

+

+ The source rows are grouped by group_name and summed using{' '} + sample_count. The table retains the exact values behind the + comparison. +

+
    +
  • Compare the groups within the same sample population.
  • +
  • + Volume alone does not establish performance or causality. +
  • +
+
+ +
+)); +function App() { + return ( + + + + ); +} +injectDataAppStyles(); +mountDataApp({ app: dataApp, component: App }); diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/.bun-version b/packages/data-app-examples/templates/starter-local-finding-data-app/.bun-version new file mode 100644 index 00000000..1c99cf0e --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/.bun-version @@ -0,0 +1 @@ +1.4.4 diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/.node-version b/packages/data-app-examples/templates/starter-local-finding-data-app/.node-version new file mode 100644 index 00000000..ca5c3500 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/.node-version @@ -0,0 +1 @@ +24.18.0 diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/.oxfmtrc.json b/packages/data-app-examples/templates/starter-local-finding-data-app/.oxfmtrc.json new file mode 100644 index 00000000..569314e7 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/.oxfmtrc.json @@ -0,0 +1,11 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": ["dist/**", "bun.lock"], + "singleQuote": true, + "trailingComma": "es5", + "arrowParens": "avoid", + "printWidth": 80, + "endOfLine": "lf", + "sortImports": true, + "sortPackageJson": false +} diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/.oxlintrc.json b/packages/data-app-examples/templates/starter-local-finding-data-app/.oxlintrc.json new file mode 100644 index 00000000..ce4285ad --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/.oxlintrc.json @@ -0,0 +1,33 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "jsx-a11y"], + "categories": { + "correctness": "error" + }, + "options": { + "typeAware": true + }, + "rules": { + "react/unsupported-syntax": "error", + "eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "regex": "^\\.\\.?/", + "message": "Use #app/* or #config for app-owned files." + } + ] + } + ], + "func-style": [ + "error", + "declaration", + { + "overrides": { + "namedExports": "declaration" + } + } + ] + } +} diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/AGENTS.md b/packages/data-app-examples/templates/starter-local-finding-data-app/AGENTS.md new file mode 100644 index 00000000..56d374be --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/AGENTS.md @@ -0,0 +1,18 @@ +# Build this local data app + +Read `node_modules/@altertable/data-app/docs/app-authoring.md` and use this project +as the selected scaffold. If dependencies are unavailable, read the +[version-matched recipe](https://github.com/altertable-ai/data-app/blob/v0.74.0/docs/app-authoring.md). + +| Task | App-owned files | +| ------------------------------------------ | -------------------------------------------------------- | +| Define queries and inputs | `app.ts`, `src/operations.ts`, optional shared contracts | +| Compose app content and supported features | `src/App.tsx` | +| Bind definitions and source evidence | `src/data-context.ts` | +| Change identity and appearance | `app.ts` | +| Inject styles and mount React | `src/main.tsx` | +| Configure local serving | `src/server.ts` | + +See [README.md](README.md) for setup and checks. + +Keep reusable app-specific conventions here. diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/LICENSE b/packages/data-app-examples/templates/starter-local-finding-data-app/LICENSE new file mode 100644 index 00000000..845a62ce --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Altertable + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/README.md b/packages/data-app-examples/templates/starter-local-finding-data-app/README.md new file mode 100644 index 00000000..39e80ff6 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/README.md @@ -0,0 +1,43 @@ +# Local finding data app starter + +Use `node_modules/@altertable/data-app/docs/app-authoring.md` for this SDK version. +Otherwise, read the [version-matched authoring recipe](https://github.com/altertable-ai/data-app/blob/v0.74.0/docs/app-authoring.md). +See [AGENTS.md](AGENTS.md) for app-owned files. + +The example queries require the seeded sample data described in the +[local preview guide](https://github.com/altertable-ai/data-app/blob/v0.74.0/docs/local-data-apps.md#preview-the-use-case-starters). +Use that mock for development, or replace the example queries with verified data +in your lakehouse. The starter does not install a mock or create demo tables. + +## Run the app + +From the copied starter directory, install dependencies and set server-only +credentials before starting: + +```fish +bun install +set -gx ALTERTABLE_LAKEHOUSE_USERNAME your-username +read --silent --prompt-str 'Lakehouse password: ' ALTERTABLE_LAKEHOUSE_PASSWORD +set -gx ALTERTABLE_LAKEHOUSE_PASSWORD $ALTERTABLE_LAKEHOUSE_PASSWORD +bun run dev +``` + +Open . Alternatively, configure +`ALTERTABLE_DATA_PROXY_URL` and `ALTERTABLE_DATA_PROXY_TOKEN` for an existing +CLI proxy. Missing credentials stop startup. + +## Build the app + +Replace the fixture queries and example content with the evidence and content +specified by the app brief. Change identity, appearance, and queries in `app.ts`. + +```fish +bun run check +``` + +`bun run check` includes the build, which writes browser assets to `dist/`. Hosting them requires an authorized +`/api/data` backend; the local adapter does not authenticate hosted viewers. + +## License + +[MIT](LICENSE) diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/_gitignore b/packages/data-app-examples/templates/starter-local-finding-data-app/_gitignore new file mode 100644 index 00000000..8854acb7 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/_gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.env* diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/app.ts b/packages/data-app-examples/templates/starter-local-finding-data-app/app.ts new file mode 100644 index 00000000..c4eba4cf --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/app.ts @@ -0,0 +1,25 @@ +import { defineDataApp } from '@altertable/data-app'; +export const dataApp = defineDataApp({ + title: 'Sample finding', + description: + 'Fixture evidence; replace with the Finding and its supporting queries.', + scope: { organization: 'demo', environment: 'sample' }, + appearance: { theme: 'system' }, + queries: { + sampleCountsByGroup: { + projections: { + group_name: { type: 'string' }, + sample_count: { type: 'number' }, + }, + statement: ` +SELECT group_name, SUM(sample_count) AS sample_count +FROM starter_demo.sample_counts +WHERE $groupName = '' OR group_name = $groupName +GROUP BY group_name ORDER BY sample_count DESC LIMIT $limit`, + params: { + groupName: { type: 'string', defaultValue: '' }, + limit: { type: 'number', defaultValue: 10 }, + }, + }, + }, +}); diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/bunfig.toml b/packages/data-app-examples/templates/starter-local-finding-data-app/bunfig.toml new file mode 100644 index 00000000..b6874be1 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/bunfig.toml @@ -0,0 +1,2 @@ +[install] +exact = true diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/package.json b/packages/data-app-examples/templates/starter-local-finding-data-app/package.json new file mode 100644 index 00000000..76230eb9 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/package.json @@ -0,0 +1,36 @@ +{ + "name": "data-app-finding-starter", + "private": true, + "type": "module", + "imports": { + "#app/*": "./src/*", + "#config": "./app.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "oxlint --type-aware src", + "format": "oxfmt src app.ts", + "format:check": "oxfmt --check src app.ts", + "dev": "bun --hot src/server.ts", + "build": "bun run typecheck && bun build src/index.html --outdir=dist --production", + "check": "bun run lint && bun run format:check && bun run build", + "lint:fix": "oxlint --type-aware --fix src" + }, + "dependencies": { + "@altertable/data-app": "0.74.0", + "@altertable/data-app-bun": "0.74.0", + "@altertable/data-app-react": "0.74.0", + "react": "19.3.1", + "react-dom": "19.3.1" + }, + "devDependencies": { + "@types/bun": "1.4.3", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "oxfmt": "0.71.0", + "oxlint": "1.86.0", + "oxlint-tsgolint": "7.0.2003", + "typescript": "7.0.2" + }, + "packageManager": "bun@1.4.4" +} diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/src/App.tsx b/packages/data-app-examples/templates/starter-local-finding-data-app/src/App.tsx new file mode 100644 index 00000000..744daeae --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/src/App.tsx @@ -0,0 +1,122 @@ +import { + createDataHooks, + DataApp, + DataSection, + VisualizationWidget, + DataValue, + TableWidget, + Ranking, + Stack, + TextContent, +} from '@altertable/data-app-react'; +import { createDataClient } from '@altertable/data-app/client'; + +import { sampleDataContext } from '#app/data-context.ts'; +import type { operations } from '#app/operations.ts'; +const { defineDataView } = + createDataHooks(createDataClient()); +const sampleCountsView = defineDataView({ + dataContext: sampleDataContext, + operation: 'sampleCountsByGroup', + describeInput: () => 'fixture groups', + isEmpty: sampleCounts => sampleCounts.length === 0, + emptyFallback: { + title: 'No matching groups', + description: 'No observations are available for this query.', + }, +}); +const sampleCounts = sampleCountsView.dataset({ + name: 'Sample counts', + select: rows => rows, + rowKey: row => row.groupName, + columns: { + groupName: { label: 'Group', value: row => row.groupName }, + sampleCount: { value: row => row.sampleCount, format: { kind: 'count' } }, + }, + evidence: { + id: 'counts-by-group', + glossaryIds: ['sampleCount'], + }, +}); +const totalSamples = sampleCountsView.metric( + { + id: 'total-samples', + label: 'Total samples', + glossaryId: 'sampleCount', + format: { kind: 'count' }, + }, + rows => ({ current: rows.reduce((sum, row) => sum + row.sampleCount, 0) }) +); +const alphaSamples = sampleCountsView.metric( + { + id: 'alpha-samples', + label: 'Alpha samples', + glossaryId: 'sampleCount', + format: { kind: 'count' }, + }, + rows => { + const alpha = rows.find(row => row.groupName === 'Alpha'); + if (!alpha) + throw new Error('The sample Finding requires evidence for Alpha.'); + return { current: alpha.sampleCount }; + } +); +// This fictional Finding demonstrates prose, inline evidence, and a Markdown-table equivalent. +// Preserve each real Finding's own headings and argument instead of reusing these sections. +const sampleContent = sampleCountsView.content(result => ( + + +

Contribution by group

+

+ Alpha contributed{' '} + of the{' '} + recorded samples. + The group comparison below supports that concentration; it does not + explain its cause. +

+
+ + {rows => ( + ({ + id: row.groupName, + label: row.groupName, + value: row.sampleCount, + }))} + /> + )} + + +

Supporting evidence

+

+ The source rows are grouped by group_name and summed using{' '} + sample_count. The table retains the exact values behind the + comparison. +

+
    +
  • Compare the groups within the same sample population.
  • +
  • + Volume alone does not establish performance or causality. +
  • +
+
+ +
+)); +export function App() { + return ( + + + + ); +} diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/src/assets.d.ts b/packages/data-app-examples/templates/starter-local-finding-data-app/src/assets.d.ts new file mode 100644 index 00000000..35306c6f --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/src/assets.d.ts @@ -0,0 +1 @@ +declare module '*.css'; diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/src/data-context.ts b/packages/data-app-examples/templates/starter-local-finding-data-app/src/data-context.ts new file mode 100644 index 00000000..4f0ebd05 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/src/data-context.ts @@ -0,0 +1,13 @@ +import { createDataContext } from '@altertable/data-app-react'; +const queryNames = { sampleCountsByGroup: 'sampleCountsByGroup' } as const; +export const sampleDataContext = createDataContext(queryNames)({ + description: + 'Fictional sample volumes grouped from starter_demo.sample_counts. Replace this evidence with the actual Finding queries.', + glossary: { + sampleCount: { + term: 'Sample count', + definition: 'A fixture value: Alpha is 2,200,000 and Beta is 600,000.', + queryNames: [queryNames.sampleCountsByGroup], + }, + }, +}); diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/src/favicon.svg b/packages/data-app-examples/templates/starter-local-finding-data-app/src/favicon.svg new file mode 100644 index 00000000..11ce6117 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/src/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/src/index.html b/packages/data-app-examples/templates/starter-local-finding-data-app/src/index.html new file mode 100644 index 00000000..1edaa873 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/src/index.html @@ -0,0 +1,14 @@ + + + + + + + Altertable app + + + + +
+ + diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/src/main.tsx b/packages/data-app-examples/templates/starter-local-finding-data-app/src/main.tsx new file mode 100644 index 00000000..b71279bc --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/src/main.tsx @@ -0,0 +1,8 @@ +import { injectDataAppStyles, mountDataApp } from '@altertable/data-app-react'; + +import { App } from '#app/App.tsx'; +import { dataApp } from '#config'; + +injectDataAppStyles(); + +mountDataApp({ app: dataApp, component: App }); diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/src/operations.ts b/packages/data-app-examples/templates/starter-local-finding-data-app/src/operations.ts new file mode 100644 index 00000000..af9f2a08 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/src/operations.ts @@ -0,0 +1,17 @@ +import { parseCount } from '@altertable/data-app/contract'; + +import { dataApp } from '#config'; +export const operations = { + sampleCountsByGroup: dataApp.defineOperation({ + input: () => ({}), + checks: [{}], + policy: { maxQueryRows: 10, maxDurationMs: 15000 }, + async run({ query }) { + const result = await query('sampleCountsByGroup', {}); + return result.rows.map(row => ({ + groupName: row.group_name, + sampleCount: parseCount(row.sample_count), + })); + }, + }), +}; diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/src/server.ts b/packages/data-app-examples/templates/starter-local-finding-data-app/src/server.ts new file mode 100644 index 00000000..da9c04f3 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/src/server.ts @@ -0,0 +1,7 @@ +import { serveLocalApp } from '@altertable/data-app-bun'; + +import page from '#app/index.html'; +import { operations } from '#app/operations.ts'; +import { dataApp } from '#config'; + +serveLocalApp({ page, operations, title: dataApp.title }); diff --git a/packages/data-app-examples/templates/starter-local-finding-data-app/tsconfig.json b/packages/data-app-examples/templates/starter-local-finding-data-app/tsconfig.json new file mode 100644 index 00000000..86196070 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-finding-data-app/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "Preserve", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "jsx": "react-jsx", + "strict": true, + "noUncheckedIndexedAccess": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["bun"], + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "allowUnreachableCode": false, + "noUncheckedSideEffectImports": true + }, + "include": ["src", "app.ts"] +} diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/.bun-version b/packages/data-app-examples/templates/starter-local-metric-data-app/.bun-version new file mode 100644 index 00000000..1c99cf0e --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/.bun-version @@ -0,0 +1 @@ +1.4.4 diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/.node-version b/packages/data-app-examples/templates/starter-local-metric-data-app/.node-version new file mode 100644 index 00000000..ca5c3500 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/.node-version @@ -0,0 +1 @@ +24.18.0 diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/.oxfmtrc.json b/packages/data-app-examples/templates/starter-local-metric-data-app/.oxfmtrc.json new file mode 100644 index 00000000..569314e7 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/.oxfmtrc.json @@ -0,0 +1,11 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": ["dist/**", "bun.lock"], + "singleQuote": true, + "trailingComma": "es5", + "arrowParens": "avoid", + "printWidth": 80, + "endOfLine": "lf", + "sortImports": true, + "sortPackageJson": false +} diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/.oxlintrc.json b/packages/data-app-examples/templates/starter-local-metric-data-app/.oxlintrc.json new file mode 100644 index 00000000..ce4285ad --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/.oxlintrc.json @@ -0,0 +1,33 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "jsx-a11y"], + "categories": { + "correctness": "error" + }, + "options": { + "typeAware": true + }, + "rules": { + "react/unsupported-syntax": "error", + "eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "regex": "^\\.\\.?/", + "message": "Use #app/* or #config for app-owned files." + } + ] + } + ], + "func-style": [ + "error", + "declaration", + { + "overrides": { + "namedExports": "declaration" + } + } + ] + } +} diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/AGENTS.md b/packages/data-app-examples/templates/starter-local-metric-data-app/AGENTS.md new file mode 100644 index 00000000..56d374be --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/AGENTS.md @@ -0,0 +1,18 @@ +# Build this local data app + +Read `node_modules/@altertable/data-app/docs/app-authoring.md` and use this project +as the selected scaffold. If dependencies are unavailable, read the +[version-matched recipe](https://github.com/altertable-ai/data-app/blob/v0.74.0/docs/app-authoring.md). + +| Task | App-owned files | +| ------------------------------------------ | -------------------------------------------------------- | +| Define queries and inputs | `app.ts`, `src/operations.ts`, optional shared contracts | +| Compose app content and supported features | `src/App.tsx` | +| Bind definitions and source evidence | `src/data-context.ts` | +| Change identity and appearance | `app.ts` | +| Inject styles and mount React | `src/main.tsx` | +| Configure local serving | `src/server.ts` | + +See [README.md](README.md) for setup and checks. + +Keep reusable app-specific conventions here. diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/LICENSE b/packages/data-app-examples/templates/starter-local-metric-data-app/LICENSE new file mode 100644 index 00000000..845a62ce --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Altertable + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/README.md b/packages/data-app-examples/templates/starter-local-metric-data-app/README.md new file mode 100644 index 00000000..ca073ab3 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/README.md @@ -0,0 +1,43 @@ +# Local metric data app starter + +Use `node_modules/@altertable/data-app/docs/app-authoring.md` for this SDK version. +Otherwise, read the [version-matched authoring recipe](https://github.com/altertable-ai/data-app/blob/v0.74.0/docs/app-authoring.md). +See [AGENTS.md](AGENTS.md) for app-owned files. + +The example queries require the seeded sample data described in the +[local preview guide](https://github.com/altertable-ai/data-app/blob/v0.74.0/docs/local-data-apps.md#preview-the-use-case-starters). +Use that mock for development, or replace the example queries with verified data +in your lakehouse. The starter does not install a mock or create demo tables. + +## Run the app + +From the copied starter directory, install dependencies and set server-only +credentials before starting: + +```fish +bun install +set -gx ALTERTABLE_LAKEHOUSE_USERNAME your-username +read --silent --prompt-str 'Lakehouse password: ' ALTERTABLE_LAKEHOUSE_PASSWORD +set -gx ALTERTABLE_LAKEHOUSE_PASSWORD $ALTERTABLE_LAKEHOUSE_PASSWORD +bun run dev +``` + +Open . Alternatively, configure +`ALTERTABLE_DATA_PROXY_URL` and `ALTERTABLE_DATA_PROXY_TOKEN` for an existing +CLI proxy. Missing credentials stop startup. + +## Build the app + +Replace the fixture queries and example content with the evidence and content +specified by the app brief. Change identity, appearance, and queries in `app.ts`. + +```fish +bun run check +``` + +`bun run check` includes the build, which writes browser assets to `dist/`. Hosting them requires an authorized +`/api/data` backend; the local adapter does not authenticate hosted viewers. + +## License + +[MIT](LICENSE) diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/_gitignore b/packages/data-app-examples/templates/starter-local-metric-data-app/_gitignore new file mode 100644 index 00000000..8854acb7 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/_gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.env* diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/app.ts b/packages/data-app-examples/templates/starter-local-metric-data-app/app.ts new file mode 100644 index 00000000..beb7766b --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/app.ts @@ -0,0 +1,39 @@ +import { defineDataApp } from '@altertable/data-app'; +export const dataApp = defineDataApp({ + title: 'Sample metric', + description: 'Fixture counts by group; replace with the requested metric.', + scope: { organization: 'demo', environment: 'sample' }, + appearance: { theme: 'system' }, + queries: { + sampleCountsBySegment: { + projections: { + segment_name: { type: 'string' }, + sample_count: { type: 'number' }, + }, + statement: ` +SELECT segment_name, sample_count FROM SEMANTIC_VIEW( + starter_sample_counts DIMENSIONS samples.segment_name METRICS samples.sample_count + WHERE $groupName = '' OR samples.group_name = $groupName +) ORDER BY segment_name LIMIT $limit`, + params: { + groupName: { type: 'string', defaultValue: '' }, + limit: { type: 'number', defaultValue: 10 }, + }, + }, + sampleCountsByGroup: { + projections: { + group_name: { type: 'string' }, + sample_count: { type: 'number' }, + }, + statement: ` +SELECT group_name, sample_count FROM SEMANTIC_VIEW( + starter_sample_counts DIMENSIONS samples.group_name METRICS samples.sample_count + WHERE $groupName = '' OR samples.group_name = $groupName +) ORDER BY group_name LIMIT $limit`, + params: { + groupName: { type: 'string', defaultValue: '' }, + limit: { type: 'number', defaultValue: 10 }, + }, + }, + }, +}); diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/bunfig.toml b/packages/data-app-examples/templates/starter-local-metric-data-app/bunfig.toml new file mode 100644 index 00000000..b6874be1 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/bunfig.toml @@ -0,0 +1,2 @@ +[install] +exact = true diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/package.json b/packages/data-app-examples/templates/starter-local-metric-data-app/package.json new file mode 100644 index 00000000..667d2c5c --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/package.json @@ -0,0 +1,36 @@ +{ + "name": "data-app-metric-starter", + "private": true, + "type": "module", + "imports": { + "#app/*": "./src/*", + "#config": "./app.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "oxlint --type-aware src", + "format": "oxfmt src app.ts", + "format:check": "oxfmt --check src app.ts", + "dev": "bun --hot src/server.ts", + "build": "bun run typecheck && bun build src/index.html --outdir=dist --production", + "check": "bun run lint && bun run format:check && bun run build", + "lint:fix": "oxlint --type-aware --fix src" + }, + "dependencies": { + "@altertable/data-app": "0.74.0", + "@altertable/data-app-bun": "0.74.0", + "@altertable/data-app-react": "0.74.0", + "react": "19.3.1", + "react-dom": "19.3.1" + }, + "devDependencies": { + "@types/bun": "1.4.3", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "oxfmt": "0.71.0", + "oxlint": "1.86.0", + "oxlint-tsgolint": "7.0.2003", + "typescript": "7.0.2" + }, + "packageManager": "bun@1.4.4" +} diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/src/App.tsx b/packages/data-app-examples/templates/starter-local-metric-data-app/src/App.tsx new file mode 100644 index 00000000..e51e3f66 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/src/App.tsx @@ -0,0 +1,124 @@ +import { + createDataHooks, + DataApp, + DataSection, + VisualizationWidget, + Stack, + choiceVariable, +} from '@altertable/data-app-react'; +import { BarChart } from '@altertable/data-app-react/ui'; +import { createDataClient } from '@altertable/data-app/client'; + +import { sampleDataContext } from '#app/data-context.ts'; +import type { operations } from '#app/operations.ts'; +const { defineDataView } = + createDataHooks(createDataClient()); +const sampleCountsView = defineDataView({ + dataContext: sampleDataContext, + operation: 'sampleCountsByGroup', + variables: { + groupName: choiceVariable({ + key: 'group', + label: 'Group', + defaultValue: 'all', + options: [ + { id: 'all', label: 'All groups' }, + { id: 'Alpha', label: 'Alpha' }, + { id: 'Beta', label: 'Beta' }, + ], + }), + }, + describeInput: ({ groupName }) => + groupName === 'all' ? 'all groups' : groupName, + isEmpty: result => result.groups.length === 0, + emptyFallback: { + title: 'No matching groups', + description: 'No observations are available for this query.', + }, +}); +const sampleCounts = sampleCountsView.dataset({ + name: 'Sample counts', + select: result => result.groups, + rowKey: row => row.groupName, + columns: { + groupName: { label: 'Group', value: row => row.groupName }, + sampleCount: { value: row => row.sampleCount, format: { kind: 'count' } }, + }, + evidence: { + id: 'counts-by-group', + glossaryIds: ['sampleCount'], + }, +}); +const segments = sampleCountsView.dataset({ + name: 'Counts by segment', + select: result => result.segments, + rowKey: row => row.segmentName, + columns: { + segmentName: { label: 'Segment', value: row => row.segmentName }, + sampleCount: { value: row => row.sampleCount, format: { kind: 'count' } }, + }, + evidence: { id: 'counts-by-segment', glossaryIds: ['sampleCount'] }, +}); +const groupActions = sampleCounts.actions( + action => ({ + drill: action.filter({ + label: 'Drill into group', + target: sampleCountsView.filter(), + values: row => ({ groupName: row.groupName }), + }), + }), + { primary: 'drill' } +); +// Keep only the breakdowns and interactions relevant to the requested metric. +const sampleContent = sampleCountsView.content(result => ( + + + {(rows, actions) => ( + ({ + ...row, + id: row.groupName, + label: row.groupName, + value: row.sampleCount, + }))} + itemActions={actions} + /> + )} + + + {(rows, actions) => ( + ({ + ...row, + id: row.segmentName, + label: row.segmentName, + value: row.sampleCount, + }))} + itemActions={actions} + /> + )} + + +)); +export function App() { + return ( + + + + ); +} diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/src/assets.d.ts b/packages/data-app-examples/templates/starter-local-metric-data-app/src/assets.d.ts new file mode 100644 index 00000000..35306c6f --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/src/assets.d.ts @@ -0,0 +1 @@ +declare module '*.css'; diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/src/data-context.ts b/packages/data-app-examples/templates/starter-local-metric-data-app/src/data-context.ts new file mode 100644 index 00000000..1ef9b431 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/src/data-context.ts @@ -0,0 +1,19 @@ +import { createDataContext } from '@altertable/data-app-react'; +const queryNames = { + sampleCountsByGroup: 'sampleCountsByGroup', + sampleCountsBySegment: 'sampleCountsBySegment', +} as const; +export const sampleDataContext = createDataContext(queryNames)({ + description: + 'The starter_sample_counts semantic view measures fictional samples by group and segment. Replace it with the requested semantic metric before publishing.', + glossary: { + sampleCount: { + term: 'Sample count', + definition: 'A fixture value: Alpha is 2,200,000 and Beta is 600,000.', + queryNames: [ + queryNames.sampleCountsByGroup, + queryNames.sampleCountsBySegment, + ], + }, + }, +}); diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/src/favicon.svg b/packages/data-app-examples/templates/starter-local-metric-data-app/src/favicon.svg new file mode 100644 index 00000000..11ce6117 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/src/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/src/index.html b/packages/data-app-examples/templates/starter-local-metric-data-app/src/index.html new file mode 100644 index 00000000..1edaa873 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/src/index.html @@ -0,0 +1,14 @@ + + + + + + + Altertable app + + + + +
+ + diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/src/main.tsx b/packages/data-app-examples/templates/starter-local-metric-data-app/src/main.tsx new file mode 100644 index 00000000..b71279bc --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/src/main.tsx @@ -0,0 +1,8 @@ +import { injectDataAppStyles, mountDataApp } from '@altertable/data-app-react'; + +import { App } from '#app/App.tsx'; +import { dataApp } from '#config'; + +injectDataAppStyles(); + +mountDataApp({ app: dataApp, component: App }); diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/src/operations.ts b/packages/data-app-examples/templates/starter-local-metric-data-app/src/operations.ts new file mode 100644 index 00000000..f6d0f052 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/src/operations.ts @@ -0,0 +1,50 @@ +import { parseCount } from '@altertable/data-app/contract'; + +import { dataApp } from '#config'; +export const operations = { + sampleCountsByGroup: dataApp.defineOperation({ + input: value => { + if ( + !value || + typeof value !== 'object' || + !('groupName' in value) || + typeof value.groupName !== 'string' || + !['all', 'Alpha', 'Beta'].includes(value.groupName) + ) + throw new Error('Expected a fixture group.'); + return { groupName: value.groupName }; + }, + checks: [ + { groupName: 'all' }, + { groupName: 'Alpha' }, + { groupName: 'Beta' }, + ], + policy: { maxQueryRows: 10, maxDurationMs: 15000 }, + async run({ query }, { groupName }) { + const [groups, segments] = await Promise.all([ + query('sampleCountsByGroup', { + groupName: groupName === 'all' ? '' : groupName, + }), + query('sampleCountsBySegment', { + groupName: groupName === 'all' ? '' : groupName, + }), + ]); + return { + groups: groups.rows.map( + (row): { groupName: 'Alpha' | 'Beta'; sampleCount: number } => { + if (row.group_name !== 'Alpha' && row.group_name !== 'Beta') + throw new Error('Unexpected fixture group.'); + return { + groupName: row.group_name, + sampleCount: parseCount(row.sample_count), + }; + } + ), + segments: segments.rows.map(row => ({ + segmentName: row.segment_name, + sampleCount: parseCount(row.sample_count), + })), + }; + }, + }), +}; diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/src/server.ts b/packages/data-app-examples/templates/starter-local-metric-data-app/src/server.ts new file mode 100644 index 00000000..da9c04f3 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/src/server.ts @@ -0,0 +1,7 @@ +import { serveLocalApp } from '@altertable/data-app-bun'; + +import page from '#app/index.html'; +import { operations } from '#app/operations.ts'; +import { dataApp } from '#config'; + +serveLocalApp({ page, operations, title: dataApp.title }); diff --git a/packages/data-app-examples/templates/starter-local-metric-data-app/tsconfig.json b/packages/data-app-examples/templates/starter-local-metric-data-app/tsconfig.json new file mode 100644 index 00000000..86196070 --- /dev/null +++ b/packages/data-app-examples/templates/starter-local-metric-data-app/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "Preserve", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "jsx": "react-jsx", + "strict": true, + "noUncheckedIndexedAccess": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["bun"], + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "allowUnreachableCode": false, + "noUncheckedSideEffectImports": true + }, + "include": ["src", "app.ts"] +} diff --git a/packages/data-app-examples/templates/starter-metric-data-app/LICENSE b/packages/data-app-examples/templates/starter-metric-data-app/LICENSE new file mode 100644 index 00000000..845a62ce --- /dev/null +++ b/packages/data-app-examples/templates/starter-metric-data-app/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Altertable + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/data-app-examples/templates/starter-metric-data-app/index.tsx b/packages/data-app-examples/templates/starter-metric-data-app/index.tsx new file mode 100644 index 00000000..4da7bd40 --- /dev/null +++ b/packages/data-app-examples/templates/starter-metric-data-app/index.tsx @@ -0,0 +1,229 @@ +import { defineDataApp } from '@altertable/data-app'; +import { + createDataContext, + injectDataAppStyles, + mountDataApp, +} from '@altertable/data-app-react'; +import { + createDataHooks, + DataApp, + DataSection, + VisualizationWidget, + Stack, + choiceVariable, +} from '@altertable/data-app-react'; +import { BarChart } from '@altertable/data-app-react/ui'; +import { createDataClient } from '@altertable/data-app/client'; +import { parseCount } from '@altertable/data-app/contract'; +const dataApp = defineDataApp({ + title: 'Sample metric', + description: 'Fixture counts by group; replace with the requested metric.', + scope: { organization: 'demo', environment: 'sample' }, + appearance: { theme: 'system' }, + queries: { + sampleCountsBySegment: { + projections: { + segment_name: { type: 'string' }, + sample_count: { type: 'number' }, + }, + statement: ` +SELECT segment_name, sample_count FROM SEMANTIC_VIEW( + starter_sample_counts DIMENSIONS samples.segment_name METRICS samples.sample_count + WHERE $groupName = '' OR samples.group_name = $groupName +) ORDER BY segment_name LIMIT $limit`, + params: { + groupName: { type: 'string', defaultValue: '' }, + limit: { type: 'number', defaultValue: 10 }, + }, + }, + sampleCountsByGroup: { + projections: { + group_name: { type: 'string' }, + sample_count: { type: 'number' }, + }, + statement: ` +SELECT group_name, sample_count FROM SEMANTIC_VIEW( + starter_sample_counts DIMENSIONS samples.group_name METRICS samples.sample_count + WHERE $groupName = '' OR samples.group_name = $groupName +) ORDER BY group_name LIMIT $limit`, + params: { + groupName: { type: 'string', defaultValue: '' }, + limit: { type: 'number', defaultValue: 10 }, + }, + }, + }, +}); +const operations = { + sampleCountsByGroup: dataApp.defineOperation({ + input: value => { + if ( + !value || + typeof value !== 'object' || + !('groupName' in value) || + typeof value.groupName !== 'string' || + !['all', 'Alpha', 'Beta'].includes(value.groupName) + ) + throw new Error('Expected a fixture group.'); + return { groupName: value.groupName }; + }, + checks: [ + { groupName: 'all' }, + { groupName: 'Alpha' }, + { groupName: 'Beta' }, + ], + policy: { maxQueryRows: 10, maxDurationMs: 15000 }, + async run({ query }, { groupName }) { + const [groups, segments] = await Promise.all([ + query('sampleCountsByGroup', { + groupName: groupName === 'all' ? '' : groupName, + }), + query('sampleCountsBySegment', { + groupName: groupName === 'all' ? '' : groupName, + }), + ]); + return { + groups: groups.rows.map( + (row): { groupName: 'Alpha' | 'Beta'; sampleCount: number } => { + if (row.group_name !== 'Alpha' && row.group_name !== 'Beta') + throw new Error('Unexpected fixture group.'); + return { + groupName: row.group_name, + sampleCount: parseCount(row.sample_count), + }; + } + ), + segments: segments.rows.map(row => ({ + segmentName: row.segment_name, + sampleCount: parseCount(row.sample_count), + })), + }; + }, + }), +}; +const queryNames = operations.sampleCountsByGroup.queryNames; +const sampleDataContext = createDataContext(queryNames)({ + description: + 'The starter_sample_counts semantic view measures fictional samples by group and segment. Replace it with the requested semantic metric before publishing.', + glossary: { + sampleCount: { + term: 'Sample count', + definition: 'A fixture value: Alpha is 2,200,000 and Beta is 600,000.', + queryNames: [ + queryNames.sampleCountsByGroup, + queryNames.sampleCountsBySegment, + ], + }, + }, +}); +const { defineDataView } = createDataHooks(createDataClient({ operations })); +const sampleCountsView = defineDataView({ + dataContext: sampleDataContext, + operation: 'sampleCountsByGroup', + variables: { + groupName: choiceVariable({ + key: 'group', + label: 'Group', + defaultValue: 'all', + options: [ + { id: 'all', label: 'All groups' }, + { id: 'Alpha', label: 'Alpha' }, + { id: 'Beta', label: 'Beta' }, + ], + }), + }, + describeInput: ({ groupName }) => + groupName === 'all' ? 'all groups' : groupName, + isEmpty: result => result.groups.length === 0, + emptyFallback: { + title: 'No matching groups', + description: 'No observations are available for this query.', + }, +}); +const sampleCounts = sampleCountsView.dataset({ + name: 'Sample counts', + select: result => result.groups, + rowKey: row => row.groupName, + columns: { + groupName: { label: 'Group', value: row => row.groupName }, + sampleCount: { value: row => row.sampleCount, format: { kind: 'count' } }, + }, + evidence: { + id: 'counts-by-group', + glossaryIds: ['sampleCount'], + }, +}); +const segments = sampleCountsView.dataset({ + name: 'Counts by segment', + select: result => result.segments, + rowKey: row => row.segmentName, + columns: { + segmentName: { label: 'Segment', value: row => row.segmentName }, + sampleCount: { value: row => row.sampleCount, format: { kind: 'count' } }, + }, + evidence: { id: 'counts-by-segment', glossaryIds: ['sampleCount'] }, +}); +const groupActions = sampleCounts.actions( + action => ({ + drill: action.filter({ + label: 'Drill into group', + target: sampleCountsView.filter(), + values: row => ({ groupName: row.groupName }), + }), + }), + { primary: 'drill' } +); +// Keep only the breakdowns and interactions relevant to the requested metric. +const sampleContent = sampleCountsView.content(result => ( + + + {(rows, actions) => ( + ({ + ...row, + id: row.groupName, + label: row.groupName, + value: row.sampleCount, + }))} + itemActions={actions} + /> + )} + + + {(rows, actions) => ( + ({ + ...row, + id: row.segmentName, + label: row.segmentName, + value: row.sampleCount, + }))} + itemActions={actions} + /> + )} + + +)); +function App() { + return ( + + + + ); +} +injectDataAppStyles(); +mountDataApp({ app: dataApp, component: App }); diff --git a/packages/data-app-react/src/ui/DataApp.tsx b/packages/data-app-react/src/ui/DataApp.tsx index a241c503..50e88f09 100644 --- a/packages/data-app-react/src/ui/DataApp.tsx +++ b/packages/data-app-react/src/ui/DataApp.tsx @@ -34,13 +34,15 @@ export type DataAppProps = Omit< }; /** Show supplied datasets’ record suggestions on the first declared search. Defaults to true. */ showSearchSuggestions?: boolean; - story: (snapshot: DisplayedSnapshot) => readonly (Omit< + /** Omit to leave presentation out of the toolbar. */ + story?: (snapshot: DisplayedSnapshot) => readonly (Omit< StoryFinding, 'evidence' > & { evidence: RegisteredBinding; })[]; - datasets: readonly [ + /** Omit to leave CSV export out of the toolbar. */ + datasets?: readonly [ ExportDataset, ...ExportDataset[], ]; @@ -67,7 +69,7 @@ export function DataApp(props: DataAppProps) { ); const suggestionGroups = showSearchSuggestions && frame.showSearch !== false && result.searchControls - ? datasets.flatMap((dataset, index) => { + ? (datasets ?? []).flatMap((dataset, index) => { const reading = dataset.readSearchSuggestions(source); if (!reading) return []; return [ @@ -98,19 +100,25 @@ export function DataApp(props: DataAppProps) { {...frame} request={result} dataContext={result.dataContext} - story={snapshot => - story(ownedSource(view, snapshot)).map(finding => ({ - ...finding, - evidence: registeredEvidence(finding.evidence, view), - })) + story={ + story + ? snapshot => + story(ownedSource(view, snapshot)).map(finding => ({ + ...finding, + evidence: registeredEvidence(finding.evidence, view), + })) + : undefined } - csvExport={(snapshot: DisplayedSnapshot) => - exportDatasets( - datasets, - ownedSource(view, snapshot), - view.scope(snapshot), - app.title - ) + csvExport={ + datasets + ? (snapshot: DisplayedSnapshot) => + exportDatasets( + datasets, + ownedSource(view, snapshot), + view.scope(snapshot), + app.title + ) + : undefined } > {children} diff --git a/packages/data-app-react/src/ui/DataAppFrame.tsx b/packages/data-app-react/src/ui/DataAppFrame.tsx index f823bba4..d6478de5 100644 --- a/packages/data-app-react/src/ui/DataAppFrame.tsx +++ b/packages/data-app-react/src/ui/DataAppFrame.tsx @@ -31,6 +31,8 @@ export type DataAppBaseProps = { dataContext: DataContext; /** Override the declared subtitle; null hides it. */ description?: ReactNode; + /** Show page actions. Defaults to true; does not hide filters or data states. */ + showToolbar?: boolean; toolbarActions?: ReactNode; footerActions?: ReactNode; /** Optional source, coverage, or methodology note below the app content. @@ -65,9 +67,9 @@ export type DataAppProps = DataAppControlProps & { request: DataAppRequest; /** Findings are always derived from the result currently visible to the user. */ - story: BoundStory; + story?: BoundStory; /** Export every distinct dataset in the displayed result. */ - csvExport: (snapshot: DisplayedSnapshot) => CsvExport; + csvExport?: (snapshot: DisplayedSnapshot) => CsvExport; queries?: never; }) | (StaticDataAppProps & { @@ -100,7 +102,7 @@ export function DataAppFrame(props: DataAppProps) { }); const snapshot = request && displayedSnapshot(request.view); - const findings = request && snapshot ? props.story(snapshot) : undefined; + const findings = request && snapshot ? props.story?.(snapshot) : undefined; const story = findings?.length ? { findings } : undefined; const scope = ( (props: DataAppProps) { environment={app.scope.environment} /> ); - const toolbar = ( - void request.refetch(), - onCancel: request.cancel - ? () => void request.cancel?.() - : undefined, - } - : undefined - } - story={ - story - ? { - ...story, - title: app.title, - scope, - dataContext, - theme: themeController, - } - : request - ? null + const toolbar = + props.showToolbar === false ? null : ( + void request.refetch(), + onCancel: request.cancel + ? () => void request.cancel?.() + : undefined, + } : undefined - } - aboutData={ - - } - > - {presentation?.annotations?.enabled && ( - - )} - {toolbarActions} - - ); + } + story={ + story + ? { + ...story, + title: app.title, + scope, + dataContext, + theme: themeController, + } + : request && props.story + ? null + : undefined + } + aboutData={ + + } + > + {presentation?.annotations?.enabled && ( + + )} + {toolbarActions} + + ); const searchRegion = request?.searchControls && props.showSearch !== false ? ( diff --git a/release-please-config.json b/release-please-config.json index 6d5c7a85..323a2d9e 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -126,6 +126,52 @@ "type": "json", "path": "packages/data-app-manifest/package.json", "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/data-app-examples/templates/starter-local-metric-data-app/package.json", + "jsonpath": "$.dependencies['@altertable/data-app']" + }, + { + "type": "json", + "path": "packages/data-app-examples/templates/starter-local-metric-data-app/package.json", + "jsonpath": "$.dependencies['@altertable/data-app-react']" + }, + { + "type": "json", + "path": "packages/data-app-examples/templates/starter-local-metric-data-app/package.json", + "jsonpath": "$.dependencies['@altertable/data-app-bun']" + }, + { + "type": "generic", + "path": "packages/data-app-examples/templates/starter-local-metric-data-app/AGENTS.md" + }, + { + "type": "generic", + "path": "packages/data-app-examples/templates/starter-local-metric-data-app/README.md" + }, + { + "type": "json", + "path": "packages/data-app-examples/templates/starter-local-finding-data-app/package.json", + "jsonpath": "$.dependencies['@altertable/data-app']" + }, + { + "type": "json", + "path": "packages/data-app-examples/templates/starter-local-finding-data-app/package.json", + "jsonpath": "$.dependencies['@altertable/data-app-react']" + }, + { + "type": "json", + "path": "packages/data-app-examples/templates/starter-local-finding-data-app/package.json", + "jsonpath": "$.dependencies['@altertable/data-app-bun']" + }, + { + "type": "generic", + "path": "packages/data-app-examples/templates/starter-local-finding-data-app/AGENTS.md" + }, + { + "type": "generic", + "path": "packages/data-app-examples/templates/starter-local-finding-data-app/README.md" } ] } diff --git a/scripts/check-boundaries.ts b/scripts/check-boundaries.ts index 7738572a..18b906cb 100644 --- a/scripts/check-boundaries.ts +++ b/scripts/check-boundaries.ts @@ -5,6 +5,8 @@ import { relative, resolve } from 'node:path'; import released from '@/.release-please-manifest.json'; import workspace from '@/package.json'; import starter from '@/packages/data-app-examples/templates/starter-local-data-app/package.json'; +import findingStarter from '@/packages/data-app-examples/templates/starter-local-finding-data-app/package.json'; +import metricStarter from '@/packages/data-app-examples/templates/starter-local-metric-data-app/package.json'; import release from '@/release-please-config.json'; import { packages } from '@/scripts/packages'; @@ -27,37 +29,43 @@ function releaseVersion(path: string, field: string) { `Release Please does not update ${path}:${field}` ); } -for (const filename of ['AGENTS.md', 'README.md']) { - const path = `packages/data-app-examples/templates/starter-local-data-app/${filename}`; - assert( - release.packages['.']['extra-files'].some( - update => update.type === 'generic' && update.path === path - ), - `Release Please does not update starter guidance: ${path}` - ); - const source = await Bun.file(path).text(); - const taggedLinks = [ - ...source.matchAll( - /github\.com\/altertable-ai\/data-app\/blob\/v([^/]+)\//g - ), - ]; - assert(taggedLinks.length > 0, `Missing release-matched guidance: ${path}`); - for (const match of taggedLinks) - assert.equal( - match[1], - workspace.version, - `${path}: guidance version differs` - ); - const annotatedLines = source - .split('\n') - .filter(line => line.includes('')); - assert(annotatedLines.length > 0, `${path}: missing release annotation`); - for (const line of annotatedLines) - assert.equal( - [...line.matchAll(/\d+\.\d+\.\d+/g)].length, - 1, - `${path}: release annotations must contain exactly one version` +for (const id of [ + 'starter-local-data-app', + 'starter-local-metric-data-app', + 'starter-local-finding-data-app', +]) { + for (const filename of ['AGENTS.md', 'README.md']) { + const path = `packages/data-app-examples/templates/${id}/${filename}`; + assert( + release.packages['.']['extra-files'].some( + update => update.type === 'generic' && update.path === path + ), + `Release Please does not update starter guidance: ${path}` ); + const source = await Bun.file(path).text(); + const taggedLinks = [ + ...source.matchAll( + /github\.com\/altertable-ai\/data-app\/blob\/v([^/]+)\//g + ), + ]; + assert(taggedLinks.length > 0, `Missing release-matched guidance: ${path}`); + for (const match of taggedLinks) + assert.equal( + match[1], + workspace.version, + `${path}: guidance version differs` + ); + const annotatedLines = source + .split('\n') + .filter(line => line.includes('')); + assert(annotatedLines.length > 0, `${path}: missing release annotation`); + for (const line of annotatedLines) + assert.equal( + [...line.matchAll(/\d+\.\d+\.\d+/g)].length, + 1, + `${path}: release annotations must contain exactly one version` + ); + } } for (const { directory } of packages) releaseVersion(`${directory}/package.json`, '$.version'); @@ -67,6 +75,14 @@ for (const [path, manifest] of [ 'packages/data-app-examples/templates/starter-local-data-app/package.json', starter, ], + [ + 'packages/data-app-examples/templates/starter-local-metric-data-app/package.json', + metricStarter, + ], + [ + 'packages/data-app-examples/templates/starter-local-finding-data-app/package.json', + findingStarter, + ], ...packages.map( pkg => [`${pkg.directory}/package.json`, pkg.manifest] as const ), diff --git a/scripts/check-package.ts b/scripts/check-package.ts index bd963cc4..d1708a9f 100644 --- a/scripts/check-package.ts +++ b/scripts/check-package.ts @@ -196,6 +196,10 @@ try { if (manifest.name === '@altertable/data-app-examples') { for (const path of [ 'templates/starter-data-app/index.tsx', + 'templates/starter-metric-data-app/index.tsx', + 'templates/starter-finding-data-app/index.tsx', + 'templates/starter-local-metric-data-app/src/App.tsx', + 'templates/starter-local-finding-data-app/src/App.tsx', 'templates/starter-local-data-app/package.json', 'templates/starter-local-data-app/.bun-version', 'templates/starter-local-data-app/_gitignore', @@ -387,6 +391,10 @@ console.log(JSON.stringify(dataAppExamples.map(example => ({...example, director [ { id: 'starter-data-app', mode: 'hosted' }, { id: 'starter-local-data-app', mode: 'local' }, + { id: 'starter-metric-data-app', mode: 'hosted' }, + { id: 'starter-finding-data-app', mode: 'hosted' }, + { id: 'starter-local-metric-data-app', mode: 'local' }, + { id: 'starter-local-finding-data-app', mode: 'local' }, ] ); const localExample = catalog.find(example => example.mode === 'local'); diff --git a/scripts/check-starter.ts b/scripts/check-starter.ts index a073c06b..99ce66fd 100644 --- a/scripts/check-starter.ts +++ b/scripts/check-starter.ts @@ -4,30 +4,36 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; -import { copyExample } from '@altertable/data-app-examples'; +import { copyExample, dataAppExamples } from '@altertable/data-app-examples'; const root = resolve(import.meta.dir, '..'); -// Verify the starter against the already installed workspace packages. -const child = Bun.spawn(['bun', 'run', 'check'], { - cwd: resolve( - root, - 'packages/data-app-examples/templates/starter-local-data-app' - ), - stdout: 'inherit', - stderr: 'inherit', -}); -if ((await child.exited) !== 0) throw new Error('Starter check failed.'); +// Verify every local starter against the installed workspace packages. +for (const example of dataAppExamples.filter( + example => example.mode === 'local' +)) { + const child = Bun.spawn(['bun', 'run', 'check'], { + cwd: resolve(root, 'packages/data-app-examples/templates', example.id), + stdout: 'inherit', + stderr: 'inherit', + }); + if ((await child.exited) !== 0) + throw new Error(`${example.id} check failed.`); +} // Linked consumers must also omit the build output just created in the source template. const temporary = await mkdtemp(join(tmpdir(), 'data-app-starter-')); try { - const destination = join(temporary, 'app'); - await copyExample('starter-local-data-app', destination); - assert(existsSync(join(destination, '.gitignore'))); - assert(existsSync(join(destination, 'package.json'))); - assert(!existsSync(join(destination, 'dist'))); - assert(!existsSync(join(destination, 'node_modules'))); + for (const example of dataAppExamples.filter( + example => example.mode === 'local' + )) { + const destination = join(temporary, example.id); + await copyExample(example.id, destination); + assert(existsSync(join(destination, '.gitignore'))); + assert(existsSync(join(destination, 'package.json'))); + assert(!existsSync(join(destination, 'dist'))); + assert(!existsSync(join(destination, 'node_modules'))); + } } finally { await rm(temporary, { recursive: true, force: true }); } diff --git a/scripts/dev-gallery.ts b/scripts/dev-gallery.ts index 736c8ff5..2c9b4927 100644 --- a/scripts/dev-gallery.ts +++ b/scripts/dev-gallery.ts @@ -4,13 +4,14 @@ import { resolve } from 'node:path'; import { GenericContainer, Wait } from 'testcontainers'; import { seedStatements } from '@/dev/seed'; +import { starterMockImage, starterSeedStatements } from '@/dev/starter-mock'; import { packages } from '@/scripts/packages'; const root = resolve(import.meta.dir, '..'); const mockApiPort = 15000; async function seedMockApi(apiUrl: string) { - for (const statement of seedStatements) { + for (const statement of [...seedStatements, ...starterSeedStatements]) { const response = await fetch(`${apiUrl}/query`, { method: 'POST', headers: { @@ -28,9 +29,7 @@ async function seedMockApi(apiUrl: string) { async function startMockApi() { try { - const container = await new GenericContainer( - 'ghcr.io/altertable-ai/altertable-mock:latest' - ) + const container = await new GenericContainer(starterMockImage) .withEnvironment({ ALTERTABLE_MOCK_USERS: 'dev:dev' }) .withExposedPorts(mockApiPort) .withWaitStrategy(Wait.forListeningPorts()) diff --git a/tests/public-api/apps-export.test.ts b/tests/public-api/apps-export.test.ts index 5421796a..72f8da98 100644 --- a/tests/public-api/apps-export.test.ts +++ b/tests/public-api/apps-export.test.ts @@ -228,3 +228,29 @@ describe('export', () => { .toBe(true); }); }); + +test('omitting presentation and export retains refresh and loaded content', async ({ + page, +}) => { + await page.goto('/declared-app?no-exports'); + await page + .getByRole('button', { name: 'Resolve alpha 1', exact: true }) + .click(); + await page.getByRole('button', { name: 'Show primary', exact: true }).click(); + await expect + .poll(() => + page + .getByRole('button', { name: 'Refresh data', exact: true }) + .isEnabled() + ) + .toBe(true); + expect( + await page.getByRole('button', { name: 'Present story' }).count() + ).toBe(0); + expect(await page.getByRole('button', { name: /Export/ }).count()).toBe(0); + await expect + .poll(() => + page.getByRole('region', { name: 'Alpha visualization' }).isVisible() + ) + .toBe(true); +}); diff --git a/tests/public-api/authoring-entry.types.tsx b/tests/public-api/authoring-entry.types.tsx index b121fe5e..2bfb679c 100644 --- a/tests/public-api/authoring-entry.types.tsx +++ b/tests/public-api/authoring-entry.types.tsx @@ -274,12 +274,12 @@ view.dataset({ />; [ { id: 'raw', headline: 'Raw', visual: null, - // @ts-expect-error Story evidence must be the registered binding, not raw metadata. evidence: boundMetric.definition, }, ]} diff --git a/tests/public-api/fixtures/declared-app.tsx b/tests/public-api/fixtures/declared-app.tsx index e2955e56..a6d7f92d 100644 --- a/tests/public-api/fixtures/declared-app.tsx +++ b/tests/public-api/fixtures/declared-app.tsx @@ -158,7 +158,12 @@ export const dataApp = defineDataApp({ export function DeclaredApp() { const [shown, setShown] = useState(false); return ( - []} datasets={[alphaDataset]}> + [], datasets: [alphaDataset] as const })} + > diff --git a/tests/public-api/manifest.test.ts b/tests/public-api/manifest.test.ts index cbadaee9..f8d1d066 100644 --- a/tests/public-api/manifest.test.ts +++ b/tests/public-api/manifest.test.ts @@ -300,10 +300,14 @@ test('preserves the shared Node and Bun archive consumer contract', async () => verifyManifestConsumer(); }); -test('extracts the documented hosted starter declaration', async () => { +test.each([ + ['starter-data-app', 'Sample counts'], + ['starter-metric-data-app', 'Sample metric'], + ['starter-finding-data-app', 'Sample finding'], +])('extracts the %s declaration', async (starter, title) => { const text = await readFile( new URL( - '../../packages/data-app-examples/templates/starter-data-app/index.tsx', + `../../packages/data-app-examples/templates/${starter}/index.tsx`, import.meta.url ), 'utf8' @@ -311,7 +315,7 @@ test('extracts the documented hosted starter declaration', async () => { const result = extractDataAppManifest(text); expect(result.diagnostics).toEqual([]); expect(result.manifest).toMatchObject({ - title: 'Sample counts', + title, scope: { organization: 'demo', environment: 'sample' }, queries: { sampleCountsByGroup: { diff --git a/tsconfig.json b/tsconfig.json index b2ef2821..bcb22c63 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,6 +29,8 @@ "tests", "scripts", "dev", - "packages/data-app-examples/templates/starter-data-app/index.tsx" + "packages/data-app-examples/templates/starter-data-app/index.tsx", + "packages/data-app-examples/templates/starter-metric-data-app/index.tsx", + "packages/data-app-examples/templates/starter-finding-data-app/index.tsx" ] }