@malloy-publisher/sdk 0.0.17 → 0.0.18

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/.prettierrc CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "printWidth": 80,
3
- "tabWidth": 3
4
- }
2
+ "printWidth": 80,
3
+ "tabWidth": 3
4
+ }
package/README.md CHANGED
@@ -1,4 +1,3 @@
1
1
  # Malloy Publisher SDK
2
2
 
3
3
  The Malloy Publisher SDK is a comprehensive toolkit designed to facilitate the development and testing of Malloy packages.
4
-
package/openapitools.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
3
- "spaces": 2,
4
- "generator-cli": {
5
- "version": "7.8.0"
6
- }
2
+ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
3
+ "spaces": 2,
4
+ "generator-cli": {
5
+ "version": "7.8.0"
6
+ }
7
7
  }
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.17",
4
+ "version": "0.0.18",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",
7
7
  "module": "dist/index.es.js",
@@ -41,6 +41,7 @@
41
41
  "@mui/x-tree-view": "^7.16.0",
42
42
  "@react-spring/web": "^9.7.5",
43
43
  "@tanstack/react-query": "^5.59.16",
44
+ "@uiw/react-md-editor": "^4.0.6",
44
45
  "axios": "^1.7.7",
45
46
  "markdown-to-jsx": "^7.5.0",
46
47
  "react-router-dom": "^6.26.1"
@@ -11,47 +11,28 @@ import {
11
11
  Tabs,
12
12
  Tab,
13
13
  } from "@mui/material";
14
- import { QueryClient, useMutation, useQuery } from "@tanstack/react-query";
14
+ import { QueryClient, useQuery } from "@tanstack/react-query";
15
15
  import LinkOutlinedIcon from "@mui/icons-material/LinkOutlined";
16
16
  import ContentCopyIcon from "@mui/icons-material/ContentCopy";
17
- import axios from "axios";
18
- import { Configuration, ModelsApi, QueryresultsApi } from "../../client";
17
+ import { Configuration, ModelsApi } from "../../client";
19
18
  import { ModelCell } from "./ModelCell";
20
- import {
21
- StyledCard,
22
- StyledCardContent,
23
- StyledCardMedia,
24
- StyledExplorerContent,
25
- StyledExplorerPage,
26
- } from "../styles";
19
+ import { StyledCard, StyledCardContent, StyledCardMedia } from "../styles";
27
20
  import { highlight } from "../highlighter";
28
- import {
29
- MalloyExplorerProvider,
30
- SourcePanel,
31
- QueryPanel,
32
- ResultPanel,
33
- } from "@malloydata/malloy-explorer";
34
- import * as Malloy from "@malloydata/malloy-interfaces";
35
- import * as QueryBuilder from "@malloydata/malloy-query-builder";
36
21
 
37
22
  import "@malloydata/malloy-explorer/styles.css";
38
- axios.defaults.baseURL = "http://localhost:4000";
23
+ import { usePublisherPackage } from "../Package/PublisherPackageProvider";
24
+ import { SourceExplorerComponent } from "./SourcesExplorer";
39
25
  const modelsApi = new ModelsApi(new Configuration());
40
- const queryResultsApi = new QueryresultsApi(new Configuration());
41
26
 
42
27
  const queryClient = new QueryClient();
43
28
 
44
29
  interface ModelProps {
45
- server?: string;
46
- projectName: string;
47
- packageName: string;
48
30
  modelPath: string;
49
31
  versionId?: string;
50
32
  expandResults?: boolean;
51
33
  hideResultIcons?: boolean;
52
34
  expandEmbeddings?: boolean;
53
35
  hideEmbeddingIcons?: boolean;
54
- accessToken?: string;
55
36
  }
56
37
 
57
38
  // Note: For this to properly render outside of publisher,
