@malloy-publisher/sdk 0.0.37 → 0.0.38

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.
Files changed (31) hide show
  1. package/dist/RenderedResult-D0NN1YlV.js +79 -0
  2. package/dist/RenderedResult-JN8rBUlE.cjs +1 -0
  3. package/dist/client/api.d.ts +2 -2
  4. package/dist/components/ApiErrorDisplay.d.ts +1 -1
  5. package/dist/components/Loading.d.ts +37 -0
  6. package/dist/components/index.d.ts +1 -0
  7. package/dist/{index-7mmEVMId.cjs → index-BH85bTLt.cjs} +517 -517
  8. package/dist/{index-DdtGkJIk.js → index-fYVnpzww.js} +23021 -22978
  9. package/dist/index.cjs.js +1 -1
  10. package/dist/index.es.js +17 -16
  11. package/dist/{vendor-C6w9JpPA.js → vendor-BCM56_2K.js} +60 -59
  12. package/dist/{vendor-DAoHVNwq.cjs → vendor-Bg9-K32d.cjs} +66 -66
  13. package/package.json +1 -1
  14. package/src/components/ApiErrorDisplay.tsx +1 -1
  15. package/src/components/Home/Home.tsx +2 -3
  16. package/src/components/Loading.tsx +104 -0
  17. package/src/components/Model/Model.tsx +2 -6
  18. package/src/components/Notebook/Notebook.tsx +10 -4
  19. package/src/components/Package/Databases.tsx +2 -3
  20. package/src/components/Package/Models.tsx +6 -6
  21. package/src/components/Package/Notebooks.tsx +7 -5
  22. package/src/components/Package/Schedules.tsx +2 -5
  23. package/src/components/Project/About.tsx +2 -5
  24. package/src/components/Project/ConnectionExplorer.tsx +4 -15
  25. package/src/components/Project/Packages.tsx +2 -5
  26. package/src/components/QueryResult/QueryResult.tsx +2 -6
  27. package/src/components/RenderedResult/RenderedResult.tsx +107 -18
  28. package/src/components/RenderedResult/ResultContainer.tsx +5 -2
  29. package/src/components/index.ts +1 -0
  30. package/dist/RenderedResult-ChKXzRBq.cjs +0 -1
  31. package/dist/RenderedResult-DkEsDzXb.js +0 -51
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/sdk",
3
3
  "description": "Malloy Publisher SDK",
4
- "version": "0.0.37",
4
+ "version": "0.0.38",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",
7
7
  "module": "dist/index.es.js",
@@ -4,7 +4,7 @@ import React from "react";
4
4
  export interface ApiError extends Error {
5
5
  status?: number;
6
6
  data?: {
7
- code: string;
7
+ code: number;
8
8
  message: string;
9
9
  };
10
10
  }
@@ -1,10 +1,9 @@
1
1
  import { Grid, Typography } from "@mui/material";
2
2
  import { QueryClient, useQuery } from "@tanstack/react-query";
3
3
  import { ProjectsApi, Configuration } from "../../client";
4
- import axios from "axios";
5
4
  import { ApiErrorDisplay } from "../ApiErrorDisplay";
5
+ import { Loading } from "../Loading";
6
6
 
7
- axios.defaults.baseURL = "http://localhost:4000";
8
7
  const projectsApi = new ProjectsApi(new Configuration());
9
8
  const queryClient = new QueryClient();
10
9
 
@@ -63,6 +62,6 @@ export default function Home({ server, navigate }: HomeProps) {
63
62
  );
64
63
  }
65
64
  } else {
66
- return <Typography variant="h6">Loading projects...</Typography>;
65
+ return <Loading text="Loading projects..." />;
67
66
  }
68
67
  }
