@marimo-team/islands 0.18.2 → 0.18.4
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/dist/{constants-DWBOe162.js → constants-D_G8vnDk.js} +5 -4
- package/dist/{formats-7RSCCoSI.js → formats-Bi_tbdwB.js} +21 -22
- package/dist/{glide-data-editor-D-Ia_Jsv.js → glide-data-editor-DXF8E-QD.js} +2 -2
- package/dist/main.js +280 -148
- package/dist/style.css +1 -1
- package/dist/{types-Dunk85GC.js → types-DclGb0Yh.js} +1 -1
- package/dist/{vega-component-kU4hFYYJ.js → vega-component-BFcH2SqR.js} +8 -8
- package/package.json +1 -1
- package/src/components/app-config/user-config-form.tsx +14 -1
- package/src/components/data-table/context-menu.tsx +7 -3
- package/src/components/data-table/filter-pills.tsx +2 -1
- package/src/components/data-table/filters.ts +11 -2
- package/src/components/editor/cell/CreateCellButton.tsx +5 -3
- package/src/components/editor/cell/collapse.tsx +2 -2
- package/src/components/editor/chrome/components/contribute-snippet-button.tsx +22 -103
- package/src/components/editor/controls/duplicate-shortcut-banner.tsx +50 -0
- package/src/components/editor/controls/keyboard-shortcuts.tsx +25 -2
- package/src/components/editor/notebook-banner.tsx +1 -1
- package/src/components/editor/notebook-cell.tsx +4 -3
- package/src/components/editor/output/__tests__/ansi-reduce.test.ts +6 -6
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +3 -3
- package/src/components/pages/home-page.tsx +6 -0
- package/src/components/scratchpad/scratchpad.tsx +2 -1
- package/src/core/constants.ts +10 -0
- package/src/core/layout/useTogglePresenting.ts +69 -25
- package/src/core/state/__mocks__/mocks.ts +1 -0
- package/src/hooks/__tests__/useDuplicateShortcuts.test.ts +449 -0
- package/src/hooks/useDuplicateShortcuts.ts +145 -0
- package/src/plugins/impl/NumberPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/NumberPlugin.test.tsx +1 -1
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +67 -47
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +2 -57
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +23 -19
- package/src/plugins/impl/anywidget/model.ts +68 -41
- package/src/plugins/impl/data-frames/utils/__tests__/operators.test.ts +2 -0
- package/src/plugins/impl/data-frames/utils/operators.ts +1 -0
- package/src/plugins/impl/vega/vega.css +5 -0
- package/src/plugins/layout/NavigationMenuPlugin.tsx +24 -22
- package/src/plugins/layout/StatPlugin.tsx +43 -23
- package/src/utils/__tests__/data-views.test.ts +495 -13
- package/src/utils/__tests__/json-parser.test.ts +1 -1
- package/src/utils/data-views.ts +134 -16
- package/src/utils/json/base64.ts +8 -0
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
3
|
|
|
4
4
|
import type { AnyModel } from "@anywidget/types";
|
|
5
|
-
import { dequal } from "dequal";
|
|
6
5
|
import { debounce } from "lodash-es";
|
|
7
6
|
import { z } from "zod";
|
|
8
7
|
import { getRequestClient } from "@/core/network/requests";
|
|
9
8
|
import { assertNever } from "@/utils/assertNever";
|
|
10
9
|
import { Deferred } from "@/utils/Deferred";
|
|
11
|
-
import {
|
|
10
|
+
import { decodeFromWire, serializeBuffersToBase64 } from "@/utils/data-views";
|
|
12
11
|
import { throwNotImplemented } from "@/utils/functions";
|
|
12
|
+
import type { Base64String } from "@/utils/json/base64";
|
|
13
13
|
import { Logger } from "@/utils/Logger";
|
|
14
14
|
|
|
15
15
|
export type EventHandler = (...args: any[]) => void;
|
|
@@ -63,28 +63,30 @@ export const MODEL_MANAGER = new ModelManager();
|
|
|
63
63
|
|
|
64
64
|
export class Model<T extends Record<string, any>> implements AnyModel<T> {
|
|
65
65
|
private ANY_CHANGE_EVENT = "change";
|
|
66
|
-
private dirtyFields
|
|
66
|
+
private dirtyFields: Map<keyof T, unknown>;
|
|
67
67
|
public static _modelManager: ModelManager = MODEL_MANAGER;
|
|
68
68
|
private data: T;
|
|
69
69
|
private onChange: (value: Partial<T>) => void;
|
|
70
70
|
private sendToWidget: (req: {
|
|
71
|
-
content
|
|
72
|
-
buffers
|
|
71
|
+
content: unknown;
|
|
72
|
+
buffers: Base64String[];
|
|
73
73
|
}) => Promise<null | undefined>;
|
|
74
74
|
|
|
75
75
|
constructor(
|
|
76
76
|
data: T,
|
|
77
77
|
onChange: (value: Partial<T>) => void,
|
|
78
78
|
sendToWidget: (req: {
|
|
79
|
-
content
|
|
80
|
-
buffers
|
|
79
|
+
content: unknown;
|
|
80
|
+
buffers: Base64String[];
|
|
81
81
|
}) => Promise<null | undefined>,
|
|
82
82
|
initialDirtyFields: Set<keyof T>,
|
|
83
83
|
) {
|
|
84
84
|
this.data = data;
|
|
85
85
|
this.onChange = onChange;
|
|
86
86
|
this.sendToWidget = sendToWidget;
|
|
87
|
-
this.dirtyFields = new
|
|
87
|
+
this.dirtyFields = new Map(
|
|
88
|
+
[...initialDirtyFields].map((key) => [key, this.data[key]]),
|
|
89
|
+
);
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
private listeners: Record<string, Set<EventHandler>> = {};
|
|
@@ -106,12 +108,16 @@ export class Model<T extends Record<string, any>> implements AnyModel<T> {
|
|
|
106
108
|
send(
|
|
107
109
|
content: any,
|
|
108
110
|
callbacks?: any,
|
|
109
|
-
|
|
111
|
+
_buffers?: ArrayBuffer[] | ArrayBufferView[],
|
|
110
112
|
): void {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
const { state, bufferPaths, buffers } = serializeBuffersToBase64(content);
|
|
114
|
+
this.sendToWidget({
|
|
115
|
+
content: {
|
|
116
|
+
state: state,
|
|
117
|
+
bufferPaths: bufferPaths,
|
|
118
|
+
},
|
|
119
|
+
buffers: buffers,
|
|
120
|
+
}).then(callbacks);
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
widget_manager = {
|
|
@@ -134,7 +140,7 @@ export class Model<T extends Record<string, any>> implements AnyModel<T> {
|
|
|
134
140
|
|
|
135
141
|
set<K extends keyof T>(key: K, value: T[K]): void {
|
|
136
142
|
this.data = { ...this.data, [key]: value };
|
|
137
|
-
this.dirtyFields.
|
|
143
|
+
this.dirtyFields.set(key, value);
|
|
138
144
|
this.emit(`change:${key as K & string}`, value);
|
|
139
145
|
this.emitAnyChange();
|
|
140
146
|
}
|
|
@@ -143,24 +149,25 @@ export class Model<T extends Record<string, any>> implements AnyModel<T> {
|
|
|
143
149
|
if (this.dirtyFields.size === 0) {
|
|
144
150
|
return;
|
|
145
151
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
// stores the last value sent, and not a merge of the values.
|
|
154
|
-
// When the backend knows to merge the partial updates, then we can clear
|
|
155
|
-
// the dirty fields.
|
|
156
|
-
// this.dirtyFields.clear();
|
|
152
|
+
// Only send the dirty fields, not the entire state.
|
|
153
|
+
const partialData = Object.fromEntries(
|
|
154
|
+
this.dirtyFields.entries(),
|
|
155
|
+
) as Partial<T>;
|
|
156
|
+
|
|
157
|
+
// Clear the dirty fields to avoid sending again.
|
|
158
|
+
this.dirtyFields.clear();
|
|
157
159
|
this.onChange(partialData);
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
updateAndEmitDiffs(value: T): void {
|
|
163
|
+
if (value == null) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
161
167
|
Object.keys(value).forEach((key) => {
|
|
162
168
|
const k = key as keyof T;
|
|
163
|
-
|
|
169
|
+
// Shallow equal since these can be large objects
|
|
170
|
+
if (this.data[k] !== value[k]) {
|
|
164
171
|
this.set(k, value[k]);
|
|
165
172
|
}
|
|
166
173
|
});
|
|
@@ -170,13 +177,22 @@ export class Model<T extends Record<string, any>> implements AnyModel<T> {
|
|
|
170
177
|
* When receiving a message from the backend.
|
|
171
178
|
* We want to notify all listeners with `msg:custom`
|
|
172
179
|
*/
|
|
173
|
-
receiveCustomMessage(
|
|
180
|
+
receiveCustomMessage(
|
|
181
|
+
message: unknown,
|
|
182
|
+
buffers: readonly DataView[] = [],
|
|
183
|
+
): void {
|
|
174
184
|
const response = AnyWidgetMessageSchema.safeParse(message);
|
|
175
185
|
if (response.success) {
|
|
176
186
|
const data = response.data;
|
|
177
187
|
switch (data.method) {
|
|
178
188
|
case "update":
|
|
179
|
-
this.updateAndEmitDiffs(
|
|
189
|
+
this.updateAndEmitDiffs(
|
|
190
|
+
decodeFromWire<T>({
|
|
191
|
+
state: data.state as T,
|
|
192
|
+
bufferPaths: data.buffer_paths ?? [],
|
|
193
|
+
buffers,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
180
196
|
break;
|
|
181
197
|
case "custom":
|
|
182
198
|
this.listeners["msg:custom"]?.forEach((cb) =>
|
|
@@ -184,7 +200,19 @@ export class Model<T extends Record<string, any>> implements AnyModel<T> {
|
|
|
184
200
|
);
|
|
185
201
|
break;
|
|
186
202
|
case "open":
|
|
187
|
-
this.updateAndEmitDiffs(
|
|
203
|
+
this.updateAndEmitDiffs(
|
|
204
|
+
decodeFromWire<T>({
|
|
205
|
+
state: data.state as T,
|
|
206
|
+
bufferPaths: data.buffer_paths ?? [],
|
|
207
|
+
buffers,
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
break;
|
|
211
|
+
case "echo_update":
|
|
212
|
+
// We don't need to do anything with this message
|
|
213
|
+
break;
|
|
214
|
+
default:
|
|
215
|
+
Logger.error("[anywidget] Unknown message method", data.method);
|
|
188
216
|
break;
|
|
189
217
|
}
|
|
190
218
|
} else {
|
|
@@ -269,7 +297,7 @@ export async function handleWidgetMessage({
|
|
|
269
297
|
|
|
270
298
|
if (msg.method === "custom") {
|
|
271
299
|
const model = await modelManager.get(modelId);
|
|
272
|
-
model.receiveCustomMessage(msg);
|
|
300
|
+
model.receiveCustomMessage(msg, buffers);
|
|
273
301
|
return;
|
|
274
302
|
}
|
|
275
303
|
|
|
@@ -279,24 +307,23 @@ export async function handleWidgetMessage({
|
|
|
279
307
|
}
|
|
280
308
|
|
|
281
309
|
const { method, state, buffer_paths = [] } = msg;
|
|
282
|
-
const stateWithBuffers =
|
|
310
|
+
const stateWithBuffers = decodeFromWire({
|
|
311
|
+
state,
|
|
312
|
+
bufferPaths: buffer_paths,
|
|
313
|
+
buffers,
|
|
314
|
+
});
|
|
283
315
|
|
|
284
316
|
if (method === "open") {
|
|
285
317
|
const handleDataChange = (changeData: Record<string, any>) => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
"Changed data with buffer paths may not be supported",
|
|
289
|
-
changeData,
|
|
290
|
-
);
|
|
291
|
-
// TODO: we may want to extract/undo DataView, to get back buffers and buffer_paths
|
|
292
|
-
}
|
|
318
|
+
const { state, buffers, bufferPaths } =
|
|
319
|
+
serializeBuffersToBase64(changeData);
|
|
293
320
|
getRequestClient().sendModelValue({
|
|
294
321
|
modelId: modelId,
|
|
295
322
|
message: {
|
|
296
|
-
state
|
|
297
|
-
bufferPaths
|
|
323
|
+
state,
|
|
324
|
+
bufferPaths,
|
|
298
325
|
},
|
|
299
|
-
buffers
|
|
326
|
+
buffers,
|
|
300
327
|
});
|
|
301
328
|
};
|
|
302
329
|
|
|
@@ -100,12 +100,14 @@ describe("isConditionValueValid", () => {
|
|
|
100
100
|
);
|
|
101
101
|
expect(isConditionValueValid("contains", "test")).toBe(true);
|
|
102
102
|
expect(isConditionValueValid("in", ["test"])).toBe(true);
|
|
103
|
+
expect(isConditionValueValid("not_in", ["test"])).toBe(true);
|
|
103
104
|
});
|
|
104
105
|
|
|
105
106
|
it("should return false if the value is not valid according to the schema for the given operator", () => {
|
|
106
107
|
expect(isConditionValueValid("==", "not a number")).toBe(false);
|
|
107
108
|
expect(isConditionValueValid("contains", 123)).toBe(false);
|
|
108
109
|
expect(isConditionValueValid("in", "not an array")).toBe(false);
|
|
110
|
+
expect(isConditionValueValid("not_in", "not an array")).toBe(false);
|
|
109
111
|
});
|
|
110
112
|
|
|
111
113
|
it("should return true if the operator does not require a value", () => {
|
|
@@ -113,28 +113,30 @@ const NavMenuComponent = ({
|
|
|
113
113
|
const renderMenuItem = (item: MenuItem | MenuItemGroup) => {
|
|
114
114
|
if ("items" in item) {
|
|
115
115
|
return orientation === "horizontal" ? (
|
|
116
|
-
<
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
116
|
+
<NavigationMenu orientation="horizontal" key={item.label}>
|
|
117
|
+
<NavigationMenuList>
|
|
118
|
+
<NavigationMenuItem>
|
|
119
|
+
<NavigationMenuTrigger>
|
|
120
|
+
{renderHTML({ html: item.label })}
|
|
121
|
+
</NavigationMenuTrigger>
|
|
122
|
+
<NavigationMenuContent>
|
|
123
|
+
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
|
|
124
|
+
{item.items.map((subItem) => (
|
|
125
|
+
<ListItem
|
|
126
|
+
key={subItem.label}
|
|
127
|
+
label={subItem.label}
|
|
128
|
+
href={preserveQueryParams(subItem.href)}
|
|
129
|
+
target={target(subItem.href)}
|
|
130
|
+
>
|
|
131
|
+
{subItem.description &&
|
|
132
|
+
renderHTML({ html: subItem.description })}
|
|
133
|
+
</ListItem>
|
|
134
|
+
))}
|
|
135
|
+
</ul>
|
|
136
|
+
</NavigationMenuContent>
|
|
137
|
+
</NavigationMenuItem>
|
|
138
|
+
</NavigationMenuList>
|
|
139
|
+
</NavigationMenu>
|
|
138
140
|
) : (
|
|
139
141
|
<NavigationMenuItem key={item.label}>
|
|
140
142
|
<div
|
|
@@ -4,8 +4,11 @@ import { TriangleIcon } from "lucide-react";
|
|
|
4
4
|
import type { JSX } from "react";
|
|
5
5
|
import { useLocale } from "react-aria";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
import { getMimeValues } from "@/components/data-table/mime-cell";
|
|
7
8
|
import { cn } from "@/utils/cn";
|
|
9
|
+
import { Logger } from "@/utils/Logger";
|
|
8
10
|
import { prettyNumber } from "@/utils/numbers";
|
|
11
|
+
import { renderHTML } from "../core/RenderHTML";
|
|
9
12
|
import type {
|
|
10
13
|
IStatelessPlugin,
|
|
11
14
|
IStatelessPluginProps,
|
|
@@ -18,6 +21,7 @@ interface Data {
|
|
|
18
21
|
bordered?: boolean;
|
|
19
22
|
direction?: "increase" | "decrease";
|
|
20
23
|
target_direction?: "increase" | "decrease";
|
|
24
|
+
slot?: object;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export class StatPlugin implements IStatelessPlugin<Data> {
|
|
@@ -30,6 +34,7 @@ export class StatPlugin implements IStatelessPlugin<Data> {
|
|
|
30
34
|
bordered: z.boolean().default(false),
|
|
31
35
|
direction: z.enum(["increase", "decrease"]).optional(),
|
|
32
36
|
target_direction: z.enum(["increase", "decrease"]).default("increase"),
|
|
37
|
+
slot: z.any().optional(),
|
|
33
38
|
});
|
|
34
39
|
|
|
35
40
|
render({ data }: IStatelessPluginProps<Data>): JSX.Element {
|
|
@@ -44,6 +49,7 @@ export const StatComponent: React.FC<Data> = ({
|
|
|
44
49
|
bordered,
|
|
45
50
|
direction,
|
|
46
51
|
target_direction,
|
|
52
|
+
slot,
|
|
47
53
|
}) => {
|
|
48
54
|
const { locale } = useLocale();
|
|
49
55
|
|
|
@@ -71,39 +77,53 @@ export const StatComponent: React.FC<Data> = ({
|
|
|
71
77
|
const fillColor = onTarget ? "var(--grass-8)" : "var(--red-8)";
|
|
72
78
|
const strokeColor = onTarget ? "var(--grass-9)" : "var(--red-9)";
|
|
73
79
|
|
|
80
|
+
const renderSlot = () => {
|
|
81
|
+
const mimeValues = getMimeValues(slot);
|
|
82
|
+
if (mimeValues?.[0]) {
|
|
83
|
+
const { mimetype, data } = mimeValues[0];
|
|
84
|
+
if (mimetype !== "text/html") {
|
|
85
|
+
Logger.warn(`Expected text/html, got ${mimetype}`);
|
|
86
|
+
}
|
|
87
|
+
return renderHTML({ html: data, alwaysSanitizeHtml: true });
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
74
91
|
return (
|
|
75
92
|
<div
|
|
76
93
|
className={cn(
|
|
77
|
-
"text-card-foreground",
|
|
94
|
+
"text-card-foreground p-6",
|
|
78
95
|
bordered && "rounded-xl border shadow bg-card",
|
|
79
96
|
)}
|
|
80
97
|
>
|
|
81
98
|
{label && (
|
|
82
|
-
<div className="
|
|
99
|
+
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
83
100
|
<h3 className="tracking-tight text-sm font-medium">{label}</h3>
|
|
84
101
|
</div>
|
|
85
102
|
)}
|
|
86
|
-
<div className="
|
|
87
|
-
<div
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
<div className="pt-0 flex flex-row gap-3.5">
|
|
104
|
+
<div>
|
|
105
|
+
<div className="text-2xl font-bold">{renderPrettyValue()}</div>
|
|
106
|
+
{caption && (
|
|
107
|
+
<p className="pt-1 text-xs text-muted-foreground flex align-center whitespace-nowrap">
|
|
108
|
+
{direction === "increase" && (
|
|
109
|
+
<TriangleIcon
|
|
110
|
+
className="w-4 h-4 mr-1 p-0.5"
|
|
111
|
+
fill={fillColor}
|
|
112
|
+
stroke={strokeColor}
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
{direction === "decrease" && (
|
|
116
|
+
<TriangleIcon
|
|
117
|
+
className="w-4 h-4 mr-1 p-0.5 transform rotate-180"
|
|
118
|
+
fill={fillColor}
|
|
119
|
+
stroke={strokeColor}
|
|
120
|
+
/>
|
|
121
|
+
)}
|
|
122
|
+
{caption}
|
|
123
|
+
</p>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
{slot && <div className="[--slot:true]">{renderSlot()}</div>}
|
|
107
127
|
</div>
|
|
108
128
|
</div>
|
|
109
129
|
);
|