@marimo-team/islands 0.23.9-dev45 → 0.23.9-dev47
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/{code-visibility-CDsEg2QC.js → code-visibility-B_FaVMb1.js} +1 -1
- package/dist/main.js +1097 -1071
- package/dist/{reveal-component-X3DvD8Y2.js → reveal-component-BsF-TmCD.js} +300 -279
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/editor/Disconnected.tsx +1 -60
- package/src/components/editor/__tests__/viewer-banner.test.tsx +89 -0
- package/src/components/editor/header/__tests__/status.test.tsx +0 -15
- package/src/components/editor/header/app-header.tsx +1 -4
- package/src/components/editor/header/status.tsx +4 -13
- package/src/components/editor/renderers/slides-layout/__tests__/compute-slide-cells.test.ts +30 -17
- package/src/components/editor/renderers/slides-layout/compute-slide-cells.ts +17 -8
- package/src/components/editor/renderers/slides-layout/slides-layout.tsx +9 -11
- package/src/components/editor/viewer-banner.tsx +82 -0
- package/src/components/slides/minimap.tsx +45 -9
- package/src/components/slides/reveal-component.tsx +79 -34
- package/src/core/edit-app.tsx +3 -0
- package/src/core/islands/bootstrap.ts +2 -0
- package/src/core/kernel/__tests__/handlers.test.ts +5 -0
- package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +0 -13
- package/src/core/websocket/types.ts +0 -6
- package/src/core/websocket/useMarimoKernelConnection.tsx +3 -12
|
@@ -66,6 +66,7 @@ interface SlideThumbnailCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
66
66
|
isActiveDragSource?: boolean;
|
|
67
67
|
isOverlay?: boolean;
|
|
68
68
|
isVisible?: boolean;
|
|
69
|
+
isNoOutput?: boolean;
|
|
69
70
|
slideType?: SlideType;
|
|
70
71
|
ref?: React.Ref<HTMLDivElement>;
|
|
71
72
|
}
|
|
@@ -77,6 +78,7 @@ interface SlideThumbnailRowProps extends React.ButtonHTMLAttributes<HTMLButtonEl
|
|
|
77
78
|
dropIndicator?: DropPosition | null;
|
|
78
79
|
isActiveDragSource?: boolean;
|
|
79
80
|
isVisible?: boolean;
|
|
81
|
+
isNoOutput?: boolean;
|
|
80
82
|
slideType?: SlideType;
|
|
81
83
|
ref?: React.Ref<HTMLButtonElement>;
|
|
82
84
|
}
|
|
@@ -86,15 +88,23 @@ interface SlidesMinimapProps {
|
|
|
86
88
|
thumbnailWidth: number;
|
|
87
89
|
canReorder: boolean;
|
|
88
90
|
activeCellId: CellId | null;
|
|
89
|
-
// Set of cell ids that
|
|
91
|
+
// Set of cell ids that should be visually treated like skipped entries.
|
|
90
92
|
skippedIds?: ReadonlySet<CellId>;
|
|
93
|
+
// Set of cell ids that currently have no rendered output.
|
|
94
|
+
noOutputIds?: ReadonlySet<CellId>;
|
|
91
95
|
slideTypes?: ReadonlyMap<CellId, SlideType>;
|
|
92
96
|
onSlideClick: (index: number) => void;
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
interface ThumbnailVisual {
|
|
100
|
+
label: string;
|
|
101
|
+
description: string;
|
|
102
|
+
Icon?: LucideIcon;
|
|
103
|
+
}
|
|
104
|
+
|
|
95
105
|
function getSlideTypeVisual(
|
|
96
106
|
slideType: SlideType | undefined,
|
|
97
|
-
):
|
|
107
|
+
): ThumbnailVisual | null {
|
|
98
108
|
if (!slideType || slideType === "slide") {
|
|
99
109
|
return null;
|
|
100
110
|
}
|
|
@@ -102,8 +112,13 @@ function getSlideTypeVisual(
|
|
|
102
112
|
return { label, description, Icon };
|
|
103
113
|
}
|
|
104
114
|
|
|
115
|
+
const NO_OUTPUT_VISUAL: ThumbnailVisual = {
|
|
116
|
+
label: "No output",
|
|
117
|
+
description: "Hidden because this cell has no output.",
|
|
118
|
+
};
|
|
119
|
+
|
|
105
120
|
const SLIDE_ASPECT_RATIO = 16 / 9;
|
|
106
|
-
const SLIDE_BASE_WIDTH =
|
|
121
|
+
const SLIDE_BASE_WIDTH = 520;
|
|
107
122
|
|
|
108
123
|
function computeThumbnailDimensions(width: number): ThumbnailDimensions {
|
|
109
124
|
return {
|
|
@@ -196,6 +211,7 @@ export const SlidesMinimap = ({
|
|
|
196
211
|
canReorder,
|
|
197
212
|
activeCellId,
|
|
198
213
|
skippedIds,
|
|
214
|
+
noOutputIds,
|
|
199
215
|
slideTypes,
|
|
200
216
|
onSlideClick,
|
|
201
217
|
}: SlidesMinimapProps) => {
|
|
@@ -288,6 +304,7 @@ export const SlidesMinimap = ({
|
|
|
288
304
|
dimensions={dimensions}
|
|
289
305
|
isActiveSlide={cell.id === activeCellId}
|
|
290
306
|
isVisible={visibleIds.has(cell.id)}
|
|
307
|
+
isNoOutput={noOutputIds?.has(cell.id)}
|
|
291
308
|
slideType={resolveSlideType({
|
|
292
309
|
cellId: cell.id,
|
|
293
310
|
slideTypes,
|
|
@@ -325,6 +342,7 @@ export const SlidesMinimap = ({
|
|
|
325
342
|
isActive={activeId === cell.id}
|
|
326
343
|
isActiveSlide={cell.id === activeCellId}
|
|
327
344
|
isVisible={visibleIds.has(cell.id)}
|
|
345
|
+
isNoOutput={noOutputIds?.has(cell.id)}
|
|
328
346
|
slideType={resolveSlideType({
|
|
329
347
|
cellId: cell.id,
|
|
330
348
|
slideTypes,
|
|
@@ -347,6 +365,7 @@ export const SlidesMinimap = ({
|
|
|
347
365
|
dimensions={dimensions}
|
|
348
366
|
isOverlay={true}
|
|
349
367
|
isActiveDragSource={true}
|
|
368
|
+
isNoOutput={noOutputIds?.has(activeCell.id)}
|
|
350
369
|
/>
|
|
351
370
|
)}
|
|
352
371
|
</DragOverlay>
|
|
@@ -378,6 +397,7 @@ interface SortableSlideThumbnailProps {
|
|
|
378
397
|
isActive: boolean;
|
|
379
398
|
isActiveSlide?: boolean;
|
|
380
399
|
isVisible?: boolean;
|
|
400
|
+
isNoOutput?: boolean;
|
|
381
401
|
slideType?: SlideType;
|
|
382
402
|
onClick?: () => void;
|
|
383
403
|
}
|
|
@@ -389,6 +409,7 @@ const SortableSlideThumbnail = ({
|
|
|
389
409
|
isActive,
|
|
390
410
|
isActiveSlide,
|
|
391
411
|
isVisible,
|
|
412
|
+
isNoOutput,
|
|
392
413
|
slideType,
|
|
393
414
|
onClick,
|
|
394
415
|
}: SortableSlideThumbnailProps) => {
|
|
@@ -405,6 +426,7 @@ const SortableSlideThumbnail = ({
|
|
|
405
426
|
isActiveDragSource={isActive}
|
|
406
427
|
isActiveSlide={isActiveSlide}
|
|
407
428
|
isVisible={isVisible}
|
|
429
|
+
isNoOutput={isNoOutput}
|
|
408
430
|
slideType={slideType}
|
|
409
431
|
onClick={onClick}
|
|
410
432
|
{...attributes}
|
|
@@ -422,6 +444,7 @@ const SlideThumbnailRow = ({
|
|
|
422
444
|
isActiveSlide = false,
|
|
423
445
|
isActiveDragSource = false,
|
|
424
446
|
isVisible,
|
|
447
|
+
isNoOutput,
|
|
425
448
|
slideType,
|
|
426
449
|
onClick,
|
|
427
450
|
ref,
|
|
@@ -462,6 +485,7 @@ const SlideThumbnailRow = ({
|
|
|
462
485
|
isActiveSlide={isActiveSlide}
|
|
463
486
|
isActiveDragSource={isActiveDragSource}
|
|
464
487
|
isVisible={isVisible}
|
|
488
|
+
isNoOutput={isNoOutput}
|
|
465
489
|
slideType={slideType}
|
|
466
490
|
/>
|
|
467
491
|
</button>
|
|
@@ -477,13 +501,14 @@ const SlideThumbnailCard = ({
|
|
|
477
501
|
isActiveDragSource = false,
|
|
478
502
|
isOverlay = false,
|
|
479
503
|
isVisible = false,
|
|
504
|
+
isNoOutput = false,
|
|
480
505
|
slideType,
|
|
481
506
|
ref,
|
|
482
507
|
...props
|
|
483
508
|
}: SlideThumbnailCardProps) => {
|
|
484
509
|
const { width, height, scale } = dimensions;
|
|
485
|
-
const visual = getSlideTypeVisual(slideType);
|
|
486
|
-
const isSkipped = slideType === "skip";
|
|
510
|
+
const visual = isNoOutput ? NO_OUTPUT_VISUAL : getSlideTypeVisual(slideType);
|
|
511
|
+
const isSkipped = isNoOutput || slideType === "skip";
|
|
487
512
|
|
|
488
513
|
const outerStyle: React.CSSProperties = {
|
|
489
514
|
width,
|
|
@@ -525,16 +550,20 @@ const SlideThumbnailCard = ({
|
|
|
525
550
|
height: height / scale,
|
|
526
551
|
}}
|
|
527
552
|
>
|
|
528
|
-
|
|
553
|
+
{isNoOutput ? (
|
|
554
|
+
<MiniCodePreview code={cell.code} />
|
|
555
|
+
) : (
|
|
556
|
+
<Slide cellId={cell.id} status={cell.status} output={cell.output} />
|
|
557
|
+
)}
|
|
529
558
|
</div>
|
|
530
559
|
)}
|
|
531
560
|
{isSkipped && (
|
|
532
561
|
<div
|
|
533
|
-
className="absolute inset-0 bg-muted/
|
|
562
|
+
className="absolute inset-0 bg-muted/50 pointer-events-none"
|
|
534
563
|
aria-hidden={true}
|
|
535
564
|
/>
|
|
536
565
|
)}
|
|
537
|
-
{visual && (
|
|
566
|
+
{visual?.Icon && (
|
|
538
567
|
<Tooltip
|
|
539
568
|
content={
|
|
540
569
|
<span className="text-xs opacity-80">{visual.description}</span>
|
|
@@ -550,7 +579,6 @@ const SlideThumbnailCard = ({
|
|
|
550
579
|
aria-label={visual.label}
|
|
551
580
|
>
|
|
552
581
|
<visual.Icon className="h-3.5 w-3.5" />
|
|
553
|
-
{/* <span>{visual.label}</span> */}
|
|
554
582
|
</span>
|
|
555
583
|
</Tooltip>
|
|
556
584
|
)}
|
|
@@ -558,6 +586,14 @@ const SlideThumbnailCard = ({
|
|
|
558
586
|
);
|
|
559
587
|
};
|
|
560
588
|
|
|
589
|
+
const MiniCodePreview = ({ code }: { code: string }) => {
|
|
590
|
+
return (
|
|
591
|
+
<pre className="my-auto w-full overflow-hidden whitespace-pre-wrap wrap-break-word text-lg">
|
|
592
|
+
{code}
|
|
593
|
+
</pre>
|
|
594
|
+
);
|
|
595
|
+
};
|
|
596
|
+
|
|
561
597
|
function projectDropTarget(
|
|
562
598
|
event: DragMoveEvent | DragOverEvent,
|
|
563
599
|
): ProjectedDropTarget | null {
|
|
@@ -221,22 +221,50 @@ const SubslideView = ({
|
|
|
221
221
|
);
|
|
222
222
|
};
|
|
223
223
|
|
|
224
|
+
const ParkedPreviewContent = ({
|
|
225
|
+
cell,
|
|
226
|
+
isNoOutputPreview,
|
|
227
|
+
isEditable,
|
|
228
|
+
codeShown,
|
|
229
|
+
}: {
|
|
230
|
+
cell: RuntimeCell;
|
|
231
|
+
isNoOutputPreview: boolean;
|
|
232
|
+
isEditable: boolean;
|
|
233
|
+
codeShown: boolean;
|
|
234
|
+
}) => {
|
|
235
|
+
if (isNoOutputPreview && isEditable) {
|
|
236
|
+
return <SlideCellView cell={cell} />;
|
|
237
|
+
}
|
|
238
|
+
if (isNoOutputPreview && codeShown) {
|
|
239
|
+
return <SlideCellReadOnlyView cell={cell} />;
|
|
240
|
+
}
|
|
241
|
+
return (
|
|
242
|
+
<CellOutputSlide
|
|
243
|
+
cellId={cell.id}
|
|
244
|
+
status={cell.status}
|
|
245
|
+
output={cell.output}
|
|
246
|
+
/>
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
224
250
|
// There is an upstream react bug in dev mode (https://github.com/facebook/react/issues/34840)
|
|
225
251
|
// Uncaught SecurityError: Failed to read a named property '$$typeof' from 'Window'
|
|
226
252
|
// Happens with cells containing iframes / external content
|
|
227
253
|
const RevealSlidesComponent = ({
|
|
228
|
-
|
|
254
|
+
slideCells,
|
|
229
255
|
layout,
|
|
230
256
|
setLayout,
|
|
257
|
+
noOutputIds,
|
|
231
258
|
activeIndex,
|
|
232
259
|
onSlideChange,
|
|
233
260
|
mode,
|
|
234
261
|
configWidth, // px
|
|
235
262
|
isEditable = false,
|
|
236
263
|
}: {
|
|
237
|
-
|
|
264
|
+
slideCells: RuntimeCell[];
|
|
238
265
|
layout: SlidesLayout;
|
|
239
266
|
setLayout: (layout: SlidesLayout) => void;
|
|
267
|
+
noOutputIds: ReadonlySet<CellId>;
|
|
240
268
|
activeIndex?: number;
|
|
241
269
|
onSlideChange?: (index: number) => void;
|
|
242
270
|
mode: AppMode;
|
|
@@ -255,42 +283,46 @@ const RevealSlidesComponent = ({
|
|
|
255
283
|
);
|
|
256
284
|
|
|
257
285
|
const [showCode, setShowCode] = useState(false);
|
|
258
|
-
const codeAvailable = useNotebookCodeAvailable(
|
|
286
|
+
const codeAvailable = useNotebookCodeAvailable(slideCells);
|
|
259
287
|
const codeToggleEnabled = !isIslands() && codeAvailable;
|
|
260
288
|
const codeShown = codeToggleEnabled && showCode;
|
|
261
289
|
|
|
262
|
-
const activeCell =
|
|
263
|
-
activeIndex != null ? cellsWithOutput[activeIndex] : undefined;
|
|
290
|
+
const activeCell = activeIndex != null ? slideCells[activeIndex] : undefined;
|
|
264
291
|
// Fall back to the first cell while the deck settles on an initial slide.
|
|
265
292
|
// Still `undefined` when the deck is empty (handled below).
|
|
266
|
-
const activeConfigCell = activeCell ??
|
|
293
|
+
const activeConfigCell = activeCell ?? slideCells.at(0);
|
|
267
294
|
|
|
268
295
|
const composition = useMemo(
|
|
269
296
|
() =>
|
|
270
297
|
composeSlides({
|
|
271
|
-
cells:
|
|
298
|
+
cells: slideCells,
|
|
272
299
|
getType: (cell) =>
|
|
273
|
-
|
|
300
|
+
noOutputIds.has(cell.id)
|
|
301
|
+
? "skip"
|
|
302
|
+
: (layout.cells.get(cell.id)?.type ?? DEFAULT_SLIDE_TYPE),
|
|
274
303
|
}),
|
|
275
|
-
[
|
|
304
|
+
[slideCells, noOutputIds, layout.cells],
|
|
276
305
|
);
|
|
277
306
|
|
|
278
|
-
//
|
|
279
|
-
// minimap we render a preview over the deck and park reveal on
|
|
280
|
-
// real slide; keyboard nav while parked is handled below.
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
307
|
+
// Skipped and output-less cells aren't part of the composed deck. When one is
|
|
308
|
+
// selected in the minimap we render a preview over the deck and park reveal on
|
|
309
|
+
// a neighboring real slide; keyboard nav while parked is handled below.
|
|
310
|
+
const activeCellSlideType = activeCell
|
|
311
|
+
? layout.cells.get(activeCell.id)?.type
|
|
312
|
+
: undefined;
|
|
313
|
+
const isNoOutputPreview =
|
|
314
|
+
activeCell != null && noOutputIds.has(activeCell.id);
|
|
315
|
+
const isParkedPreview = activeCellSlideType === "skip" || isNoOutputPreview;
|
|
316
|
+
const parkedPreviewCell = isParkedPreview ? activeCell : null;
|
|
285
317
|
|
|
286
318
|
const { cellToTarget, targetToCellIndex } = useMemo(
|
|
287
319
|
() =>
|
|
288
320
|
buildSlideIndices({
|
|
289
321
|
composition,
|
|
290
|
-
cells:
|
|
322
|
+
cells: slideCells,
|
|
291
323
|
getId: (c) => c.id,
|
|
292
324
|
}),
|
|
293
|
-
[composition,
|
|
325
|
+
[composition, slideCells],
|
|
294
326
|
);
|
|
295
327
|
|
|
296
328
|
const deckTransition = layout.deck?.transition ?? DEFAULT_DECK_TRANSITION;
|
|
@@ -322,7 +354,7 @@ const RevealSlidesComponent = ({
|
|
|
322
354
|
const navigateDeckToActiveCell = useEvent((deck: RevealApi) => {
|
|
323
355
|
const target = resolveDeckNavigationTarget({
|
|
324
356
|
activeIndex,
|
|
325
|
-
cells:
|
|
357
|
+
cells: slideCells,
|
|
326
358
|
cellToTarget,
|
|
327
359
|
getId: (cell) => cell.id,
|
|
328
360
|
});
|
|
@@ -340,7 +372,7 @@ const RevealSlidesComponent = ({
|
|
|
340
372
|
return;
|
|
341
373
|
}
|
|
342
374
|
navigateDeckToActiveCell(deck);
|
|
343
|
-
}, [activeIndex, cellToTarget,
|
|
375
|
+
}, [activeIndex, cellToTarget, slideCells, navigateDeckToActiveCell]);
|
|
344
376
|
|
|
345
377
|
// Toggling code (re)mounts a CodeMirror editor on the active slide. Defer
|
|
346
378
|
// the state update so the button/keypress paints first and the heavier mount
|
|
@@ -380,12 +412,12 @@ const RevealSlidesComponent = ({
|
|
|
380
412
|
return { h: target.h, v: target.v };
|
|
381
413
|
}, [activeCell, cellToTarget]);
|
|
382
414
|
|
|
383
|
-
// Forward the deck's current cell to the parent, except while a
|
|
415
|
+
// Forward the deck's current cell to the parent, except while a parked
|
|
384
416
|
// preview is parked: every reveal.js event during that window is an echo
|
|
385
417
|
// of the programmatic park (possibly with transient indices), so ignoring
|
|
386
|
-
// them keeps `activeCellId` pinned on the
|
|
418
|
+
// them keeps `activeCellId` pinned on the minimap cell.
|
|
387
419
|
const reportCurrentCell = useEvent(() => {
|
|
388
|
-
if (
|
|
420
|
+
if (parkedPreviewCell != null) {
|
|
389
421
|
return;
|
|
390
422
|
}
|
|
391
423
|
const deck = deckRef.current;
|
|
@@ -401,10 +433,10 @@ const RevealSlidesComponent = ({
|
|
|
401
433
|
}
|
|
402
434
|
});
|
|
403
435
|
|
|
404
|
-
// While parked on a
|
|
436
|
+
// While parked on a preview, step through minimap order instead of
|
|
405
437
|
// letting reveal.js advance from the parked slide the user can't see.
|
|
406
438
|
const handleParkedNavKey = useEvent((event: KeyboardEvent) => {
|
|
407
|
-
if (!
|
|
439
|
+
if (!parkedPreviewCell || activeIndex == null) {
|
|
408
440
|
return;
|
|
409
441
|
}
|
|
410
442
|
if (Events.fromInput(event)) {
|
|
@@ -418,7 +450,7 @@ const RevealSlidesComponent = ({
|
|
|
418
450
|
event.preventDefault();
|
|
419
451
|
event.stopPropagation();
|
|
420
452
|
const nextIndex = activeIndex + direction;
|
|
421
|
-
if (nextIndex < 0 || nextIndex >=
|
|
453
|
+
if (nextIndex < 0 || nextIndex >= slideCells.length) {
|
|
422
454
|
return;
|
|
423
455
|
}
|
|
424
456
|
onSlideChange?.(nextIndex);
|
|
@@ -431,6 +463,10 @@ const RevealSlidesComponent = ({
|
|
|
431
463
|
|
|
432
464
|
useEventListener(document, "keydown", handleParkedNavKey, { capture: true });
|
|
433
465
|
|
|
466
|
+
const parkedPreviewLabel = isNoOutputPreview
|
|
467
|
+
? "Hidden as there is no output"
|
|
468
|
+
: "Skipped in presentation";
|
|
469
|
+
|
|
434
470
|
const slideArea = (
|
|
435
471
|
<div
|
|
436
472
|
ref={containerRef}
|
|
@@ -480,21 +516,30 @@ const RevealSlidesComponent = ({
|
|
|
480
516
|
);
|
|
481
517
|
})}
|
|
482
518
|
</Deck>
|
|
483
|
-
{
|
|
519
|
+
{parkedPreviewCell && (
|
|
484
520
|
<div
|
|
521
|
+
key={parkedPreviewCell.id}
|
|
485
522
|
className="absolute inset-0 z-10 border rounded bg-background flex flex-col overflow-hidden"
|
|
486
|
-
aria-label=
|
|
523
|
+
aria-label={parkedPreviewLabel}
|
|
487
524
|
>
|
|
488
525
|
<div className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground border-b bg-muted/40">
|
|
489
526
|
<EyeOffIcon className="h-3.5 w-3.5" />
|
|
490
|
-
<span>
|
|
527
|
+
<span>{parkedPreviewLabel}</span>
|
|
491
528
|
</div>
|
|
492
529
|
<div className="flex-1 overflow-auto flex">
|
|
493
|
-
<div
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
530
|
+
<div
|
|
531
|
+
className={
|
|
532
|
+
isNoOutputPreview && (isEditable || codeShown)
|
|
533
|
+
? "mo-slide-content flex flex-col gap-3"
|
|
534
|
+
: "mo-slide-content"
|
|
535
|
+
}
|
|
536
|
+
style={{ margin: "auto 20px" }}
|
|
537
|
+
>
|
|
538
|
+
<ParkedPreviewContent
|
|
539
|
+
cell={parkedPreviewCell}
|
|
540
|
+
isNoOutputPreview={isNoOutputPreview}
|
|
541
|
+
isEditable={isEditable}
|
|
542
|
+
codeShown={codeShown}
|
|
498
543
|
/>
|
|
499
544
|
</div>
|
|
500
545
|
</div>
|
package/src/core/edit-app.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import { Controls } from "@/components/editor/controls/Controls";
|
|
|
12
12
|
import { AppHeader } from "@/components/editor/header/app-header";
|
|
13
13
|
import { FilenameForm } from "@/components/editor/header/filename-form";
|
|
14
14
|
import { MultiCellActionToolbar } from "@/components/editor/navigation/multi-cell-action-toolbar";
|
|
15
|
+
import { ViewerBanner } from "@/components/editor/viewer-banner";
|
|
15
16
|
import { cn } from "@/utils/cn";
|
|
16
17
|
import { Paths } from "@/utils/paths";
|
|
17
18
|
import { AppContainer } from "../components/editor/app-container";
|
|
@@ -164,6 +165,8 @@ export const EditApp: React.FC<AppProps> = ({
|
|
|
164
165
|
)}
|
|
165
166
|
</AppHeader>
|
|
166
167
|
|
|
168
|
+
<ViewerBanner />
|
|
169
|
+
|
|
167
170
|
{/* Don't render until we have a single cell */}
|
|
168
171
|
{hasCells && (
|
|
169
172
|
<CellsRenderer appConfig={appConfig} mode={viewState.mode}>
|
|
@@ -90,6 +90,7 @@ describe("buildCellData", () => {
|
|
|
90
90
|
terminal: false,
|
|
91
91
|
},
|
|
92
92
|
auto_instantiated: false,
|
|
93
|
+
consumer_capabilities: { edit: true, interact: true },
|
|
93
94
|
};
|
|
94
95
|
|
|
95
96
|
const cells = buildCellData(kernelReadyData);
|
|
@@ -158,6 +159,7 @@ describe("buildCellData", () => {
|
|
|
158
159
|
terminal: false,
|
|
159
160
|
},
|
|
160
161
|
auto_instantiated: false,
|
|
162
|
+
consumer_capabilities: { edit: true, interact: true },
|
|
161
163
|
};
|
|
162
164
|
|
|
163
165
|
const cells = buildCellData(kernelReadyData);
|
|
@@ -191,6 +193,7 @@ describe("buildCellData", () => {
|
|
|
191
193
|
terminal: false,
|
|
192
194
|
},
|
|
193
195
|
auto_instantiated: false,
|
|
196
|
+
consumer_capabilities: { edit: true, interact: true },
|
|
194
197
|
};
|
|
195
198
|
|
|
196
199
|
const cells = buildCellData(kernelReadyData);
|
|
@@ -223,6 +226,7 @@ describe("buildLayoutState", () => {
|
|
|
223
226
|
terminal: false,
|
|
224
227
|
},
|
|
225
228
|
auto_instantiated: false,
|
|
229
|
+
consumer_capabilities: { edit: true, interact: true },
|
|
226
230
|
};
|
|
227
231
|
|
|
228
232
|
const cells = buildCellData(kernelReadyData);
|
|
@@ -271,6 +275,7 @@ describe("buildLayoutState", () => {
|
|
|
271
275
|
terminal: false,
|
|
272
276
|
},
|
|
273
277
|
auto_instantiated: false,
|
|
278
|
+
consumer_capabilities: { edit: true, interact: true },
|
|
274
279
|
};
|
|
275
280
|
|
|
276
281
|
const cells = buildCellData(kernelReadyData);
|
|
@@ -31,19 +31,6 @@ describe("classifyCloseEvent", () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
describe("terminal closes (server-initiated)", () => {
|
|
34
|
-
it("MARIMO_ALREADY_CONNECTED → terminal + closeTransport, with takeover", () => {
|
|
35
|
-
const decision = classify("MARIMO_ALREADY_CONNECTED");
|
|
36
|
-
expect(decision.kind).toBe("terminal");
|
|
37
|
-
expect(decision.status).toMatchObject({
|
|
38
|
-
state: WebSocketState.CLOSED,
|
|
39
|
-
code: WebSocketClosedReason.ALREADY_RUNNING,
|
|
40
|
-
canTakeover: true,
|
|
41
|
-
});
|
|
42
|
-
if (decision.kind === "terminal") {
|
|
43
|
-
expect(decision.closeTransport).toBe(true);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
34
|
it.each([
|
|
48
35
|
"MARIMO_WRONG_KERNEL_ID",
|
|
49
36
|
"MARIMO_NO_FILE_KEY",
|
|
@@ -14,7 +14,6 @@ export type WebSocketState =
|
|
|
14
14
|
|
|
15
15
|
export const WebSocketClosedReason = {
|
|
16
16
|
KERNEL_DISCONNECTED: "KERNEL_DISCONNECTED",
|
|
17
|
-
ALREADY_RUNNING: "ALREADY_RUNNING",
|
|
18
17
|
MALFORMED_QUERY: "MALFORMED_QUERY",
|
|
19
18
|
KERNEL_STARTUP_ERROR: "KERNEL_STARTUP_ERROR",
|
|
20
19
|
} as const;
|
|
@@ -30,11 +29,6 @@ export type ConnectionStatus =
|
|
|
30
29
|
* Human-readable reason for closing the connection.
|
|
31
30
|
*/
|
|
32
31
|
reason: string;
|
|
33
|
-
/**
|
|
34
|
-
* Whether the current session can be taken over by another session,
|
|
35
|
-
* since we only allow single-user editing.
|
|
36
|
-
*/
|
|
37
|
-
canTakeover?: boolean;
|
|
38
32
|
}
|
|
39
33
|
| {
|
|
40
34
|
state:
|
|
@@ -82,7 +82,6 @@ const SUPPORTS_LAZY_KERNELS = true;
|
|
|
82
82
|
// (marimo/_server/api/endpoints/ws_endpoint.py and ws/*.py). Keep in sync with
|
|
83
83
|
// the backend literals.
|
|
84
84
|
export type CloseReason =
|
|
85
|
-
| "MARIMO_ALREADY_CONNECTED"
|
|
86
85
|
| "MARIMO_WRONG_KERNEL_ID"
|
|
87
86
|
| "MARIMO_NO_FILE_KEY"
|
|
88
87
|
| "MARIMO_NO_SESSION_ID"
|
|
@@ -99,17 +98,6 @@ export type CloseDecision =
|
|
|
99
98
|
|
|
100
99
|
export function classifyCloseEvent(event: { reason?: string }): CloseDecision {
|
|
101
100
|
switch (event.reason as CloseReason | undefined) {
|
|
102
|
-
case "MARIMO_ALREADY_CONNECTED":
|
|
103
|
-
return {
|
|
104
|
-
kind: "terminal",
|
|
105
|
-
status: {
|
|
106
|
-
state: WebSocketState.CLOSED,
|
|
107
|
-
code: WebSocketClosedReason.ALREADY_RUNNING,
|
|
108
|
-
reason: "another browser tab is already connected to the kernel",
|
|
109
|
-
canTakeover: true,
|
|
110
|
-
},
|
|
111
|
-
closeTransport: true,
|
|
112
|
-
};
|
|
113
101
|
case TRANSPORT_EXHAUSTED_REASON:
|
|
114
102
|
return {
|
|
115
103
|
kind: "gave-up",
|
|
@@ -421,6 +409,9 @@ export function useMarimoKernelConnection(opts: {
|
|
|
421
409
|
case "notebook-document-transaction":
|
|
422
410
|
handleDocumentTransaction(msg.data.transaction);
|
|
423
411
|
return;
|
|
412
|
+
case "consumer-capabilities":
|
|
413
|
+
setKioskMode(!msg.data.consumer_capabilities.edit);
|
|
414
|
+
return;
|
|
424
415
|
default:
|
|
425
416
|
logNever(msg.data);
|
|
426
417
|
}
|