@@ -59,16 +40,11 @@ interface ModelProps {
59
40
  // import "@malloy-publisher/sdk/malloy-explorer.css";
60
41
 
61
42
  export default function Model({
62
- server,
63
- projectName,
64
- packageName,
65
43
  modelPath,
66
- versionId,
67
44
  expandResults,
68
45
  hideResultIcons,
69
46
  expandEmbeddings,
70
47
  hideEmbeddingIcons,
71
- accessToken,
72
48
  }: ModelProps) {
73
49
  const [embeddingExpanded, setEmbeddingExpanded] =
74
50
  React.useState<boolean>(false);
@@ -76,8 +52,9 @@ export default function Model({
76
52
  React.useState<string>();
77
53
  const [selectedTab, setSelectedTab] = React.useState(0);
78
54
 
55
+ const { server, projectName, packageName, versionId, accessToken } =
56
+ usePublisherPackage();
79
57
  const modelCodeSnippet = getModelCodeSnippet(server, packageName, modelPath);
80
-
81
58
  useEffect(() => {
82
59
  highlight(modelCodeSnippet, "typescript").then((code) => {
83
60
  setHighlightedEmbedCode(code);
@@ -105,7 +82,6 @@ export default function Model({
105
82
  },
106
83
  queryClient,
107
84
  );
108
-
109
85
  if (isLoading) {
110
86
  return (
111
87
  <Typography sx={{ p: "20px", m: "auto" }}>
@@ -225,13 +201,12 @@ export default function Model({
225
201
  {Array.isArray(data.data.sourceInfos) &&
226
202
  data.data.sourceInfos.length > 0 && (
227
203
  <SourceExplorerComponent
228
- server={server}
229
- versionId={versionId}
230
- accessToken={accessToken}
231
- modelPath={modelPath}
232
- projectName={projectName}
233
- packageName={packageName}
234
- source={data.data.sourceInfos[selectedTab]}
204
+ sourceAndPath={{
205
+ modelPath,
206
+ sourceInfo: JSON.parse(
207
+ data.data.sourceInfos[selectedTab],
208
+ ),
209
+ }}
235
210
  />
236
211
  )}
237
212
  {data.data.queries?.length > 0 && (
@@ -286,129 +261,3 @@ function getModelCodeSnippet(
286
261
  accessToken={accessToken}
287
262
  />`;
288
263
  }
289
-
290
- function SourceExplorerComponent({
291
- server,
292
- versionId,
293
- accessToken,
294
- modelPath,
295
- projectName,
296
- packageName,
297
- source,
298
- }) {
299
- const [query, setQuery] = React.useState<Malloy.Query | undefined>(
300
- undefined,
301
- );
302
- const [result, setResult] = React.useState<Malloy.Result | undefined>(
303
- undefined,
304
- );
305
-
306
- if (!source) return null;
307
- let sourceInfo;
308
- try {
309
- sourceInfo = JSON.parse(source);
310
- } catch {
311
- return null;
312
- }
313
-
314
- const mutation = useMutation(
315
- {
316
- mutationFn: () =>
317
- queryResultsApi.executeQuery(
318
- projectName,
319
- packageName,
320
- modelPath,
321
- new QueryBuilder.ASTQuery({
322
- source: sourceInfo,
323
- query,
324
- }).toMalloy(),
325
- undefined,
326
- // sourceInfo.name,
327
- undefined,
328
- versionId,
329
- {
330
- baseURL: server,
331
- withCredentials: !accessToken,
332
- headers: {
333
- Authorization: accessToken && `Bearer ${accessToken}`,
334
- },
335
- },
336
- ),
337
- onSuccess: (data) => {
338
- if (data) {
339
- const parsedResult = JSON.parse(data.data.result);
340
- setResult(parsedResult as Malloy.Result);
341
- }
342
- },
343
- },
344
- queryClient,
345
- );
346
-
347
- const [oldSourceInfo, setOldSourceInfo] = React.useState(sourceInfo.name);
348
-
349
- // This hack is needed since sourceInfo is updated before
350
- // query is reset, which results in the query not being found
351
- // because it does not exist on the new source.
352
- React.useEffect(() => {
353
- if (oldSourceInfo !== sourceInfo.name) {
354
- setOldSourceInfo(sourceInfo.name);
355
- setQuery(undefined);
356
- setResult(undefined);
357
- }
358
- }, [source, sourceInfo]);
359
-
360
- if (oldSourceInfo !== sourceInfo.name) {
361
- return <div>Loading...</div>;
362
- }
363
-
364
- return (
365
- <StyledExplorerPage key={sourceInfo.name}>
366
- <StyledExplorerContent>
367
- <MalloyExplorerProvider
368
- source={sourceInfo}
369
- query={query}
370
- setQuery={setQuery}
371
- >
372
- <div style={{ height: "100%", width: "20%" }}>
373
- <SourcePanel
374
- onRefresh={() => {
375
- setQuery(undefined);
376
- setResult(undefined);
377
- }}
378
- />
379
- </div>
380
- <div style={{ height: "100%", width: "30%" }}>
381
- <QueryPanel
382
- runQuery={() => {
383
- mutation.mutate();
384
- }}
385
- />
386
- </div>
387
- <div style={{ height: "100%", width: "50%" }}>
388
- <ResultPanel
389
- source={sourceInfo}
390
- draftQuery={query}
391
- setDraftQuery={setQuery}
392
- submittedQuery={
393
- query
394
- ? {
395
- executionState: mutation.isPending
396
- ? "running"
397
- : "finished",
398
- response: {
399
- result: result,
400
- },
401
- query,
402
- queryResolutionStartMillis: Date.now(),
403
- onCancel: mutation.reset,
404
- }
405
- : undefined
406
- }
407
- options={{ showRawQuery: true }}
408
- />
409
- </div>
410
- </MalloyExplorerProvider>
411
- </StyledExplorerContent>
412
- </StyledExplorerPage>
413
- );
414
- }
@@ -0,0 +1,302 @@
1
+ import { CardActions, Button } from "@mui/material";
2
+ import { Box, Stack } from "@mui/system";
3
+ import {
4
+ StyledCard,
5
+ StyledCardContent,
6
+ StyledCardMedia,
7
+ StyledExplorerContent,
8
+ StyledExplorerPage,
9
+ } from "../styles";
10
+ import * as Malloy from "@malloydata/malloy-interfaces";
11
+ import * as QueryBuilder from "@malloydata/malloy-query-builder";
12
+
13
+ import React from "react";
14
+ import { QueryClient, useMutation } from "@tanstack/react-query";
15
+ import { Configuration, QueryresultsApi } from "../../client";
16
+ import { usePublisherPackage } from "../Package/PublisherPackageProvider";
17
+ import {
18
+ MalloyExplorerProvider,
19
+ QueryPanel,
20
+ ResultPanel,
21
+ SourcePanel,
22
+ } from "@malloydata/malloy-explorer";
23
+ import { styled } from "@mui/material/styles";
24
+
25
+ const queryResultsApi = new QueryresultsApi(new Configuration());
26
+ const queryClient = new QueryClient();
27
+
28
+ export interface SourceAndPath {
29
+ modelPath: string;
30
+ sourceInfo: Malloy.SourceInfo;
31
+ }
32
+
33
+ // Add a styled component for the multi-row tab bar
34
+ const MultiRowTabBar = styled(Box)(({ theme }) => ({
35
+ display: "flex",
36
+ flexWrap: "wrap",
37
+ gap: theme.spacing(0.5),
38
+ borderBottom: `1px solid ${theme.palette.divider}`,
39
+ minHeight: 36,
40
+ }));
41
+
42
+ const MultiRowTab = styled(Button)<{ selected?: boolean }>(
43
+ ({ theme, selected }) => ({
44
+ minHeight: 36,
45
+ padding: theme.spacing(0.5, 2),
46
+ borderRadius: theme.shape.borderRadius,
47
+ background: selected ? theme.palette.action.selected : "none",
48
+ color: selected ? theme.palette.primary.main : theme.palette.text.primary,
49
+ fontWeight: selected ? 600 : 400,
50
+ border: selected
51
+ ? `1px solid ${theme.palette.primary.main}`
52
+ : `1px solid transparent`,
53
+ boxShadow: selected ? theme.shadows[1] : "none",
54
+ textTransform: "uppercase",
55
+ "&:hover": {
56
+ background: theme.palette.action.hover,
57
+ border: `1px solid ${theme.palette.primary.light}`,
58
+ },
59
+ }),
60
+ );
61
+
62
+ export interface SourceExplorerProps {
63
+ sourceAndPaths: SourceAndPath[];
64
+ existingQer?: QueryExplorerResult;
65
+ existingSourceName?: string;
66
+ saveResult?: (
67
+ modelPath: string,
68
+ sourceName: string,
69
+ qer: QueryExplorerResult,
70
+ ) => void;
71
+ }
72
+
73
+ /**
74
+ * Component for Exploring a set of sources.
75
+ * Sources are provided as a list of SourceAndPath objects where each entry
76
+ * Maps from a model path to a source info object.
77
+ * It is expected that multiple sourceInfo entries will correspond to the same
78
+ * model path.
79
+ */
80
+ export function SourcesExplorer({
81
+ sourceAndPaths,
82
+ saveResult,
83
+ existingQer,
84
+ existingSourceName,
85
+ }: SourceExplorerProps) {
86
+ const [selectedTab, setSelectedTab] = React.useState(
87
+ existingSourceName
88
+ ? sourceAndPaths.findIndex(
89
+ (entry) => entry.sourceInfo.name === existingSourceName,
90
+ )
91
+ : 0,
92
+ );
93
+
94
+ const [qer, setQer] = React.useState<QueryExplorerResult | undefined>(
95
+ existingQer || emptyQueryExplorerResult(),
96
+ );
97
+
98
+ return (
99
+ <StyledCard variant="outlined">
100
+ <StyledCardContent>
101
+ <Stack
102
+ sx={{
103
+ flexDirection: "row",
104
+ justifyContent: "space-between",
105
+ }}
106
+ >
107
+ {sourceAndPaths.length > 0 && (
108
+ <MultiRowTabBar>
109
+ {sourceAndPaths.map((sourceAndPath, idx) => (
110
+ <MultiRowTab
111
+ key={sourceAndPath.sourceInfo.name || idx}
112
+ selected={selectedTab === idx}
113
+ onClick={() => setSelectedTab(idx)}
114
+ >
115
+ {sourceAndPath.sourceInfo.name ||
116
+ `Source ${idx + 1}`}
117
+ </MultiRowTab>
118
+ ))}
119
+ </MultiRowTabBar>
120
+ )}
121
+ {saveResult && (
122
+ <CardActions
123
+ sx={{
124
+ padding: "0px 10px 0px 10px",
125
+ mb: "auto",
126
+ mt: "auto",
127
+ }}
128
+ >
129
+ <Button
130
+ onClick={() =>
131
+ saveResult(
132
+ sourceAndPaths[selectedTab].modelPath,
133
+ sourceAndPaths[selectedTab].sourceInfo.name,
134
+ qer,
135
+ )
136
+ }
137
+ >
138
+ Save
139
+ </Button>
140
+ </CardActions>
141
+ )}
142
+ </Stack>
143
+ </StyledCardContent>
144
+ <StyledCardMedia>
145
+ <Stack spacing={2} component="section">
146
+ <SourceExplorerComponent
147
+ sourceAndPath={sourceAndPaths[selectedTab]}
148
+ existingQer={qer}
149
+ onChange={setQer}
150
+ />
151
+ <Box height="5px" />
152
+ </Stack>
153
+ </StyledCardMedia>
154
+ </StyledCard>
155
+ );
156
+ }
157
+
158
+ interface SourceExplorerComponentProps {
159
+ sourceAndPath: SourceAndPath;
160
+ existingQer?: QueryExplorerResult;
161
+ onChange?: (qer: QueryExplorerResult) => void;
162
+ }
163
+
164
+ export interface QueryExplorerResult {
165
+ query: string | undefined;
166
+ malloyQuery: Malloy.Query;
167
+ malloyResult: Malloy.Result | undefined;
168
+ }
169
+
170
+ export function emptyQueryExplorerResult(): QueryExplorerResult {
171
+ return {
172
+ query: undefined,
173
+ malloyQuery: undefined,
174
+ malloyResult: undefined,
175
+ };
176
+ }
177
+ export function SourceExplorerComponent({
178
+ sourceAndPath,
179
+ onChange,
180
+ existingQer,
181
+ }: SourceExplorerComponentProps) {
182
+ const [qer, setQer] = React.useState<QueryExplorerResult>(
183
+ existingQer || emptyQueryExplorerResult(),
184
+ );
185
+
186
+ React.useEffect(() => {
187
+ if (onChange) {
188
+ onChange(qer);
189
+ }
190
+ }, [onChange, qer]);
191
+ const { server, projectName, packageName, versionId, accessToken } =
192
+ usePublisherPackage();
193
+ const mutation = useMutation(
194
+ {
195
+ mutationFn: () => {
196
+ const malloy = new QueryBuilder.ASTQuery({
197
+ source: sourceAndPath.sourceInfo,
198
+ query: qer?.malloyQuery,
199
+ }).toMalloy();
200
+ setQer({
201
+ ...qer,
202
+ query: malloy,
203
+ });
204
+ return queryResultsApi.executeQuery(
205
+ projectName,
206
+ packageName,
207
+ sourceAndPath.modelPath,
208
+ malloy,
209
+ undefined,
210
+ // sourceInfo.name,
211
+ undefined,
212
+ versionId,
213
+ {
214
+ baseURL: server,
215
+ withCredentials: !accessToken,
216
+ headers: {
217
+ Authorization: accessToken && `Bearer ${accessToken}`,
218
+ },
219
+ },
220
+ );
221
+ },
222
+ onSuccess: (data) => {
223
+ if (data) {
224
+ const parsedResult = JSON.parse(data.data.result);
225
+ setQer({
226
+ ...qer,
227
+ malloyResult: parsedResult as Malloy.Result,
228
+ });
229
+ }
230
+ },
231
+ },
232
+ queryClient,
233
+ );
234
+
235
+ const [oldSourceInfo, setOldSourceInfo] = React.useState(
236
+ sourceAndPath.sourceInfo.name,
237
+ );
238
+
239
+ // This hack is needed since sourceInfo is updated before
240
+ // query is reset, which results in the query not being found
241
+ // because it does not exist on the new source.
242
+ React.useEffect(() => {
243
+ if (oldSourceInfo !== sourceAndPath.sourceInfo.name) {
244
+ setOldSourceInfo(sourceAndPath.sourceInfo.name);
245
+ setQer(emptyQueryExplorerResult());
246
+ }
247
+ }, [sourceAndPath, oldSourceInfo]);
248
+
249
+ if (oldSourceInfo !== sourceAndPath.sourceInfo.name) {
250
+ return <div>Loading...</div>;
251
+ }
252
+
253
+ return (
254
+ <StyledExplorerPage key={sourceAndPath.sourceInfo.name}>
255
+ <StyledExplorerContent>
256
+ <MalloyExplorerProvider
257
+ source={sourceAndPath.sourceInfo}
258
+ query={qer?.malloyQuery}
259
+ setQuery={(query) => setQer({ ...qer, malloyQuery: query })}
260
+ >
261
+ <div style={{ height: "100%", width: "20%" }}>
262
+ <SourcePanel
263
+ onRefresh={() => setQer(emptyQueryExplorerResult())}
264
+ />
265
+ </div>
266
+ <div style={{ height: "100%", width: "30%" }}>
267
+ <QueryPanel
268
+ runQuery={() => {
269
+ mutation.mutate();
270
+ }}
271
+ />
272
+ </div>
273
+ <div style={{ height: "100%", width: "50%" }}>
274
+ <ResultPanel
275
+ source={sourceAndPath.sourceInfo}
276
+ draftQuery={qer?.malloyQuery}
277
+ setDraftQuery={(malloyQuery) =>
278
+ setQer({ ...qer, malloyQuery: malloyQuery })
279
+ }
280
+ submittedQuery={
281
+ qer?.malloyQuery
282
+ ? {
283
+ executionState: mutation.isPending
284
+ ? "running"
285
+ : "finished",
286
+ response: {
287
+ result: qer.malloyResult,
288
+ },
289
+ query: qer.malloyQuery,
290
+ queryResolutionStartMillis: Date.now(),
291
+ onCancel: mutation.reset,
292
+ }
293
+ : undefined
294
+ }
295
+ options={{ showRawQuery: true }}
296
+ />
297
+ </div>
298
+ </MalloyExplorerProvider>
299
+ </StyledExplorerContent>
300
+ </StyledExplorerPage>
301
+ );
302
+ }
@@ -1 +1,4 @@
1
1
  export { default as Model } from "./Model";
2
+ export { SourcesExplorer } from "./SourcesExplorer";
3
+ export { SourceExplorerComponent } from "./SourcesExplorer";
4
+ export type { SourceAndPath } from "./SourcesExplorer";
@@ -0,0 +1,58 @@
1
+ import type { NotebookStorage, UserContext } from "./NotebookStorage";
2
+
3
+ export class BrowserNotebookStorage implements NotebookStorage {
4
+ private makeKey(context: UserContext, path?: string): string {
5
+ let key = `BROWSER_NOTEBOOK_STORAGE__${context.project}/${context.package}`;
6
+ if (path) {
7
+ key += `/${path}`;
8
+ }
9
+ return key;
10
+ }
11
+
12
+ listNotebooks(context: UserContext): string[] {
13
+ const prefix = this.makeKey(context);
14
+ const keys: string[] = [];
15
+ for (let i = 0; i < localStorage.length; i++) {
16
+ const key = localStorage.key(i);
17
+ if (key && key.startsWith(prefix + "/")) {
18
+ // Extract the notebook path after the prefix
19
+ const notebookPath = key.substring(prefix.length + 1);
20
+ keys.push(notebookPath);
21
+ }
22
+ }
23
+ return keys;
24
+ }
25
+
26
+ getNotebook(context: UserContext, path: string): string {
27
+ const key = this.makeKey(context, path);
28
+ const notebook = localStorage.getItem(key);
29
+ if (notebook === null) {
30
+ throw new Error(`Notebook not found at path: ${path}`);
31
+ }
32
+ return notebook;
33
+ }
34
+
35
+ deleteNotebook(context: UserContext, path: string): void {
36
+ const key = this.makeKey(context, path);
37
+ if (localStorage.getItem(key) === null) {
38
+ throw new Error(`Notebook not found at path: ${path}`);
39
+ }
40
+ localStorage.removeItem(key);
41
+ }
42
+
43
+ saveNotebook(context: UserContext, path: string, notebook: string): void {
44
+ const key = this.makeKey(context, path);
45
+ localStorage.setItem(key, notebook);
46
+ }
47
+
48
+ moveNotebook(context: UserContext, from: string, to: string): void {
49
+ const fromKey = this.makeKey(context, from);
50
+ const toKey = this.makeKey(context, to);
51
+ const notebook = localStorage.getItem(fromKey);
52
+ if (notebook === null) {
53
+ throw new Error(`Notebook not found at path: ${from}`);
54
+ }
55
+ localStorage.setItem(toKey, notebook);
56
+ localStorage.removeItem(fromKey);
57
+ }
58
+ }
@@ -0,0 +1,47 @@
1
+ import { SourceAndPath, SourcesExplorer } from "../Model";
2
+ import { NotebookCellValue } from "../NotebookManager";
3
+
4
+ interface EditableMalloyCellProps {
5
+ cell: NotebookCellValue;
6
+ sourceAndPaths: SourceAndPath[];
7
+ onCellChange: (cell: NotebookCellValue) => void;
8
+
9
+ onClose: () => void;
10
+ }
11
+
12
+ export function EditableMalloyCell({
13
+ cell,
14
+ sourceAndPaths,
15
+ onCellChange,
16
+ onClose,
17
+ }: EditableMalloyCellProps) {
18
+ const qer = {
19
+ query: cell.value,
20
+ malloyResult: cell.result ? JSON.parse(cell.result) : undefined,
21
+ malloyQuery: cell.queryInfo ? JSON.parse(cell.queryInfo) : undefined,
22
+ };
23
+ return (
24
+ <SourcesExplorer
25
+ sourceAndPaths={sourceAndPaths}
26
+ existingQer={qer}
27
+ existingSourceName={cell.sourceName}
28
+ saveResult={(modelPath, sourceName, qer) => {
29
+ // Convert the results of the Query Explorer into
30
+ // the stringified JSON objects that are stored in the cell.
31
+ onCellChange({
32
+ ...cell,
33
+ value: qer.query,
34
+ result: qer.malloyResult
35
+ ? JSON.stringify(qer.malloyResult)
36
+ : undefined,
37
+ queryInfo: qer.malloyQuery
38
+ ? JSON.stringify(qer.malloyQuery)
39
+ : undefined,
40
+ sourceName,
41
+ modelPath,
42
+ });
43
+ onClose();
44
+ }}
45
+ />
46
+ );
47
+ }