@lightdash/query-sdk 0.2853.1 → 0.2855.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.
- package/dist/client.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/inspector.d.ts +33 -0
- package/dist/inspector.js +220 -0
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* const lightdash = createClient() // auto-detects from environment
|
|
6
6
|
*/
|
|
7
7
|
import { createApiTransport } from './apiTransport';
|
|
8
|
+
import { mountInspector } from './inspector';
|
|
8
9
|
import { createPostMessageTransport } from './postMessageTransport';
|
|
9
10
|
import { QueryBuilder } from './query';
|
|
10
11
|
export class LightdashClient {
|
|
@@ -60,6 +61,7 @@ export function createClient() {
|
|
|
60
61
|
const params = new URLSearchParams(window.location.hash.replace(/^#/, ''));
|
|
61
62
|
if (params.get('transport') === 'postMessage') {
|
|
62
63
|
const projectUuid = params.get('projectUuid') ?? '';
|
|
64
|
+
mountInspector(window.parent);
|
|
63
65
|
return new LightdashClient({ apiKey: '', baseUrl: '', projectUuid }, createPostMessageTransport({ targetWindow: window.parent, projectUuid }));
|
|
64
66
|
}
|
|
65
67
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export { createApiTransport, type FetchAdapter } from './apiTransport';
|
|
|
7
7
|
export { createPostMessageTransport } from './postMessageTransport';
|
|
8
8
|
export type { AdditionalMetric, Column, CustomDimension, Filter, FilterOperator, FilterValue, FormatFunction, LightdashClientConfig, LightdashUser, MetricType, QueryDefinition, QueryResult, Row, Sort, TableCalculation, Transport, UnitOfTime, } from './types';
|
|
9
9
|
export type { SdkFetchRequest, SdkFetchResponse, SdkReadyMessage, } from './postMessageTransport';
|
|
10
|
+
export type { InspectAvailableMessage, InspectSelectedMessage, } from './inspector';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element inspector ("click-to-edit") runtime that lives inside the sandboxed
|
|
3
|
+
* iframe. The Lightdash parent window enables it via postMessage; on click,
|
|
4
|
+
* this module captures a human-readable label for the clicked element and
|
|
5
|
+
* posts it back. The parent inserts that label as a pill at the prompt
|
|
6
|
+
* editor cursor so the user can compose targeted edits like:
|
|
7
|
+
*
|
|
8
|
+
* [button "Total Revenue" @src/Toolbar.tsx:42] make this blue
|
|
9
|
+
* [div "$2.4M" @src/Dashboard.tsx:88] rename to Net Revenue
|
|
10
|
+
*
|
|
11
|
+
* Claude opens the file at `<path>:<line>` directly. When the loc is missing
|
|
12
|
+
* (DOM node injected outside JSX, or pre-transform build), it falls back to
|
|
13
|
+
* grepping `/app/src/` for the quoted text.
|
|
14
|
+
*/
|
|
15
|
+
export type InspectSelectedMessage = {
|
|
16
|
+
type: 'lightdash:inspect:selected';
|
|
17
|
+
label: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Announced by the iframe SDK on mount so the parent can detect that the
|
|
21
|
+
* inspector is wired up. Older SDKs (loaded by resumed sandboxes) never
|
|
22
|
+
* send this, so the parent leaves the Inspect button hidden for them.
|
|
23
|
+
*/
|
|
24
|
+
export type InspectAvailableMessage = {
|
|
25
|
+
type: 'lightdash:inspect:available';
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Mounts the inspector. Listens for `lightdash:inspect:enable` /
|
|
29
|
+
* `lightdash:inspect:disable` from the parent window and posts back a
|
|
30
|
+
* `lightdash:inspect:selected` event when the user clicks an element while
|
|
31
|
+
* inspect mode is active. Idempotent — safe to call multiple times.
|
|
32
|
+
*/
|
|
33
|
+
export declare function mountInspector(parent: Window): void;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element inspector ("click-to-edit") runtime that lives inside the sandboxed
|
|
3
|
+
* iframe. The Lightdash parent window enables it via postMessage; on click,
|
|
4
|
+
* this module captures a human-readable label for the clicked element and
|
|
5
|
+
* posts it back. The parent inserts that label as a pill at the prompt
|
|
6
|
+
* editor cursor so the user can compose targeted edits like:
|
|
7
|
+
*
|
|
8
|
+
* [button "Total Revenue" @src/Toolbar.tsx:42] make this blue
|
|
9
|
+
* [div "$2.4M" @src/Dashboard.tsx:88] rename to Net Revenue
|
|
10
|
+
*
|
|
11
|
+
* Claude opens the file at `<path>:<line>` directly. When the loc is missing
|
|
12
|
+
* (DOM node injected outside JSX, or pre-transform build), it falls back to
|
|
13
|
+
* grepping `/app/src/` for the quoted text.
|
|
14
|
+
*/
|
|
15
|
+
const OVERLAY_ID = 'lightdash-inspector-overlay';
|
|
16
|
+
const LABEL_ID = 'lightdash-inspector-label';
|
|
17
|
+
const HIGHLIGHT_COLOR = '#7c3aed';
|
|
18
|
+
const MAX_TEXT_LEN = 40;
|
|
19
|
+
const ANCESTOR_TEXT_LOOKBACK = 3;
|
|
20
|
+
function isOurOwnElement(el) {
|
|
21
|
+
if (!el)
|
|
22
|
+
return false;
|
|
23
|
+
return el.id === OVERLAY_ID || el.id === LABEL_ID;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Builds a label like `[tag "text" @src/path:line]` for a clicked element.
|
|
27
|
+
*
|
|
28
|
+
* The `@<path>:<line>` suffix is the primary key — it comes from a build-
|
|
29
|
+
* time JSX transform (see sandbox `vite.config.js`) that stamps every
|
|
30
|
+
* element with `data-loc="<rel-path>:<line>"`. With prop spreading, the
|
|
31
|
+
* caller's call site wins over any nested component's own loc, so the
|
|
32
|
+
* value reflects the user-facing source. The visible text and tag stay in
|
|
33
|
+
* the label so the user can confirm they clicked what they meant to click.
|
|
34
|
+
*
|
|
35
|
+
* Falls back to tag-only for elements without text — class names from
|
|
36
|
+
* shadcn/Tailwind are utility-heavy and not grep-useful, so we don't
|
|
37
|
+
* bother emitting them.
|
|
38
|
+
*/
|
|
39
|
+
function buildLabel(el) {
|
|
40
|
+
const tag = el.tagName.toLowerCase();
|
|
41
|
+
let text = '';
|
|
42
|
+
let cur = el;
|
|
43
|
+
for (let i = 0; i < ANCESTOR_TEXT_LOOKBACK && cur; i += 1) {
|
|
44
|
+
const raw = (cur.textContent ?? '').replace(/\s+/g, ' ').trim();
|
|
45
|
+
if (raw) {
|
|
46
|
+
text = raw.length > MAX_TEXT_LEN
|
|
47
|
+
? `${raw.slice(0, MAX_TEXT_LEN)}…`
|
|
48
|
+
: raw;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
cur = cur.parentElement;
|
|
52
|
+
}
|
|
53
|
+
// Walk up to the closest ancestor that carries a build-time source loc.
|
|
54
|
+
// For elements whose call site lives in user code, this is set on the
|
|
55
|
+
// element itself; for icon SVGs and other library-rendered children it
|
|
56
|
+
// lives on a wrapping ancestor.
|
|
57
|
+
const locEl = el.closest('[data-loc]');
|
|
58
|
+
const loc = locEl?.getAttribute('data-loc') ?? '';
|
|
59
|
+
// Inner double quotes would break the bracketed format — replace with
|
|
60
|
+
// single quotes so the label is always well-formed and grep-friendly.
|
|
61
|
+
const safe = text ? text.replace(/"/g, "'") : '';
|
|
62
|
+
const head = safe ? `${tag} "${safe}"` : tag;
|
|
63
|
+
return loc ? `[${head} @${loc}]` : `[${head}]`;
|
|
64
|
+
}
|
|
65
|
+
let overlayBox = null;
|
|
66
|
+
let overlayLabel = null;
|
|
67
|
+
function ensureOverlay() {
|
|
68
|
+
if (overlayBox && overlayLabel) {
|
|
69
|
+
return { box: overlayBox, label: overlayLabel };
|
|
70
|
+
}
|
|
71
|
+
const box = document.createElement('div');
|
|
72
|
+
box.id = OVERLAY_ID;
|
|
73
|
+
Object.assign(box.style, {
|
|
74
|
+
position: 'fixed',
|
|
75
|
+
pointerEvents: 'none',
|
|
76
|
+
zIndex: '2147483647',
|
|
77
|
+
border: `2px solid ${HIGHLIGHT_COLOR}`,
|
|
78
|
+
borderRadius: '4px',
|
|
79
|
+
background: 'rgba(124, 58, 237, 0.12)',
|
|
80
|
+
boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0)',
|
|
81
|
+
display: 'none',
|
|
82
|
+
});
|
|
83
|
+
const label = document.createElement('div');
|
|
84
|
+
label.id = LABEL_ID;
|
|
85
|
+
Object.assign(label.style, {
|
|
86
|
+
position: 'fixed',
|
|
87
|
+
pointerEvents: 'none',
|
|
88
|
+
zIndex: '2147483647',
|
|
89
|
+
background: HIGHLIGHT_COLOR,
|
|
90
|
+
color: 'white',
|
|
91
|
+
font: '11px ui-sans-serif, system-ui, sans-serif',
|
|
92
|
+
padding: '2px 6px',
|
|
93
|
+
borderRadius: '3px',
|
|
94
|
+
whiteSpace: 'nowrap',
|
|
95
|
+
maxWidth: '320px',
|
|
96
|
+
overflow: 'hidden',
|
|
97
|
+
textOverflow: 'ellipsis',
|
|
98
|
+
display: 'none',
|
|
99
|
+
});
|
|
100
|
+
document.body.appendChild(box);
|
|
101
|
+
document.body.appendChild(label);
|
|
102
|
+
overlayBox = box;
|
|
103
|
+
overlayLabel = label;
|
|
104
|
+
return { box, label };
|
|
105
|
+
}
|
|
106
|
+
function hideOverlay() {
|
|
107
|
+
if (overlayBox)
|
|
108
|
+
overlayBox.style.display = 'none';
|
|
109
|
+
if (overlayLabel)
|
|
110
|
+
overlayLabel.style.display = 'none';
|
|
111
|
+
}
|
|
112
|
+
function positionOverlay(el, labelText) {
|
|
113
|
+
const rect = el.getBoundingClientRect();
|
|
114
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
115
|
+
hideOverlay();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const { box, label } = ensureOverlay();
|
|
119
|
+
box.style.display = 'block';
|
|
120
|
+
box.style.top = `${rect.top}px`;
|
|
121
|
+
box.style.left = `${rect.left}px`;
|
|
122
|
+
box.style.width = `${rect.width}px`;
|
|
123
|
+
box.style.height = `${rect.height}px`;
|
|
124
|
+
label.textContent = labelText;
|
|
125
|
+
label.style.display = 'block';
|
|
126
|
+
label.style.top = `${Math.max(0, rect.top - 22)}px`;
|
|
127
|
+
label.style.left = `${rect.left}px`;
|
|
128
|
+
}
|
|
129
|
+
let enabled = false;
|
|
130
|
+
let currentTarget = null;
|
|
131
|
+
let parentWindow = null;
|
|
132
|
+
let listenersAttached = false;
|
|
133
|
+
let messageListenerAttached = false;
|
|
134
|
+
function onPointerMove(e) {
|
|
135
|
+
if (!enabled)
|
|
136
|
+
return;
|
|
137
|
+
const target = e.target;
|
|
138
|
+
if (!target || isOurOwnElement(target))
|
|
139
|
+
return;
|
|
140
|
+
if (target === currentTarget)
|
|
141
|
+
return;
|
|
142
|
+
currentTarget = target;
|
|
143
|
+
positionOverlay(target, buildLabel(target));
|
|
144
|
+
}
|
|
145
|
+
function onClick(e) {
|
|
146
|
+
if (!enabled)
|
|
147
|
+
return;
|
|
148
|
+
const target = e.target;
|
|
149
|
+
if (!target || isOurOwnElement(target))
|
|
150
|
+
return;
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
e.stopPropagation();
|
|
153
|
+
if (typeof e.stopImmediatePropagation === 'function') {
|
|
154
|
+
e.stopImmediatePropagation();
|
|
155
|
+
}
|
|
156
|
+
const label = buildLabel(target);
|
|
157
|
+
parentWindow?.postMessage({
|
|
158
|
+
type: 'lightdash:inspect:selected',
|
|
159
|
+
label,
|
|
160
|
+
}, '*');
|
|
161
|
+
}
|
|
162
|
+
function onScrollOrResize() {
|
|
163
|
+
if (enabled && currentTarget) {
|
|
164
|
+
positionOverlay(currentTarget, buildLabel(currentTarget));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function setEnabled(next) {
|
|
168
|
+
if (enabled === next)
|
|
169
|
+
return;
|
|
170
|
+
enabled = next;
|
|
171
|
+
if (enabled) {
|
|
172
|
+
document.documentElement.style.cursor = 'crosshair';
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
document.documentElement.style.cursor = '';
|
|
176
|
+
currentTarget = null;
|
|
177
|
+
hideOverlay();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function attachListeners() {
|
|
181
|
+
if (listenersAttached)
|
|
182
|
+
return;
|
|
183
|
+
listenersAttached = true;
|
|
184
|
+
// Capture phase — we need to see events before the app's own handlers.
|
|
185
|
+
window.addEventListener('pointermove', onPointerMove, true);
|
|
186
|
+
window.addEventListener('click', onClick, true);
|
|
187
|
+
window.addEventListener('scroll', onScrollOrResize, true);
|
|
188
|
+
window.addEventListener('resize', onScrollOrResize, true);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Mounts the inspector. Listens for `lightdash:inspect:enable` /
|
|
192
|
+
* `lightdash:inspect:disable` from the parent window and posts back a
|
|
193
|
+
* `lightdash:inspect:selected` event when the user clicks an element while
|
|
194
|
+
* inspect mode is active. Idempotent — safe to call multiple times.
|
|
195
|
+
*/
|
|
196
|
+
export function mountInspector(parent) {
|
|
197
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
parentWindow = parent;
|
|
201
|
+
attachListeners();
|
|
202
|
+
// Announce capability so the parent can show the Inspect button. Older
|
|
203
|
+
// SDKs running in resumed sandboxes don't send this, leaving the button
|
|
204
|
+
// hidden — they keep working as before.
|
|
205
|
+
parent.postMessage({ type: 'lightdash:inspect:available' }, '*');
|
|
206
|
+
if (messageListenerAttached)
|
|
207
|
+
return;
|
|
208
|
+
messageListenerAttached = true;
|
|
209
|
+
window.addEventListener('message', (event) => {
|
|
210
|
+
const data = event.data;
|
|
211
|
+
if (!data || typeof data.type !== 'string')
|
|
212
|
+
return;
|
|
213
|
+
if (data.type === 'lightdash:inspect:enable') {
|
|
214
|
+
setEnabled(true);
|
|
215
|
+
}
|
|
216
|
+
else if (data.type === 'lightdash:inspect:disable') {
|
|
217
|
+
setEnabled(false);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|