@malloy-publisher/sdk 0.0.76 → 0.0.78

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.
@@ -1,9 +1,13 @@
1
1
  /* eslint-disable react/prop-types */
2
- import type { MalloyRenderProps } from "@malloydata/render";
3
- import { MalloyRenderer } from "@malloydata/render";
4
- import React, { useEffect, useRef, useState, useLayoutEffect } from "react";
2
+ import React, {
3
+ useEffect,
4
+ useRef,
5
+ useState,
6
+ useLayoutEffect,
7
+ Suspense,
8
+ } from "react";
5
9
 
6
- type MalloyRenderElement = HTMLElement & MalloyRenderProps;
10
+ type MalloyRenderElement = HTMLElement & Record<string, unknown>;
7
11
 
8
12
  declare global {
9
13
  // eslint-disable-next-line @typescript-eslint/no-namespace
@@ -22,23 +26,25 @@ interface RenderedResultProps {
22
26
  height?: number;
23
27
  isFillElement?: (boolean) => void;
24
28
  onSizeChange?: (height: number) => void;
25
- onDrill?: (element: any) => void;
29
+ onDrill?: (element: unknown) => void;
26
30
  }
27
31
 
28
- // RendererResult does some magic to make it work for both fill and non-fill elements.
29
- // Generally text results and complicated results are non-fill. That is, they have a natural, fixed height.
30
- // Simple charts are fill. That is, they scale to fill the available space.
31
- // We only know what kind of element we're dealing with after the viz is rendered.
32
- // So we use a callback to notify the parent that we're in a fill element.
33
- // The parent can then set the height for the element, otherwise it will have size 0.
34
- //
35
- // In order to make this work when contained in a "Suspend" component, we need to
36
- // make sure that the rendering process is started after the DOM element is available.
37
- // We do this by using a useLayoutEffect to start the rendering process.
38
- // We also need to make sure that the rendering process is started before the
39
- // Suspense component is rendered.
40
- // We do this by using a useEffect to start the rendering process.
41
- export default function RenderedResult({
32
+ // Simple dynamic import function
33
+ const createRenderer = async (onDrill?: (element: unknown) => void) => {
34
+ // Only import when we're in a browser environment
35
+ if (typeof window === "undefined") {
36
+ throw new Error("MalloyRenderer can only be used in browser environment");
37
+ }
38
+
39
+ const { MalloyRenderer } = await import("@malloydata/render");
40
+ const renderer = new MalloyRenderer({
41
+ onClick: onDrill,
42
+ });
43
+ return renderer.createViz();
44
+ };
45
+
46
+ // Inner component that actually renders the visualization
47
+ function RenderedResultInner({
42
48
  result,
43
49
  height,
44
50
  isFillElement,
@@ -47,66 +53,47 @@ export default function RenderedResult({
47
53
  }: RenderedResultProps) {
48
54
  const ref = useRef<HTMLDivElement>(null);
49
55
  const [isRendered, setIsRendered] = useState(false);
50
- const [renderingStarted, setRenderingStarted] = useState(false);
51
- const [wasMeasured, setWasMeasured] = useState(false);
52
- // Each component instance manages its own promise and resolver
53
- const renderingPromiseRef = useRef<Promise<void> | null>(null);
54
- const renderingResolverRef = useRef<(() => void) | null>(null);
55
56
 
56
- // Start rendering process after DOM element is available
57
+ // Render the visualization once the component mounts
57
58
  useLayoutEffect(() => {
58
- if (ref.current && result && !renderingStarted) {
59
- setRenderingStarted(true);
59
+ if (!ref.current || !result) return;
60
60
 
61
- // Create the promise now that we're ready to start rendering
62
- if (!renderingPromiseRef.current) {
63
- renderingPromiseRef.current = new Promise<void>((resolve) => {
64
- renderingResolverRef.current = resolve;
65
- });
66
- }
67
-
68
- const renderer = new MalloyRenderer({
69
- onClick: onDrill,
70
- });
71
- const viz = renderer.createViz();
61
+ let isMounted = true;
62
+ const element = ref.current;
72
63
 
73
- // Remove all content from ref.current before rendering new viz
74
- while (ref.current.firstChild) {
75
- ref.current.removeChild(ref.current.firstChild);
76
- }
64
+ // Clear previous content
65
+ while (element.firstChild) {
66
+ element.removeChild(element.firstChild);
67
+ }
77
68
 
78
- // Set up a mutation observer to detect when content is added
79
- const observer = new MutationObserver((mutations) => {
80
- for (const mutation of mutations) {
81
- if (
82
- mutation.type === "childList" &&
83
- mutation.addedNodes.length > 0
84
- ) {
85
- // Check if actual content (not just empty elements) was added
86
- const hasContent = Array.from(mutation.addedNodes).some(
87
- (node) => {
88
- return node.nodeType === Node.ELEMENT_NODE;
89
- },
90
- );
91
- if (hasContent) {
92
- // Content detected, mark as rendered
93
- observer.disconnect();
94
- setTimeout(() => {
95
- setIsRendered(true);
96
- if (renderingResolverRef.current) {
97
- renderingResolverRef.current();
98
- renderingResolverRef.current = null;
99
- renderingPromiseRef.current = null;
100
- }
101
- }, 50); // Small delay to ensure content is fully rendered
102
- break;
69
+ createRenderer(onDrill)
70
+ .then((viz) => {
71
+ if (!isMounted) return;
72
+
73
+ // Set up a mutation observer to detect when content is added
74
+ const observer = new MutationObserver((mutations) => {
75
+ for (const mutation of mutations) {
76
+ if (
77
+ mutation.type === "childList" &&
78
+ mutation.addedNodes.length > 0
79
+ ) {
80
+ const hasContent = Array.from(mutation.addedNodes).some(
81
+ (node) => node.nodeType === Node.ELEMENT_NODE,
82
+ );
83
+ if (hasContent) {
84
+ observer.disconnect();
85
+ setTimeout(() => {
86
+ if (isMounted) {
87
+ setIsRendered(true);
88
+ }
89
+ }, 50);
90
+ break;
91
+ }
103
92
  }
104
93
  }
105
- }
106
- });
94
+ });
107
95
 
108
- if (ref.current) {
109
- observer.observe(ref.current, {
96
+ observer.observe(element, {
110
97
  childList: true,
111
98
  subtree: true,
112
99
  characterData: true,
@@ -114,33 +101,27 @@ export default function RenderedResult({
114
101
 
115
102
  try {
116
103
  viz.setResult(JSON.parse(result));
117
- viz.render(ref.current);
104
+ viz.render(element);
118
105
  } catch (error) {
119
106
  console.error("Error rendering visualization:", error);
120
107
  observer.disconnect();
121
- setIsRendered(true);
122
- if (renderingResolverRef.current) {
123
- renderingResolverRef.current();
124
- renderingResolverRef.current = null;
125
- renderingPromiseRef.current = null;
108
+ if (isMounted) {
109
+ setIsRendered(true);
126
110
  }
127
111
  }
128
- }
129
- }
130
- }, [result, onDrill, renderingStarted]);
112
+ })
113
+ .catch((error) => {
114
+ console.error("Failed to create renderer:", error);
115
+ if (isMounted) {
116
+ setIsRendered(true);
117
+ }
118
+ });
119
+
120
+ return () => {
121
+ isMounted = false;
122
+ };
123
+ }, [result, onDrill]);
131
124
 
132
- // Reset rendering state when result changes
133
- useEffect(() => {
134
- setIsRendered(false);
135
- setRenderingStarted(false);
136
- renderingPromiseRef.current = null;
137
- renderingResolverRef.current = null;
138
- }, [result]);
139
-
140
- // If rendering has started but not completed, throw the promise to trigger Suspense
141
- if (renderingStarted && !isRendered && renderingPromiseRef.current) {
142
- throw renderingPromiseRef.current;
143
- }
144
125
  // Set up size measurement using scrollHeight instead of ResizeObserver
145
126
  useEffect(() => {
146
127
  if (!ref.current || !isRendered) return;
@@ -151,7 +132,9 @@ export default function RenderedResult({
151
132
  if (element) {
152
133
  const measuredHeight = element.offsetHeight;
153
134
  if (measuredHeight > 0) {
154
- onSizeChange && onSizeChange(measuredHeight);
135
+ if (onSizeChange) {
136
+ onSizeChange(measuredHeight);
137
+ }
155
138
  } else if (isFillElement && element.firstChild) {
156
139
  // HACK- we If there's a child and it's height is 0, then we're in a fill element
157
140
  // We use the callback `isFillElement` to notify the parent that we're in a fill element
@@ -172,14 +155,12 @@ export default function RenderedResult({
172
155
 
173
156
  let observer: MutationObserver | null = null;
174
157
  // Also measure when the malloy result changes
175
- if (!wasMeasured) {
176
- observer = new MutationObserver(measureSize);
177
- observer.observe(element, {
178
- childList: true,
179
- subtree: true,
180
- attributes: true,
181
- });
182
- }
158
+ observer = new MutationObserver(measureSize);
159
+ observer.observe(element, {
160
+ childList: true,
161
+ subtree: true,
162
+ attributes: true,
163
+ });
183
164
 
184
165
  // Cleanup
185
166
  return () => {
@@ -198,3 +179,45 @@ export default function RenderedResult({
198
179
  />
199
180
  );
200
181
  }
182
+
183
+ // Main component with error boundary and fallback
184
+ export default function RenderedResult(props: RenderedResultProps) {
185
+ // Show loading state if we're in server-side rendering
186
+ if (typeof window === "undefined") {
187
+ return (
188
+ <div
189
+ style={{
190
+ width: "100%",
191
+ height: props.height ? `${props.height}px` : "100%",
192
+ display: "flex",
193
+ alignItems: "center",
194
+ justifyContent: "center",
195
+ color: "#666",
196
+ }}
197
+ >
198
+ Loading...
199
+ </div>
200
+ );
201
+ }
202
+
203
+ return (
204
+ <Suspense
205
+ fallback={
206
+ <div
207
+ style={{
208
+ width: "100%",
209
+ height: props.height ? `${props.height}px` : "100%",
210
+ display: "flex",
211
+ alignItems: "center",
212
+ justifyContent: "center",
213
+ color: "#666",
214
+ }}
215
+ >
216
+ Loading visualization...
217
+ </div>
218
+ }
219
+ >
220
+ <RenderedResultInner {...props} />
221
+ </Suspense>
222
+ );
223
+ }
@@ -1 +0,0 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const M=require("react/jsx-runtime"),S=require("@malloydata/render"),t=require("react");function C({result:i,height:b,isFillElement:f,onSizeChange:l,onDrill:m}){const e=t.useRef(null),[a,h]=t.useState(!1),[R,v]=t.useState(!1),[y,N]=t.useState(!1),s=t.useRef(null),n=t.useRef(null);if(t.useLayoutEffect(()=>{if(e.current&&i&&!R){v(!0),s.current||(s.current=new Promise(r=>{n.current=r}));const o=new S.MalloyRenderer({onClick:m}).createViz();for(;e.current.firstChild;)e.current.removeChild(e.current.firstChild);const d=new MutationObserver(r=>{for(const c of r)if(c.type==="childList"&&c.addedNodes.length>0&&Array.from(c.addedNodes).some(g=>g.nodeType===Node.ELEMENT_NODE)){d.disconnect(),setTimeout(()=>{h(!0),n.current&&(n.current(),n.current=null,s.current=null)},50);break}});if(e.current){d.observe(e.current,{childList:!0,subtree:!0,characterData:!0});try{o.setResult(JSON.parse(i)),o.render(e.current)}catch(r){console.error("Error rendering visualization:",r),d.disconnect(),h(!0),n.current&&(n.current(),n.current=null,s.current=null)}}}},[i,m,R]),t.useEffect(()=>{h(!1),v(!1),s.current=null,n.current=null},[i]),R&&!a&&s.current)throw s.current;return t.useEffect(()=>{if(!e.current||!a)return;const u=e.current,o=()=>{if(u){const c=u.offsetHeight;c>0?l&&l(c):f&&u.firstChild&&(u.firstChild.offsetHeight==0?f(!0):f(!1))}},d=setTimeout(o,100);let r=null;return y||(r=new MutationObserver(o),r.observe(u,{childList:!0,subtree:!0,attributes:!0})),()=>{clearTimeout(d),r?.disconnect()}},[l,i,f,a]),M.jsx("div",{ref:e,style:{width:"100%",height:b?`${b}px`:"100%"}})}exports.default=C;
@@ -1,79 +0,0 @@
1
- import { jsx as M } from "react/jsx-runtime";
2
- import { MalloyRenderer as N } from "@malloydata/render";
3
- import { useRef as m, useState as v, useLayoutEffect as H, useEffect as w } from "react";
4
- function x({
5
- result: c,
6
- height: R,
7
- isFillElement: d,
8
- onSizeChange: f,
9
- onDrill: b
10
- }) {
11
- const e = m(null), [l, a] = v(!1), [h, g] = v(!1), [y, L] = v(!1), n = m(null), t = m(null);
12
- if (H(() => {
13
- if (e.current && c && !h) {
14
- g(!0), n.current || (n.current = new Promise((r) => {
15
- t.current = r;
16
- }));
17
- const i = new N({
18
- onClick: b
19
- }).createViz();
20
- for (; e.current.firstChild; )
21
- e.current.removeChild(e.current.firstChild);
22
- const o = new MutationObserver((r) => {
23
- for (const u of r)
24
- if (u.type === "childList" && u.addedNodes.length > 0 && Array.from(u.addedNodes).some(
25
- (p) => p.nodeType === Node.ELEMENT_NODE
26
- )) {
27
- o.disconnect(), setTimeout(() => {
28
- a(!0), t.current && (t.current(), t.current = null, n.current = null);
29
- }, 50);
30
- break;
31
- }
32
- });
33
- if (e.current) {
34
- o.observe(e.current, {
35
- childList: !0,
36
- subtree: !0,
37
- characterData: !0
38
- });
39
- try {
40
- i.setResult(JSON.parse(c)), i.render(e.current);
41
- } catch (r) {
42
- console.error("Error rendering visualization:", r), o.disconnect(), a(!0), t.current && (t.current(), t.current = null, n.current = null);
43
- }
44
- }
45
- }
46
- }, [c, b, h]), w(() => {
47
- a(!1), g(!1), n.current = null, t.current = null;
48
- }, [c]), h && !l && n.current)
49
- throw n.current;
50
- return w(() => {
51
- if (!e.current || !l) return;
52
- const s = e.current, i = () => {
53
- if (s) {
54
- const u = s.offsetHeight;
55
- u > 0 ? f && f(u) : d && s.firstChild && (s.firstChild.offsetHeight == 0 ? d(!0) : d(!1));
56
- }
57
- }, o = setTimeout(i, 100);
58
- let r = null;
59
- return y || (r = new MutationObserver(i), r.observe(s, {
60
- childList: !0,
61
- subtree: !0,
62
- attributes: !0
63
- })), () => {
64
- clearTimeout(o), r?.disconnect();
65
- };
66
- }, [f, c, d, l]), /* @__PURE__ */ M(
67
- "div",
68
- {
69
- ref: e,
70
- style: {
71
- width: "100%",
72
- height: R ? `${R}px` : "100%"
73
- }
74
- }
75
- );
76
- }
77
- export {
78
- x as default
79
- };