@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 +3 -3
- package/README.md +0 -1
- package/openapitools.json +5 -5
- package/package.json +2 -1
- package/src/components/Model/Model.tsx +13 -164
- package/src/components/Model/SourcesExplorer.tsx +302 -0
- package/src/components/Model/index.ts +3 -0
- package/src/components/MutableNotebook/BrowserNotebookStorage.ts +58 -0
- package/src/components/MutableNotebook/EditableMalloyCell.tsx +47 -0
- package/src/components/MutableNotebook/ModelPicker.tsx +152 -0
- package/src/components/MutableNotebook/MutableCell.tsx +328 -0
- package/src/components/MutableNotebook/MutableNotebook.tsx +372 -0
- package/src/components/MutableNotebook/MutableNotebookList.tsx +138 -0
- package/src/components/MutableNotebook/NotebookStorage.ts +27 -0
- package/src/components/MutableNotebook/NotebookStorageProvider.tsx +43 -0
- package/src/components/MutableNotebook/index.ts +5 -0
- package/src/components/NotebookManager.ts +220 -0
- package/src/components/Package/Databases.tsx +121 -71
- package/src/components/Package/Package.tsx +10 -1
- package/src/components/Package/PublisherPackageProvider.tsx +48 -0
- package/src/components/Package/index.ts +1 -0
- package/src/components/QueryResult/QueryResult.tsx +1 -3
- package/src/components/index.ts +1 -0
- package/vite.config.ts.timestamp-1732998513502-40117fb4923d1.mjs +39 -41
package/.prettierrc
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
"printWidth": 80,
|
|
3
|
+
"tabWidth": 3
|
|
4
|
+
}
|
package/README.md
CHANGED
package/openapitools.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|