@lightdash/query-sdk 3.96.1 → 3.98.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.
@@ -8,6 +8,7 @@
8
8
  * → returns results when status is 'ready'
9
9
  */
10
10
  import type { Column, LightdashClientConfig, Transport } from './types';
11
+ import { type SpaceChartsResult } from './types';
11
12
  /**
12
13
  * A function that performs HTTP requests and returns parsed JSON results.
13
14
  * The default implementation uses `Authorization: ApiKey` header.
@@ -24,10 +25,32 @@ export declare const createFieldQualifier: (exploreName: string, baseTable: stri
24
25
  export type ExploreBaseTableResolver = (exploreName: string) => Promise<string | null>;
25
26
  export type ApiTransportOptions = {
26
27
  resolveExploreBaseTable: ExploreBaseTableResolver | null;
28
+ /** Omitted/null = the transport reports the capability unavailable. */
29
+ listSpaceCharts?: SpaceChartsResolver | null;
27
30
  };
28
31
  export declare const createApiExploreBaseTableResolver: (fetchFn: FetchAdapter, projectUuid: string) => ExploreBaseTableResolver;
29
32
  /** Asks the host (app bridge or CLI preview proxy) via its virtual route. */
30
33
  export declare const createHostExploreBaseTableResolver: (fetchFn: FetchAdapter) => ExploreBaseTableResolver;
34
+ /** Lists the saved charts the viewer can see in a space (slug or uuid). */
35
+ export type SpaceChartsResolver = (space: string) => Promise<SpaceChartsResult>;
36
+ /**
37
+ * Thrown when the transport has the capability but the running host does not
38
+ * (a current SDK bundle on an older Lightdash). `useSpaceCharts` maps it to
39
+ * `available: false` instead of an error, so galleries hide cleanly.
40
+ */
41
+ export declare class SpaceChartsUnsupportedError extends Error {
42
+ constructor();
43
+ }
44
+ /** Normalize an untrusted body into a `SpaceChartsResult`. */
45
+ export declare const normalizeSpaceChartsResult: (value: unknown) => SpaceChartsResult;
46
+ /** Asks the host (app bridge or CLI preview proxy) via its virtual route. */
47
+ export declare const createHostSpaceChartsResolver: (fetchFn: FetchAdapter) => SpaceChartsResolver;
48
+ /**
49
+ * Resolves against the real API (local dev with a PAT): the spaces list to
50
+ * match the slug/uuid, then the space detail for its chart summaries — the
51
+ * same two calls the host makes when answering the virtual route.
52
+ */
53
+ export declare const createApiSpaceChartsResolver: (fetchFn: FetchAdapter, projectUuid: string) => SpaceChartsResolver;
31
54
  /**
32
55
  * Thrown for a non-2xx response. `status` lets hosts tell a recoverable
33
56
  * 429 (rate limited, retry later) from a real failure.
@@ -8,7 +8,7 @@
8
8
  * → returns results when status is 'ready'
9
9
  */
10
10
  import { AppStateError } from './appState';
11
- import { VIZ_DRILL_DOWN_PATH, VIZ_POINT_MENU_PATH, VIZ_UNDERLYING_DATA_OPEN_PATH, VIZ_UNDERLYING_DATA_PATH, VIZ_SUBTOTALS_PATH, EXPLORE_BASE_TABLE_PATH, } from './types';
11
+ import { VIZ_DRILL_DOWN_PATH, VIZ_POINT_MENU_PATH, VIZ_UNDERLYING_DATA_OPEN_PATH, VIZ_UNDERLYING_DATA_PATH, VIZ_SUBTOTALS_PATH, EXPLORE_BASE_TABLE_PATH, SPACE_CHARTS_PATH, } from './types';
12
12
  // Mirrors the explorer's `useInfiniteQueryResults` polling rhythm so the
13
13
  // SDK behaves like a normal Lightdash chart: 500-row pages, exponential
14
14
  // backoff starting at 250ms, capped at 1000ms.
@@ -98,6 +98,92 @@ export const createHostExploreBaseTableResolver = (fetchFn) => async (exploreNam
98
98
  const result = await fetchFn('POST', EXPLORE_BASE_TABLE_PATH, body);
99
99
  return toBaseTable(result?.baseTable);
100
100
  };
