@lightdash/cli 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -23,6 +23,7 @@ Users describe features by what they see in the Lightdash editor. Translate:
|
|
|
23
23
|
| drill into a viz data point (reusable visualization) | `viz-drill-down` | app code opt-in |
|
|
24
24
|
| "ask the AI", one-line takeaway, explain this row | `ai-prompt` | app code opt-in |
|
|
25
25
|
| "share this view", URL that restores state | `url-state` | app code opt-in |
|
|
26
|
+
| "remember my filters", "keep my inputs", shared comments/targets/notes, "who changed this" | `app-state` | app code opt-in — only when the host has it enabled |
|
|
26
27
|
| Google Sheets export | `gsheet-export` | app code opt-in |
|
|
27
28
|
| "delivery has all tabs", full data in scheduled deliveries | `delivery-render` | app code opt-in |
|
|
28
29
|
| external API data | `external-fetch` | app code opt-in |
|
|
@@ -166,6 +167,11 @@ order, `sortBy` describes result ordering, `totalColumnCount` exposes truncation
|
|
|
166
167
|
helper.
|
|
167
168
|
- `url-state`: `useUrlState(...)` syncs a piece of app state into the page URL
|
|
168
169
|
so views can be shared and restored.
|
|
170
|
+
- `app-state`: `useAppState(key, default)` remembers a value per person;
|
|
171
|
+
`useSharedAppState(key, default)` keeps a value every viewer shares, with a
|
|
172
|
+
version, author and history (`client.appState.history(key)`). Offer it only
|
|
173
|
+
when the build prompt says saved app state is available; the hooks fall back
|
|
174
|
+
to memory otherwise. Full contract in `/app/references/app-state.md`.
|
|
169
175
|
- `gsheet-export`: `exportToSheets(...)` sends tabular results to a new
|
|
170
176
|
Google Sheet — offer it wherever the app renders a table.
|
|
171
177
|
- `external-fetch`: `client.externalFetch(alias, opts)` calls an external
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Saved app state (`useAppState`, `useSharedAppState`)
|
|
2
|
+
|
|
3
|
+
> Read this when the build prompt says saved app state is available. It covers remembering what a person entered, values every viewer shares, and who changed what.
|
|
4
|
+
|
|
5
|
+
Lightdash stores values the app saves, so they come back on the next open, on any device. Two hooks, both `useState`-shaped, both JSON values:
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { useAppState, useSharedAppState } from '@lightdash/query-sdk';
|
|
9
|
+
|
|
10
|
+
const [filters, setFilters] = useAppState('filters', DEFAULT_FILTERS); // this person only
|
|
11
|
+
const [targets, setTargets] = useSharedAppState('targets', {}); // every viewer
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- **`useAppState(key, default)`** saves per person. Only that person reads it back. Overwrites in place, no history.
|
|
15
|
+
- **`useSharedAppState(key, default)`** saves one value every viewer of the app reads. Each write is a version with its author and time; `client.appState.history(key)` lists them newest first.
|
|
16
|
+
|
|
17
|
+
## Pick the scope from what is being saved
|
|
18
|
+
|
|
19
|
+
| Saving… | Hook |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| A person's own inputs: filters, selections, sort, active tab, collapsed panels, draft text, preferences | `useAppState` |
|
|
22
|
+
| Something other people read: comments, targets, annotations, approvals, assignments, notes, AI commentary | `useSharedAppState` |
|
|
23
|
+
| "Who changed what", an audit trail, a change log | `useSharedAppState` plus `client.appState.history(key)` |
|
|
24
|
+
|
|
25
|
+
Prompt words that mean shared: comments, targets, annotations, approvals, "everyone", "the team", "who changed". When it is genuinely unclear whether a value is personal or for everyone ("save the targets"), ask one question before building: *Should everyone see these targets, or only the person who sets them?* That is the only storage question to ask.
|
|
26
|
+
|
|
27
|
+
## What to do without being asked
|
|
28
|
+
|
|
29
|
+
- **Remember a person's inputs by default.** Any input a person would be annoyed to lose on refresh goes in `useAppState`. The user does not have to ask for this.
|
|
30
|
+
- **Inputs that define the view** (period, active tab, segment, sort) use `useAppState(key, default, { url: true })`: the value also lands in the shareable link, so a colleague who opens the link sees that view, and the person gets their last view back when they open the app from the menu. A value in the link wins until the person changes the control.
|
|
31
|
+
- **Global filters stay on `useGlobalFilters()`** (URL state); never wire them through these hooks yourself.
|
|
32
|
+
- **Transient UI** (open menus, hover, modal visibility, text being typed before submit) stays in plain `useState`.
|
|
33
|
+
- **Add a "Reset" control** next to any filters or selections persisted per person, restoring the defaults with `setFilters(DEFAULT_FILTERS)`.
|
|
34
|
+
- **No save button for per-user state.** It saves as the person changes things. Shared edits made in a form may keep an explicit Save or Post.
|
|
35
|
+
|
|
36
|
+
## Shared values: updater or plain value
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
const [comments, setComments] = useSharedAppState('comments', []);
|
|
40
|
+
|
|
41
|
+
// Additive edit: use the updater form. If someone else wrote in between, the
|
|
42
|
+
// updater is re-applied on their value and retried, so both comments land.
|
|
43
|
+
setComments((prev) => [...prev, { id: crypto.randomUUID(), text, at: new Date().toISOString() }]);
|
|
44
|
+
|
|
45
|
+
// Whole-value replace (a form's Save): pass the value. If it is stale, the
|
|
46
|
+
// write is refused, the hook shows what the server has, and `error.code`
|
|
47
|
+
// is 'conflict'. Tell the person and let them re-apply their change.
|
|
48
|
+
setTargets(nextTargets);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The third element of both hooks carries state you should use:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const [targets, setTargets, { loading, error, persisted, version, updatedAt, updatedBy }] =
|
|
55
|
+
useSharedAppState('targets', {});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- `loading`: true until the saved values have loaded. Render the defaults meanwhile; do not block the page.
|
|
59
|
+
- `saved`: true when this key's latest value is confirmed stored. A value you just set shows immediately but is not saved until the server accepts it. For anything a person submits (a note, a form), keep their input visible until `saved` is true or an `error` arrives; never clear a composer on submit alone.
|
|
60
|
+
- `error`: the last failure for this key. Handle every code, not just the two you expect:
|
|
61
|
+
- `conflict`: someone else changed the value and you are now seeing theirs. Say so and let the person re-apply their change.
|
|
62
|
+
- `forbidden`: this viewer cannot write (embedded viewers read shared values but never write). Disable the control and say it is read-only here.
|
|
63
|
+
- `invalid`: the server refused the value (too large, bad key). Show the message and keep the person's input so they can shorten it.
|
|
64
|
+
- `unavailable` / `unknown`: the save did not reach the server. Say it was not saved; the SDK retries on the next change or `client.appState.flush()`, so keep the input.
|
|
65
|
+
- `persisted`: whether saving is possible at all. False when the feature is off, in local development without an app, or when the first load failed. The app keeps working in memory; show a small "not saved" hint. It says nothing about one key's value; that is `saved`.
|
|
66
|
+
- `updatedBy` / `updatedAt` (shared only): show "Last changed by Alice, 2 hours ago" where it helps.
|
|
67
|
+
|
|
68
|
+
## Sharing views and seeing other people's edits
|
|
69
|
+
|
|
70
|
+
- **Do not build "copy link" or "share" buttons.** The host page owns the URL; sharing is copying the address bar. `useAppState(key, default, { url: true })` is what puts a view control into that link, including a restored saved value. `document.referrer` and `window.parent.location` do not give the current link inside the sandbox.
|
|
71
|
+
- **Shared values are not live.** Other people's edits appear when the app loads, when the tab comes back to the foreground, or when the app calls `client.appState.refresh()`. Put a Refresh control on any shared panel (comments, notes, targets) and never describe it as real-time.
|
|
72
|
+
|
|
73
|
+
## Change history
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
const client = useLightdashClient();
|
|
77
|
+
const versions = await client.appState.history('targets', { limit: 50 });
|
|
78
|
+
// [{ version, value, createdAt, author: { kind: 'user', userUuid, name } }, ...] newest first
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Render the list; diff two versions' values yourself if the user asked for "what changed".
|
|
82
|
+
|
|
83
|
+
## Limits and keys
|
|
84
|
+
|
|
85
|
+
- JSON values only. At most 64 KB per key and 200 keys per app per scope; history keeps the last 1,000 versions of a shared key.
|
|
86
|
+
- Keys are 1–128 characters of letters, numbers, `_`, `.`, `:` or `-`.
|
|
87
|
+
- **One key per concept, never a key per row or per selection.** Commentary per selection, notes per row, flags per item: keep a map inside one key (`{ [selectionId]: {...} }`). A key per item hits the cap.
|
|
88
|
+
- Keep generated content with its inputs: AI commentary saved under one shared key as `{ text, generatedAt, inputs }` so the app can show when it was generated and offer a regenerate.
|
|
89
|
+
- A saved value whose JSON kind differs from the hook's default (an object where the app now expects an array, a string where it expects a number) is ignored and the default is used. That is the only check: an old object with different fields still comes back, so when you rename or add fields, read the saved value defensively and fill in defaults for what is missing.
|
|
90
|
+
|
|
91
|
+
## Do not
|
|
92
|
+
|
|
93
|
+
- Do not use these hooks when the build prompt does not say saved app state is available: values would only live in memory for the session.
|
|
94
|
+
- Do not store query results or large datasets. The data lives in the warehouse; save selections and inputs.
|
|
95
|
+
- Do not build your own persistence (localStorage, cookies) for the same purpose.
|
|
96
|
+
- Do not put a person's private inputs in `useSharedAppState`.
|
|
@@ -190,6 +190,10 @@ If the prompt announces an attached dashboard (blueprint at `/tmp/dashboard/blue
|
|
|
190
190
|
|
|
191
191
|
If the prompt lists external connections (files under `/tmp/external-data/`), read `/app/references/external-apis.md` before calling any external API — it documents each connection file and the `externalFetch` rules.
|
|
192
192
|
|
|
193
|
+
## Saved app state
|
|
194
|
+
|
|
195
|
+
If the prompt says saved app state is available, read `/app/references/app-state.md` before building anything that should survive a reload: a person's inputs are remembered with `useAppState` by default, without being asked; shared values (comments, targets, annotations, "everyone", "the team", "who changed") use `useSharedAppState`. Without that line in the prompt, do not use these hooks.
|
|
196
|
+
|
|
193
197
|
## Attached images
|
|
194
198
|
|
|
195
199
|
If the prompt references images under `/tmp/images/`, read `/app/references/attached-images.md` first — screenshots (`screenshot-*` files) describe the current app state, plain-uuid files are design references to approximate, and only design references may be embedded in the app.
|
|
@@ -704,6 +708,9 @@ const [period, setPeriod] = useUrlState('period', 'last_month');
|
|
|
704
708
|
- Ephemeral UI state (open dropdowns, hover, modal visibility, in-progress form input)
|
|
705
709
|
stays in plain `useState` — only state that defines *which view* the user is looking
|
|
706
710
|
at belongs in the URL.
|
|
711
|
+
- When the prompt says saved app state is available, view controls use
|
|
712
|
+
`useAppState(key, default, { url: true })` instead, which keeps the URL in sync and
|
|
713
|
+
also remembers the value for the person. See `/app/references/app-state.md`.
|
|
707
714
|
|
|
708
715
|
### External APIs
|
|
709
716
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightdash/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Lightdash CLI tool",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"@actions/core": "1.11.1",
|
|
23
23
|
"@aws-sdk/credential-providers": "3.995.0",
|
|
24
24
|
"@casl/ability": "6.8.0",
|
|
25
|
-
"@lightdash/common": "3.
|
|
26
|
-
"@lightdash/warehouses": "3.
|
|
25
|
+
"@lightdash/common": "3.5.0",
|
|
26
|
+
"@lightdash/warehouses": "3.5.0",
|
|
27
27
|
"@sidvind/better-ajv-errors": "7.0.0",
|
|
28
28
|
"@types/columnify": "1.5.1",
|
|
29
29
|
"ajv": "8.18.0",
|