@marimo-team/frontend 0.22.5-dev9 → 0.22.5
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/assets/{CellStatus-vLQ0PRhL.js → CellStatus-CNNGwOIK.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-CAllw3YA.js → ConnectedDataExplorerComponent-CfU-ThkK.js} +1 -1
- package/dist/assets/JsonOutput-9XtRRx5l.js +49 -0
- package/dist/assets/{MarimoErrorOutput-CbOtWgTE.js → MarimoErrorOutput-Bc9JufDr.js} +2 -2
- package/dist/assets/{RenderHTML-HmYLAtrW.js → RenderHTML-0dk6-mYI.js} +1 -1
- package/dist/assets/{add-cell-with-ai-DVz3Rqa3.js → add-cell-with-ai-CLklC7KS.js} +8 -8
- package/dist/assets/{add-connection-dialog-Bu5E77IS.js → add-connection-dialog-ux7eCDRM.js} +1 -1
- package/dist/assets/{agent-panel-HMju_soU.js → agent-panel-CiMrqUfl.js} +3 -3
- package/dist/assets/{ai-model-dropdown-DUK_vWZh.js → ai-model-dropdown-CRtaHcCu.js} +3 -3
- package/dist/assets/{app-config-button-CVraCieA.js → app-config-button-CnX21edo.js} +1 -1
- package/dist/assets/{cache-panel-5QBCQUqv.js → cache-panel-8E_Y5OSb.js} +1 -1
- package/dist/assets/{cell-editor-CEok7I_G.js → cell-editor-D7IQ3F4W.js} +10 -10
- package/dist/assets/{cell-link-D5GhiNrn.js → cell-link-CcAqXeeg.js} +1 -1
- package/dist/assets/{cells-CuaAKcwV.js → cells-EJo3u4za.js} +43 -43
- package/dist/assets/{chat-display-ktpBhrn7.js → chat-display-BxDRpNsl.js} +1 -1
- package/dist/assets/{chat-panel-BzN2cf87.js → chat-panel-dBoLqgjH.js} +1 -1
- package/dist/assets/{chat-ui-Da4qjTuA.js → chat-ui-DdZo1L-v.js} +1 -1
- package/dist/assets/{column-preview-BLzfoQuq.js → column-preview-DrU255Z3.js} +1 -1
- package/dist/assets/{command-palette-DNvYHUpi.js → command-palette-n6NnK6GP.js} +1 -1
- package/dist/assets/{common-BKyn8lE1.js → common-Bty2yo-n.js} +1 -1
- package/dist/assets/{components-CtOW1DR4.js → components-B8TZ_vT_.js} +1 -1
- package/dist/assets/{components-CBihADZo.js → components-Dh-L-jYg.js} +1 -1
- package/dist/assets/config-DoZCLcOb.js +1 -0
- package/dist/assets/{datasource-DwmhT5-D.js → datasource-DY0N42ZB.js} +1 -1
- package/dist/assets/{dependency-graph-panel-Cq1gKP3a.js → dependency-graph-panel-C23HsAdh.js} +1 -1
- package/dist/assets/{documentation-panel-Dd6ys__C.js → documentation-panel-okcEKCQM.js} +1 -1
- package/dist/assets/{download-CxGVI9eo.js → download-TSo32ofd.js} +3 -3
- package/dist/assets/{edit-page-DJprVtJ6.js → edit-page-RhmoqI7E.js} +7 -7
- package/dist/assets/{error-panel-BiGfbiiW.js → error-panel-aq2j0jIa.js} +1 -1
- package/dist/assets/{file-explorer-panel-DiNhLdAc.js → file-explorer-panel-CzYUz358.js} +1 -1
- package/dist/assets/{file-icons-DaGma7HH.js → file-icons-DBaXCICA.js} +1 -1
- package/dist/assets/{floating-outline-oPCmn9_F.js → floating-outline-BTmyhMGv.js} +1 -1
- package/dist/assets/{focus-B524Cy57.js → focus-DXeddo75.js} +1 -1
- package/dist/assets/{form-BV-yji2Y.js → form-BiDLPu7R.js} +1 -1
- package/dist/assets/{gallery-page-CI72Q71Y.js → gallery-page-XSrY7bw_.js} +1 -1
- package/dist/assets/{globals-Bh85lAn7.js → globals-DQM2RvzM.js} +1 -1
- package/dist/assets/{home-page-kYSYG7Zh.js → home-page-BntiR5eS.js} +2 -2
- package/dist/assets/{hooks-SmuOPKfj.js → hooks-BgwM3Mb2.js} +1 -1
- package/dist/assets/{html-to-image-C-c-Hfuw.js → html-to-image-BJiJlwQY.js} +1 -1
- package/dist/assets/index-CMEhtk8a.js +42 -0
- package/dist/assets/index-DBs2il8a.css +2 -0
- package/dist/assets/{kiosk-mode-CnJjuo6B.js → kiosk-mode-JCcLyeoQ.js} +1 -1
- package/dist/assets/{layout-BxUONa-J.js → layout-CF-7BNtf.js} +3 -3
- package/dist/assets/{logs-panel-BH5Q_Nct.js → logs-panel-BzhPrie8.js} +1 -1
- package/dist/assets/{markdown-renderer-2XpTunxF.js → markdown-renderer-B9RsGqHb.js} +1 -1
- package/dist/assets/{mermaid-KL-Hgqp7.js → mermaid-BJFSZcG6.js} +1 -1
- package/dist/assets/{name-cell-input-CIZAWlBG.js → name-cell-input-CYsY4A1G.js} +1 -1
- package/dist/assets/{outline-panel-BXefyCJ4.js → outline-panel-BCAWCKi6.js} +1 -1
- package/dist/assets/{packages-panel-De0Fg43N.js → packages-panel-5axf3DuF.js} +1 -1
- package/dist/assets/{panels-DuR2pNy9.js → panels-7-kbDRzv.js} +1 -1
- package/dist/assets/{process-output-C-VBRULx.js → process-output-DqiZsqG9.js} +1 -1
- package/dist/assets/{readonly-python-code-BhME4c6A.js → readonly-python-code-D8ITm60r.js} +1 -1
- package/dist/assets/{run-page-BDV1C8Oi.js → run-page-9OQqe8IY.js} +1 -1
- package/dist/assets/{scratchpad-panel-CWU7CyZh.js → scratchpad-panel-DkqxnSH6.js} +1 -1
- package/dist/assets/{secrets-panel-CyJ4KdCC.js → secrets-panel-C6X5jB8Q.js} +1 -1
- package/dist/assets/{session-panel-B8t0Xymv.js → session-panel-BuzMiMf3.js} +1 -1
- package/dist/assets/{snippets-panel-BaV08Ib1.js → snippets-panel--mh2FUXA.js} +1 -1
- package/dist/assets/{state-WTTs5oP6.js → state-6D_2UAw3.js} +2 -2
- package/dist/assets/{state-DuVk71Dw.js → state-BDrig0S2.js} +1 -1
- package/dist/assets/{state-9-n7I_Bo.js → state-BgrGQPFs.js} +1 -1
- package/dist/assets/{switch-C2idsSNO.js → switch-C6xjg01T.js} +1 -1
- package/dist/assets/{terminal-BBTjIXBz.js → terminal-BEaHyVIQ.js} +1 -1
- package/dist/assets/{textarea-CI3yaazO.js → textarea-Cfp3upzK.js} +1 -1
- package/dist/assets/{tracing-ChWqFQa-.js → tracing-BExYhl1z.js} +1 -1
- package/dist/assets/{tracing-panel-B46P3LAM.js → tracing-panel-Co5DeX-F.js} +2 -2
- package/dist/assets/{useAddCell-BMYemCZ-.js → useAddCell-BaTlDxTu.js} +1 -1
- package/dist/assets/{useAsyncData-CaAFMbY9.js → useAsyncData-aCoWDe-l.js} +1 -1
- package/dist/assets/{useBoolean-ugd5JdXd.js → useBoolean-BvsK1Xcs.js} +1 -1
- package/dist/assets/{useCellActionButton-BKZyr81R.js → useCellActionButton-DftkIqUl.js} +1 -1
- package/dist/assets/{useDeleteCell-6SLN_jZa.js → useDeleteCell-d6yWnL3H.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-JOuBqQ1y.js → useDependencyPanelTab-BaVcOBM4.js} +1 -1
- package/dist/assets/{useNotebookActions-D2fp_HNm.js → useNotebookActions-DihtSJ4g.js} +1 -1
- package/dist/assets/{useRunCells-B5o8P7HV.js → useRunCells-d2edY6Tu.js} +1 -1
- package/dist/assets/{useSplitCell-Dn4N4Evl.js → useSplitCell-DOiFyMgH.js} +1 -1
- package/dist/assets/{vega-component-D_AgSSfE.js → vega-component-CiVPyAwP.js} +1 -1
- package/dist/index.html +29 -28
- package/package.json +2 -2
- package/src/components/data-table/__tests__/columns.test.tsx +92 -13
- package/src/components/data-table/column-header.tsx +81 -56
- package/src/components/data-table/columns.tsx +25 -32
- package/src/components/data-table/data-table.tsx +8 -1
- package/src/components/data-table/renderers.tsx +19 -6
- package/src/components/data-table/types.ts +4 -0
- package/src/components/editor/Output.tsx +1 -1
- package/src/components/editor/__tests__/Output.test.tsx +36 -1
- package/src/core/cells/__tests__/cells.test.ts +41 -0
- package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +38 -0
- package/src/core/cells/cells.ts +1 -1
- package/src/core/cells/collapseConsoleOutputs.tsx +3 -0
- package/src/core/cells/document-changes.ts +12 -0
- package/src/core/runtime/__tests__/runtime.test.ts +138 -2
- package/src/core/runtime/runtime.ts +25 -5
- package/src/core/saving/file-state.ts +16 -0
- package/src/hooks/useAsyncData.ts +1 -1
- package/src/mount.tsx +17 -1
- package/src/plugins/impl/DataTablePlugin.tsx +1 -1
- package/src/plugins/impl/plotly/__tests__/selection.test.ts +22 -0
- package/src/plugins/impl/plotly/selection.ts +1 -0
- package/dist/assets/JsonOutput-Dl2dfhmz.js +0 -49
- package/dist/assets/config-Cgj0Ahvb.js +0 -1
- package/dist/assets/index-BNN_F0CC.css +0 -2
- package/dist/assets/index-By2ge4IZ.js +0 -42
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
"use no memo";
|
|
3
3
|
|
|
4
4
|
import type { Column, Table } from "@tanstack/react-table";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
EllipsisIcon,
|
|
7
|
+
FilterIcon,
|
|
8
|
+
MinusIcon,
|
|
9
|
+
TextIcon,
|
|
10
|
+
XIcon,
|
|
11
|
+
} from "lucide-react";
|
|
6
12
|
import { useMemo, useRef, useState } from "react";
|
|
7
13
|
import { useLocale } from "react-aria";
|
|
8
14
|
import {
|
|
@@ -69,7 +75,7 @@ interface DataTableColumnHeaderProps<
|
|
|
69
75
|
> extends React.HTMLAttributes<HTMLDivElement> {
|
|
70
76
|
column: Column<TData, TValue>;
|
|
71
77
|
header: React.ReactNode;
|
|
72
|
-
|
|
78
|
+
subheader?: React.ReactNode;
|
|
73
79
|
calculateTopKRows?: CalculateTopKRows;
|
|
74
80
|
table?: Table<TData>;
|
|
75
81
|
}
|
|
@@ -77,7 +83,7 @@ interface DataTableColumnHeaderProps<
|
|
|
77
83
|
export const DataTableColumnHeader = <TData, TValue>({
|
|
78
84
|
column,
|
|
79
85
|
header,
|
|
80
|
-
|
|
86
|
+
subheader,
|
|
81
87
|
className,
|
|
82
88
|
calculateTopKRows,
|
|
83
89
|
table,
|
|
@@ -92,49 +98,51 @@ export const DataTableColumnHeader = <TData, TValue>({
|
|
|
92
98
|
|
|
93
99
|
// No sorting or filtering
|
|
94
100
|
if (!column.getCanSort() && !column.getCanFilter()) {
|
|
95
|
-
return
|
|
101
|
+
return (
|
|
102
|
+
<div className={cn(className)}>
|
|
103
|
+
{header}
|
|
104
|
+
{subheader}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
const hasFilter = column.getFilterValue() !== undefined;
|
|
99
|
-
const hideIcon = !column.getIsSorted() && !hasFilter;
|
|
100
110
|
|
|
101
111
|
return (
|
|
102
112
|
<>
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
>
|
|
121
|
-
{
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
</DropdownMenuContent>
|
|
137
|
-
</DropdownMenu>
|
|
113
|
+
<div
|
|
114
|
+
className={cn("group flex flex-col my-1 w-full select-none", className)}
|
|
115
|
+
>
|
|
116
|
+
<div className="flex items-center gap-1">
|
|
117
|
+
<span>{header}</span>
|
|
118
|
+
{column.getCanSort() && <SortButton column={column} />}
|
|
119
|
+
<DropdownMenu modal={false}>
|
|
120
|
+
<DropdownMenuTrigger asChild={true}>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
className="inline-flex items-center justify-center h-5 w-5 rounded hover:bg-(--slate-4) text-muted-foreground opacity-0 group-hover:opacity-100 focus:opacity-100 group-focus-within:opacity-100 data-[state=open]:opacity-100 data-[state=open]:text-accent-foreground"
|
|
124
|
+
aria-label="Column options"
|
|
125
|
+
data-testid="data-table-column-menu-button"
|
|
126
|
+
>
|
|
127
|
+
<EllipsisIcon className="h-3.5 w-3.5" />
|
|
128
|
+
</button>
|
|
129
|
+
</DropdownMenuTrigger>
|
|
130
|
+
<DropdownMenuContent align="start">
|
|
131
|
+
{renderDataType(column)}
|
|
132
|
+
{renderSorts(column, table)}
|
|
133
|
+
{renderCopyColumn(column)}
|
|
134
|
+
{renderColumnPinning(column)}
|
|
135
|
+
{renderColumnWrapping(column)}
|
|
136
|
+
{renderFormatOptions(column, locale)}
|
|
137
|
+
<DropdownMenuSeparator />
|
|
138
|
+
{renderMenuItemFilter(column)}
|
|
139
|
+
{renderFilterByValues(column, setIsFilterValueOpen)}
|
|
140
|
+
{hasFilter && <ClearFilterMenuItem column={column} />}
|
|
141
|
+
</DropdownMenuContent>
|
|
142
|
+
</DropdownMenu>
|
|
143
|
+
</div>
|
|
144
|
+
{subheader}
|
|
145
|
+
</div>
|
|
138
146
|
{isFilterValueOpen && (
|
|
139
147
|
<PopoverFilterByValues
|
|
140
148
|
setIsFilterValueOpen={setIsFilterValueOpen}
|
|
@@ -146,28 +154,45 @@ export const DataTableColumnHeader = <TData, TValue>({
|
|
|
146
154
|
);
|
|
147
155
|
};
|
|
148
156
|
|
|
149
|
-
|
|
157
|
+
const SortButton = <TData, TValue>({
|
|
150
158
|
column,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
className,
|
|
154
|
-
}: DataTableColumnHeaderProps<TData, TValue> & {
|
|
155
|
-
summary: React.ReactNode;
|
|
159
|
+
}: {
|
|
160
|
+
column: Column<TData, TValue>;
|
|
156
161
|
}) => {
|
|
162
|
+
const sortDirection = column.getIsSorted();
|
|
163
|
+
|
|
164
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
165
|
+
e.stopPropagation();
|
|
166
|
+
if (!sortDirection) {
|
|
167
|
+
column.toggleSorting(false, true); // asc
|
|
168
|
+
} else if (sortDirection === "asc") {
|
|
169
|
+
column.toggleSorting(true, true); // desc
|
|
170
|
+
} else {
|
|
171
|
+
column.clearSorting();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
157
175
|
return (
|
|
158
|
-
<
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
onClick={handleClick}
|
|
159
179
|
className={cn(
|
|
160
|
-
"flex
|
|
161
|
-
|
|
180
|
+
"inline-flex items-center justify-center h-5 w-5 rounded hover:bg-(--slate-4)",
|
|
181
|
+
sortDirection
|
|
182
|
+
? "text-accent-foreground"
|
|
183
|
+
: "text-muted-foreground opacity-0 group-hover:opacity-100 focus:opacity-100 group-focus-within:opacity-100",
|
|
162
184
|
)}
|
|
185
|
+
aria-label={
|
|
186
|
+
sortDirection === "asc"
|
|
187
|
+
? "Sorted ascending, click to sort descending"
|
|
188
|
+
: sortDirection === "desc"
|
|
189
|
+
? "Sorted descending, click to clear sort"
|
|
190
|
+
: "Sort column ascending"
|
|
191
|
+
}
|
|
192
|
+
data-testid="data-table-sort-button"
|
|
163
193
|
>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
header={header}
|
|
167
|
-
className={className}
|
|
168
|
-
/>
|
|
169
|
-
{summary}
|
|
170
|
-
</div>
|
|
194
|
+
{renderSortFilterIcon(column)}
|
|
195
|
+
</button>
|
|
171
196
|
);
|
|
172
197
|
};
|
|
173
198
|
|
|
@@ -206,41 +206,30 @@ export function generateColumns<T>({
|
|
|
206
206
|
</div>
|
|
207
207
|
) : null;
|
|
208
208
|
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
const headerWithType = (
|
|
212
|
-
<div
|
|
209
|
+
const headerName = (
|
|
210
|
+
<span
|
|
213
211
|
className={cn(
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
justify === "right" && "items-end",
|
|
212
|
+
"font-bold",
|
|
213
|
+
headerTitle && "underline decoration-dotted",
|
|
217
214
|
)}
|
|
218
215
|
>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
"font-bold",
|
|
222
|
-
headerTitle && "underline decoration-dotted",
|
|
223
|
-
)}
|
|
224
|
-
>
|
|
225
|
-
{key === "" ? " " : key}
|
|
226
|
-
</span>
|
|
227
|
-
{dtypeHeader}
|
|
228
|
-
</div>
|
|
216
|
+
{key === "" ? " " : key}
|
|
217
|
+
</span>
|
|
229
218
|
);
|
|
230
219
|
|
|
231
220
|
const headerWithTooltip = headerTitle ? (
|
|
232
221
|
<Tooltip content={headerTitle} delayDuration={300}>
|
|
233
|
-
{
|
|
222
|
+
{headerName}
|
|
234
223
|
</Tooltip>
|
|
235
224
|
) : (
|
|
236
|
-
|
|
225
|
+
headerName
|
|
237
226
|
);
|
|
238
227
|
|
|
239
228
|
const dataTableColumnHeader = (
|
|
240
229
|
<DataTableColumnHeader
|
|
241
230
|
header={headerWithTooltip}
|
|
231
|
+
subheader={dtypeHeader}
|
|
242
232
|
column={column}
|
|
243
|
-
justify={justify}
|
|
244
233
|
calculateTopKRows={calculateTopKRows}
|
|
245
234
|
table={table}
|
|
246
235
|
/>
|
|
@@ -255,8 +244,6 @@ export function generateColumns<T>({
|
|
|
255
244
|
<div
|
|
256
245
|
className={cn(
|
|
257
246
|
"flex flex-col h-full pt-0.5 pb-3 justify-between items-start",
|
|
258
|
-
justify === "center" && "items-center",
|
|
259
|
-
justify === "right" && "items-end",
|
|
260
247
|
)}
|
|
261
248
|
>
|
|
262
249
|
{dataTableColumnHeader}
|
|
@@ -283,13 +270,13 @@ export function generateColumns<T>({
|
|
|
283
270
|
|
|
284
271
|
const dataType = column.columnDef.meta?.dataType;
|
|
285
272
|
const isNumeric = dataType === "number" || dataType === "integer";
|
|
286
|
-
const cellStyles = getCellStyleClass(
|
|
273
|
+
const cellStyles = getCellStyleClass({
|
|
287
274
|
justify,
|
|
288
275
|
wrapped,
|
|
289
276
|
canSelectCell,
|
|
290
|
-
isCellSelected,
|
|
277
|
+
isSelected: isCellSelected,
|
|
291
278
|
isNumeric,
|
|
292
|
-
);
|
|
279
|
+
});
|
|
293
280
|
|
|
294
281
|
const renderedCell = renderCellValue({
|
|
295
282
|
column,
|
|
@@ -448,13 +435,19 @@ function getFilterTypeForFieldType(
|
|
|
448
435
|
}
|
|
449
436
|
}
|
|
450
437
|
|
|
451
|
-
function getCellStyleClass(
|
|
452
|
-
justify
|
|
453
|
-
wrapped
|
|
454
|
-
canSelectCell
|
|
455
|
-
isSelected
|
|
456
|
-
isNumeric
|
|
457
|
-
|
|
438
|
+
function getCellStyleClass({
|
|
439
|
+
justify = "left",
|
|
440
|
+
wrapped,
|
|
441
|
+
canSelectCell,
|
|
442
|
+
isSelected,
|
|
443
|
+
isNumeric = false,
|
|
444
|
+
}: {
|
|
445
|
+
justify: "left" | "center" | "right" | undefined;
|
|
446
|
+
wrapped: boolean | undefined;
|
|
447
|
+
canSelectCell: boolean;
|
|
448
|
+
isSelected: boolean;
|
|
449
|
+
isNumeric?: boolean;
|
|
450
|
+
}): string {
|
|
458
451
|
return cn(
|
|
459
452
|
canSelectCell && "cursor-pointer",
|
|
460
453
|
isSelected &&
|
|
@@ -47,6 +47,7 @@ import { DataTableBody, renderTableHeader } from "./renderers";
|
|
|
47
47
|
import { TableBottomBar } from "./TableBottomBar";
|
|
48
48
|
import { TableTopBar } from "./TableTopBar";
|
|
49
49
|
import {
|
|
50
|
+
AUTO_WIDTH_MAX_COLUMNS,
|
|
50
51
|
type DataTableSelection,
|
|
51
52
|
MIN_ROWS_TO_VIRTUALIZE,
|
|
52
53
|
type TooManyRows,
|
|
@@ -300,7 +301,13 @@ const DataTableInternal = <TData,>({
|
|
|
300
301
|
isAnyPanelOpen={isAnyPanelOpen}
|
|
301
302
|
downloadAs={downloadAs}
|
|
302
303
|
/>
|
|
303
|
-
<Table
|
|
304
|
+
<Table
|
|
305
|
+
className={cn(
|
|
306
|
+
"relative",
|
|
307
|
+
columns.length <= AUTO_WIDTH_MAX_COLUMNS ? "w-auto" : "w-full",
|
|
308
|
+
)}
|
|
309
|
+
ref={tableRef}
|
|
310
|
+
>
|
|
304
311
|
{showLoadingBar && (
|
|
305
312
|
<thead className="absolute top-0 left-0 h-[3px] w-1/2 bg-primary animate-slide" />
|
|
306
313
|
)}
|
|
@@ -27,7 +27,7 @@ import { DataTableContextMenu } from "./context-menu";
|
|
|
27
27
|
import { CellRangeSelectionIndicator } from "./range-focus/cell-selection-indicator";
|
|
28
28
|
import { useCellRangeSelection } from "./range-focus/use-cell-range-selection";
|
|
29
29
|
import { useScrollIntoViewOnFocus } from "./range-focus/use-scroll-into-view";
|
|
30
|
-
import { TABLE_ROW_HEIGHT_PX } from "./types";
|
|
30
|
+
import { AUTO_WIDTH_MAX_COLUMNS, TABLE_ROW_HEIGHT_PX } from "./types";
|
|
31
31
|
import { stringifyUnknownValue } from "./utils";
|
|
32
32
|
|
|
33
33
|
export function renderTableHeader<TData>(
|
|
@@ -46,7 +46,7 @@ export function renderTableHeader<TData>(
|
|
|
46
46
|
<TableHead
|
|
47
47
|
key={header.id}
|
|
48
48
|
className={cn(
|
|
49
|
-
"h-auto min-h-10 whitespace-pre align-top",
|
|
49
|
+
"h-auto min-h-10 whitespace-pre align-top border-r border-r-border/75",
|
|
50
50
|
className,
|
|
51
51
|
)}
|
|
52
52
|
style={style}
|
|
@@ -69,6 +69,13 @@ export function renderTableHeader<TData>(
|
|
|
69
69
|
{renderHeaderGroup(table.getLeftHeaderGroups())}
|
|
70
70
|
{renderHeaderGroup(table.getCenterHeaderGroups())}
|
|
71
71
|
{renderHeaderGroup(table.getRightHeaderGroups())}
|
|
72
|
+
{table.getAllColumns().length <= AUTO_WIDTH_MAX_COLUMNS && (
|
|
73
|
+
<th
|
|
74
|
+
className="w-full border-0"
|
|
75
|
+
aria-hidden="true"
|
|
76
|
+
role="presentation"
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
72
79
|
</TableRow>
|
|
73
80
|
</TableHeader>
|
|
74
81
|
);
|
|
@@ -163,7 +170,7 @@ export const DataTableBody = <TData,>({
|
|
|
163
170
|
{...getCellDomProps(cell.id)}
|
|
164
171
|
key={cell.id}
|
|
165
172
|
className={cn(
|
|
166
|
-
"whitespace-pre truncate max-w-[300px] outline-hidden",
|
|
173
|
+
"whitespace-pre truncate max-w-[300px] outline-hidden border-r border-r-border/75",
|
|
167
174
|
cell.column.getColumnWrapping &&
|
|
168
175
|
cell.column.getColumnWrapping?.() === "wrap" &&
|
|
169
176
|
COLUMN_WRAPPING_STYLES,
|
|
@@ -230,15 +237,21 @@ export const DataTableBody = <TData,>({
|
|
|
230
237
|
{renderCells(row.getLeftVisibleCells())}
|
|
231
238
|
{renderCells(row.getCenterVisibleCells())}
|
|
232
239
|
{renderCells(row.getRightVisibleCells())}
|
|
240
|
+
{columns.length <= AUTO_WIDTH_MAX_COLUMNS && (
|
|
241
|
+
<td className="border-0" aria-hidden="true" role="presentation" />
|
|
242
|
+
)}
|
|
233
243
|
</TableRow>
|
|
234
244
|
);
|
|
235
245
|
};
|
|
236
246
|
|
|
247
|
+
const hasFillerColumn = columns.length <= AUTO_WIDTH_MAX_COLUMNS;
|
|
248
|
+
const totalColSpan = columns.length + (hasFillerColumn ? 1 : 0);
|
|
249
|
+
|
|
237
250
|
const renderRows = () => {
|
|
238
251
|
if (rows.length === 0) {
|
|
239
252
|
return (
|
|
240
253
|
<TableRow>
|
|
241
|
-
<TableCell colSpan={
|
|
254
|
+
<TableCell colSpan={totalColSpan} className="h-24 text-center">
|
|
242
255
|
No results.
|
|
243
256
|
</TableCell>
|
|
244
257
|
</TableRow>
|
|
@@ -255,7 +268,7 @@ export const DataTableBody = <TData,>({
|
|
|
255
268
|
data-virtual-spacer=""
|
|
256
269
|
style={{ height: virtualItems[0].start }}
|
|
257
270
|
>
|
|
258
|
-
<td colSpan={
|
|
271
|
+
<td colSpan={totalColSpan} />
|
|
259
272
|
</tr>
|
|
260
273
|
)}
|
|
261
274
|
{virtualItems.map((vItem) => renderRow(rows[vItem.index]))}
|
|
@@ -266,7 +279,7 @@ export const DataTableBody = <TData,>({
|
|
|
266
279
|
height: totalSize - (virtualItems.at(-1)?.end ?? totalSize),
|
|
267
280
|
}}
|
|
268
281
|
>
|
|
269
|
-
<td colSpan={
|
|
282
|
+
<td colSpan={totalColSpan} />
|
|
270
283
|
</tr>
|
|
271
284
|
)}
|
|
272
285
|
</>
|
|
@@ -16,6 +16,10 @@ declare module "@tanstack/react-table" {
|
|
|
16
16
|
export const TABLE_ROW_HEIGHT_PX = 24;
|
|
17
17
|
export const TABLE_HEADER_HEIGHT_PX = 40;
|
|
18
18
|
|
|
19
|
+
// Below this column count, the table uses w-auto with a filler column
|
|
20
|
+
// to prevent columns from stretching unnecessarily
|
|
21
|
+
export const AUTO_WIDTH_MAX_COLUMNS = 4;
|
|
22
|
+
|
|
19
23
|
// Default number of visible rows when virtualizing without an explicit maxHeight.
|
|
20
24
|
export const DEFAULT_VIRTUAL_ROWS = 15;
|
|
21
25
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { render, screen } from "@testing-library/react";
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import { cellId } from "@/__tests__/branded";
|
|
5
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
6
|
+
import { OutputArea, OutputRenderer } from "../Output";
|
|
5
7
|
|
|
6
8
|
describe("OutputRenderer renderFallback prop", () => {
|
|
7
9
|
it("should use renderFallback for unsupported mimetypes", () => {
|
|
@@ -65,6 +67,39 @@ describe("OutputRenderer renderFallback prop", () => {
|
|
|
65
67
|
});
|
|
66
68
|
});
|
|
67
69
|
|
|
70
|
+
describe("OutputArea null/undefined handling", () => {
|
|
71
|
+
it("should render null when output is null", () => {
|
|
72
|
+
const { container } = render(
|
|
73
|
+
<TooltipProvider>
|
|
74
|
+
<OutputArea
|
|
75
|
+
output={null}
|
|
76
|
+
cellId={cellId("test")}
|
|
77
|
+
stale={false}
|
|
78
|
+
loading={false}
|
|
79
|
+
allowExpand={true}
|
|
80
|
+
/>
|
|
81
|
+
</TooltipProvider>,
|
|
82
|
+
);
|
|
83
|
+
expect(container.innerHTML).toBe("");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should render null when output is undefined", () => {
|
|
87
|
+
const { container } = render(
|
|
88
|
+
<TooltipProvider>
|
|
89
|
+
<OutputArea
|
|
90
|
+
// @ts-expect-error -- testing runtime safety for undefined output
|
|
91
|
+
output={undefined}
|
|
92
|
+
cellId={cellId("test")}
|
|
93
|
+
stale={false}
|
|
94
|
+
loading={false}
|
|
95
|
+
allowExpand={true}
|
|
96
|
+
/>
|
|
97
|
+
</TooltipProvider>,
|
|
98
|
+
);
|
|
99
|
+
expect(container.innerHTML).toBe("");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
68
103
|
describe("OutputRenderer image and SVG rendering", () => {
|
|
69
104
|
const plainSvgString =
|
|
70
105
|
'<svg><rect x="0" y="0" width="10" height="10"></rect></svg>';
|
|
@@ -1209,6 +1209,47 @@ describe("cell reducer", () => {
|
|
|
1209
1209
|
]);
|
|
1210
1210
|
});
|
|
1211
1211
|
|
|
1212
|
+
it("does not crash when setStdinResponse has out-of-bounds outputIndex", () => {
|
|
1213
|
+
const STDOUT: OutputMessage = {
|
|
1214
|
+
channel: "stdout",
|
|
1215
|
+
mimetype: "text/plain",
|
|
1216
|
+
data: "hello!",
|
|
1217
|
+
timestamp: 1,
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
// Set the cell to running with a console output
|
|
1221
|
+
actions.prepareForRun({ cellId: firstCellId });
|
|
1222
|
+
actions.handleCellMessage({
|
|
1223
|
+
cell_id: firstCellId,
|
|
1224
|
+
output: undefined,
|
|
1225
|
+
console: null,
|
|
1226
|
+
status: "running",
|
|
1227
|
+
stale_inputs: null,
|
|
1228
|
+
timestamp: new Date(20).getTime() as Seconds,
|
|
1229
|
+
});
|
|
1230
|
+
actions.handleCellMessage({
|
|
1231
|
+
cell_id: firstCellId,
|
|
1232
|
+
output: undefined,
|
|
1233
|
+
console: STDOUT,
|
|
1234
|
+
status: undefined,
|
|
1235
|
+
stale_inputs: null,
|
|
1236
|
+
timestamp: new Date(22).getTime() as Seconds,
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
// Try to set stdin response with an out-of-bounds index
|
|
1240
|
+
// This should not crash - it should return state unchanged
|
|
1241
|
+
actions.setStdinResponse({
|
|
1242
|
+
response: "test",
|
|
1243
|
+
cellId: firstCellId,
|
|
1244
|
+
outputIndex: 999,
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
// Cell state should be unchanged
|
|
1248
|
+
const cell = cells[0];
|
|
1249
|
+
expect(cell.consoleOutputs).toHaveLength(1);
|
|
1250
|
+
expect(cell.consoleOutputs[0]).toMatchObject(STDOUT);
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1212
1253
|
it("can receive console when the cell is idle and will clear when starts again", () => {
|
|
1213
1254
|
const OLD_STDOUT: OutputMessage = {
|
|
1214
1255
|
channel: "stdout",
|
|
@@ -241,6 +241,44 @@ describe("collapseConsoleOutputs", () => {
|
|
|
241
241
|
expect(result[2].data).toBe("<pre>E\nF\nG\nH\n</pre>");
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
+
it("should not crash when truncating with a single output at the limit boundary", () => {
|
|
245
|
+
// Create outputs that push truncation to the exact boundary
|
|
246
|
+
const consoleOutputs: OutputMessage[] = [
|
|
247
|
+
{
|
|
248
|
+
mimetype: "text/html",
|
|
249
|
+
channel: "output",
|
|
250
|
+
data: "<div>html1</div>",
|
|
251
|
+
timestamp: 0,
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
mimetype: "text/html",
|
|
255
|
+
channel: "output",
|
|
256
|
+
data: "<div>html2</div>",
|
|
257
|
+
timestamp: 0,
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
// With limit=1, truncation must handle edge cases gracefully
|
|
261
|
+
const result = collapseConsoleOutputs(consoleOutputs, 1);
|
|
262
|
+
expect(result[0].data).toContain("Streaming output truncated");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("should handle truncation when cutoff indexes past the end of the array", () => {
|
|
266
|
+
// With maxLines=0, the truncation loop never runs, causing cutoff
|
|
267
|
+
// to index past the array. This exercises the `output == null`
|
|
268
|
+
// defensive branch in truncateHead().
|
|
269
|
+
const consoleOutputs: OutputMessage[] = [
|
|
270
|
+
{
|
|
271
|
+
mimetype: "text/html",
|
|
272
|
+
channel: "output",
|
|
273
|
+
data: "<div>content</div>",
|
|
274
|
+
timestamp: 0,
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
const result = collapseConsoleOutputs(consoleOutputs, 0);
|
|
278
|
+
expect(result).toHaveLength(1);
|
|
279
|
+
expect(result[0].data).toContain("Streaming output truncated");
|
|
280
|
+
});
|
|
281
|
+
|
|
244
282
|
describe("ANSI escape sequences", () => {
|
|
245
283
|
it("should handle cursor movement with collapse", () => {
|
|
246
284
|
const consoleOutputs: OutputMessage[] = [
|
package/src/core/cells/cells.ts
CHANGED
|
@@ -947,7 +947,7 @@ const {
|
|
|
947
947
|
cellReducer: (cell) => {
|
|
948
948
|
const consoleOutputs = [...cell.consoleOutputs];
|
|
949
949
|
const stdinOutput = consoleOutputs[outputIndex];
|
|
950
|
-
if (stdinOutput.channel !== "stdin") {
|
|
950
|
+
if (stdinOutput == null || stdinOutput.channel !== "stdin") {
|
|
951
951
|
Logger.warn("Expected stdin output");
|
|
952
952
|
return cell;
|
|
953
953
|
}
|
|
@@ -108,6 +108,9 @@ function truncateHead(consoleOutputs: OutputMessage[], limit: number) {
|
|
|
108
108
|
timestamp: -1,
|
|
109
109
|
};
|
|
110
110
|
const output = consoleOutputs[cutoff];
|
|
111
|
+
if (output == null) {
|
|
112
|
+
return [warningOutput, ...consoleOutputs.slice(cutoff + 1)];
|
|
113
|
+
}
|
|
111
114
|
if (output.mimetype === "text/plain") {
|
|
112
115
|
invariant(typeof output.data === "string", "expected string");
|
|
113
116
|
const outputLines = output.data.split("\n");
|
|
@@ -25,6 +25,7 @@ import type { NotebookDocumentTransactionRequest } from "../network/types";
|
|
|
25
25
|
import { store } from "../state/jotai";
|
|
26
26
|
import type { CellActions, NotebookState } from "./cells";
|
|
27
27
|
import type { CellId } from "./ids";
|
|
28
|
+
import { SCRATCH_CELL_ID } from "./ids";
|
|
28
29
|
import type { CellData } from "./types";
|
|
29
30
|
|
|
30
31
|
export type DocumentChange =
|
|
@@ -572,10 +573,21 @@ const flushChanges = debounce(() => {
|
|
|
572
573
|
void getRequestClient().sendDocumentTransaction({ changes });
|
|
573
574
|
}, 400);
|
|
574
575
|
|
|
576
|
+
function isScratchChange(change: DocumentChange): boolean {
|
|
577
|
+
if ("cellId" in change && change.cellId === SCRATCH_CELL_ID) {
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
575
583
|
function enqueue(change: DocumentChange) {
|
|
576
584
|
if (store.get(kioskModeAtom)) {
|
|
577
585
|
return;
|
|
578
586
|
}
|
|
587
|
+
// The scratchpad cell is local-only — don't sync it to the document.
|
|
588
|
+
if (isScratchChange(change)) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
579
591
|
pendingChanges.push(change);
|
|
580
592
|
flushChanges();
|
|
581
593
|
}
|