@marimo-team/islands 0.25.3-dev95 → 0.25.3-dev97
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/{ErrorBoundary-Cghv7nv2.js → ErrorBoundary-DrRRjQqE.js} +1 -1
- package/dist/{chat-ui-D0jT6WV3.js → chat-ui-BIgPxfPP.js} +5 -4
- package/dist/{code-visibility-CLLrmROz.js → code-visibility-B72H-y9s.js} +2 -2
- package/dist/{common-BUSxx69_.js → common-Cm_wLVIe.js} +27 -26
- package/dist/{data-grid-overlay-editor-CWCIs2Mn.js → data-grid-overlay-editor-DWrrVdAU.js} +1 -1
- package/dist/{error-entries-BZiemZfD.js → error-entries-TAO6kEhF.js} +243 -230
- package/dist/{feedback-button-Ct9dNjaH.js → feedback-button--tVNH0ec.js} +3 -3
- package/dist/{glide-data-editor-D30nP9Xm.js → glide-data-editor-DWkgIB7l.js} +3 -3
- package/dist/{globals-B7SyAmCL.js → globals-D3_nrphz.js} +1 -1
- package/dist/{html-to-image-DBOTlHF9.js → html-to-image-CWdayGZU.js} +1 -1
- package/dist/main.js +1872 -1852
- package/dist/{number-overlay-editor-8Y4ehcLw.js → number-overlay-editor-DYj-4eDE.js} +1 -1
- package/dist/{process-output-BKLVbFnU.js → process-output-DD-oSvZk.js} +2 -2
- package/dist/{reveal-component-D8VAiQY5.js → reveal-component-pH1bfDAz.js} +7 -7
- package/package.json +1 -1
- package/src/components/app-config/ai-config.tsx +1 -0
- package/src/components/chat/acp/blocks.tsx +2 -2
- package/src/components/chat/chat-components.tsx +2 -0
- package/src/components/chat/chat-panel.tsx +6 -1
- package/src/components/data-table/__tests__/column-explorer.test.tsx +27 -1
- package/src/components/data-table/charts/__tests__/chart-panel.test.tsx +74 -1
- package/src/components/data-table/charts/__tests__/storage.test.ts +4 -1
- package/src/components/data-table/charts/charts.tsx +21 -6
- package/src/components/data-table/charts/storage.ts +16 -5
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +1 -1
- package/src/components/datasources/__tests__/column-actions.test.tsx +97 -0
- package/src/components/datasources/__tests__/column-preview.test.tsx +63 -2
- package/src/components/datasources/column-preview.tsx +8 -2
- package/src/components/datasources/datasources.tsx +12 -14
- package/src/components/debugger/debugger-code.tsx +5 -0
- package/src/components/dependency-graph/panels.tsx +2 -1
- package/src/components/editor/ai/ai-completion-editor.tsx +1 -0
- package/src/components/editor/alerts/startup-logs-alert.tsx +1 -0
- package/src/components/editor/cell/DeleteButton.tsx +1 -0
- package/src/components/editor/chrome/panels/cache-panel.tsx +1 -0
- package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +58 -1
- package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +12 -2
- package/src/components/editor/notebook-banner.tsx +1 -0
- package/src/components/editor/output/ansi-reduce.ts +6 -0
- package/src/components/editor/output/console/ConsoleOutput.tsx +11 -7
- package/src/components/editor/package-alert.tsx +6 -4
- package/src/components/editor/renderers/vertical-layout/sidebar/toggle.tsx +7 -1
- package/src/components/mcp/mcp-status-indicator.tsx +1 -1
- package/src/components/scratchpad/scratchpad.tsx +2 -2
- package/src/components/slides/reveal-component.tsx +4 -2
- package/src/components/slides/slide-form.tsx +1 -0
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +4 -0
- package/src/core/cells/__tests__/cell.test.ts +39 -0
- package/src/core/cells/__tests__/cells.test.ts +3 -1
- package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +54 -0
- package/src/core/cells/cell.ts +14 -3
- package/src/core/cells/cells.ts +1 -0
- package/src/core/cells/types.ts +10 -1
- package/src/core/codemirror/language/panel/panel.tsx +1 -0
- package/src/plugins/impl/RefreshPlugin.tsx +1 -0
- package/src/plugins/impl/chat/chat-ui.tsx +2 -1
- package/src/stories/button.stories.tsx +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
|
|
2
2
|
import { t } from "./react-sHtZdjeP.js";
|
|
3
|
-
import { a as n } from "./glide-data-editor-
|
|
3
|
+
import { a as n } from "./glide-data-editor-DWkgIB7l.js";
|
|
4
4
|
//#region ../node_modules/.pnpm/@glideapps+glide-data-grid@6.0.4-alpha24_lodash@4.17.21_marked@15.0.12_react-dom@19.3.0_27cffe7f1bc30287d59ac27db3240c5b/node_modules/@glideapps/glide-data-grid/dist/esm/internal/data-grid-overlay-editor/private/number-overlay-editor-style.js
|
|
5
5
|
var r = /* @__PURE__ */ e(t()), i = /*#__PURE__*/ n("div")({
|
|
6
6
|
name: "NumberOverlayEditorStyle",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { O as e } from "./html-to-image-
|
|
1
|
+
import { O as e } from "./html-to-image-CWdayGZU.js";
|
|
2
2
|
import { h as t } from "./tooltip-CKO1pHJo.js";
|
|
3
|
-
import { C as n, w as r } from "./error-entries-
|
|
3
|
+
import { C as n, w as r } from "./error-entries-TAO6kEhF.js";
|
|
4
4
|
import { b as i } from "./select-B0LRDH0g.js";
|
|
5
5
|
import { n as a } from "./semaphore-89jsoHtS.js";
|
|
6
6
|
var o = t("file", [["path", {
|
|
@@ -3,15 +3,15 @@ import { c as t, g as n, h as r, m as i, t as a } from "./button-BxB3RDjN.js";
|
|
|
3
3
|
import { t as o } from "./react-sHtZdjeP.js";
|
|
4
4
|
import { t as s } from "./compiler-runtime-CldRdSsv.js";
|
|
5
5
|
import { h as c, t as l } from "./tooltip-CKO1pHJo.js";
|
|
6
|
-
import { O as u } from "./error-entries-
|
|
7
|
-
import { Q as d, X as f, Z as p, an as m, cn as h, i as g, in as _, l as v, n as y, r as b, t as x, u as S } from "./common-
|
|
6
|
+
import { O as u } from "./error-entries-TAO6kEhF.js";
|
|
7
|
+
import { Q as d, X as f, Z as p, an as m, cn as h, i as g, in as _, l as v, n as y, r as b, t as x, u as S } from "./common-Cm_wLVIe.js";
|
|
8
8
|
import { M as C } from "./select-B0LRDH0g.js";
|
|
9
9
|
import { q as ee } from "./dropdown-menu-B_7xD2X4.js";
|
|
10
10
|
import { _ as w } from "./toDate-DKH0_kMo.js";
|
|
11
11
|
import { t as T } from "./jsx-runtime-3BD1kT0s.js";
|
|
12
12
|
import { q as E } from "./use-restore-focus-DBCITLeF.js";
|
|
13
13
|
import { A as D, C as O, b as k } from "./useTheme-CfeaVw5D.js";
|
|
14
|
-
import { a as A, n as te, t as ne } from "./code-visibility-
|
|
14
|
+
import { a as A, n as te, t as ne } from "./code-visibility-B72H-y9s.js";
|
|
15
15
|
/* empty css */
|
|
16
16
|
var j = c("sticky-note", [["path", {
|
|
17
17
|
d: "M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z",
|
|
@@ -6516,13 +6516,13 @@ var ia = (e) => {
|
|
|
6516
6516
|
c = K, s = tr, u = m ? "mo-slide-content flex flex-col gap-3" : "mo-slide-content", d = o, f = n.blocks.map((e, t) => {
|
|
6517
6517
|
let n = e.cells.map((e) => r(e.id) ? i ? /* @__PURE__ */ (0, G.jsx)(cr, { cell: e }, e.id) : /* @__PURE__ */ (0, G.jsx)(lr, { cell: e }, e.id) : /* @__PURE__ */ (0, G.jsx)(ia, { cell: e }, e.id));
|
|
6518
6518
|
if (e.isFragment) {
|
|
6519
|
-
let
|
|
6519
|
+
let r = p.get(t);
|
|
6520
6520
|
return /* @__PURE__ */ (0, G.jsxs)(Jn, {
|
|
6521
6521
|
as: "div",
|
|
6522
|
-
children: [n,
|
|
6523
|
-
},
|
|
6522
|
+
children: [n, r && /* @__PURE__ */ (0, G.jsx)(Qi, { text: r })]
|
|
6523
|
+
}, e.cells[0].id);
|
|
6524
6524
|
}
|
|
6525
|
-
return /* @__PURE__ */ (0, G.jsx)(M.Fragment, { children: n },
|
|
6525
|
+
return /* @__PURE__ */ (0, G.jsx)(M.Fragment, { children: n }, e.cells[0].id);
|
|
6526
6526
|
}), t[0] = o, t[1] = i, t[2] = r, t[3] = a, t[4] = n, t[5] = s, t[6] = c, t[7] = l, t[8] = u, t[9] = d, t[10] = f;
|
|
6527
6527
|
} else s = t[5], c = t[6], l = t[7], u = t[8], d = t[9], f = t[10];
|
|
6528
6528
|
let p;
|
package/package.json
CHANGED
|
@@ -328,9 +328,9 @@ export const PlansBlock = (props: { data: PlanNotificationEvent[] }) => {
|
|
|
328
328
|
</span>
|
|
329
329
|
</div>
|
|
330
330
|
<ul className="flex flex-col gap-1">
|
|
331
|
-
{plans.map((item
|
|
331
|
+
{plans.map((item) => (
|
|
332
332
|
<li
|
|
333
|
-
key={
|
|
333
|
+
key={item.content}
|
|
334
334
|
className={`
|
|
335
335
|
flex items-center gap-2 px-2 py-1 rounded
|
|
336
336
|
`}
|
|
@@ -124,6 +124,7 @@ export const AddContextButton = ({
|
|
|
124
124
|
return (
|
|
125
125
|
<Tooltip content="Add context">
|
|
126
126
|
<Button
|
|
127
|
+
aria-label="Add context"
|
|
127
128
|
variant="text"
|
|
128
129
|
size="icon"
|
|
129
130
|
onClick={handleAddContext}
|
|
@@ -148,6 +149,7 @@ export const AttachFileButton = ({
|
|
|
148
149
|
<>
|
|
149
150
|
<Tooltip content="Attach a file">
|
|
150
151
|
<Button
|
|
152
|
+
aria-label="Attach a file"
|
|
151
153
|
variant="text"
|
|
152
154
|
size="icon"
|
|
153
155
|
onClick={() => fileInputRef.current?.click()}
|
|
@@ -166,7 +166,12 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
|
|
|
166
166
|
return (
|
|
167
167
|
<div className="flex border-b px-2 py-1 justify-between shrink-0 items-center">
|
|
168
168
|
<Tooltip content="New chat">
|
|
169
|
-
<Button
|
|
169
|
+
<Button
|
|
170
|
+
aria-label="New chat"
|
|
171
|
+
variant="text"
|
|
172
|
+
size="icon"
|
|
173
|
+
onClick={onNewChat}
|
|
174
|
+
>
|
|
170
175
|
<PlusIcon className="h-4 w-4" />
|
|
171
176
|
</Button>
|
|
172
177
|
</Tooltip>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
9
9
|
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
10
10
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
11
|
+
import type { PreviewColumn } from "@/plugins/impl/DataTablePlugin";
|
|
11
12
|
import { ColumnExplorerPanel } from "../column-explorer-panel/column-explorer";
|
|
12
13
|
import type { FieldTypesWithExternalType } from "../types";
|
|
13
14
|
|
|
@@ -36,11 +37,13 @@ const TEST_COLUMNS: ColumnDef<Row>[] = [
|
|
|
36
37
|
interface HarnessProps {
|
|
37
38
|
totalColumns?: number;
|
|
38
39
|
initiallyHidden?: readonly string[];
|
|
40
|
+
previewColumn?: PreviewColumn;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
function PanelHarness({
|
|
42
44
|
totalColumns = 3,
|
|
43
45
|
initiallyHidden = EMPTY_HIDDEN_COLUMNS,
|
|
46
|
+
previewColumn = vi.fn().mockResolvedValue({}),
|
|
44
47
|
}: HarnessProps) {
|
|
45
48
|
const table = useReactTable<Row>({
|
|
46
49
|
data: [],
|
|
@@ -55,7 +58,7 @@ function PanelHarness({
|
|
|
55
58
|
});
|
|
56
59
|
return (
|
|
57
60
|
<ColumnExplorerPanel
|
|
58
|
-
previewColumn={
|
|
61
|
+
previewColumn={previewColumn}
|
|
59
62
|
fieldTypes={FIELD_TYPES}
|
|
60
63
|
totalRows={3}
|
|
61
64
|
totalColumns={totalColumns}
|
|
@@ -124,6 +127,29 @@ describe("ColumnExplorerPanel search", () => {
|
|
|
124
127
|
});
|
|
125
128
|
});
|
|
126
129
|
|
|
130
|
+
describe("ColumnExplorerPanel chart actions", () => {
|
|
131
|
+
it("names chart actions for each expanded column", async () => {
|
|
132
|
+
const previewColumn = vi.fn<PreviewColumn>().mockResolvedValue({
|
|
133
|
+
chart_code: "chart_code",
|
|
134
|
+
chart_spec: null,
|
|
135
|
+
error: null,
|
|
136
|
+
missing_packages: null,
|
|
137
|
+
stats: null,
|
|
138
|
+
});
|
|
139
|
+
renderPanel({ previewColumn });
|
|
140
|
+
fireEvent.click(screen.getByText("cust_age"));
|
|
141
|
+
|
|
142
|
+
for (const column of ["customer_name", "cust_age"]) {
|
|
143
|
+
expect(
|
|
144
|
+
await screen.findByRole("button", {
|
|
145
|
+
name: `Add chart to notebook for ${column}`,
|
|
146
|
+
}),
|
|
147
|
+
).toBeInTheDocument();
|
|
148
|
+
expect(previewColumn).toHaveBeenCalledWith({ column });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
127
153
|
describe("ColumnExplorerPanel header counts", () => {
|
|
128
154
|
it("uses rendered-subset total when a clipped column is hidden", () => {
|
|
129
155
|
// Dataset has 100 columns server-side; only 3 are rendered into the
|
|
@@ -10,13 +10,27 @@ import { cellId } from "@/__tests__/branded";
|
|
|
10
10
|
import { LazyVegaEmbed } from "@/components/charts/lazy";
|
|
11
11
|
import { vegaLoader } from "@/plugins/impl/vega/loader";
|
|
12
12
|
import { ChartPanel, TablePanel, type TablePanelProps } from "../charts";
|
|
13
|
-
import { getChartTabName, tabsStorageAtom } from "../storage";
|
|
13
|
+
import { createTabId, getChartTabName, tabsStorageAtom } from "../storage";
|
|
14
14
|
import { ChartType, NONE_VALUE } from "../types";
|
|
15
|
+
import { TOO_MANY_ROWS } from "../../types";
|
|
15
16
|
|
|
16
17
|
vi.mock("@/components/charts/lazy", () => ({
|
|
17
18
|
LazyVegaEmbed: vi.fn(() => null),
|
|
18
19
|
}));
|
|
19
20
|
|
|
21
|
+
vi.mock("../components/chart-items", async (importOriginal) => ({
|
|
22
|
+
...(await importOriginal<typeof import("../components/chart-items")>()),
|
|
23
|
+
ChartTypeSelect: ({
|
|
24
|
+
onValueChange,
|
|
25
|
+
}: {
|
|
26
|
+
onValueChange: (value: string) => void;
|
|
27
|
+
}) => (
|
|
28
|
+
<button type="button" onClick={() => onValueChange("line")}>
|
|
29
|
+
Change chart type
|
|
30
|
+
</button>
|
|
31
|
+
),
|
|
32
|
+
}));
|
|
33
|
+
|
|
20
34
|
beforeAll(() => {
|
|
21
35
|
SetupMocks.resizeObserver();
|
|
22
36
|
});
|
|
@@ -38,6 +52,7 @@ describe("ChartPanel", () => {
|
|
|
38
52
|
id,
|
|
39
53
|
[
|
|
40
54
|
{
|
|
55
|
+
id: createTabId(),
|
|
41
56
|
tabName,
|
|
42
57
|
chartType: ChartType.BAR,
|
|
43
58
|
config: {
|
|
@@ -283,4 +298,62 @@ describe("ChartPanel", () => {
|
|
|
283
298
|
});
|
|
284
299
|
},
|
|
285
300
|
);
|
|
301
|
+
it("keeps panel state when a chart type change renames the tab", async () => {
|
|
302
|
+
const store = createStore();
|
|
303
|
+
const id = cellId("rename-keeps-state");
|
|
304
|
+
const tabName = getChartTabName(0, ChartType.BAR);
|
|
305
|
+
store.set(
|
|
306
|
+
tabsStorageAtom,
|
|
307
|
+
new Map([
|
|
308
|
+
[
|
|
309
|
+
id,
|
|
310
|
+
[
|
|
311
|
+
{
|
|
312
|
+
id: createTabId(),
|
|
313
|
+
tabName,
|
|
314
|
+
chartType: ChartType.BAR,
|
|
315
|
+
config: {
|
|
316
|
+
general: {
|
|
317
|
+
xColumn: { field: "value", type: "number" },
|
|
318
|
+
yColumn: {
|
|
319
|
+
field: "value",
|
|
320
|
+
type: "integer",
|
|
321
|
+
aggregate: NONE_VALUE,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
],
|
|
328
|
+
]),
|
|
329
|
+
);
|
|
330
|
+
const sample = [{ value: 1 }];
|
|
331
|
+
const props: TablePanelProps = {
|
|
332
|
+
cellId: id,
|
|
333
|
+
data: sample,
|
|
334
|
+
dataTable: <div />,
|
|
335
|
+
totalRows: TOO_MANY_ROWS,
|
|
336
|
+
columns: 1,
|
|
337
|
+
displayHeader: true,
|
|
338
|
+
fieldTypes: [["value", ["number", "float64"]]],
|
|
339
|
+
};
|
|
340
|
+
const { getByRole, findByRole, queryByRole } = render(
|
|
341
|
+
<Provider store={store}>
|
|
342
|
+
<Tooltip.Provider>
|
|
343
|
+
<TablePanel {...props} />
|
|
344
|
+
</Tooltip.Provider>
|
|
345
|
+
</Provider>,
|
|
346
|
+
);
|
|
347
|
+
fireEvent.click(getByRole("tab", { name: tabName }));
|
|
348
|
+
fireEvent.click(await findByRole("button", { name: "Proceed" }));
|
|
349
|
+
await waitFor(() => {
|
|
350
|
+
expect(queryByRole("button", { name: "Proceed" })).toBeNull();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
fireEvent.click(getByRole("button", { name: "Change chart type" }));
|
|
354
|
+
|
|
355
|
+
const renamed = getChartTabName(0, ChartType.LINE);
|
|
356
|
+
await findByRole("tab", { name: renamed });
|
|
357
|
+
expect(queryByRole("button", { name: "Proceed" })).toBeNull();
|
|
358
|
+
});
|
|
286
359
|
});
|
|
@@ -16,7 +16,7 @@ import { cellId } from "@/__tests__/branded";
|
|
|
16
16
|
import { availableStorage } from "@/utils/storage/storage";
|
|
17
17
|
import { ChartSchema } from "../schemas";
|
|
18
18
|
import type { TabName } from "../storage";
|
|
19
|
-
import { KEY, tabsStorageAtom } from "../storage";
|
|
19
|
+
import { createTabId, KEY, tabsStorageAtom } from "../storage";
|
|
20
20
|
import { ChartType } from "../types";
|
|
21
21
|
|
|
22
22
|
describe("Chart Transforms Storage", () => {
|
|
@@ -39,6 +39,7 @@ describe("Chart Transforms Storage", () => {
|
|
|
39
39
|
const store = getDefaultStore();
|
|
40
40
|
const cid = cellId("cell-1");
|
|
41
41
|
const tabData = {
|
|
42
|
+
id: createTabId(),
|
|
42
43
|
tabName: "Tab 1" as TabName,
|
|
43
44
|
chartType: ChartType.LINE,
|
|
44
45
|
config: ChartSchema.parse({
|
|
@@ -63,6 +64,7 @@ describe("Chart Transforms Storage", () => {
|
|
|
63
64
|
const store = getDefaultStore();
|
|
64
65
|
const cid = cellId("cell-1");
|
|
65
66
|
const tabData1 = {
|
|
67
|
+
id: createTabId(),
|
|
66
68
|
tabName: "Tab 1" as TabName,
|
|
67
69
|
chartType: ChartType.LINE,
|
|
68
70
|
config: ChartSchema.parse({
|
|
@@ -73,6 +75,7 @@ describe("Chart Transforms Storage", () => {
|
|
|
73
75
|
}),
|
|
74
76
|
};
|
|
75
77
|
const tabData2 = {
|
|
78
|
+
id: createTabId(),
|
|
76
79
|
tabName: "Tab 2" as TabName,
|
|
77
80
|
chartType: ChartType.BAR,
|
|
78
81
|
config: ChartSchema.parse({
|
|
@@ -48,7 +48,12 @@ import { HeatmapForm } from "./forms/heatmap";
|
|
|
48
48
|
import { PieForm } from "./forms/pie";
|
|
49
49
|
import { LazyChart } from "./lazy-chart";
|
|
50
50
|
import { ChartSchema, type ChartSchemaType, getChartDefaults } from "./schemas";
|
|
51
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
createTabId,
|
|
53
|
+
getChartTabName,
|
|
54
|
+
type TabName,
|
|
55
|
+
tabsStorageAtom,
|
|
56
|
+
} from "./storage";
|
|
52
57
|
import { ChartType } from "./types";
|
|
53
58
|
|
|
54
59
|
const NEW_CHART_TYPE = "bar" as ChartType;
|
|
@@ -114,7 +119,12 @@ export const TablePanel: React.FC<TablePanelProps> = ({
|
|
|
114
119
|
const tabName = getChartTabName(0, NEW_CHART_TYPE);
|
|
115
120
|
const newTabs = new Map(tabsMap);
|
|
116
121
|
newTabs.set(cellId, [
|
|
117
|
-
{
|
|
122
|
+
{
|
|
123
|
+
id: createTabId(),
|
|
124
|
+
tabName,
|
|
125
|
+
chartType: NEW_CHART_TYPE,
|
|
126
|
+
config: getChartDefaults(),
|
|
127
|
+
},
|
|
118
128
|
]);
|
|
119
129
|
saveTabsMap(newTabs);
|
|
120
130
|
setTabCounter(1);
|
|
@@ -135,6 +145,7 @@ export const TablePanel: React.FC<TablePanelProps> = ({
|
|
|
135
145
|
newTabs.set(cellId, [
|
|
136
146
|
...tabs,
|
|
137
147
|
{
|
|
148
|
+
id: createTabId(),
|
|
138
149
|
tabName,
|
|
139
150
|
chartType: NEW_CHART_TYPE,
|
|
140
151
|
config: getChartDefaults(),
|
|
@@ -233,9 +244,9 @@ export const TablePanel: React.FC<TablePanelProps> = ({
|
|
|
233
244
|
>
|
|
234
245
|
Table
|
|
235
246
|
</TabsTrigger>
|
|
236
|
-
{tabs.map((tab
|
|
247
|
+
{tabs.map((tab) => (
|
|
237
248
|
<TabsTrigger
|
|
238
|
-
key={
|
|
249
|
+
key={tab.id}
|
|
239
250
|
className="text-xs"
|
|
240
251
|
value={tab.tabName}
|
|
241
252
|
onClick={() => setSelectedTab(tab.tabName)}
|
|
@@ -263,7 +274,7 @@ export const TablePanel: React.FC<TablePanelProps> = ({
|
|
|
263
274
|
<TabsContent className="mt-1 overflow-hidden" value={DEFAULT_TAB_NAME}>
|
|
264
275
|
{dataTable}
|
|
265
276
|
</TabsContent>
|
|
266
|
-
{tabs.map((tab
|
|
277
|
+
{tabs.map((tab) => {
|
|
267
278
|
const saveChart = (formValues: ChartSchemaType) => {
|
|
268
279
|
saveTabChart({
|
|
269
280
|
tabName: tab.tabName,
|
|
@@ -275,7 +286,11 @@ export const TablePanel: React.FC<TablePanelProps> = ({
|
|
|
275
286
|
saveTabChartType(tab.tabName, chartType);
|
|
276
287
|
};
|
|
277
288
|
return (
|
|
278
|
-
<TabsContent
|
|
289
|
+
<TabsContent
|
|
290
|
+
key={tab.id}
|
|
291
|
+
value={tab.tabName}
|
|
292
|
+
className="h-[400px] mt-0"
|
|
293
|
+
>
|
|
279
294
|
<ChartPanel
|
|
280
295
|
tableData={data}
|
|
281
296
|
chartConfig={tab.config}
|
|
@@ -7,13 +7,16 @@ import { Logger } from "@/utils/Logger";
|
|
|
7
7
|
import { NotebookScopedLocalStorage } from "@/utils/storage/typed";
|
|
8
8
|
import { capitalize } from "@/utils/strings";
|
|
9
9
|
import type { TypedString } from "@/utils/typed";
|
|
10
|
+
import { generateUUID } from "@/utils/uuid";
|
|
10
11
|
import { ChartSchema, type ChartSchemaType } from "./schemas";
|
|
11
12
|
import type { ChartType } from "./types";
|
|
12
13
|
|
|
13
14
|
export type TabName = TypedString<"TabName">;
|
|
15
|
+
export type TabId = TypedString<"TabId">;
|
|
14
16
|
export const KEY = "marimo:charts:v2";
|
|
15
17
|
|
|
16
18
|
interface TabStorage {
|
|
19
|
+
id: TabId;
|
|
17
20
|
tabName: TabName; // unique within cell
|
|
18
21
|
chartType: ChartType;
|
|
19
22
|
config: ChartSchemaType;
|
|
@@ -21,11 +24,15 @@ interface TabStorage {
|
|
|
21
24
|
|
|
22
25
|
type TabStorageMap = Map<CellId, TabStorage[]>;
|
|
23
26
|
|
|
24
|
-
const TabStorageSchema = z
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const TabStorageSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
id: z.string().optional(),
|
|
30
|
+
tabName: z.string().transform((name) => name as TabName),
|
|
31
|
+
chartType: z.string().transform((type) => type as ChartType),
|
|
32
|
+
config: ChartSchema,
|
|
33
|
+
})
|
|
34
|
+
// Stored tabs without an id use their name, which is unique within a cell.
|
|
35
|
+
.transform((tab) => ({ ...tab, id: (tab.id ?? tab.tabName) as TabId }));
|
|
29
36
|
const TabStorageEntriesSchema = z.array(
|
|
30
37
|
z.tuple([
|
|
31
38
|
z.string().transform((name) => name as CellId),
|
|
@@ -64,6 +71,10 @@ export const tabsStorageAtom = atomWithStorage<TabStorageMap>(
|
|
|
64
71
|
mapStorage,
|
|
65
72
|
);
|
|
66
73
|
|
|
74
|
+
export function createTabId(): TabId {
|
|
75
|
+
return generateUUID() as TabId;
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
/**
|
|
68
79
|
* Convenience function to get the tab name for a given tab number and chart type
|
|
69
80
|
*/
|
|
@@ -345,7 +345,7 @@ const ColumnPreview = ({
|
|
|
345
345
|
const chart = chart_spec && renderChart(chart_spec, theme);
|
|
346
346
|
|
|
347
347
|
const addDataframeChart = chart_code && (
|
|
348
|
-
<AddDataframeChart chartCode={chart_code} />
|
|
348
|
+
<AddDataframeChart chartCode={chart_code} columnName={columnName} />
|
|
349
349
|
);
|
|
350
350
|
|
|
351
351
|
return (
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
fireEvent,
|
|
5
|
+
render,
|
|
6
|
+
screen,
|
|
7
|
+
waitFor,
|
|
8
|
+
within,
|
|
9
|
+
} from "@testing-library/react";
|
|
10
|
+
import { createStore, Provider } from "jotai";
|
|
11
|
+
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
13
|
+
import { Command, CommandList } from "@/components/ui/command";
|
|
14
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
15
|
+
import type { DataTable } from "@/core/kernel/messages";
|
|
16
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
17
|
+
import { copyToClipboard } from "@/utils/copy";
|
|
18
|
+
import { TableList } from "../datasources";
|
|
19
|
+
|
|
20
|
+
vi.mock("@/utils/copy", () => ({
|
|
21
|
+
copyToClipboard: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
beforeAll(() => {
|
|
25
|
+
HTMLElement.prototype.scrollIntoView = vi.fn();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const table: DataTable = {
|
|
29
|
+
name: "users",
|
|
30
|
+
columns: [
|
|
31
|
+
{
|
|
32
|
+
name: "email",
|
|
33
|
+
type: "string",
|
|
34
|
+
external_type: "VARCHAR",
|
|
35
|
+
sample_values: [],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "name",
|
|
39
|
+
type: "string",
|
|
40
|
+
external_type: "VARCHAR",
|
|
41
|
+
sample_values: [],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
source: "memory",
|
|
45
|
+
source_type: "local",
|
|
46
|
+
type: "table",
|
|
47
|
+
engine: null,
|
|
48
|
+
indexes: null,
|
|
49
|
+
num_columns: 2,
|
|
50
|
+
num_rows: null,
|
|
51
|
+
variable_name: null,
|
|
52
|
+
primary_keys: null,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
describe("Column actions", () => {
|
|
56
|
+
it("copies each column through one named button without expanding its preview", async () => {
|
|
57
|
+
const store = createStore();
|
|
58
|
+
const client = MockRequestClient.create();
|
|
59
|
+
store.set(requestClientAtom, client);
|
|
60
|
+
render(
|
|
61
|
+
<Provider store={store}>
|
|
62
|
+
<TooltipProvider>
|
|
63
|
+
<Command>
|
|
64
|
+
<CommandList>
|
|
65
|
+
<TableList tables={[table]} />
|
|
66
|
+
</CommandList>
|
|
67
|
+
</Command>
|
|
68
|
+
</TooltipProvider>
|
|
69
|
+
</Provider>,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
fireEvent.click(await screen.findByText("users"));
|
|
73
|
+
|
|
74
|
+
for (const column of table.columns) {
|
|
75
|
+
const copyButton = await screen.findByRole("button", {
|
|
76
|
+
name: `Copy column name ${column.name}`,
|
|
77
|
+
});
|
|
78
|
+
const row = screen.getByRole("option", {
|
|
79
|
+
name: `${column.name} ${column.external_type}`,
|
|
80
|
+
});
|
|
81
|
+
expect(within(row).getAllByRole("button")).toEqual([copyButton]);
|
|
82
|
+
expect(copyButton.querySelector("button")).toBeNull();
|
|
83
|
+
|
|
84
|
+
fireEvent.focus(copyButton);
|
|
85
|
+
expect(
|
|
86
|
+
await screen.findByRole("tooltip", { name: "Copy column name" }),
|
|
87
|
+
).toBeInTheDocument();
|
|
88
|
+
|
|
89
|
+
fireEvent.click(copyButton);
|
|
90
|
+
await waitFor(() => {
|
|
91
|
+
expect(copyToClipboard).toHaveBeenLastCalledWith(column.name);
|
|
92
|
+
});
|
|
93
|
+
expect(client.previewDatasetColumn).not.toHaveBeenCalled();
|
|
94
|
+
}
|
|
95
|
+
expect(copyToClipboard).toHaveBeenCalledTimes(2);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { render } from "@testing-library/react";
|
|
4
|
-
import { Provider } from "jotai";
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
|
+
import { createStore, Provider } from "jotai";
|
|
5
5
|
import type React from "react";
|
|
6
6
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
import { MockRequestClient } from "@/__mocks__/requests";
|
|
8
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
8
9
|
import type { SQLTableContext } from "@/core/datasets/data-source-connections";
|
|
9
10
|
import type { DataTable, DataTableColumn } from "@/core/kernel/messages";
|
|
10
11
|
import { requestClientAtom } from "@/core/network/requests";
|
|
11
12
|
import { store } from "@/core/state/jotai";
|
|
12
13
|
import { DatasetColumnPreview } from "../column-preview";
|
|
13
14
|
|
|
15
|
+
const { createNewCell } = vi.hoisted(() => ({ createNewCell: vi.fn() }));
|
|
16
|
+
|
|
17
|
+
vi.mock("@/core/cells/cells", async (importOriginal) => ({
|
|
18
|
+
...(await importOriginal<typeof import("@/core/cells/cells")>()),
|
|
19
|
+
useCellActions: () => ({ createNewCell }),
|
|
20
|
+
}));
|
|
21
|
+
|
|
14
22
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
15
23
|
<Provider store={store}>{children}</Provider>
|
|
16
24
|
);
|
|
@@ -95,3 +103,56 @@ describe("DatasetColumnPreview fullyQualifiedTableName", () => {
|
|
|
95
103
|
);
|
|
96
104
|
});
|
|
97
105
|
});
|
|
106
|
+
|
|
107
|
+
describe("DatasetColumnPreview chart actions", () => {
|
|
108
|
+
it("identifies each column and inserts its corresponding chart", () => {
|
|
109
|
+
const store = createStore();
|
|
110
|
+
store.set(requestClientAtom, MockRequestClient.create());
|
|
111
|
+
const columns: DataTableColumn[] = [
|
|
112
|
+
{
|
|
113
|
+
name: "email",
|
|
114
|
+
type: "string",
|
|
115
|
+
external_type: "str",
|
|
116
|
+
sample_values: [],
|
|
117
|
+
},
|
|
118
|
+
{ name: "age", type: "integer", external_type: "int", sample_values: [] },
|
|
119
|
+
];
|
|
120
|
+
render(
|
|
121
|
+
<Provider store={store}>
|
|
122
|
+
<TooltipProvider>
|
|
123
|
+
{columns.map((column) => (
|
|
124
|
+
<DatasetColumnPreview
|
|
125
|
+
key={column.name}
|
|
126
|
+
table={{ ...table, source_type: "local", engine: null }}
|
|
127
|
+
column={column}
|
|
128
|
+
onAddColumnChart={vi.fn()}
|
|
129
|
+
preview={{
|
|
130
|
+
table_name: table.name,
|
|
131
|
+
column_name: column.name,
|
|
132
|
+
chart_code: `chart_${column.name}`,
|
|
133
|
+
chart_spec: null,
|
|
134
|
+
error: null,
|
|
135
|
+
missing_packages: null,
|
|
136
|
+
stats: {},
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
))}
|
|
140
|
+
</TooltipProvider>
|
|
141
|
+
</Provider>,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
for (const column of columns) {
|
|
145
|
+
fireEvent.click(
|
|
146
|
+
screen.getByRole("button", {
|
|
147
|
+
name: `Add chart to notebook for ${column.name}`,
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
expect(createNewCell).toHaveBeenLastCalledWith({
|
|
151
|
+
code: `chart_${column.name}`,
|
|
152
|
+
before: false,
|
|
153
|
+
cellId: "__end__",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
expect(createNewCell).toHaveBeenCalledTimes(2);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -112,12 +112,16 @@ export const DatasetColumnPreview: React.FC<{
|
|
|
112
112
|
|
|
113
113
|
const addDataframeChart = preview.chart_code &&
|
|
114
114
|
table.source_type === "local" && (
|
|
115
|
-
<AddDataframeChart
|
|
115
|
+
<AddDataframeChart
|
|
116
|
+
chartCode={preview.chart_code}
|
|
117
|
+
columnName={column.name}
|
|
118
|
+
/>
|
|
116
119
|
);
|
|
117
120
|
|
|
118
121
|
const addSQLChart = table.source_type === "duckdb" && (
|
|
119
122
|
<Tooltip content="Add SQL cell" delayDuration={400}>
|
|
120
123
|
<Button
|
|
124
|
+
aria-label={`Add SQL cell for ${column.name}`}
|
|
121
125
|
variant="outline"
|
|
122
126
|
size="icon"
|
|
123
127
|
className="z-10 bg-background absolute right-1 -top-1"
|
|
@@ -238,7 +242,8 @@ export function renderChart(chartSpec: string, theme: Theme) {
|
|
|
238
242
|
|
|
239
243
|
export const AddDataframeChart: React.FC<{
|
|
240
244
|
chartCode: string;
|
|
241
|
-
|
|
245
|
+
columnName: string;
|
|
246
|
+
}> = ({ chartCode, columnName }) => {
|
|
242
247
|
const autoInstantiate = useAtomValue(autoInstantiateAtom);
|
|
243
248
|
const lastFocusedCellId = useLastFocusedCellId();
|
|
244
249
|
const { createNewCell } = useCellActions();
|
|
@@ -261,6 +266,7 @@ export const AddDataframeChart: React.FC<{
|
|
|
261
266
|
return (
|
|
262
267
|
<Tooltip content="Add chart to notebook" delayDuration={400}>
|
|
263
268
|
<Button
|
|
269
|
+
aria-label={`Add chart to notebook for ${columnName}`}
|
|
264
270
|
variant="outline"
|
|
265
271
|
size="icon"
|
|
266
272
|
className="z-10 bg-background absolute right-1 -top-0.5"
|