@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.
@@ -0,0 +1,372 @@
1
+ // TODO(jjs) - Export to .malloynb
2
+ // TOOD(jjs) - Import via Publisher API that parses whole NB
3
+
4
+ import React from "react";
5
+ import Stack from "@mui/material/Stack";
6
+ import {
7
+ Box,
8
+ Button,
9
+ CardActions,
10
+ TextField,
11
+ Typography,
12
+ Menu,
13
+ MenuItem,
14
+ } from "@mui/material";
15
+ import { StyledCard, StyledCardContent, StyledCardMedia } from "../styles";
16
+ import { MutableCell } from "./MutableCell";
17
+ import { Configuration, ModelsApi } from "../../client";
18
+ import { ModelPicker } from "./ModelPicker";
19
+ import { usePublisherPackage } from "../Package";
20
+ import { NotebookManager } from "../NotebookManager";
21
+ import { SourceAndPath } from "../Model/SourcesExplorer";
22
+ import { useNotebookStorage } from "./NotebookStorageProvider";
23
+
24
+ import * as Malloy from "@malloydata/malloy-interfaces";
25
+
26
+ const modelsApi = new ModelsApi(new Configuration());
27
+
28
+ interface MutableNotebookProps {
29
+ inputNotebookPath: string | undefined;
30
+ expandCodeCells?: boolean;
31
+ expandEmbeddings?: boolean;
32
+ }
33
+
34
+ function getNotebookPath() {
35
+ const params = new URLSearchParams(window.location.hash.substring(1));
36
+ return params.get("notebookPath");
37
+ }
38
+ interface PathToSources {
39
+ modelPath: string;
40
+ sourceInfos: Malloy.SourceInfo[];
41
+ }
42
+
43
+ export default function MutableNotebook({
44
+ inputNotebookPath,
45
+ expandCodeCells,
46
+ expandEmbeddings,
47
+ }: MutableNotebookProps) {
48
+ const { server, projectName, packageName, versionId, accessToken } =
49
+ usePublisherPackage();
50
+ if (!projectName || !packageName) {
51
+ throw new Error(
52
+ "Project and package must be provided via PubliserPackageProvider",
53
+ );
54
+ }
55
+ const { notebookStorage, userContext } = useNotebookStorage();
56
+ if (!notebookStorage || !userContext) {
57
+ throw new Error(
58
+ "Notebook storage and user context must be provided via NotebookStorageProvider",
59
+ );
60
+ }
61
+ const [notebookData, setNotebookData] = React.useState<
62
+ NotebookManager | undefined
63
+ >();
64
+ const [notebookPath, setNotebookPath] = React.useState(inputNotebookPath);
65
+ const [editingMalloyIndex, setEditingMalloyIndex] = React.useState<
66
+ number | undefined
67
+ >();
68
+ const [editingMarkdownIndex, setEditingMarkdownIndex] = React.useState<
69
+ number | undefined
70
+ >();
71
+ const [sourceAndPaths, setSourceAndPaths] = React.useState<PathToSources[]>(
72
+ [],
73
+ );
74
+ const [menuAnchorEl, setMenuAnchorEl] = React.useState<null | HTMLElement>(
75
+ null,
76
+ );
77
+ const [menuIndex, setMenuIndex] = React.useState<number | null>(null);
78
+ const menuOpen = Boolean(menuAnchorEl);
79
+ const handleMenuClick = (
80
+ event: React.MouseEvent<HTMLButtonElement>,
81
+ index: number,
82
+ ) => {
83
+ setMenuAnchorEl(event.currentTarget);
84
+ setMenuIndex(index);
85
+ };
86
+ const handleMenuClose = () => {
87
+ setMenuAnchorEl(null);
88
+ setMenuIndex(null);
89
+ };
90
+ const handleAddCell = (isMarkdown: boolean, index: number) => {
91
+ notebookData.insertCell(index, {
92
+ isMarkdown,
93
+ value: "",
94
+ });
95
+ saveNotebook();
96
+ if (isMarkdown) {
97
+ setEditingMarkdownIndex(index);
98
+ } else {
99
+ setEditingMalloyIndex(index);
100
+ }
101
+ handleMenuClose();
102
+ };
103
+ const saveNotebook = React.useCallback(() => {
104
+ setNotebookData(notebookData.saveNotebook());
105
+ }, [notebookData]);
106
+ React.useEffect(() => {
107
+ // Load SourceInfos from selected models and sync PathsToSources
108
+ if (!notebookData) {
109
+ return;
110
+ }
111
+ const modelPathToSourceInfo = new Map(
112
+ sourceAndPaths.map(({ modelPath, sourceInfos }) => [
113
+ modelPath,
114
+ sourceInfos,
115
+ ]),
116
+ );
117
+ const newSourceAndPaths = [];
118
+ const promises = [];
119
+ for (const model of notebookData.getModels()) {
120
+ if (!modelPathToSourceInfo.has(model)) {
121
+ console.log("Fetching model from Publisher", model);
122
+ promises.push(
123
+ modelsApi
124
+ .getModel(projectName, packageName, model, versionId, {
125
+ baseURL: server,
126
+ withCredentials: !accessToken,
127
+ })
128
+ .then((data) => ({
129
+ modelPath: model,
130
+ sourceInfos: data.data.sourceInfos.map((source) =>
131
+ JSON.parse(source),
132
+ ),
133
+ })),
134
+ );
135
+ } else {
136
+ newSourceAndPaths.push({
137
+ modelPath: model,
138
+ sourceInfos: modelPathToSourceInfo.get(model),
139
+ });
140
+ }
141
+ }
142
+ if (promises.length > 0) {
143
+ Promise.all(promises).then((loadedSourceAndPaths) => {
144
+ setSourceAndPaths([...newSourceAndPaths, ...loadedSourceAndPaths]);
145
+ });
146
+ }
147
+ }, [
148
+ accessToken,
149
+ notebookData,
150
+ packageName,
151
+ projectName,
152
+ server,
153
+ sourceAndPaths,
154
+ versionId,
155
+ ]);
156
+
157
+ React.useEffect(() => {
158
+ if (!notebookData) {
159
+ setNotebookData(
160
+ NotebookManager.loadNotebook(
161
+ notebookStorage,
162
+ userContext,
163
+ getNotebookPath(),
164
+ ),
165
+ );
166
+ setNotebookPath(getNotebookPath());
167
+ }
168
+ }, [notebookData, packageName, projectName, notebookStorage, userContext]);
169
+
170
+ if (!notebookData) {
171
+ return <div>Loading...</div>;
172
+ }
173
+ const getSourceList = (sourceAndPaths: PathToSources[]): SourceAndPath[] => {
174
+ const sourceAndPath = [];
175
+ for (const sources of sourceAndPaths) {
176
+ for (const sourceInfo of sources.sourceInfos) {
177
+ sourceAndPath.push({
178
+ modelPath: sources.modelPath,
179
+ sourceInfo: sourceInfo,
180
+ });
181
+ }
182
+ }
183
+ return sourceAndPath;
184
+ };
185
+ const createButtons = (index: number) => {
186
+ return (
187
+ <CardActions
188
+ sx={{
189
+ padding: "0px 10px 0px 10px",
190
+ mb: "auto",
191
+ mt: "auto",
192
+ justifyContent: "flex-end",
193
+ }}
194
+ >
195
+ <Button
196
+ variant="outlined"
197
+ size="small"
198
+ startIcon={<AddIcon />}
199
+ onClick={(e) => handleMenuClick(e, index)}
200
+ >
201
+ New Cell
202
+ </Button>
203
+ </CardActions>
204
+ );
205
+ };
206
+ return (
207
+ <StyledCard variant="outlined">
208
+ <StyledCardContent>
209
+ <Stack
210
+ sx={{
211
+ flexDirection: "row",
212
+ justifyContent: "space-between",
213
+ }}
214
+ >
215
+ <Box sx={{ display: "flex", alignItems: "top", gap: 1 }}>
216
+ <Typography
217
+ sx={{
218
+ fontSize: "150%",
219
+ minHeight: "56px",
220
+ fontWeight: "bold",
221
+ }}
222
+ >
223
+ Notebook :
224
+ </Typography>
225
+ <TextField
226
+ value={notebookPath}
227
+ onChange={(e) => {
228
+ setNotebookPath(e.target.value);
229
+ }}
230
+ onBlur={(e) => {
231
+ const url = new URL(window.location.href);
232
+ url.hash = `notebookPath=${e.target.value}`;
233
+ notebookData.renameNotebook(e.target.value);
234
+ window.history.pushState({}, "", url);
235
+ }}
236
+ size="medium"
237
+ variant="standard"
238
+ error={!notebookPath}
239
+ helperText={
240
+ !notebookPath
241
+ ? "Please enter a notebook name"
242
+ : undefined
243
+ }
244
+ sx={{ flex: 1 }}
245
+ />
246
+ </Box>
247
+ <Box
248
+ sx={{ display: "flex", alignItems: "center", mt: 1, mb: 1 }}
249
+ >
250
+ <ExportMalloyButton notebookData={notebookData} />
251
+ </Box>
252
+ </Stack>
253
+ </StyledCardContent>
254
+ <ModelPicker
255
+ initialSelectedModels={notebookData.getModels()}
256
+ onModelChange={(models) => {
257
+ setNotebookData(notebookData.setModels(models));
258
+ saveNotebook();
259
+ }}
260
+ />
261
+
262
+ <StyledCardMedia>
263
+ <Stack>
264
+ {notebookData.getCells().map((cell, index) => (
265
+ <React.Fragment
266
+ key={`${index}-${notebookData.getCells().length}`}
267
+ >
268
+ <MutableCell
269
+ cell={cell}
270
+ newCell={createButtons(index)}
271
+ sourceAndPaths={getSourceList(sourceAndPaths)}
272
+ expandCodeCell={expandCodeCells}
273
+ expandEmbedding={expandEmbeddings}
274
+ editingMarkdown={editingMarkdownIndex === index}
275
+ editingMalloy={editingMalloyIndex === index}
276
+ onDelete={() => {
277
+ setNotebookData(notebookData.deleteCell(index));
278
+ saveNotebook();
279
+ }}
280
+ onCellChange={(cell) => {
281
+ setNotebookData(notebookData.setCell(index, cell));
282
+ saveNotebook();
283
+ }}
284
+ onEdit={() => {
285
+ if (cell.isMarkdown) {
286
+ setEditingMarkdownIndex(index);
287
+ } else {
288
+ setEditingMalloyIndex(index);
289
+ }
290
+ }}
291
+ onClose={() => {
292
+ if (cell.isMarkdown) {
293
+ setEditingMarkdownIndex(undefined);
294
+ } else {
295
+ setEditingMalloyIndex(undefined);
296
+ }
297
+ }}
298
+ />
299
+ </React.Fragment>
300
+ ))}
301
+ <Box style={{ paddingRight: "7px", paddingTop: "10px" }}>
302
+ {createButtons(notebookData.getCells().length)}
303
+ </Box>
304
+ <Menu
305
+ anchorEl={menuAnchorEl}
306
+ open={menuOpen}
307
+ onClose={handleMenuClose}
308
+ anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
309
+ transformOrigin={{ vertical: "top", horizontal: "right" }}
310
+ >
311
+ <MenuItem onClick={() => handleAddCell(true, menuIndex ?? 0)}>
312
+ Add Markdown
313
+ </MenuItem>
314
+ <MenuItem
315
+ onClick={() => handleAddCell(false, menuIndex ?? 0)}
316
+ >
317
+ Add Malloy
318
+ </MenuItem>
319
+ </Menu>
320
+ <Stack
321
+ sx={{
322
+ flexDirection: "row",
323
+ justifyContent: "flex-end",
324
+ p: 1,
325
+ }}
326
+ ></Stack>
327
+ </Stack>
328
+ </StyledCardMedia>
329
+ </StyledCard>
330
+ );
331
+ }
332
+ function AddIcon() {
333
+ return (
334
+ <svg
335
+ width="24"
336
+ height="24"
337
+ viewBox="0 0 24 24"
338
+ fill="none"
339
+ xmlns="http://www.w3.org/2000/svg"
340
+ >
341
+ <path
342
+ d="M19 13H13V19H11V13H5V11H11V5H13V11H19V13Z"
343
+ fill="currentColor"
344
+ />
345
+ </svg>
346
+ );
347
+ }
348
+
349
+ function ExportMalloyButton({
350
+ notebookData,
351
+ }: {
352
+ notebookData: NotebookManager;
353
+ }) {
354
+ const [copied, setCopied] = React.useState(false);
355
+ const handleExport = async () => {
356
+ if (!notebookData) return;
357
+ const malloy = notebookData.toMalloyNotebook();
358
+ try {
359
+ await navigator.clipboard.writeText(malloy);
360
+ setCopied(true);
361
+ setTimeout(() => setCopied(false), 1500);
362
+ } catch {
363
+ setCopied(false);
364
+ alert("Failed to copy to clipboard");
365
+ }
366
+ };
367
+ return (
368
+ <Button variant="contained" color="primary" onClick={handleExport}>
369
+ {copied ? "Copied!" : "Export To Malloy"}
370
+ </Button>
371
+ );
372
+ }
@@ -0,0 +1,138 @@
1
+ import React from "react";
2
+ import { useNavigate, useParams } from "react-router-dom";
3
+ import {
4
+ List,
5
+ ListItem,
6
+ ListItemText,
7
+ IconButton,
8
+ Dialog,
9
+ DialogTitle,
10
+ DialogContent,
11
+ DialogContentText,
12
+ DialogActions,
13
+ Button,
14
+ Typography,
15
+ Box,
16
+ ListItemButton,
17
+ } from "@mui/material";
18
+ import DeleteIcon from "@mui/icons-material/Delete";
19
+ import { useNotebookStorage } from "./NotebookStorageProvider";
20
+
21
+ export function MutableNotebookList() {
22
+ const { projectName, packageName } = useParams();
23
+ const navigate = useNavigate();
24
+ const { notebookStorage, userContext } = useNotebookStorage();
25
+ const [notebooks, setNotebooks] = React.useState<string[]>([]);
26
+ const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
27
+ const [notebookToDelete, setNotebookToDelete] = React.useState<
28
+ string | null
29
+ >(null);
30
+
31
+ React.useEffect(() => {
32
+ if (notebookStorage && userContext) {
33
+ setNotebooks(notebookStorage.listNotebooks(userContext));
34
+ }
35
+ }, [notebookStorage, userContext]);
36
+
37
+ const handleDeleteClick = (notebook: string) => {
38
+ setNotebookToDelete(notebook);
39
+ setDeleteDialogOpen(true);
40
+ };
41
+
42
+ const handleDeleteConfirm = () => {
43
+ if (notebookToDelete && notebookStorage && userContext) {
44
+ notebookStorage.deleteNotebook(userContext, notebookToDelete);
45
+ setNotebooks(notebookStorage.listNotebooks(userContext));
46
+ }
47
+ setDeleteDialogOpen(false);
48
+ setNotebookToDelete(null);
49
+ };
50
+
51
+ const handleDeleteCancel = () => {
52
+ setDeleteDialogOpen(false);
53
+ setNotebookToDelete(null);
54
+ };
55
+
56
+ const handleNotebookClick = (notebook: string) => {
57
+ if (projectName && packageName) {
58
+ // Navigate to the ScratchNotebookPage with anchor text for notebookPath
59
+ navigate(
60
+ `/${projectName}/${packageName}/scratch_notebook#notebookPath=${encodeURIComponent(notebook)}`,
61
+ );
62
+ }
63
+ };
64
+
65
+ return (
66
+ <Box>
67
+ <Typography variant="h5" sx={{ mb: 2 }}>
68
+ Notebooks
69
+ </Typography>
70
+ <Button
71
+ variant="contained"
72
+ onClick={() => handleNotebookClick("")}
73
+ sx={{ mb: 2 }}
74
+ >
75
+ New Notebook
76
+ </Button>
77
+
78
+ <List>
79
+ {notebooks.length === 0 && (
80
+ <ListItem>
81
+ <ListItemText primary="No notebooks found." />
82
+ </ListItem>
83
+ )}
84
+ {notebooks.map((notebook) => (
85
+ <ListItem
86
+ key={notebook}
87
+ secondaryAction={
88
+ <IconButton
89
+ edge="end"
90
+ aria-label="delete"
91
+ onClick={(e) => {
92
+ e.stopPropagation();
93
+ handleDeleteClick(notebook);
94
+ }}
95
+ >
96
+ <DeleteIcon />
97
+ </IconButton>
98
+ }
99
+ disablePadding
100
+ >
101
+ <ListItemButton onClick={() => handleNotebookClick(notebook)}>
102
+ <ListItemText
103
+ primary={
104
+ <Typography
105
+ component="span"
106
+ sx={{
107
+ color: "primary.main",
108
+ textDecoration: "underline",
109
+ }}
110
+ >
111
+ {notebook}
112
+ </Typography>
113
+ }
114
+ />
115
+ </ListItemButton>
116
+ </ListItem>
117
+ ))}
118
+ </List>
119
+ <Dialog open={deleteDialogOpen} onClose={handleDeleteCancel}>
120
+ <DialogTitle>Delete Notebook</DialogTitle>
121
+ <DialogContent>
122
+ <DialogContentText>
123
+ Are you sure you want to delete the notebook &quot;
124
+ {notebookToDelete}&quot;? This action cannot be undone.
125
+ </DialogContentText>
126
+ </DialogContent>
127
+ <DialogActions>
128
+ <Button onClick={handleDeleteCancel} color="primary">
129
+ Cancel
130
+ </Button>
131
+ <Button onClick={handleDeleteConfirm} color="error" autoFocus>
132
+ Delete
133
+ </Button>
134
+ </DialogActions>
135
+ </Dialog>
136
+ </Box>
137
+ );
138
+ }
@@ -0,0 +1,27 @@
1
+ export interface UserContext {
2
+ // UserContext holds the project & package associated with
3
+ // the notebook. PublisherStorage interfaces will
4
+ // implement this and add data representing configuration,
5
+ // user permissions, and access tokens.
6
+ project: string;
7
+ package: string;
8
+ }
9
+
10
+ export interface NotebookStorage {
11
+ // Lists all available notebooks for the context.
12
+ // Notebooks names are like S3 paths- / denote hierarchical
13
+ // folders, but otherwise folders are not "real" objects
14
+ listNotebooks(context: UserContext): string[];
15
+
16
+ // Returns the notebook at the specific path, throws an exception if no such notebook exists (or cannot be accessed)
17
+ getNotebook(context: UserContext, path: string): string;
18
+
19
+ // Deletes the notebook at the specified path, or throws an
20
+ // Exception on failure
21
+ deleteNotebook(context: UserContext, path: string): void;
22
+
23
+ saveNotebook(context: UserContext, path: string, notebook: string): void;
24
+
25
+ // Moves notebook from the "from" path to the "to" path
26
+ moveNotebook(context: UserContext, from: string, to: string): void;
27
+ }
@@ -0,0 +1,43 @@
1
+ import React, { createContext, useContext, useMemo } from "react";
2
+ import type { NotebookStorage, UserContext } from "./NotebookStorage";
3
+
4
+ interface NotebookStorageProviderProps {
5
+ children: React.ReactNode;
6
+ userContext: UserContext;
7
+ notebookStorage: NotebookStorage;
8
+ }
9
+
10
+ interface NotebookStorageContextValue {
11
+ notebookStorage: NotebookStorage;
12
+ userContext: UserContext;
13
+ }
14
+
15
+ const NotebookStorageContext = createContext<
16
+ NotebookStorageContextValue | undefined
17
+ >(undefined);
18
+
19
+ export default function NotebookStorageProvider({
20
+ children,
21
+ userContext,
22
+ notebookStorage,
23
+ }: NotebookStorageProviderProps) {
24
+ const value = useMemo(
25
+ () => ({ notebookStorage, userContext }),
26
+ [notebookStorage, userContext],
27
+ );
28
+ return (
29
+ <NotebookStorageContext.Provider value={value}>
30
+ {children}
31
+ </NotebookStorageContext.Provider>
32
+ );
33
+ }
34
+
35
+ export function useNotebookStorage() {
36
+ const context = useContext(NotebookStorageContext);
37
+ if (!context) {
38
+ throw new Error(
39
+ "useNotebookStorage must be used within a NotebookStorageProvider",
40
+ );
41
+ }
42
+ return context;
43
+ }
@@ -0,0 +1,5 @@
1
+ export { default as MutableNotebook } from "./MutableNotebook";
2
+ export { default as NotebookStorageProvider } from "./NotebookStorageProvider";
3
+ export { BrowserNotebookStorage } from "./BrowserNotebookStorage";
4
+ export type { UserContext, NotebookStorage } from "./NotebookStorage";
5
+ export { MutableNotebookList } from "./MutableNotebookList";