@@ -0,0 +1,104 @@
1
+ import React from "react";
2
+ import { Box, CircularProgress, Typography } from "@mui/material";
3
+
4
+ export interface LoadingProps {
5
+ /**
6
+ * The text to display below the spinner
7
+ */
8
+ text?: string;
9
+ /**
10
+ * The size of the CircularProgress component
11
+ * @default 40
12
+ */
13
+ size?: number | string;
14
+ /**
15
+ * The color of the CircularProgress component
16
+ * @default "primary"
17
+ */
18
+ color?:
19
+ | "primary"
20
+ | "secondary"
21
+ | "error"
22
+ | "info"
23
+ | "success"
24
+ | "warning"
25
+ | "inherit";
26
+ /**
27
+ * The thickness of the circular progress
28
+ * @default 3.6
29
+ */
30
+ thickness?: number;
31
+ /**
32
+ * Whether to center the component
33
+ * @default true
34
+ */
35
+ centered?: boolean;
36
+ /**
37
+ * Custom spacing between spinner and text
38
+ * @default 2
39
+ */
40
+ spacing?: number;
41
+ /**
42
+ * Typography variant for the text
43
+ * @default "body1"
44
+ */
45
+ textVariant?:
46
+ | "h1"
47
+ | "h2"
48
+ | "h3"
49
+ | "h4"
50
+ | "h5"
51
+ | "h6"
52
+ | "subtitle1"
53
+ | "subtitle2"
54
+ | "body1"
55
+ | "body2"
56
+ | "caption"
57
+ | "overline";
58
+ }
59
+
60
+ export function Loading({
61
+ text,
62
+ size = 40,
63
+ color = "primary",
64
+ thickness = 3.6,
65
+ centered = true,
66
+ spacing = 2,
67
+ textVariant = "body1",
68
+ }: LoadingProps) {
69
+ const content = (
70
+ <Box
71
+ sx={{
72
+ display: "flex",
73
+ flexDirection: "column",
74
+ alignItems: "center",
75
+ gap: spacing,
76
+ }}
77
+ >
78
+ <CircularProgress size={size} color={color} thickness={thickness} />
79
+ {text && (
80
+ <Typography variant={textVariant} color="text.secondary">
81
+ {text}
82
+ </Typography>
83
+ )}
84
+ </Box>
85
+ );
86
+
87
+ if (centered) {
88
+ return (
89
+ <Box
90
+ sx={{
91
+ display: "flex",
92
+ justifyContent: "center",
93
+ alignItems: "center",
94
+ minHeight: "200px",
95
+ width: "100%",
96
+ }}
97
+ >
98
+ {content}
99
+ </Box>
100
+ );
101
+ }
102
+
103
+ return content;
104
+ }
@@ -23,7 +23,7 @@ import { ApiErrorDisplay, ApiError } from "../ApiErrorDisplay";
23
23
  import "@malloydata/malloy-explorer/styles.css";
24
24
  import { usePackage } from "../Package/PackageProvider";
25
25
  import { SourceExplorerComponent } from "./SourcesExplorer";
26
- import NamedQueries from "./NamedQueries";
26
+ import { Loading } from "../Loading";
27
27
 
28
28
  const modelsApi = new ModelsApi(new Configuration());
29
29
 