101
+ /**
102
+ * Thrown when the transport has the capability but the running host does not
103
+ * (a current SDK bundle on an older Lightdash). `useSpaceCharts` maps it to
104
+ * `available: false` instead of an error, so galleries hide cleanly.
105
+ */
106
+ export class SpaceChartsUnsupportedError extends Error {
107
+ constructor() {
108
+ super('Space charts listing is not supported by this Lightdash host');
109
+ this.name = 'SpaceChartsUnsupportedError';
110
+ }
111
+ }
112
+ const EMPTY_SPACE_CHARTS = { space: null, charts: [] };
113
+ const toNullableString = (value) => typeof value === 'string' && value.length > 0 ? value : null;
114
+ /** Normalize an untrusted body into a `SpaceChartsResult`. */
115
+ export const normalizeSpaceChartsResult = (value) => {
116
+ if (typeof value !== 'object' || value === null)
117
+ return EMPTY_SPACE_CHARTS;
118
+ const { space, charts } = value;
119
+ let spaceInfo = null;
120
+ if (typeof space === 'object' && space !== null) {
121
+ const { uuid, name, slug } = space;
122
+ if (typeof uuid === 'string' &&
123
+ typeof name === 'string' &&
124
+ typeof slug === 'string') {
125
+ spaceInfo = { uuid, name, slug };
126
+ }
127
+ }
128
+ const chartList = (Array.isArray(charts) ? charts : []).flatMap((chart) => {
129
+ if (typeof chart !== 'object' || chart === null)
130
+ return [];
131
+ const entry = chart;
132
+ if (typeof entry.uuid !== 'string' || typeof entry.name !== 'string') {
133
+ return [];
134
+ }
135
+ return [
136
+ {
137
+ uuid: entry.uuid,
138
+ name: entry.name,
139
+ description: toNullableString(entry.description),
140
+ chartKind: toNullableString(entry.chartKind),
141
+ slug: toNullableString(entry.slug),
142
+ updatedAt: toNullableString(entry.updatedAt),
143
+ },
144
+ ];
145
+ });
146
+ return { space: spaceInfo, charts: chartList };
147
+ };
148
+ /** Asks the host (app bridge or CLI preview proxy) via its virtual route. */
149
+ export const createHostSpaceChartsResolver = (fetchFn) => async (space) => {
150
+ const body = { space };
151
+ const result = await fetchFn('POST', SPACE_CHARTS_PATH, body);
152
+ return normalizeSpaceChartsResult(result);
153
+ };
154
+ /**
155
+ * Resolves against the real API (local dev with a PAT): the spaces list to
156
+ * match the slug/uuid, then the space detail for its chart summaries — the
157
+ * same two calls the host makes when answering the virtual route.
158
+ */
159
+ export const createApiSpaceChartsResolver = (fetchFn, projectUuid) => async (space) => {
160
+ try {
161
+ const spaces = await fetchFn('GET', `/api/v1/projects/${projectUuid}/spaces`);
162
+ const match = (Array.isArray(spaces) ? spaces : []).find((entry) => {
163
+ if (typeof entry !== 'object' || entry === null)
164
+ return false;
165
+ const { uuid, slug } = entry;
166
+ return uuid === space || slug === space;
167
+ });
168
+ if (typeof match?.uuid !== 'string')
169
+ return EMPTY_SPACE_CHARTS;
170
+ const detail = await fetchFn('GET', `/api/v1/projects/${projectUuid}/spaces/${match.uuid}`);
171
+ if (typeof detail !== 'object' || detail === null)
172
+ return EMPTY_SPACE_CHARTS;
173
+ return normalizeSpaceChartsResult({
174
+ space: detail,
175
+ charts: detail.queries,
176
+ });
177
+ }
178
+ catch (e) {
179
+ // Same face as the host route: no access answers like not found.
180
+ if (e instanceof LightdashApiError &&
181
+ (e.status === 403 || e.status === 404)) {
182
+ return EMPTY_SPACE_CHARTS;
183
+ }
184
+ throw e;
185
+ }
186
+ };
101
187
  function buildUnderlyingDataFilters(query, row, qualify) {
102
188
  const sourceFilters = buildApiFilters(query.filters.map((f) => ({
103
189
  ...f,
@@ -564,14 +650,21 @@ export function createApiTransport(config, adapter, options) {
564
650
  // endpoint, so only the default adapter looks base tables up by default.
565
651
  // Behind a dev proxy (CLI preview) the proxy answers the host route.
566
652
  let defaultResolver = null;
653
+ let defaultSpaceCharts = null;
567
654
  if (adapter === undefined) {
568
655
  defaultResolver = config.useProxy
569
656
  ? createHostExploreBaseTableResolver(fetchFn)
570
657
  : createApiExploreBaseTableResolver(fetchFn, config.projectUuid);
658
+ defaultSpaceCharts = config.useProxy
659
+ ? createHostSpaceChartsResolver(fetchFn)
660
+ : createApiSpaceChartsResolver(fetchFn, config.projectUuid);
571
661
  }
572
662
  const resolveExploreBaseTable = options === undefined
573
663
  ? defaultResolver
574
664
  : options.resolveExploreBaseTable;
665
+ const listSpaceCharts = options === undefined
666
+ ? defaultSpaceCharts
667
+ : (options.listSpaceCharts ?? null);
575
668
  const baseTableByExplore = new Map();
576
669
  const getFieldQualifier = async (query) => {
577
670
  const { exploreName } = query;
@@ -879,5 +972,8 @@ export function createApiTransport(config, adapter, options) {
879
972
  async openVizPointMenu(intent) {
880
973
  return fetchFn('POST', VIZ_POINT_MENU_PATH, intent);
881
974
  },
975
+ // Left absent (not a thrower) when no resolver exists, so
976
+ // `useSpaceCharts().available` honestly reports the capability.
977
+ ...(listSpaceCharts ? { listSpaceCharts } : {}),
882
978
  };
883
979
  }
package/dist/features.js CHANGED
@@ -169,6 +169,13 @@ export const SDK_FEATURES = [
169
169
  description: 'Author-written guidance for the AI analysis — language and formatting rules (e.g. British English, amounts in EUR) and a line or two of domain vocabulary — applied to every summary the app shows.',
170
170
  wiring: "Pass the guidance where the app renders its view-level analysis: useInsights({ instructions: '<guidance>' }), or the instructions prop on the shipped InsightsSummary component. One string per app; keep it to a few short sentences.",
171
171
  },
172
+ {
173
+ key: 'analysis-on-load',
174
+ appliesTo: ['data_app'],
175
+ label: 'AI analysis on load',
176
+ description: 'Run the AI analysis as soon as the app loads, without the viewer clicking Analyse. A stored analysis of the same rows is reused.',
177
+ wiring: 'Only when the user asks for it: set the analyseOnLoad prop on the shipped InsightsSummary component, or useInsights({ analyseOnLoad: true }) in a custom block. One value per app.',
178
+ },
172
179
  {
173
180
  key: 'ai-prompt',
174
181
  appliesTo: ['data_app'],
@@ -218,6 +225,13 @@ export const SDK_FEATURES = [
218
225
  description: "Result columns carry the semantic layer's url link patterns, so values with configured links render as links, matching dashboards.",
219
226
  wiring: 'In cell renderers (tables, underlying-data dialogs), call resolveColumnUrls({ column, row, columns, format }) and render the value as a link when it returns entries, styled visibly as a link.',
220
227
  },
228
+ {
229
+ key: 'space-charts',
230
+ appliesTo: ['data_app'],
231
+ label: 'List charts in a space',
232
+ description: 'List the saved charts in a Lightdash space at runtime, so chart galleries update as charts are saved — limited to spaces the viewer can already access.',
233
+ wiring: "Call useSpaceCharts('<space slug or uuid>') where the app needs the list, render from its charts array (uuid, name, description, chartKind), and pass chart.uuid to savedChart(...) to show one. Check available first — it stays false on hosts without the capability.",
234
+ },
221
235
  ];
222
236
  export const SDK_FEATURE_KEYS = SDK_FEATURES.map((f) => f.key);
223
237
  /**
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "3.96.1";
1
+ export declare const SDK_VERSION = "3.98.0";
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/generateSdkVersion.mjs (prebuild) — do not edit.
2
- export const SDK_VERSION = '3.96.1';
2
+ export const SDK_VERSION = '3.98.0';
@@ -1,11 +1,23 @@
1
1
  /**
2
- * Identity the host rides on its `lightdash:sdk:ready` handshake: the app
3
- * uuid this bundle is deployed as. Absent for older hosts and for bundles run
4
- * outside Lightdash (CLI preview), where app-scoped host features stay off.
2
+ * Identity and capabilities the host rides on its `lightdash:sdk:ready`
3
+ * handshake: the app uuid this bundle is deployed as, and which negotiated
4
+ * host features exist (`spaceCharts`). Absent for older hosts and for
5
+ * bundles run outside Lightdash (CLI preview), where host-negotiated
6
+ * features stay off.
5
7
  */
6
8
  type HostContext = {
7
9
  appUuid: string | null;
10
+ /** Host answers the space-charts virtual route. False for older hosts. */
11
+ spaceCharts: boolean;
12
+ /** A ready handshake has been seen at all. */
13
+ ready: boolean;
8
14
  };
15
+ /**
16
+ * Resolves once a ready handshake has been seen, or after `timeoutMs` —
17
+ * mirrors the fetch adapter's readiness gate so capability checks don't
18
+ * hang on hosts that never handshake.
19
+ */
20
+ export declare function waitForHostContext(timeoutMs: number): Promise<HostContext>;
9
21
  /** Called by `createPostMessageTransport()`; one listener per bundle. */
10
22
  export declare function mountHostContext(targetWindow: Window): () => void;
11
23
  export declare function useHostAppUuid(): string | null;
@@ -1,18 +1,31 @@
1
1
  /**
2
- * Identity the host rides on its `lightdash:sdk:ready` handshake: the app
3
- * uuid this bundle is deployed as. Absent for older hosts and for bundles run
4
- * outside Lightdash (CLI preview), where app-scoped host features stay off.
2
+ * Identity and capabilities the host rides on its `lightdash:sdk:ready`
3
+ * handshake: the app uuid this bundle is deployed as, and which negotiated
4
+ * host features exist (`spaceCharts`). Absent for older hosts and for
5
+ * bundles run outside Lightdash (CLI preview), where host-negotiated
6
+ * features stay off.
5
7
  */
6
8
  import { useSyncExternalStore } from 'react';
7
9
  const SDK_READY_MESSAGE = 'lightdash:sdk:ready';
8
- const EMPTY_CONTEXT = { appUuid: null };
10
+ const EMPTY_CONTEXT = {
11
+ appUuid: null,
12
+ spaceCharts: false,
13
+ ready: false,
14
+ };
9
15
  let value = EMPTY_CONTEXT;
10
16
  const listeners = new Set();
17
+ let readyResolvers = [];
11
18
  const set = (next) => {
12
- if (next.appUuid === value.appUuid)
19
+ if (next.appUuid === value.appUuid &&
20
+ next.spaceCharts === value.spaceCharts &&
21
+ next.ready === value.ready)
13
22
  return;
14
23
  value = next;
15
24
  listeners.forEach((listener) => listener());
25
+ if (next.ready) {
26
+ readyResolvers.forEach((resolve) => resolve());
27
+ readyResolvers = [];
28
+ }
16
29
  };
17
30
  const subscribe = (listener) => {
18
31
  listeners.add(listener);
@@ -20,6 +33,22 @@ const subscribe = (listener) => {
20
33
  listeners.delete(listener);
21
34
  };
22
35
  };
36
+ /**
37
+ * Resolves once a ready handshake has been seen, or after `timeoutMs` —
38
+ * mirrors the fetch adapter's readiness gate so capability checks don't
39
+ * hang on hosts that never handshake.
40
+ */
41
+ export function waitForHostContext(timeoutMs) {
42
+ if (value.ready)
43
+ return Promise.resolve(value);
44
+ return new Promise((resolve) => {
45
+ const timer = setTimeout(() => resolve(value), timeoutMs);
46
+ readyResolvers.push(() => {
47
+ clearTimeout(timer);
48
+ resolve(value);
49
+ });
50
+ });
51
+ }
23
52
  let activeCleanup = null;
24
53
  /** Called by `createPostMessageTransport()`; one listener per bundle. */
25
54
  export function mountHostContext(targetWindow) {
@@ -33,6 +62,8 @@ export function mountHostContext(targetWindow) {
33
62
  return;
34
63
  set({
35
64
  appUuid: typeof data.appUuid === 'string' ? data.appUuid : null,
65
+ spaceCharts: data.spaceCharts === true,
66
+ ready: true,
36
67
  });
37
68
  };
38
69
  activeCleanup?.();
@@ -54,4 +85,5 @@ export function peekHostContext() {
54
85
  }
55
86
  export function resetHostContext() {
56
87
  value = EMPTY_CONTEXT;
88
+ readyResolvers = [];
57
89
  }
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { drillDown } from './drillDown';
4
4
  export { createClient, LightdashClient } from './client';
5
5
  export { useLightdash } from './useLightdash';
6
6
  export { LightdashProvider, useLightdashClient } from './LightdashProvider';
7
- export { createApiTransport, LightdashApiError, type FetchAdapter, } from './apiTransport';
7
+ export { createApiTransport, LightdashApiError, SpaceChartsUnsupportedError, type FetchAdapter, } from './apiTransport';
8
8
  export { createPostMessageTransport } from './postMessageTransport';
9
9
  export { resolveColumnUrls, type ResolvedColumnUrl } from './columnUrls';
10
10
  export { SDK_FEATURES, SDK_FEATURE_KEYS, SDK_FIXES, SDK_FIX_KEYS, SDK_FEATURE_TARGETS, SDK_MANIFEST_MESSAGE_TYPE, type SdkFeature, type SdkFeatureTarget, type SdkFix, type SdkManifestMessage, } from './features';
@@ -28,6 +28,9 @@ export type { AppStateAuthor, AppStateErrorCode, AppStateErrorPayload, AppStateH
28
28
  export type { SdkAppStateOp, SdkAppStateRequest, SdkAppStateResponse, } from './postMessageTransport';
29
29
  export { useAiPrompt } from './aiPrompt';
30
30
  export type { AiPrompt, AiPromptOptions, AiPromptSource } from './aiPrompt';
31
+ export { useSpaceCharts } from './spaceCharts';
32
+ export type { SpaceCharts } from './spaceCharts';
33
+ export type { SpaceChartsResult, SpaceChartSummary } from './types';
31
34
  export { useInsights } from './insights';
32
35
  export type { Insight, InsightInvestigation, InsightSeverity, InsightsPayload, InsightsStatus, QueryInsights, ViewInsights, ViewInsightsOptions, } from './insights';
33
36
  export { createGradientColorScale } from './gradientColorScale';
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ export { useLightdash } from './useLightdash';
10
10
  // Provider
11
11
  export { LightdashProvider, useLightdashClient } from './LightdashProvider';
12
12
  // Transports
13
- export { createApiTransport, LightdashApiError, } from './apiTransport';
13
+ export { createApiTransport, LightdashApiError, SpaceChartsUnsupportedError, } from './apiTransport';
14
14
  export { createPostMessageTransport } from './postMessageTransport';
15
15
  export { resolveColumnUrls } from './columnUrls';
16
16
  // Feature manifest (also exported via the "./features" subpath for hosts)
@@ -32,6 +32,8 @@ export { useUrlState } from './urlState';
32
32
  export { AppStateError, useAppState, useSharedAppState, matchesDefaultShape, } from './appState';
33
33
  // Free-form AI prompts over loaded results (data apps)
34
34
  export { useAiPrompt } from './aiPrompt';
35
+ // List the saved charts the viewer can see in a space (data apps)
36
+ export { useSpaceCharts } from './spaceCharts';
35
37
  // AI analysis pushed by the host (data apps)
36
38
  export { useInsights } from './insights';
37
39
  export { createGradientColorScale } from './gradientColorScale';
@@ -69,6 +69,8 @@ export type InsightsRequestMessage = {
69
69
  /** Author guidance the host forwards to the analysis model, set via
70
70
  * `useInsights({ instructions })`. One string per app. */
71
71
  instructions?: string;
72
+ /** Set via `useInsights({ analyseOnLoad })`; absent = not declared. */
73
+ analyseOnLoad?: boolean;
72
74
  };
73
75
  export type InsightAction = {
74
76
  action: 'analyse';
@@ -98,6 +100,8 @@ export declare const EMPTY_INSIGHTS: InsightsPayload;
98
100
  export declare const parseInsightsPayload: (value: unknown) => InsightsPayload | null;
99
101
  /** Module-internal (exported for tests); `useInsights({ instructions })` wraps it. */
100
102
  export declare const setInsightInstructions: (value: string | undefined) => void;
103
+ /** Module-internal (exported for tests); `useInsights({ analyseOnLoad })` wraps it. */
104
+ export declare const setInsightAnalyseOnLoad: (value: boolean | undefined) => void;
101
105
  /**
102
106
  * Listen for the host's analysis and ask for the current one now. Called by
103
107
  * `createClient()` on the postMessage transport. One listener per bundle.
@@ -119,6 +123,9 @@ export type ViewInsightsOptions = {
119
123
  * line or two of domain vocabulary. One string per app (the last value
120
124
  * set wins); the host caps its length. */
121
125
  instructions?: string;
126
+ /** Analyse the view as soon as its queries settle, without a click.
127
+ * A stored analysis of the same rows is reused. One value per app. */
128
+ analyseOnLoad?: boolean;
122
129
  };
123
130
  export type ViewInsights = InsightsPayload & {
124
131
  /** Ask the host to analyse the current view (or re-analyse when stale). */
@@ -155,6 +162,7 @@ export declare function useHostAiAvailable(): boolean;
155
162
  *
156
163
  * const view = useInsights(); // executive summary + every anomaly
157
164
  * const view = useInsights({ instructions: 'Use British English.' });
165
+ * const view = useInsights({ analyseOnLoad: true });
158
166
  * const orders = useLightdash(ordersQuery);
159
167
  * const insights = useInsights(orders); // anomalies on this chart's rows
160
168
  *
package/dist/insights.js CHANGED
@@ -128,12 +128,17 @@ let activeCleanup = null;
128
128
  let inUse = false;
129
129
  // App-wide author guidance for the analysis model; last writer wins.
130
130
  let authorInstructions = null;
131
+ // App-wide analyse-on-load choice; null = not declared, last writer wins.
132
+ let authorAnalyseOnLoad = null;
131
133
  const requestMessage = () => ({
132
134
  type: INSIGHTS_REQUEST_MESSAGE,
133
135
  ...(inUse ? { inUse: true } : {}),
134
136
  ...(authorInstructions !== null
135
137
  ? { instructions: authorInstructions }
136
138
  : {}),
139
+ ...(authorAnalyseOnLoad !== null
140
+ ? { analyseOnLoad: authorAnalyseOnLoad }
141
+ : {}),
137
142
  });
138
143
  const markInUse = () => {
139
144
  if (inUse)
@@ -149,6 +154,14 @@ export const setInsightInstructions = (value) => {
149
154
  authorInstructions = next;
150
155
  hostWindow?.postMessage(requestMessage(), '*');
151
156
  };
157
+ /** Module-internal (exported for tests); `useInsights({ analyseOnLoad })` wraps it. */
158
+ export const setInsightAnalyseOnLoad = (value) => {
159
+ const next = value ?? null;
160
+ if (next === authorAnalyseOnLoad)
161
+ return;
162
+ authorAnalyseOnLoad = next;
163
+ hostWindow?.postMessage(requestMessage(), '*');
164
+ };
152
165
  const createStore = () => {
153
166
  let payload = EMPTY_INSIGHTS;
154
167
  const listeners = new Set();
@@ -304,6 +317,22 @@ export function useInsights(sourceOrOptions) {
304
317
  declaredInstructionsRef.current = true;
305
318
  setInsightInstructions(instructions);
306
319
  }, [instructions]);
320
+ const analyseOnLoad = source === undefined
321
+ ? sourceOrOptions
322
+ ?.analyseOnLoad
323
+ : undefined;
324
+ const declaredAnalyseOnLoadRef = useRef(false);
325
+ useEffect(() => {
326
+ if (analyseOnLoad === undefined) {
327
+ if (!declaredAnalyseOnLoadRef.current)
328
+ return;
329
+ declaredAnalyseOnLoadRef.current = false;
330
+ setInsightAnalyseOnLoad(undefined);
331
+ return;
332
+ }
333
+ declaredAnalyseOnLoadRef.current = true;
334
+ setInsightAnalyseOnLoad(analyseOnLoad);
335
+ }, [analyseOnLoad]);
307
336
  const payload = useInsightsPayload();
308
337
  const investigate = useCallback((anomalyId) => postInsightAction({ action: 'investigate', anomalyId }), []);
309
338
  const canContinue = payload.canContinue;
@@ -360,6 +389,7 @@ export function resetInsightsState() {
360
389
  hostWindow = null;
361
390
  inUse = false;
362
391
  authorInstructions = null;
392
+ authorAnalyseOnLoad = null;
363
393
  mounted.clear();
364
394
  flushScheduled = false;
365
395
  }
@@ -8,10 +8,10 @@
8
8
  * response back. All query logic (field qualification, polling, result
9
9
  * mapping) stays in the SDK's apiTransport.
10
10
  */
11
- import { createApiTransport, createHostExploreBaseTableResolver, } from './apiTransport';
11
+ import { createApiTransport, createHostExploreBaseTableResolver, createHostSpaceChartsResolver, SpaceChartsUnsupportedError, } from './apiTransport';
12
12
  import { AppStateError } from './appState';
13
13
  import { mountDeliveryRender } from './deliveryRender';
14
- import { mountHostContext } from './hostContext';
14
+ import { mountHostContext, waitForHostContext } from './hostContext';
15
15
  import { announceSdkManifest } from './manifest';
16
16
  import { mountRuntimeErrorObserver } from './runtimeErrors';
17
17
  const DEFAULT_TIMEOUT_MS = 120000;
@@ -280,6 +280,17 @@ export function createPostMessageTransport(config) {
280
280
  });
281
281
  const httpTransport = createApiTransport({ apiKey: '', baseUrl: '', projectUuid: config.projectUuid }, adapter, {
282
282
  resolveExploreBaseTable: createHostExploreBaseTableResolver(adapter),
283
+ // Negotiated on the ready handshake: only hosts that announce
284
+ // `spaceCharts: true` answer the virtual route. On an older host
285
+ // the typed rejection makes `useSpaceCharts` report
286
+ // `available: false` instead of surfacing a blocked-route error.
287
+ listSpaceCharts: async (space) => {
288
+ const host = await waitForHostContext(READY_TIMEOUT_MS);
289
+ if (!host.spaceCharts) {
290
+ throw new SpaceChartsUnsupportedError();
291
+ }
292
+ return createHostSpaceChartsResolver(adapter)(space);
293
+ },
283
294
  });
284
295
  const externalFetch = createPostMessageExternalFetch({
285
296
  targetWindow: config.targetWindow,
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `useSpaceCharts('<space slug or uuid>')` — the saved charts the viewer can
3
+ * see in a space, resolved at runtime with the viewer's own credentials. The
4
+ * list reflects existing space permissions: a space the viewer can't access
5
+ * answers exactly like one that doesn't exist (`space: null`, no charts).
6
+ *
7
+ * Feed an entry's `uuid` into `savedChart(...)` to render it.
8
+ */
9
+ import { type SpaceChartSummary } from './types';
10
+ export type SpaceCharts = {
11
+ /** False on a transport or host without the capability. On an older
12
+ * host this starts true and settles to false after the first lookup. */
13
+ available: boolean;
14
+ /** Null while loading, on error, or when the space isn't accessible. */
15
+ space: {
16
+ uuid: string;
17
+ name: string;
18
+ slug: string;
19
+ } | null;
20
+ charts: SpaceChartSummary[];
21
+ loading: boolean;
22
+ error: Error | null;
23
+ /** Re-fetch the list, e.g. after the viewer saved a chart elsewhere. */
24
+ refresh: () => void;
25
+ };
26
+ export declare function useSpaceCharts(space: string): SpaceCharts;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `useSpaceCharts('<space slug or uuid>')` — the saved charts the viewer can
3
+ * see in a space, resolved at runtime with the viewer's own credentials. The
4
+ * list reflects existing space permissions: a space the viewer can't access
5
+ * answers exactly like one that doesn't exist (`space: null`, no charts).
6
+ *
7
+ * Feed an entry's `uuid` into `savedChart(...)` to render it.
8
+ */
9
+ import { useCallback, useEffect, useRef, useState } from 'react';
10
+ import { useTransport } from './LightdashProvider';
11
+ const EMPTY_RESULT = { space: null, charts: [] };
12
+ // Instanceof is unreliable across the bundle boundary; match by name.
13
+ const isUnsupportedError = (e) => e instanceof Error &&
14
+ (e.name === 'SpaceChartsUnsupportedError' ||
15
+ // Older mediators reject the unknown route instead of negotiating:
16
+ // the bridge with `Blocked: POST /__sdk/space-charts`, the CLI
17
+ // preview proxy with "... is not a data-app SDK route ...".
18
+ /\/__sdk\/space-charts/.test(e.message));
19
+ export function useSpaceCharts(space) {
20
+ const transport = useTransport();
21
+ const listSpaceCharts = transport.listSpaceCharts;
22
+ // Hosts that predate the capability reject the first lookup with a
23
+ // typed/blocked error; that settles availability to false for good.
24
+ const [hostUnsupported, setHostUnsupported] = useState(false);
25
+ const available = listSpaceCharts !== undefined && !hostUnsupported;
26
+ const [result, setResult] = useState(EMPTY_RESULT);
27
+ const [loading, setLoading] = useState(available);
28
+ const [error, setError] = useState(null);
29
+ const [fetchCount, setFetchCount] = useState(0);
30
+ // Drops responses from superseded fetches (space change, refresh, unmount).
31
+ const fetchSeq = useRef(0);
32
+ useEffect(() => {
33
+ if (!listSpaceCharts || hostUnsupported || space.trim().length === 0) {
34
+ setResult(EMPTY_RESULT);
35
+ setLoading(false);
36
+ setError(space.trim().length === 0 && available
37
+ ? new Error('useSpaceCharts: pass a space slug or uuid')
38
+ : null);
39
+ return undefined;
40
+ }
41
+ fetchSeq.current += 1;
42
+ const seq = fetchSeq.current;
43
+ setLoading(true);
44
+ setError(null);
45
+ listSpaceCharts(space.trim())
46
+ .then((value) => {
47
+ if (fetchSeq.current !== seq)
48
+ return;
49
+ setResult(value);
50
+ setLoading(false);
51
+ })
52
+ .catch((e) => {
53
+ if (fetchSeq.current !== seq)
54
+ return;
55
+ setResult(EMPTY_RESULT);
56
+ if (isUnsupportedError(e)) {
57
+ // Not an error state: the host simply doesn't have the
58
+ // feature. `available` flips to false and stays there.
59
+ setHostUnsupported(true);
60
+ setError(null);
61
+ }
62
+ else {
63
+ setError(e instanceof Error ? e : new Error(String(e)));
64
+ }
65
+ setLoading(false);
66
+ });
67
+ return () => {
68
+ // Invalidate in-flight responses for the unmount/re-run.
69
+ if (fetchSeq.current === seq)
70
+ fetchSeq.current += 1;
71
+ };
72
+ }, [listSpaceCharts, hostUnsupported, space, fetchCount, available]);
73
+ const refresh = useCallback(() => setFetchCount((n) => n + 1), []);
74
+ return {
75
+ available,
76
+ space: result.space,
77
+ charts: result.charts,
78
+ loading,
79
+ error,
80
+ refresh,
81
+ };
82
+ }
package/dist/types.d.ts CHANGED
@@ -299,6 +299,36 @@ export type ExploreBaseTableRequest = {
299
299
  export type ExploreBaseTableResult = {
300
300
  baseTable: string | null;
301
301
  };
302
+ /**
303
+ * Host-answered virtual route returning the saved charts in a space.
304
+ * Duplicated from `@lightdash/common` (`APP_SDK_SPACE_CHARTS_PATH`) — this
305
+ * package must not depend on common. The bridge and the CLI preview proxy
306
+ * answer it with the viewer's own credentials; the backend has no such route.
307
+ */
308
+ export declare const SPACE_CHARTS_PATH = "/__sdk/space-charts";
309
+ export type SpaceChartsRequest = {
310
+ space: string;
311
+ };
312
+ /** One saved chart in a space, as listed by `useSpaceCharts`. */
313
+ export type SpaceChartSummary = {
314
+ uuid: string;
315
+ name: string;
316
+ description: string | null;
317
+ /** e.g. `line`, `vertical_bar`, `table` — null on older charts. */
318
+ chartKind: string | null;
319
+ slug: string | null;
320
+ /** ISO timestamp of the chart's last save. */
321
+ updatedAt: string | null;
322
+ };
323
+ export type SpaceChartsResult = {
324
+ /** Null when the space doesn't exist or the viewer can't access it. */
325
+ space: {
326
+ uuid: string;
327
+ name: string;
328
+ slug: string;
329
+ } | null;
330
+ charts: SpaceChartSummary[];
331
+ };
302
332
  export type LightdashClientConfig = {
303
333
  /** Lightdash instance URL */
304
334
  baseUrl: string;
@@ -474,4 +504,10 @@ export type Transport = {
474
504
  * `useAiPrompt().available` is false when absent.
475
505
  */
476
506
  aiPrompt?: (request: AiPromptRequest) => Promise<AiPromptResult>;
507
+ /**
508
+ * List the saved charts the viewer can see in a space (by slug or uuid).
509
+ * Optional so custom transports predating the capability stay valid —
510
+ * `useSpaceCharts().available` is false when absent.
511
+ */
512
+ listSpaceCharts?: (space: string) => Promise<SpaceChartsResult>;
477
513
  };
package/dist/types.js CHANGED
@@ -41,3 +41,10 @@ export const VIZ_POINT_MENU_PATH = '/__sdk/viz/point-menu';
41
41
  * the backend has no such route.
42
42
  */
43
43
  export const EXPLORE_BASE_TABLE_PATH = '/__sdk/explore-base-table';
44
+ /**
45
+ * Host-answered virtual route returning the saved charts in a space.
46
+ * Duplicated from `@lightdash/common` (`APP_SDK_SPACE_CHARTS_PATH`) — this
47
+ * package must not depend on common. The bridge and the CLI preview proxy
48
+ * answer it with the viewer's own credentials; the backend has no such route.
49
+ */
50
+ export const SPACE_CHARTS_PATH = '/__sdk/space-charts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/query-sdk",
3
- "version": "3.96.1",
3
+ "version": "3.98.0",
4
4
  "private": false,
5
5
  "description": "SDK for building custom data apps against the Lightdash semantic layer",
6
6
  "license": "MIT",
@@ -26,7 +26,7 @@
26
26
  }
27
27
  },
28
28
  "devDependencies": {
29
- "@lightdash/common": "3.96.1",
29
+ "@lightdash/common": "3.98.0",
30
30
  "@types/node": "22.13.1",
31
31
  "@types/react": "19.3.0",
32
32
  "@types/react-dom": "19.3.0",