@marimo-team/islands 0.23.15-dev73 → 0.23.15-dev75
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-B0L8DXul.js → code-visibility-DfgBK6L-.js} +1 -1
- package/dist/main.js +2 -2
- package/dist/{reveal-component-BY3OPogD.js → reveal-component-CP1pGjn-.js} +1 -1
- package/package.json +1 -1
- package/src/components/editor/chrome/components/__tests__/feedback-button.test.tsx +26 -36
- package/src/components/editor/chrome/components/feedback-button.tsx +114 -104
- package/src/core/diagnostics/__tests__/issue-details.test.ts +50 -48
- package/src/core/diagnostics/issue-details.ts +53 -42
|
@@ -36291,7 +36291,7 @@ ${d}`,
|
|
|
36291
36291
|
return Logger.warn("Failed to get version from mount config"), null;
|
|
36292
36292
|
}
|
|
36293
36293
|
}
|
|
36294
|
-
marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.15-
|
|
36294
|
+
marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.15-dev75");
|
|
36295
36295
|
showCodeInRunModeAtom = atom(true);
|
|
36296
36296
|
atom(null);
|
|
36297
36297
|
var import_compiler_runtime = require_compiler_runtime();
|
package/dist/main.js
CHANGED
|
@@ -26,7 +26,7 @@ import { $ as notebookOutline, $t as requestClientAtom, A as ChevronDownIcon, Bt
|
|
|
26
26
|
import { __tla as __tla_1 } from "./chunk-5FQGJX7Z-B9WoXK1R.js";
|
|
27
27
|
import { o as useSize, s as Root$1, u as createLucideIcon } from "./dist-B96tMAst.js";
|
|
28
28
|
import { A as SquareFunction, C as DEFAULT_COLOR_SCHEME, D as SCALE_TYPE_DESCRIPTIONS, E as EMPTY_VALUE$1, O as TIME_UNIT_DESCRIPTIONS, S as DEFAULT_AGGREGATION, T as DEFAULT_TIME_UNIT, _ as AGGREGATION_TYPE_DESCRIPTIONS, a as AGGREGATION_FNS$1, b as COLOR_SCHEMES, c as COLOR_BY_FIELDS, d as NONE_VALUE, f as SELECTABLE_DATA_TYPES, g as TIME_UNITS, h as STRING_AGGREGATION_FNS, i as convertDataTypeToSelectable, j as ChartColumn, k as escapeFieldName, l as COMBINED_TIME_UNITS, m as SORT_TYPES, n as createSpecWithoutData, o as BIN_AGGREGATION, p as SINGLE_TIME_UNITS, r as isFieldSet, s as CHART_TYPES, t as augmentSpecWithData, u as ChartType, v as AGGREGATION_TYPE_ICON, w as DEFAULT_MAX_BINS_FACET, x as COUNT_FIELD, y as CHART_TYPE_ICON } from "./spec-CnaxwDqy.js";
|
|
29
|
-
import { $ as ContextAwarePanelItem, $t as EmotionCacheProvider, A as prettifyRowColumnCount, At as loadTableData, B as useSelectList, Bt as dateToLocalISOTime, C as Filenames, Ct as ColumnChartContext, D as EmptyState, Dt as usePrevious$1, E as ColumnPreviewContainer, Et as useIntersectionObserver, F as ContextMenuContent, Ft as getMimeValues, G as ComboboxItem, Gt as ChartErrorState, H as DatePicker, Ht as TabsContent, I as ContextMenuItem, It as isNullishFilter, J as CommandInput, Jt as LazyVegaEmbed, K as Command, Kt as ChartInfoState, L as ContextMenuSeparator, Lt as Maps, M as getColumnCountForDisplay, Mt as SELECT_COLUMN_ID, N as getUserColumnVisibilityCounts, Nt as TOO_MANY_ROWS, O as ErrorState, Ot as getPageIndexForRow, P as ContextMenu, Pt as toFieldTypes, Q as smartMatch, Qt as HtmlOutput, R as ContextMenuTrigger, Rt as dateToLocalISODate, S as Progress, St as renderCellValue, T as ColumnName, Tt as DelayMount, U as DateRangePicker, Ut as TabsList, V as CompactChipRow, Vt as Tabs, W as Combobox, Wt as TabsTrigger, X as CommandList, Xt as RenderTextWithLinks, Y as CommandItem, Yt as useOverflowDetection, Z as CommandSeparator, Zt as Kbd, _ as InstallPackageButton, _n as ArrowDownWideNarrow, _t as TableHeader, an as GripHorizontal, at as SlotNames, b as downloadByURL, bt as generateColumns, d as RadioGroup, dn as Code, dt as Fill, en as hasFunctionProperty, et as PANEL_TYPES, f as RadioGroupItem, fn as ChevronsUpDown, ft as Provider$1, g as DataTable, gn as ChevronLeft, gt as TableHead, h as OutputRenderer, hn as ChevronsDownUp, ht as TableCell, in as TextWrap, it as isCellAwareAtom, j as prettifyRowCount, jt as INDEX_COLUMN_NAME, k as LoadingState, kt as loadTableAndRawData, l as Slide, ln as Ellipsis, m as OutputArea, mn as ChevronsLeft, mt as TableBody, n as marimoVersionAtom, nn as $fae977aafc393c5c$export$588937bcd60ade55, nt as contextAwarePanelOwner, on as Funnel, ot as slotsController, p as JsonOutput, pn as ChevronsRight, pt as Table, q as CommandEmpty, qt as ChartLoadingState, r as showCodeInRunModeAtom, rn as $fae977aafc393c5c$export$6b862160d295c8e, rt as contextAwarePanelType, s as SLIDE_TYPE_OPTIONS_BY_VALUE, sn as EyeOff, st as Toggle, t as useNotebookCodeAvailable, tn as isRecord, tt as contextAwarePanelOpen, u as Switch, un as Download, v as ADD_PRINTING_CLASS, vt as TableRow, w as downloadSizeLimitAtom, wt as ColumnChartSpecModel, x as downloadHTMLAsImage, xt as inferFieldTypes, y as downloadBlob, yt as NAMELESS_COLUMN_PREFIX, z as useInternalStateWithSync, zt as dateToLocalISODateTime, __tla as __tla_2 } from "./code-visibility-
|
|
29
|
+
import { $ as ContextAwarePanelItem, $t as EmotionCacheProvider, A as prettifyRowColumnCount, At as loadTableData, B as useSelectList, Bt as dateToLocalISOTime, C as Filenames, Ct as ColumnChartContext, D as EmptyState, Dt as usePrevious$1, E as ColumnPreviewContainer, Et as useIntersectionObserver, F as ContextMenuContent, Ft as getMimeValues, G as ComboboxItem, Gt as ChartErrorState, H as DatePicker, Ht as TabsContent, I as ContextMenuItem, It as isNullishFilter, J as CommandInput, Jt as LazyVegaEmbed, K as Command, Kt as ChartInfoState, L as ContextMenuSeparator, Lt as Maps, M as getColumnCountForDisplay, Mt as SELECT_COLUMN_ID, N as getUserColumnVisibilityCounts, Nt as TOO_MANY_ROWS, O as ErrorState, Ot as getPageIndexForRow, P as ContextMenu, Pt as toFieldTypes, Q as smartMatch, Qt as HtmlOutput, R as ContextMenuTrigger, Rt as dateToLocalISODate, S as Progress, St as renderCellValue, T as ColumnName, Tt as DelayMount, U as DateRangePicker, Ut as TabsList, V as CompactChipRow, Vt as Tabs, W as Combobox, Wt as TabsTrigger, X as CommandList, Xt as RenderTextWithLinks, Y as CommandItem, Yt as useOverflowDetection, Z as CommandSeparator, Zt as Kbd, _ as InstallPackageButton, _n as ArrowDownWideNarrow, _t as TableHeader, an as GripHorizontal, at as SlotNames, b as downloadByURL, bt as generateColumns, d as RadioGroup, dn as Code, dt as Fill, en as hasFunctionProperty, et as PANEL_TYPES, f as RadioGroupItem, fn as ChevronsUpDown, ft as Provider$1, g as DataTable, gn as ChevronLeft, gt as TableHead, h as OutputRenderer, hn as ChevronsDownUp, ht as TableCell, in as TextWrap, it as isCellAwareAtom, j as prettifyRowCount, jt as INDEX_COLUMN_NAME, k as LoadingState, kt as loadTableAndRawData, l as Slide, ln as Ellipsis, m as OutputArea, mn as ChevronsLeft, mt as TableBody, n as marimoVersionAtom, nn as $fae977aafc393c5c$export$588937bcd60ade55, nt as contextAwarePanelOwner, on as Funnel, ot as slotsController, p as JsonOutput, pn as ChevronsRight, pt as Table, q as CommandEmpty, qt as ChartLoadingState, r as showCodeInRunModeAtom, rn as $fae977aafc393c5c$export$6b862160d295c8e, rt as contextAwarePanelType, s as SLIDE_TYPE_OPTIONS_BY_VALUE, sn as EyeOff, st as Toggle, t as useNotebookCodeAvailable, tn as isRecord, tt as contextAwarePanelOpen, u as Switch, un as Download, v as ADD_PRINTING_CLASS, vt as TableRow, w as downloadSizeLimitAtom, wt as ColumnChartSpecModel, x as downloadHTMLAsImage, xt as inferFieldTypes, y as downloadBlob, yt as NAMELESS_COLUMN_PREFIX, z as useInternalStateWithSync, zt as dateToLocalISODateTime, __tla as __tla_2 } from "./code-visibility-DfgBK6L-.js";
|
|
30
30
|
import { c as Calendar, i as createReducerAndAtoms, n as useOnUnmount, o as ToggleLeft, t as useOnMount } from "./useLifecycle-DqPxHgAN.js";
|
|
31
31
|
import { t as Check } from "./check-Cs-KV5XY.js";
|
|
32
32
|
import { A as Icon, C as logNever, D as $18f2051aff69b9bf$export$a54013f0d02a8f82, E as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7, F as createCollection, I as X, M as clamp$2, N as usePrevious$2, P as useDirection, R as ChevronDown, S as assertNever, a as SelectGroup, c as SelectSeparator, d as NativeSelect, i as SelectContent, j as Trigger$1, l as SelectTrigger, n as capitalize, o as SelectItem, r as Select, s as SelectLabel, t as Strings, u as SelectValue, w as $a916eb452884faea$export$b7a616150fdb9f44 } from "./strings-BYAhZnQi.js";
|
|
@@ -38157,7 +38157,7 @@ ${c}
|
|
|
38157
38157
|
function _temp2$2(e) {
|
|
38158
38158
|
e.target === e.currentTarget && e.key === "Enter" && (e.preventDefault(), e.stopPropagation(), e.currentTarget.click());
|
|
38159
38159
|
}
|
|
38160
|
-
var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-
|
|
38160
|
+
var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-CP1pGjn-.js"));
|
|
38161
38161
|
const SlidesLayoutRenderer = ({ layout: e, setLayout: r, cells: c, mode: l }) => {
|
|
38162
38162
|
var _a5;
|
|
38163
38163
|
let u = useAtomValue(kioskModeAtom), d = l === "read" || u, f = useAtomValue(numColumnsAtom) > 1, [p, m] = (0, import_react.useState)(null), { slideCells: h, skippedIds: g, noOutputIds: _, slideTypes: v, startCellIndex: y } = (0, import_react.useMemo)(() => computeSlideCellsInfo(c, e), [
|
|
@@ -9,7 +9,7 @@ import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
|
9
9
|
import { dt as kioskModeAtom, ht as outputIsStale } from "./html-to-image-DDENewl2.js";
|
|
10
10
|
import "./chunk-5FQGJX7Z-B9WoXK1R.js";
|
|
11
11
|
import { u as createLucideIcon } from "./dist-B96tMAst.js";
|
|
12
|
-
import { a as DEFAULT_DECK_VERTICAL_ALIGN, c as SlideSidebar, cn as Expand, ct as Panel, dn as Code, i as DEFAULT_DECK_TRANSITION, l as Slide, lt as PanelGroup, o as DEFAULT_SLIDE_TYPE, sn as EyeOff, t as useNotebookCodeAvailable, ut as PanelResizeHandle } from "./code-visibility-
|
|
12
|
+
import { a as DEFAULT_DECK_VERTICAL_ALIGN, c as SlideSidebar, cn as Expand, ct as Panel, dn as Code, i as DEFAULT_DECK_TRANSITION, l as Slide, lt as PanelGroup, o as DEFAULT_SLIDE_TYPE, sn as EyeOff, t as useNotebookCodeAvailable, ut as PanelResizeHandle } from "./code-visibility-DfgBK6L-.js";
|
|
13
13
|
import { X as useDebouncedCallback } from "./input-DStQIuqD.js";
|
|
14
14
|
import "./toDate-B3IRfHhw.js";
|
|
15
15
|
import "./react-dom-BTJzcVJ9.js";
|
package/package.json
CHANGED
|
@@ -50,6 +50,7 @@ function wrapper({ children }: { children: React.ReactNode }) {
|
|
|
50
50
|
describe("FeedbackModal issue reporting", () => {
|
|
51
51
|
beforeEach(() => {
|
|
52
52
|
vi.clearAllMocks();
|
|
53
|
+
localStorage.clear();
|
|
53
54
|
store.set(requestClientAtom, MockRequestClient.create());
|
|
54
55
|
store.set(viewStateAtom, { mode: "edit", cellAnchor: null });
|
|
55
56
|
store.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
@@ -87,7 +88,7 @@ describe("FeedbackModal issue reporting", () => {
|
|
|
87
88
|
).toBeInTheDocument();
|
|
88
89
|
});
|
|
89
90
|
|
|
90
|
-
it("copies partial
|
|
91
|
+
it("copies partial environment JSON when the environment request fails", async () => {
|
|
91
92
|
store.set(
|
|
92
93
|
requestClientAtom,
|
|
93
94
|
MockRequestClient.create({
|
|
@@ -97,7 +98,9 @@ describe("FeedbackModal issue reporting", () => {
|
|
|
97
98
|
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
98
99
|
|
|
99
100
|
await screen.findByText("Server environment information unavailable");
|
|
100
|
-
fireEvent.click(
|
|
101
|
+
fireEvent.click(
|
|
102
|
+
screen.getByRole("button", { name: "Copy environment JSON" }),
|
|
103
|
+
);
|
|
101
104
|
await waitFor(() =>
|
|
102
105
|
expect(copyModule.copyToClipboard).toHaveBeenCalledWith(
|
|
103
106
|
expect.stringContaining("Environment Collection Error"),
|
|
@@ -125,10 +128,8 @@ describe("FeedbackModal issue reporting", () => {
|
|
|
125
128
|
);
|
|
126
129
|
});
|
|
127
130
|
|
|
128
|
-
it("
|
|
129
|
-
const readCode = vi.fn().mockResolvedValue({
|
|
130
|
-
contents: "secret = 'review before posting'",
|
|
131
|
-
});
|
|
131
|
+
it("prefills the notebook code after checking include code", async () => {
|
|
132
|
+
const readCode = vi.fn().mockResolvedValue({ contents: "import marimo" });
|
|
132
133
|
store.set(
|
|
133
134
|
requestClientAtom,
|
|
134
135
|
MockRequestClient.create({
|
|
@@ -137,51 +138,43 @@ describe("FeedbackModal issue reporting", () => {
|
|
|
137
138
|
}),
|
|
138
139
|
);
|
|
139
140
|
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
141
|
+
const link = screen.getByRole("link", { name: "Open GitHub issue" });
|
|
140
142
|
await screen.findByText("Environment details");
|
|
141
143
|
|
|
142
144
|
expect(readCode).not.toHaveBeenCalled();
|
|
145
|
+
expect(link.getAttribute("href") ?? "").not.toContain("reproduction-code=");
|
|
146
|
+
|
|
143
147
|
fireEvent.click(
|
|
144
|
-
screen.getByRole("checkbox", { name: "Include
|
|
148
|
+
screen.getByRole("checkbox", { name: "Include notebook code" }),
|
|
145
149
|
);
|
|
146
150
|
await waitFor(() => expect(readCode).toHaveBeenCalledOnce());
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
expect(
|
|
151
|
-
|
|
152
|
-
),
|
|
153
|
-
);
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
const href = link.getAttribute("href") ?? "";
|
|
153
|
+
expect(href).toContain("reproduction-code=");
|
|
154
|
+
expect(href).toContain(encodeURIComponent("import marimo"));
|
|
155
|
+
});
|
|
154
156
|
});
|
|
155
157
|
|
|
156
|
-
it("
|
|
157
|
-
|
|
158
|
+
it("disables include code and explains why when disconnected", async () => {
|
|
159
|
+
store.set(connectionAtom, { state: WebSocketState.CONNECTING });
|
|
158
160
|
store.set(
|
|
159
161
|
requestClientAtom,
|
|
160
162
|
MockRequestClient.create({
|
|
161
163
|
getEnvironmentInfo: vi.fn().mockResolvedValue(environment),
|
|
162
|
-
readCode,
|
|
163
164
|
}),
|
|
164
165
|
);
|
|
165
166
|
render(<FeedbackModal onClose={vi.fn()} />, { wrapper });
|
|
166
167
|
await screen.findByText("Environment details");
|
|
167
168
|
|
|
168
|
-
|
|
169
|
-
name: "Include
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
fireEvent.click(screen.getByRole("button", { name: "Copy issue details" }));
|
|
176
|
-
await waitFor(() =>
|
|
177
|
-
expect(copyModule.copyToClipboard).toHaveBeenCalledWith(
|
|
178
|
-
expect.not.stringContaining("Notebook source"),
|
|
179
|
-
),
|
|
180
|
-
);
|
|
169
|
+
expect(
|
|
170
|
+
screen.getByRole("checkbox", { name: "Include notebook code" }),
|
|
171
|
+
).toBeDisabled();
|
|
172
|
+
expect(
|
|
173
|
+
screen.getByText("Connect the notebook to include its source."),
|
|
174
|
+
).toBeInTheDocument();
|
|
181
175
|
});
|
|
182
176
|
|
|
183
|
-
it("disables
|
|
184
|
-
store.set(connectionAtom, { state: WebSocketState.CONNECTING });
|
|
177
|
+
it("disables include errors when there are no errors", async () => {
|
|
185
178
|
store.set(
|
|
186
179
|
requestClientAtom,
|
|
187
180
|
MockRequestClient.create({
|
|
@@ -192,11 +185,8 @@ describe("FeedbackModal issue reporting", () => {
|
|
|
192
185
|
await screen.findByText("Environment details");
|
|
193
186
|
|
|
194
187
|
expect(
|
|
195
|
-
screen.getByRole("checkbox", { name: "Include
|
|
188
|
+
screen.getByRole("checkbox", { name: "Include errors" }),
|
|
196
189
|
).toBeDisabled();
|
|
197
|
-
expect(
|
|
198
|
-
screen.getByText("Connect the notebook to include its source."),
|
|
199
|
-
).toBeInTheDocument();
|
|
200
190
|
});
|
|
201
191
|
|
|
202
192
|
it("prefills the GitHub issue link with the environment once loaded", async () => {
|
|
@@ -5,7 +5,7 @@ import { Slot as SlotPrimitive } from "radix-ui";
|
|
|
5
5
|
const Slot = SlotPrimitive.Slot;
|
|
6
6
|
|
|
7
7
|
import { useAtomValue } from "jotai";
|
|
8
|
-
import {
|
|
8
|
+
import { ExternalLinkIcon, TriangleAlertIcon } from "lucide-react";
|
|
9
9
|
import React, { type PropsWithChildren, useMemo, useState } from "react";
|
|
10
10
|
import { CopyClipboardIcon } from "@/components/icons/copy-icon";
|
|
11
11
|
import { useImperativeModal } from "@/components/modal/ImperativeModal";
|
|
@@ -18,16 +18,16 @@ import {
|
|
|
18
18
|
DialogTitle,
|
|
19
19
|
} from "@/components/ui/dialog";
|
|
20
20
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
21
|
-
import { toast } from "@/components/ui/use-toast";
|
|
22
21
|
import { notebookAtom } from "@/core/cells/cells";
|
|
23
22
|
import { Constants } from "@/core/constants";
|
|
24
23
|
import {
|
|
25
24
|
buildBugReportUrl,
|
|
26
|
-
buildIssueDetails,
|
|
27
25
|
createPartialEnvironment,
|
|
28
26
|
type EnvironmentDiagnostics,
|
|
29
27
|
enrichEnvironment,
|
|
30
|
-
|
|
28
|
+
formatCodeSection,
|
|
29
|
+
formatEnvironmentSection,
|
|
30
|
+
formatErrorsSection,
|
|
31
31
|
} from "@/core/diagnostics/issue-details";
|
|
32
32
|
import {
|
|
33
33
|
formatCellError,
|
|
@@ -41,8 +41,8 @@ import { filenameAtom } from "@/core/saving/file-state";
|
|
|
41
41
|
import { store } from "@/core/state/jotai";
|
|
42
42
|
import { WebSocketState } from "@/core/websocket/types";
|
|
43
43
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
44
|
+
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
|
44
45
|
import { cn } from "@/utils/cn";
|
|
45
|
-
import { copyToClipboard } from "@/utils/copy";
|
|
46
46
|
|
|
47
47
|
const CollapsiblePreview: React.FC<{ content: string }> = ({ content }) => {
|
|
48
48
|
const [expanded, setExpanded] = useState(false);
|
|
@@ -108,8 +108,6 @@ export const FeedbackModal: React.FC<{
|
|
|
108
108
|
codeAvailable &&
|
|
109
109
|
connection.state === WebSocketState.OPEN;
|
|
110
110
|
|
|
111
|
-
const [includeNotebook, setIncludeNotebook] = useState(false);
|
|
112
|
-
|
|
113
111
|
const notebookSourceReason = notebookSourceAvailable
|
|
114
112
|
? undefined
|
|
115
113
|
: filename === null
|
|
@@ -118,13 +116,14 @@ export const FeedbackModal: React.FC<{
|
|
|
118
116
|
? "Notebook source is hidden in this view."
|
|
119
117
|
: "Connect the notebook to include its source.";
|
|
120
118
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
119
|
+
const [includeErrors, setIncludeErrors] = useLocalStorage(
|
|
120
|
+
"marimo:issue-report:include-errors",
|
|
121
|
+
false,
|
|
122
|
+
);
|
|
123
|
+
const [includeCode, setIncludeCode] = useLocalStorage(
|
|
124
|
+
"marimo:issue-report:include-code",
|
|
125
|
+
false,
|
|
126
|
+
);
|
|
128
127
|
|
|
129
128
|
const environment: EnvironmentDiagnostics | undefined =
|
|
130
129
|
environmentRequest.data
|
|
@@ -138,83 +137,122 @@ export const FeedbackModal: React.FC<{
|
|
|
138
137
|
)
|
|
139
138
|
: undefined;
|
|
140
139
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!environment) {
|
|
145
|
-
return Constants.bugReportUrl;
|
|
140
|
+
const codeRequest = useAsyncData(async () => {
|
|
141
|
+
if (!includeCode || !notebookSourceAvailable) {
|
|
142
|
+
return undefined;
|
|
146
143
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
);
|
|
151
|
-
}, [environment, errors]);
|
|
144
|
+
const { contents } = await readCode();
|
|
145
|
+
return contents;
|
|
146
|
+
}, [includeCode, notebookSourceAvailable, readCode]);
|
|
152
147
|
|
|
153
|
-
const
|
|
148
|
+
const { url: githubIssueUrl, omitted } = useMemo(() => {
|
|
154
149
|
if (!environment) {
|
|
155
|
-
return;
|
|
150
|
+
return { url: Constants.bugReportUrl, omitted: [] as string[] };
|
|
151
|
+
}
|
|
152
|
+
const fields: Record<string, string> = {
|
|
153
|
+
env: formatEnvironmentSection(environment),
|
|
154
|
+
};
|
|
155
|
+
if (includeErrors && errors.length > 0) {
|
|
156
|
+
fields["bug-description"] = formatErrorsSection(errors);
|
|
156
157
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
? "
|
|
169
|
-
:
|
|
170
|
-
|
|
171
|
-
|
|
158
|
+
if (includeCode && codeRequest.data) {
|
|
159
|
+
fields["reproduction-code"] = formatCodeSection(codeRequest.data);
|
|
160
|
+
}
|
|
161
|
+
return buildBugReportUrl(Constants.bugReportUrl, fields);
|
|
162
|
+
}, [environment, errors, includeErrors, includeCode, codeRequest.data]);
|
|
163
|
+
|
|
164
|
+
const omittedLabels = omitted
|
|
165
|
+
.map((field) =>
|
|
166
|
+
field === "bug-description"
|
|
167
|
+
? "errors"
|
|
168
|
+
: field === "reproduction-code"
|
|
169
|
+
? "code"
|
|
170
|
+
: field,
|
|
171
|
+
)
|
|
172
|
+
.join(", ");
|
|
172
173
|
|
|
173
174
|
return (
|
|
174
175
|
<DialogContent className="w-[540px] max-w-[90vw]">
|
|
175
176
|
<DialogHeader>
|
|
176
177
|
<DialogTitle>Report an issue</DialogTitle>
|
|
177
178
|
<DialogDescription>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
before posting.
|
|
179
|
+
Open a GitHub bug report prefilled with your environment, and
|
|
180
|
+
optionally your current errors and notebook code. Nothing is uploaded
|
|
181
|
+
automatically; review the details before posting.
|
|
181
182
|
</DialogDescription>
|
|
182
183
|
</DialogHeader>
|
|
183
184
|
|
|
184
185
|
<div className="flex flex-col gap-4 max-h-[60vh] overflow-y-auto">
|
|
185
|
-
<div className="flex flex-
|
|
186
|
+
<div className="flex flex-col gap-3">
|
|
186
187
|
<Button
|
|
187
188
|
type="button"
|
|
188
189
|
variant="default"
|
|
189
190
|
size="xs"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
(includeNotebook && notebookSourceRequest.isFetching)
|
|
193
|
-
}
|
|
194
|
-
onClick={copyIssueDetails}
|
|
191
|
+
asChild={true}
|
|
192
|
+
className="self-start"
|
|
195
193
|
>
|
|
196
|
-
{issueDetailsCopied && (
|
|
197
|
-
<CheckIcon
|
|
198
|
-
className="w-4 h-4 mr-2 text-(--grass-11)"
|
|
199
|
-
color="white"
|
|
200
|
-
/>
|
|
201
|
-
)}
|
|
202
|
-
{issueDetailsCopied ? "Copied!" : "Copy issue details"}
|
|
203
|
-
</Button>
|
|
204
|
-
<Button type="button" variant="outline" size="xs" asChild={true}>
|
|
205
194
|
<a href={githubIssueUrl} target="_blank" rel="noreferrer">
|
|
206
195
|
<ExternalLinkIcon className="w-4 h-4 mr-2" />
|
|
207
196
|
Open GitHub issue
|
|
208
197
|
</a>
|
|
209
198
|
</Button>
|
|
210
|
-
</div>
|
|
211
199
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
200
|
+
<div className="flex flex-col gap-2">
|
|
201
|
+
<div className="flex items-center gap-2 text-sm">
|
|
202
|
+
<Checkbox
|
|
203
|
+
id="issue-include-errors"
|
|
204
|
+
checked={includeErrors}
|
|
205
|
+
disabled={errors.length === 0}
|
|
206
|
+
onCheckedChange={(checked) =>
|
|
207
|
+
setIncludeErrors(checked === true)
|
|
208
|
+
}
|
|
209
|
+
aria-label="Include errors"
|
|
210
|
+
/>
|
|
211
|
+
<label htmlFor="issue-include-errors">Include errors</label>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div className="flex flex-col gap-1">
|
|
215
|
+
<div className="flex items-center gap-2 text-sm">
|
|
216
|
+
<Checkbox
|
|
217
|
+
id="issue-include-code"
|
|
218
|
+
checked={includeCode}
|
|
219
|
+
disabled={!notebookSourceAvailable}
|
|
220
|
+
onCheckedChange={(checked) =>
|
|
221
|
+
setIncludeCode(checked === true)
|
|
222
|
+
}
|
|
223
|
+
aria-label="Include notebook code"
|
|
224
|
+
/>
|
|
225
|
+
<label htmlFor="issue-include-code">
|
|
226
|
+
Include notebook code
|
|
227
|
+
</label>
|
|
228
|
+
</div>
|
|
229
|
+
{notebookSourceReason && (
|
|
230
|
+
<span className="text-xs text-muted-foreground ml-6">
|
|
231
|
+
{notebookSourceReason}
|
|
232
|
+
</span>
|
|
233
|
+
)}
|
|
234
|
+
{includeCode && codeRequest.status === "error" && (
|
|
235
|
+
<span className="text-xs text-(--red-11) ml-6">
|
|
236
|
+
Notebook source could not be loaded.
|
|
237
|
+
</span>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{omittedLabels ? (
|
|
243
|
+
<div className="flex items-start gap-2 text-xs text-(--yellow-11)">
|
|
244
|
+
<TriangleAlertIcon className="w-3.5 h-3.5 shrink-0 mt-0.5" />
|
|
245
|
+
<span>
|
|
246
|
+
Too large to prefill and left out of the link: {omittedLabels}.
|
|
247
|
+
Paste them into the issue manually.
|
|
248
|
+
</span>
|
|
249
|
+
</div>
|
|
250
|
+
) : (
|
|
251
|
+
<p className="text-xs text-muted-foreground">
|
|
252
|
+
Your environment is always included.
|
|
253
|
+
</p>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
218
256
|
|
|
219
257
|
{environmentRequest.status === "pending" && (
|
|
220
258
|
<div className="flex flex-col gap-2">
|
|
@@ -261,7 +299,15 @@ export const FeedbackModal: React.FC<{
|
|
|
261
299
|
|
|
262
300
|
{environment && errors.length > 0 && (
|
|
263
301
|
<div className="flex flex-col gap-1">
|
|
264
|
-
<
|
|
302
|
+
<div className="flex items-center justify-between">
|
|
303
|
+
<span className="text-sm font-medium">Errors</span>
|
|
304
|
+
<CopyClipboardIcon
|
|
305
|
+
className="w-3.5 h-3.5"
|
|
306
|
+
value={formatErrorsSection(errors)}
|
|
307
|
+
ariaLabel="Copy errors"
|
|
308
|
+
toastTitle="Errors copied"
|
|
309
|
+
/>
|
|
310
|
+
</div>
|
|
265
311
|
<CollapsiblePreview
|
|
266
312
|
content={errors.map(formatCellError).join("\n\n---\n\n")}
|
|
267
313
|
/>
|
|
@@ -270,46 +316,10 @@ export const FeedbackModal: React.FC<{
|
|
|
270
316
|
|
|
271
317
|
{environment && errors.length === 0 && (
|
|
272
318
|
<span className="text-sm text-muted-foreground">
|
|
273
|
-
No
|
|
319
|
+
No errors detected.
|
|
274
320
|
</span>
|
|
275
321
|
)}
|
|
276
322
|
|
|
277
|
-
<div className="flex flex-col gap-1">
|
|
278
|
-
<div className="flex items-start gap-2 text-sm">
|
|
279
|
-
<Checkbox
|
|
280
|
-
id="issue-include-notebook"
|
|
281
|
-
className="mt-0.5"
|
|
282
|
-
checked={includeNotebook}
|
|
283
|
-
disabled={!notebookSourceAvailable}
|
|
284
|
-
onCheckedChange={(checked) =>
|
|
285
|
-
setIncludeNotebook(checked === true)
|
|
286
|
-
}
|
|
287
|
-
aria-label="Include full notebook source"
|
|
288
|
-
/>
|
|
289
|
-
<label htmlFor="issue-include-notebook">
|
|
290
|
-
Include full notebook source
|
|
291
|
-
</label>
|
|
292
|
-
</div>
|
|
293
|
-
<p className="text-xs text-muted-foreground ml-6">
|
|
294
|
-
Copies the entire saved Python source, including comments, literal
|
|
295
|
-
data, embedded credentials, and package metadata. Outputs and
|
|
296
|
-
external files are not included. Review it before posting.{" "}
|
|
297
|
-
<span className="font-bold">
|
|
298
|
-
For private notebooks, paste a minimal reproduction instead.
|
|
299
|
-
</span>
|
|
300
|
-
</p>
|
|
301
|
-
{notebookSourceReason && (
|
|
302
|
-
<span className="text-xs text-muted-foreground ml-6">
|
|
303
|
-
{notebookSourceReason}
|
|
304
|
-
</span>
|
|
305
|
-
)}
|
|
306
|
-
{includeNotebook && notebookSourceRequest.status === "error" && (
|
|
307
|
-
<span className="text-xs text-(--red-11) ml-6">
|
|
308
|
-
Notebook source could not be loaded.
|
|
309
|
-
</span>
|
|
310
|
-
)}
|
|
311
|
-
</div>
|
|
312
|
-
|
|
313
323
|
<div className="border-t pt-3">
|
|
314
324
|
<p className="text-sm text-muted-foreground">
|
|
315
325
|
Other feedback? Take our{" "}
|
|
@@ -6,9 +6,11 @@ import type { CellErrorEntry } from "@/core/errors/error-entries";
|
|
|
6
6
|
import type { EnvironmentInfo } from "@/core/network/types";
|
|
7
7
|
import {
|
|
8
8
|
buildBugReportUrl,
|
|
9
|
-
buildIssueDetails,
|
|
10
9
|
createPartialEnvironment,
|
|
11
10
|
enrichEnvironment,
|
|
11
|
+
formatCodeSection,
|
|
12
|
+
formatEnvironmentSection,
|
|
13
|
+
formatErrorsSection,
|
|
12
14
|
markdownCodeFence,
|
|
13
15
|
MAX_PREFILL_URL_LENGTH,
|
|
14
16
|
} from "../issue-details";
|
|
@@ -86,20 +88,16 @@ describe("markdownCodeFence", () => {
|
|
|
86
88
|
});
|
|
87
89
|
});
|
|
88
90
|
|
|
89
|
-
describe("
|
|
90
|
-
it("
|
|
91
|
-
const markdown =
|
|
92
|
-
environment,
|
|
93
|
-
errors: [],
|
|
94
|
-
notebook: undefined,
|
|
95
|
-
});
|
|
91
|
+
describe("formatEnvironmentSection", () => {
|
|
92
|
+
it("wraps the environment JSON in a collapsible section", () => {
|
|
93
|
+
const markdown = formatEnvironmentSection(environment);
|
|
96
94
|
expect(markdown).toContain("<summary>Environment</summary>");
|
|
97
95
|
expect(markdown).toContain('"marimo": "1.2.3"');
|
|
98
|
-
expect(markdown).not.toContain("Notebook source");
|
|
99
|
-
expect(markdown).not.toContain("Current errors");
|
|
100
96
|
});
|
|
97
|
+
});
|
|
101
98
|
|
|
102
|
-
|
|
99
|
+
describe("formatErrorsSection", () => {
|
|
100
|
+
it("wraps errors in a collapsible section without cell source", () => {
|
|
103
101
|
const errors: CellErrorEntry[] = [
|
|
104
102
|
{
|
|
105
103
|
cellId: cellId("cell-1"),
|
|
@@ -110,38 +108,19 @@ describe("buildIssueDetails", () => {
|
|
|
110
108
|
'<span class="gr">ValueError</span>: <span class="n">bad value</span>',
|
|
111
109
|
},
|
|
112
110
|
];
|
|
113
|
-
const markdown =
|
|
114
|
-
expect(markdown).toContain("<summary>
|
|
111
|
+
const markdown = formatErrorsSection(errors);
|
|
112
|
+
expect(markdown).toContain("<summary>Errors</summary>");
|
|
113
|
+
expect(markdown).toContain("```text\n");
|
|
115
114
|
expect(markdown).toContain("ValueError: bad value");
|
|
116
115
|
expect(markdown).not.toContain("password");
|
|
117
116
|
});
|
|
117
|
+
});
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
filename: "/project/example.py",
|
|
125
|
-
contents: "x = 1",
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
expect(markdown).toContain(
|
|
129
|
-
"<summary>Notebook source: example.py</summary>",
|
|
130
|
-
);
|
|
131
|
-
expect(markdown).toContain("x = 1");
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("escapes the summary label as text", () => {
|
|
135
|
-
const markdown = buildIssueDetails({
|
|
136
|
-
environment,
|
|
137
|
-
errors: [],
|
|
138
|
-
notebook: {
|
|
139
|
-
filename: "/project/<script>.py",
|
|
140
|
-
contents: "x = 1",
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
expect(markdown).toContain("<script>.py");
|
|
144
|
-
expect(markdown).not.toContain("<script>.py");
|
|
119
|
+
describe("formatCodeSection", () => {
|
|
120
|
+
it("wraps the code in a collapsible python section", () => {
|
|
121
|
+
const markdown = formatCodeSection("import marimo");
|
|
122
|
+
expect(markdown).toContain("<summary>Code</summary>");
|
|
123
|
+
expect(markdown).toContain("```python\nimport marimo");
|
|
145
124
|
});
|
|
146
125
|
});
|
|
147
126
|
|
|
@@ -149,25 +128,48 @@ describe("buildBugReportUrl", () => {
|
|
|
149
128
|
const baseUrl =
|
|
150
129
|
"https://github.com/marimo-team/marimo/issues/new?template=bug_report.yaml";
|
|
151
130
|
|
|
152
|
-
it("prefills
|
|
153
|
-
const url = buildBugReportUrl(baseUrl,
|
|
154
|
-
|
|
131
|
+
it("prefills each field keyed by its template id", () => {
|
|
132
|
+
const { url, omitted } = buildBugReportUrl(baseUrl, {
|
|
133
|
+
env: "hello world",
|
|
134
|
+
"bug-description": "boom",
|
|
135
|
+
});
|
|
136
|
+
expect(url).toBe(`${baseUrl}&env=hello%20world&bug-description=boom`);
|
|
137
|
+
expect(omitted).toEqual([]);
|
|
155
138
|
});
|
|
156
139
|
|
|
157
|
-
it("appends
|
|
158
|
-
const url = buildBugReportUrl("https://example.com/new", "x");
|
|
140
|
+
it("appends fields with a query separator when the base URL has none", () => {
|
|
141
|
+
const { url } = buildBugReportUrl("https://example.com/new", { env: "x" });
|
|
159
142
|
expect(url).toBe("https://example.com/new?env=x");
|
|
160
143
|
});
|
|
161
144
|
|
|
145
|
+
it("skips empty fields", () => {
|
|
146
|
+
const { url } = buildBugReportUrl(baseUrl, {
|
|
147
|
+
env: "x",
|
|
148
|
+
"bug-description": "",
|
|
149
|
+
});
|
|
150
|
+
expect(url).toBe(`${baseUrl}&env=x`);
|
|
151
|
+
expect(url).not.toContain("bug-description");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("returns the base URL when no fields have content", () => {
|
|
155
|
+
const { url } = buildBugReportUrl(baseUrl, { env: "" });
|
|
156
|
+
expect(url).toBe(baseUrl);
|
|
157
|
+
});
|
|
158
|
+
|
|
162
159
|
it("encodes markdown so it survives as a single query param", () => {
|
|
163
|
-
const details =
|
|
164
|
-
const url = buildBugReportUrl(baseUrl, details);
|
|
160
|
+
const details = formatEnvironmentSection(environment);
|
|
161
|
+
const { url } = buildBugReportUrl(baseUrl, { env: details });
|
|
165
162
|
expect(url).toContain("&env=");
|
|
166
163
|
expect(new URL(url).searchParams.get("env")).toBe(details);
|
|
167
164
|
});
|
|
168
165
|
|
|
169
|
-
it("
|
|
166
|
+
it("keeps earlier fields and omits only the oversized later one", () => {
|
|
170
167
|
const huge = "x".repeat(MAX_PREFILL_URL_LENGTH);
|
|
171
|
-
|
|
168
|
+
const { url, omitted } = buildBugReportUrl(baseUrl, {
|
|
169
|
+
env: "small",
|
|
170
|
+
"reproduction-code": huge,
|
|
171
|
+
});
|
|
172
|
+
expect(url).toBe(`${baseUrl}&env=small`);
|
|
173
|
+
expect(omitted).toEqual(["reproduction-code"]);
|
|
172
174
|
});
|
|
173
175
|
});
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
formatCellError,
|
|
6
6
|
} from "@/core/errors/error-entries";
|
|
7
7
|
import type { EnvironmentInfo } from "@/core/network/types";
|
|
8
|
-
import { Paths } from "@/utils/paths";
|
|
9
8
|
import { Strings } from "@/utils/strings";
|
|
10
9
|
|
|
11
10
|
/**
|
|
@@ -23,12 +22,6 @@ export interface NotebookSource {
|
|
|
23
22
|
contents: string;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
export interface IssueDetailsInput {
|
|
27
|
-
environment: EnvironmentDiagnostics;
|
|
28
|
-
errors: CellErrorEntry[];
|
|
29
|
-
notebook?: NotebookSource;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
25
|
/**
|
|
33
26
|
* Replace the server-detected browser with the active browser's user agent.
|
|
34
27
|
*
|
|
@@ -82,17 +75,35 @@ export function markdownCodeFence(language: string, contents: string): string {
|
|
|
82
75
|
export const MAX_PREFILL_URL_LENGTH = 6000;
|
|
83
76
|
|
|
84
77
|
/**
|
|
85
|
-
* Build a bug-report URL with `
|
|
86
|
-
* field
|
|
87
|
-
* `MAX_PREFILL_URL_LENGTH
|
|
78
|
+
* Build a bug-report URL with `fields` prefilled into the issue form, keyed by
|
|
79
|
+
* each field's template `id`. Fields are added greedily in order; any that
|
|
80
|
+
* would push the URL past `MAX_PREFILL_URL_LENGTH` are skipped and returned in
|
|
81
|
+
* `omitted`, so an oversized later field never discards the ones before it.
|
|
88
82
|
*/
|
|
89
83
|
export function buildBugReportUrl(
|
|
90
84
|
baseUrl: string,
|
|
91
|
-
|
|
92
|
-
): string {
|
|
85
|
+
fields: Record<string, string>,
|
|
86
|
+
): { url: string; omitted: string[] } {
|
|
93
87
|
const separator = baseUrl.includes("?") ? "&" : "?";
|
|
94
|
-
const
|
|
95
|
-
|
|
88
|
+
const omitted: string[] = [];
|
|
89
|
+
let url = baseUrl;
|
|
90
|
+
let added = 0;
|
|
91
|
+
|
|
92
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
93
|
+
if (value.length === 0) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const param = `${key}=${encodeURIComponent(value)}`;
|
|
97
|
+
const candidate = `${url}${added === 0 ? separator : "&"}${param}`;
|
|
98
|
+
if (candidate.length > MAX_PREFILL_URL_LENGTH) {
|
|
99
|
+
omitted.push(key);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
url = candidate;
|
|
103
|
+
added += 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { url, omitted };
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
function detailsSection(summary: string, body: string): string {
|
|
@@ -106,34 +117,34 @@ function detailsSection(summary: string, body: string): string {
|
|
|
106
117
|
].join("\n");
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
markdownCodeFence(
|
|
122
|
-
"text",
|
|
123
|
-
input.errors.map(formatCellError).join("\n\n---\n\n"),
|
|
124
|
-
),
|
|
125
|
-
),
|
|
126
|
-
);
|
|
127
|
-
}
|
|
120
|
+
/**
|
|
121
|
+
* Format the environment as a collapsible section for the issue form's
|
|
122
|
+
* `env` field.
|
|
123
|
+
*/
|
|
124
|
+
export function formatEnvironmentSection(
|
|
125
|
+
environment: EnvironmentDiagnostics,
|
|
126
|
+
): string {
|
|
127
|
+
return detailsSection(
|
|
128
|
+
"Environment",
|
|
129
|
+
markdownCodeFence("json", JSON.stringify(environment, null, 2)),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
128
132
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Format cell errors as a collapsible section for the issue form's
|
|
135
|
+
* `bug-description` field.
|
|
136
|
+
*/
|
|
137
|
+
export function formatErrorsSection(errors: CellErrorEntry[]): string {
|
|
138
|
+
return detailsSection(
|
|
139
|
+
"Errors",
|
|
140
|
+
markdownCodeFence("text", errors.map(formatCellError).join("\n\n---\n\n")),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
137
143
|
|
|
138
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Format notebook source as a collapsible section for the issue form's
|
|
146
|
+
* `reproduction-code` field.
|
|
147
|
+
*/
|
|
148
|
+
export function formatCodeSection(contents: string): string {
|
|
149
|
+
return detailsSection("Code", markdownCodeFence("python", contents));
|
|
139
150
|
}
|