@@ -118,11 +118,7 @@ export default function Model({
118
118
  );
119
119
 
120
120
  if (isLoading) {
121
- return (
122
- <Typography sx={{ p: "20px", m: "auto" }}>
123
- Fetching Model...
124
- </Typography>
125
- );
121
+ return <Loading text="Fetching Model..." />;
126
122
  }
127
123
 
128
124
  if (isError) {
@@ -18,6 +18,7 @@ import { StyledCard, StyledCardContent, StyledCardMedia } from "../styles";
18
18
  import { NotebookCell } from "./NotebookCell";
19
19
  import { ApiErrorDisplay, ApiError } from "../ApiErrorDisplay";
20
20
  import { AxiosError } from "axios";
21
+ import { Loading } from "../Loading";
21
22
 
22
23
  const notebooksApi = new NotebooksApi(new Configuration());
23
24
  const queryClient = new QueryClient();
@@ -191,9 +192,7 @@ export default function Notebook({
191
192
  <StyledCardMedia>
192
193
  <Stack spacing={1} component="section">
193
194
  {!isSuccess && !isError && (
194
- <Typography variant="body2" sx={{ p: "10px", m: "auto" }}>
195
- Fetching Notebook...
196
- </Typography>
195
+ <Loading text="Fetching Notebook..." />
197
196
  )}
198
197
  {isSuccess &&
199
198
  notebook.notebookCells?.map((cell, index) => (
@@ -214,7 +213,14 @@ export default function Notebook({
214
213
  key={index}
215
214
  />
216
215
  ))}
217
- {isError && (
216
+ {isError && error.data?.code === 404 && (
217
+ <Typography>
218
+ <code>{`${projectName} > ${packageName} > ${notebookPath}`}</code>{" "}
219
+ not found.
220
+ </Typography>
221
+ )}
222
+
223
+ {isError && error.data?.code !== 404 && (
218
224
  <ApiErrorDisplay
219
225
  error={error}
220
226
  context={`${projectName} > ${packageName} > ${notebookPath}`}
@@ -18,6 +18,7 @@ import { Configuration, Database, DatabasesApi } from "../../client";
18
18
  import { StyledCard, StyledCardContent } from "../styles";
19
19
  import { usePackage } from "./PackageProvider";
20
20
  import { ApiErrorDisplay } from "../ApiErrorDisplay";
21
+ import { Loading } from "../Loading";
21
22
 
22
23
  const databasesApi = new DatabasesApi(new Configuration());
23
24
  const queryClient = new QueryClient();
@@ -83,9 +84,7 @@ export default function Databases() {
83
84
  }}
84
85
  >
85
86
  {!isSuccess && !isError && (
86
- <Typography variant="body2" sx={{ p: "10px", m: "auto" }}>
87
- Fetching Databases...
88
- </Typography>
87
+ <Loading text="Fetching Databases..." />
89
88
  )}
90
89
  {isError && (
91
90
  <ApiErrorDisplay
@@ -6,6 +6,7 @@ import { StyledCard, StyledCardContent } from "../styles";
6
6
  import { FileTreeView } from "./FileTreeView";
7
7
  import { usePackage } from "./PackageProvider";
8
8
  import { ApiErrorDisplay } from "../ApiErrorDisplay";
9
+ import { Loading } from "../Loading";
9
10
 
10
11
  axios.defaults.baseURL = "http://localhost:4000";
11
12
  const modelsApi = new ModelsApi(new Configuration());
@@ -52,18 +53,14 @@ export default function Models({ navigate }: ModelsProps) {
52
53
  overflowY: "auto",
53
54
  }}
54
55
  >
55
- {!isSuccess && !isError && (
56
- <Typography variant="body2" sx={{ p: "10px", m: "auto" }}>
57
- Fetching Models...
58
- </Typography>
59
- )}
56
+ {!isSuccess && !isError && <Loading text="Fetching Models..." />}
60
57
  {isError && (
61
58
  <ApiErrorDisplay
62
59
  error={error}
63
60
  context={`${projectName} > ${packageName} > Models`}
64
61
  />
65
62
  )}
66
- {isSuccess && (
63
+ {isSuccess && data.data.length > 0 && (
67
64
  <FileTreeView
68
65
  items={data.data.sort((a, b) => {
69
66
  return a.path.localeCompare(b.path);
@@ -72,6 +69,9 @@ export default function Models({ navigate }: ModelsProps) {
72
69
  defaultExpandedItems={DEFAULT_EXPANDED_FOLDERS}
73
70
  />
74
71
  )}
72
+ {isSuccess && data.data.length === 0 && (
73
+ <Typography>No models found.</Typography>
74
+ )}
75
75
  </Box>
76
76
  </StyledCardContent>
77
77
  </StyledCard>
@@ -5,6 +5,7 @@ import { StyledCard, StyledCardContent } from "../styles";
5
5
  import { FileTreeView } from "./FileTreeView";
6
6
  import { usePackage } from "./PackageProvider";
7
7
  import { ApiErrorDisplay } from "../ApiErrorDisplay";
8
+ import { Loading } from "../Loading";
8
9
 
9
10
  const notebooksApi = new NotebooksApi(new Configuration());
10
11
  const queryClient = new QueryClient();
@@ -19,7 +20,7 @@ export default function Notebooks({ navigate }: NotebooksProps) {
19
20
  const { server, projectName, packageName, versionId, accessToken } =
20
21
  usePackage();
21
22
 
22
- const { data, isLoading, isError, error, isSuccess } = useQuery(
23
+ const { data, isError, error, isSuccess } = useQuery(
23
24
  {
24
25
  queryKey: ["notebooks", server, projectName, packageName, versionId],
25
26
  queryFn: () =>
@@ -51,9 +52,7 @@ export default function Notebooks({ navigate }: NotebooksProps) {
51
52
  }}
52
53
  >
53
54
  {!isSuccess && !isError && (
54
- <Typography variant="body2" sx={{ p: "10px", m: "auto" }}>
55
- Fetching Notebooks...
56
- </Typography>
55
+ <Loading text="Fetching Notebooks..." />
57
56
  )}
58
57
  {isError && (
59
58
  <ApiErrorDisplay
@@ -61,7 +60,7 @@ export default function Notebooks({ navigate }: NotebooksProps) {
61
60
  context={`${projectName} > ${packageName} > Notebooks`}
62
61
  />
63
62
  )}
64
- {isSuccess && (
63
+ {isSuccess && data.data.length > 0 && (
65
64
  <FileTreeView
66
65
  items={data.data.sort((a, b) => {
67
66
  return a.path.localeCompare(b.path);
@@ -70,6 +69,9 @@ export default function Notebooks({ navigate }: NotebooksProps) {
70
69
  navigate={navigate}
71
70
  />
72
71
  )}
72
+ {isSuccess && data.data.length === 0 && (
73
+ <Typography>No notebooks found.</Typography>
74
+ )}
73
75
  </Box>
74
76
  </StyledCardContent>
75
77
  </StyledCard>
@@ -15,6 +15,7 @@ import { Configuration, SchedulesApi } from "../../client";
15
15
  import { StyledCard, StyledCardContent } from "../styles";
16
16
  import { usePackage } from "./PackageProvider";
17
17
  import { ApiErrorDisplay } from "../ApiErrorDisplay";
18
+ import { Loading } from "../Loading";
18
19
 
19
20
  const schedulesApi = new SchedulesApi(new Configuration());
20
21
  const queryClient = new QueryClient();
@@ -41,11 +42,7 @@ export default function Schedules() {
41
42
  );
42
43
 
43
44
  if (isLoading) {
44
- return (
45
- <Typography variant="body2" sx={{ p: "20px", m: "auto" }}>
46
- Fetching Schedules...
47
- </Typography>
48
- );
45
+ return <Loading text="Fetching Schedules..." />;
49
46
  }
50
47
 
51
48
  if (isError) {
@@ -5,6 +5,7 @@ import { StyledCard, StyledCardContent, StyledCardMedia } from "../styles";
5
5
  import { QueryClient, useQuery } from "@tanstack/react-query";
6
6
  import { useProject } from "./Project";
7
7
  import { ApiErrorDisplay } from "../ApiErrorDisplay";
8
+ import { Loading } from "../Loading";
8
9
 
9
10
  const projectsApi = new ProjectsApi(new Configuration());
10
11
  const queryClient = new QueryClient();
@@ -31,11 +32,7 @@ export default function About() {
31
32
 
32
33
  return (
33
34
  <>
34
- {!isSuccess && !isError && (
35
- <Typography variant="body2" sx={{ p: "10px", m: "auto" }}>
36
- Fetching About...
37
- </Typography>
38
- )}
35
+ {!isSuccess && !isError && <Loading text="Fetching About..." />}
39
36
  {isSuccess && (
40
37
  <StyledCard variant="outlined">
41
38
  <StyledCardContent>
@@ -26,6 +26,7 @@ import { ConnectionsApi } from "../../client/api";
26
26
  import { Configuration } from "../../client/configuration";
27
27
  import { useProject } from "./Project";
28
28
  import { ApiErrorDisplay } from "../ApiErrorDisplay";
29
+ import { Loading } from "../Loading";
29
30
 
30
31
  const connectionsApi = new ConnectionsApi(new Configuration());
31
32
  const queryClient = new QueryClient();
@@ -92,11 +93,7 @@ export default function ConnectionExplorer({
92
93
  </Box>
93
94
  <Divider />
94
95
  <Box sx={{ mt: "10px", maxHeight: "600px", overflowY: "auto" }}>
95
- {isLoading && (
96
- <Typography variant="body2" sx={{ p: "20px", m: "auto" }}>
97
- Fetching Table Paths...
98
- </Typography>
99
- )}
96
+ {isLoading && <Loading text="Fetching Table Paths..." />}
100
97
  {isError && (
101
98
  <ApiErrorDisplay
102
99
  error={error}
@@ -242,11 +239,7 @@ function TableViewer({
242
239
  </IconButton>
243
240
  </DialogTitle>
244
241
  <DialogContent>
245
- {isLoading && (
246
- <Typography variant="body2" sx={{ p: "20px", m: "auto" }}>
247
- Fetching Table Details...
248
- </Typography>
249
- )}
242
+ {isLoading && <Loading text="Fetching Table Details..." />}
250
243
  {isError && (
251
244
  <ApiErrorDisplay
252
245
  error={error}
@@ -326,11 +319,7 @@ function TablesInSchema({
326
319
  </Typography>
327
320
  <Divider />
328
321
  <Box sx={{ mt: "10px", maxHeight: "600px", overflowY: "auto" }}>
329
- {isLoading && (
330
- <Typography variant="body2" sx={{ p: "20px", m: "auto" }}>
331
- Fetching Tables...
332
- </Typography>
333
- )}
322
+ {isLoading && <Loading text="Fetching Tables..." />}
334
323
  {isError && (
335
324
  <ApiErrorDisplay
336
325
  error={error}
@@ -4,6 +4,7 @@ import { StyledCard, StyledCardContent, StyledCardMedia } from "../styles";
4
4
  import { QueryClient, useQuery } from "@tanstack/react-query";
5
5
  import { useProject } from "./Project";
6
6
  import { ApiErrorDisplay } from "../ApiErrorDisplay";
7
+ import { Loading } from "../Loading";
7
8
 
8
9
  const packagesApi = new PackagesApi(new Configuration());
9
10
  const queryClient = new QueryClient();
@@ -34,11 +35,7 @@ export default function Packages({ navigate }: PackagesProps) {
34
35
 
35
36
  return (
36
37
  <>
37
- {!isSuccess && !isError && (
38
- <Typography variant="body2" sx={{ p: "20px", m: "auto" }}>
39
- Fetching Packages...
40
- </Typography>
41
- )}
38
+ {!isSuccess && !isError && <Loading text="Fetching Packages..." />}
42
39
  {isSuccess && (
43
40
  <StyledCard variant="outlined">
44
41
  <StyledCardContent>
@@ -1,14 +1,12 @@
1
1
  import { Suspense, lazy } from "react";
2
2
  import { Configuration, QueryresultsApi } from "../../client";
3
- import axios from "axios";
4
- import { Typography } from "@mui/material";
5
3
  import { QueryClient, useQuery } from "@tanstack/react-query";
6
4
  import { usePackage } from "../Package/PackageProvider";
7
5
  import { ApiErrorDisplay } from "../ApiErrorDisplay";
6
+ import { Loading } from "../Loading";
8
7
 
9
8
  const RenderedResult = lazy(() => import("../RenderedResult/RenderedResult"));
10
9
 
11
- axios.defaults.baseURL = "http://localhost:4000";
12
10
  const queryResultsApi = new QueryresultsApi(new Configuration());
13
11
  const queryClient = new QueryClient();
14
12
 
@@ -67,9 +65,7 @@ export default function QueryResult({
67
65
  return (
68
66
  <>
69
67
  {!isSuccess && !isError && (
70
- <Typography variant="body2" sx={{ p: "20px", m: "auto" }}>
71
- Fetching Query Results...
72
- </Typography>
68
+ <Loading text="Fetching Query Results..." />
73
69
  )}
74
70
  {isSuccess && (
75
71
  <Suspense fallback={<div>Loading...</div>}>
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable react/prop-types */
2
2
  import type { MalloyRenderProps } from "@malloydata/render";
3
3
  import { MalloyRenderer } from "@malloydata/render";
4
- import React, { useEffect, useRef } from "react";
4
+ import React, { useEffect, useRef, useState, useLayoutEffect } from "react";
5
5
 
6
6
  type MalloyRenderElement = HTMLElement & MalloyRenderProps;
7
7
 
@@ -31,6 +31,13 @@ interface RenderedResultProps {
31
31
  // We only know what kind of element we're dealing with after the viz is rendered.
32
32
  // So we use a callback to notify the parent that we're in a fill element.
33
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.
34
41
  export default function RenderedResult({
35
42
  result,
36
43
  height,
@@ -39,22 +46,104 @@ export default function RenderedResult({
39
46
  onDrill,
40
47
  }: RenderedResultProps) {
41
48
  const ref = useRef<HTMLDivElement>(null);
49
+ 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);
42
55
 
43
- useEffect(() => {
44
- if (ref.current) {
56
+ // Start rendering process after DOM element is available
57
+ useLayoutEffect(() => {
58
+ if (ref.current && result && !renderingStarted) {
59
+ setRenderingStarted(true);
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
+ });
45
71
  const viz = renderer.createViz();
72
+
46
73
  // Remove all content from ref.current before rendering new viz
47
74
  while (ref.current.firstChild) {
48
75
  ref.current.removeChild(ref.current.firstChild);
49
76
  }
50
- viz.setResult(JSON.parse(result));
51
- viz.render(ref.current);
77
+
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;
103
+ }
104
+ }
105
+ }
106
+ });
107
+
108
+ if (ref.current) {
109
+ observer.observe(ref.current, {
110
+ childList: true,
111
+ subtree: true,
112
+ characterData: true,
113
+ });
114
+
115
+ try {
116
+ viz.setResult(JSON.parse(result));
117
+ viz.render(ref.current);
118
+ } catch (error) {
119
+ console.error("Error rendering visualization:", error);
120
+ observer.disconnect();
121
+ setIsRendered(true);
122
+ if (renderingResolverRef.current) {
123
+ renderingResolverRef.current();
124
+ renderingResolverRef.current = null;
125
+ renderingPromiseRef.current = null;
126
+ }
127
+ }
128
+ }
52
129
  }
53
- }, [result, ref]);
130
+ }, [result, onDrill, renderingStarted]);
54
131
 
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
+ }
55
144
  // Set up size measurement using scrollHeight instead of ResizeObserver
56
145
  useEffect(() => {
57
- if (!ref.current) return;
146
+ if (!ref.current || !isRendered) return;
58
147
  const element = ref.current;
59
148
 
60
149
  // Function to measure and report size
@@ -81,24 +170,24 @@ export default function RenderedResult({
81
170
  // Initial measurement after a brief delay to let content render
82
171
  const timeoutId = setTimeout(measureSize, 100);
83
172
 
173
+ let observer: MutationObserver | null = null;
84
174
  // Also measure when the malloy result changes
85
- const observer = new MutationObserver(measureSize);
86
- observer.observe(element, {
87
- childList: true,
88
- subtree: true,
89
- attributes: true,
90
- });
175
+ if (!wasMeasured) {
176
+ observer = new MutationObserver(measureSize);
177
+ observer.observe(element, {
178
+ childList: true,
179
+ subtree: true,
180
+ attributes: true,
181
+ });
182
+ }
91
183
 
92
184
  // Cleanup
93
185
  return () => {
94
186
  clearTimeout(timeoutId);
95
- observer.disconnect();
187
+ observer?.disconnect();
96
188
  };
97
- }, [onSizeChange, result]);
189
+ }, [onSizeChange, result, isFillElement, isRendered]);
98
190
 
99
- const renderer = new MalloyRenderer({
100
- onClick: onDrill,
101
- });
102
191
  return (
103
192
  <div
104
193
  ref={ref}
@@ -8,6 +8,7 @@ import {
8
8
  useRef,
9
9
  useState,
10
10
  } from "react";
11
+ import { Loading } from "../Loading";
11
12
 
12
13
  const RenderedResult = lazy(() => import("../RenderedResult/RenderedResult"));
13
14
 
@@ -78,6 +79,7 @@ export default function ResultContainer({
78
79
  if (!result) {
79
80
  return null;
80
81
  }
82
+ const loading = <Loading text="Loading..." centered={false} size={32} />;
81
83
  const renderedHeight = isFillElement
82
84
  ? isExpanded
83
85
  ? maxHeight - 40
@@ -117,7 +119,7 @@ export default function ResultContainer({
117
119
  }}
118
120
  >
119
121
  {(result && (
120
- <Suspense fallback={<div>Loading result...</div>}>
122
+ <Suspense fallback={loading}>
121
123
  <RenderedResult
122
124
  result={result}
123
125
  height={renderedHeight}
@@ -127,7 +129,8 @@ export default function ResultContainer({
127
129
  onSizeChange={handleSizeChange}
128
130
  />
129
131
  </Suspense>
130
- )) || <div> Loading...</div>}
132
+ )) ||
133
+ loading}
131
134
  </Box>
132
135
 
133
136
  {/* Toggle button - only show if content exceeds container height */}
@@ -5,4 +5,5 @@ export * from "./Package";
5
5
  export * from "./Project";
6
6
  export * from "./QueryResult";
7
7
  export * from "./Home";
8
+ export * from "./Loading";
8
9
  export { useRouterClickHandler } from "./click_helper";
@@ -1 +0,0 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("./vendor-DAoHVNwq.cjs"),m=require("./index-7mmEVMId.cjs"),n=require("react");function v({result:r,height:c,isFillElement:i,onSizeChange:s,onDrill:f}){const e=n.useRef(null);n.useEffect(()=>{if(e.current){const t=l.createViz();for(;e.current.firstChild;)e.current.removeChild(e.current.firstChild);t.setResult(JSON.parse(r)),t.render(e.current)}},[r,e]),n.useEffect(()=>{if(!e.current)return;const t=e.current,u=()=>{if(t){const d=t.offsetHeight;d>0?s&&s(d):i&&t.firstChild&&(t.firstChild.offsetHeight==0?i(!0):i(!1))}},h=setTimeout(u,100),o=new MutationObserver(u);return o.observe(t,{childList:!0,subtree:!0,attributes:!0}),()=>{clearTimeout(h),o.disconnect()}},[s,r]);const l=new m.index_umdExports.MalloyRenderer({onClick:f});return a.jsxRuntimeExports.jsx("div",{ref:e,style:{width:"100%",height:c?`${c}px`:"100%"}})}exports.default=v;