From fa40ceaa31726463854b4b18f0adb9cb8f203067 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 18 May 2026 08:59:49 -0700 Subject: [PATCH] Add cookie editing and inherited request settings (#463) --- .../commands/openWorkspaceSettings.tsx | 11 +- .../components/CommandPaletteDialog.tsx | 16 +- apps/yaak-client/components/CookieDialog.tsx | 742 +++++++++++++++++- .../yaak-client/components/CookieDropdown.tsx | 8 +- .../components/FolderSettingsDialog.tsx | 14 +- .../components/GrpcRequestPane.tsx | 13 +- .../components/HttpRequestPane.tsx | 12 + .../components/HttpResponseTimeline.tsx | 50 +- .../components/ModelSettingsEditor.tsx | 347 ++++++++ apps/yaak-client/components/SelectFile.tsx | 4 +- .../components/Settings/SettingsGeneral.tsx | 307 ++++---- .../components/Settings/SettingsInterface.tsx | 342 ++++---- .../components/Settings/SettingsProxy.tsx | 331 ++++---- .../components/Settings/SettingsTheme.tsx | 181 ++--- .../components/SyncToFilesystemSetting.tsx | 72 +- .../components/WebsocketRequestPane.tsx | 13 +- apps/yaak-client/components/Workspace.tsx | 6 + .../components/WorkspaceEncryptionSetting.tsx | 51 +- .../components/WorkspaceSettingsDialog.tsx | 47 +- apps/yaak-client/components/core/Checkbox.tsx | 8 +- .../components/core/EventViewer.tsx | 8 +- .../components/core/KeyValueRow.tsx | 51 +- .../components/core/PlainInput.tsx | 7 +- apps/yaak-client/components/core/Select.tsx | 10 +- .../yaak-client/components/core/Separator.tsx | 6 +- .../components/core/SettingRow.tsx | 514 ++++++++++++ .../components/git/GitDropdown.tsx | 2 +- apps/yaak-client/hooks/useAuthTab.tsx | 2 +- apps/yaak-client/hooks/useHotKey.ts | 4 + apps/yaak-client/lib/requestSettings.ts | 81 ++ crates-cli/yaak-cli/src/plugin_events.rs | 8 +- crates-cli/yaak-cli/tests/send_commands.rs | 28 - crates-tauri/yaak-app-client/src/lib.rs | 8 +- .../yaak-app-client/src/plugin_events.rs | 7 +- crates-tauri/yaak-app-client/src/ws_ext.rs | 37 +- crates/yaak-git/bindings/gen_models.ts | 189 ++++- crates/yaak-http/src/cookies.rs | 62 +- crates/yaak-http/src/sender.rs | 25 +- crates/yaak-http/src/transaction.rs | 206 ++++- crates/yaak-models/bindings/gen_models.ts | 515 ++++++++++-- ...60302000000_inherited-request-settings.sql | 17 + .../20260303000000_protocol-tls-settings.sql | 3 + crates/yaak-models/src/models.rs | 311 +++++++- crates/yaak-models/src/queries/folders.rs | 61 +- .../yaak-models/src/queries/grpc_requests.rs | 30 +- .../yaak-models/src/queries/http_requests.rs | 61 +- .../src/queries/websocket_requests.rs | 44 +- crates/yaak-models/src/queries/workspaces.rs | 32 +- crates/yaak-plugins/bindings/gen_models.ts | 486 ++++++++++-- crates/yaak-sync/bindings/gen_models.ts | 202 ++++- crates/yaak/src/send.rs | 185 +++-- .../src/bindings/gen_models.ts | 486 ++++++++++-- packages/ui/src/components/Icon.tsx | 10 +- packages/ui/src/components/Table.tsx | 31 +- 54 files changed, 5203 insertions(+), 1101 deletions(-) create mode 100644 apps/yaak-client/components/ModelSettingsEditor.tsx create mode 100644 apps/yaak-client/components/core/SettingRow.tsx create mode 100644 apps/yaak-client/lib/requestSettings.ts create mode 100644 crates/yaak-models/migrations/20260302000000_inherited-request-settings.sql create mode 100644 crates/yaak-models/migrations/20260303000000_protocol-tls-settings.sql diff --git a/apps/yaak-client/commands/openWorkspaceSettings.tsx b/apps/yaak-client/commands/openWorkspaceSettings.tsx index 4f68f87c..1af368f4 100644 --- a/apps/yaak-client/commands/openWorkspaceSettings.tsx +++ b/apps/yaak-client/commands/openWorkspaceSettings.tsx @@ -1,19 +1,10 @@ import type { WorkspaceSettingsTab } from "../components/WorkspaceSettingsDialog"; import { WorkspaceSettingsDialog } from "../components/WorkspaceSettingsDialog"; import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace"; -import { showDialog } from "../lib/dialog"; import { jotaiStore } from "../lib/jotai"; export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) { const workspaceId = jotaiStore.get(activeWorkspaceIdAtom); if (workspaceId == null) return; - showDialog({ - id: "workspace-settings", - size: "md", - className: "h-[calc(100vh-5rem)] !max-h-[40rem]", - noPadding: true, - render: ({ hide }) => ( - - ), - }); + WorkspaceSettingsDialog.show(workspaceId, tab); } diff --git a/apps/yaak-client/components/CommandPaletteDialog.tsx b/apps/yaak-client/components/CommandPaletteDialog.tsx index eec1d3b9..c653cd03 100644 --- a/apps/yaak-client/components/CommandPaletteDialog.tsx +++ b/apps/yaak-client/components/CommandPaletteDialog.tsx @@ -15,6 +15,7 @@ import { import { createFolder } from "../commands/commands"; import { createSubEnvironmentAndActivate } from "../commands/createEnvironment"; import { openSettings } from "../commands/openSettings"; +import { openWorkspaceSettings } from "../commands/openWorkspaceSettings"; import { switchWorkspace } from "../commands/switchWorkspace"; import { useActiveCookieJar } from "../hooks/useActiveCookieJar"; import { useActiveEnvironment } from "../hooks/useActiveEnvironment"; @@ -36,7 +37,6 @@ import { appInfo } from "../lib/appInfo"; import { copyToClipboard } from "../lib/copy"; import { createRequestAndNavigate } from "../lib/createRequestAndNavigate"; import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm"; -import { showDialog } from "../lib/dialog"; import { editEnvironment } from "../lib/editEnvironment"; import { renameModelWithPrompt } from "../lib/renameModelWithPrompt"; import { @@ -99,6 +99,12 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) { action: "settings.show", onSelect: () => openSettings.mutate(null), }, + { + key: "workspace_settings.open", + label: "Open Workspace Settings", + action: "workspace_settings.show", + onSelect: () => openWorkspaceSettings(), + }, { key: "app.create", label: "Create Workspace", @@ -127,13 +133,9 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) { { key: "cookies.show", label: "Show Cookies", + action: "cookies_editor.show", onSelect: async () => { - showDialog({ - id: "cookies", - title: "Manage Cookies", - size: "full", - render: () => , - }); + CookieDialog.show(activeCookieJar?.id ?? null); }, }, { diff --git a/apps/yaak-client/components/CookieDialog.tsx b/apps/yaak-client/components/CookieDialog.tsx index 6c8062c5..faf27b4c 100644 --- a/apps/yaak-client/components/CookieDialog.tsx +++ b/apps/yaak-client/components/CookieDialog.tsx @@ -1,9 +1,40 @@ import type { Cookie } from "@yaakapp-internal/models"; import { cookieJarsAtom, patchModel } from "@yaakapp-internal/models"; +import { formatDate } from "date-fns/format"; import { useAtomValue } from "jotai"; +import { + type ComponentProps, + type CSSProperties, + type FormEvent, + type ReactNode, + type RefObject, + useMemo, + useRef, + useState, +} from "react"; +import { showDialog } from "../lib/dialog"; +import { jotaiStore } from "../lib/jotai"; import { cookieDomain } from "../lib/model_util"; -import { Banner, InlineCode } from "@yaakapp-internal/ui"; +import { + Icon, + SplitLayout, + Table, + TableBody, + TableCell, + TableHead, + TableHeaderCell, + TableRow, + TruncatedWideTableCell, +} from "@yaakapp-internal/ui"; import { IconButton } from "./core/IconButton"; +import { Checkbox } from "./core/Checkbox"; +import classNames from "classnames"; +import { EventDetailHeader } from "./core/EventViewer"; +import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow"; +import { EmptyStateText } from "./EmptyStateText"; +import { PlainInput } from "./core/PlainInput"; +import { Select } from "./core/Select"; +import { showAlert } from "../lib/alert"; interface Props { cookieJarId: string | null; @@ -12,56 +43,685 @@ interface Props { export const CookieDialog = ({ cookieJarId }: Props) => { const cookieJars = useAtomValue(cookieJarsAtom); const cookieJar = cookieJars?.find((c) => c.id === cookieJarId); + const [filter, setFilter] = useState(""); + const [filterUpdateKey, setFilterUpdateKey] = useState(0); + const [selectedCookieKey, setSelectedCookieKey] = useState(null); + const [editingCookieKey, setEditingCookieKey] = useState(null); + const [draftCookie, setDraftCookie] = useState(null); + const [draftExpiresInput, setDraftExpiresInput] = useState(""); + const editorFormRef = useRef(null); + const filteredCookies = useMemo(() => { + return cookieJar?.cookies.filter((cookie) => cookieMatchesFilter(cookie, filter)) ?? []; + }, [cookieJar?.cookies, filter]); + const selectedCookie = useMemo( + () => + selectedCookieKey == null + ? null + : (filteredCookies.find((cookie) => cookieKey(cookie) === selectedCookieKey) ?? null), + [filteredCookies, selectedCookieKey], + ); + const detailCookie = draftCookie ?? selectedCookie; + const isCreatingCookie = editingCookieKey === NEW_COOKIE_KEY; + const isEditingCookie = draftCookie != null; + + const handleAddCookie = () => { + setSelectedCookieKey(null); + setEditingCookieKey(NEW_COOKIE_KEY); + setDraftCookie(newCookieDraft()); + setDraftExpiresInput(""); + }; + + const handleEditCookie = () => { + if (selectedCookie == null) { + return; + } + + setEditingCookieKey(cookieKey(selectedCookie)); + setDraftCookie(selectedCookie); + setDraftExpiresInput(cookieExpiresInputValue(selectedCookie)); + }; + + const handleCancelEdit = () => { + if (isCreatingCookie) { + setSelectedCookieKey(null); + } + setEditingCookieKey(null); + setDraftCookie(null); + setDraftExpiresInput(""); + }; + + const handleCloseDetails = () => { + if (isEditingCookie) { + handleCancelEdit(); + return; + } + + setSelectedCookieKey(null); + }; + + const handleSaveCookie = (event: FormEvent) => { + event.preventDefault(); + + if (cookieJar == null || draftCookie == null) { + return; + } + + let nextCookie = normalizeCookie(draftCookie); + if (nextCookie.expires !== "SessionEnd") { + const expires = cookieExpiresFromInput(draftExpiresInput); + if (expires == null) { + showAlert({ + id: "invalid-cookie-expires", + title: "Invalid Cookie", + body: "Cookie expiration must be a valid date.", + }); + return; + } + + nextCookie = { ...nextCookie, expires }; + } + + const nextCookieKey = cookieKey(nextCookie); + const nextCookies = cookieJar.cookies.filter((cookie) => { + const key = cookieKey(cookie); + if (editingCookieKey != null && key === editingCookieKey) { + return false; + } + return key !== nextCookieKey; + }); + + patchModel(cookieJar, { cookies: [...nextCookies, nextCookie] }); + setSelectedCookieKey(nextCookieKey); + setEditingCookieKey(null); + setDraftCookie(null); + setDraftExpiresInput(""); + }; if (cookieJar == null) { return
No cookie jar selected
; } - if (cookieJar.cookies.length === 0) { + return ( +
+
+ 0 && ( + { + setFilter(""); + setFilterUpdateKey((key) => key + 1); + }} + /> + ) + } + /> + +
+ {cookieJar.cookies.length === 0 && detailCookie == null ? ( + + Cookies will appear when a response includes a Set-Cookie header. + + ) : filteredCookies.length === 0 && detailCookie == null ? ( + No cookies match the current filter. + ) : ( + + filteredCookies.length === 0 ? ( +
+ No cookies match the current filter. +
+ ) : ( + + + + Name + Value + Domain + Path + Expires + Size + HTTP Only + Secure + Same Site + + { + setSelectedCookieKey(null); + setEditingCookieKey(null); + setDraftCookie(null); + setDraftExpiresInput(""); + patchModel(cookieJar, { cookies: [] }); + }} + /> + + + + + {filteredCookies.map((c: Cookie) => { + const key = cookieKey(c); + const isSelected = key === selectedCookieKey; + + return ( + { + setSelectedCookieKey(key); + setEditingCookieKey(null); + setDraftCookie(null); + setDraftExpiresInput(""); + }} + > + + {c.name} + + + {c.value} + + {cookieDomain(c)} + {c.path} + {cookieExpires(c)} + {cookieSize(c)} + + + + + + + {c.sameSite} + + { + event.stopPropagation(); + if (isSelected) { + setSelectedCookieKey(null); + } + if (editingCookieKey === key) { + setEditingCookieKey(null); + setDraftCookie(null); + setDraftExpiresInput(""); + } + patchModel(cookieJar, { + cookies: cookieJar.cookies.filter( + (c2: Cookie) => cookieKey(c2) !== key, + ), + }); + }} + /> + + + ); + })} + +
+ ) + } + secondSlot={ + detailCookie == null + ? null + : ({ style }) => ( + + editorFormRef.current?.requestSubmit(), + }, + { + key: "cancel", + label: "Cancel", + onClick: handleCancelEdit, + }, + ] + : [ + { + key: "edit", + label: "Edit", + onClick: handleEditCookie, + }, + ] + } + onClose={handleCloseDetails} + /> + {isEditingCookie ? ( + + ) : ( + + )} + + ) + } + /> + )} +
+ ); +}; + +function CookieDetailsPane({ + children, + formRef, + isEditing, + onSubmit, + style, +}: { + children: ReactNode; + formRef: RefObject; + isEditing: boolean; + onSubmit: (event: FormEvent) => void; + style: CSSProperties; +}) { + const className = "grid grid-rows-[auto_minmax(0,1fr)] bg-surface border-t border-border pt-2"; + + if (isEditing) { return ( - - Cookies will appear when a response contains the Set-Cookie header - +
+ {children} +
); } return ( -
- - - - - - - - - {cookieJar?.cookies.map((c: Cookie) => ( - - - - - - ))} - -
DomainCookie -
- {cookieDomain(c)} - - {c.raw_cookie} - - - patchModel(cookieJar, { - cookies: cookieJar.cookies.filter((c2: Cookie) => c2 !== c), - }) - } - /> -
+
+ {children}
); +} + +CookieDialog.show = (cookieJarId: string | null) => { + const cookieJar = jotaiStore.get(cookieJarsAtom)?.find((jar) => jar.id === cookieJarId); + if (cookieJar == null) { + showAlert({ + id: "invalid-jar", + body: `Failed to find cookie jar for ID: ${cookieJarId}`, + title: "Invalid Cookie Jar", + }); + return; + } + + showDialog({ + id: "cookies", + title: `${cookieJar.name} Cookies`, + size: "full", + render: () => , + }); }; + +function CookieDetails({ cookie }: { cookie: Cookie }) { + return ( +
+ + {cookie.name} + +
{cookie.value}
+
+ {cookieDomain(cookie)} + {cookie.path} + {cookieExpires(cookie)} + {cookieSize(cookie)} + {cookie.httpOnly ? "Yes" : "No"} + {cookie.secure ? "Yes" : "No"} + {cookie.sameSite && ( + {cookie.sameSite} + )} +
+
+ ); +} + +function CookieEditor({ + cookie, + expiresInputValue, + onChange, + onExpiresInputChange, +}: { + cookie: Cookie; + expiresInputValue: string; + onChange: (cookie: Cookie) => void; + onExpiresInputChange: (value: string) => void; +}) { + const sessionCookie = cookie.expires === "SessionEnd"; + + return ( +
+ + + onChange({ ...cookie, name })} + /> + + + onChange({ ...cookie, value })} + /> + + + onChange(cookieWithDomain(cookie, domain))} + /> + + + onChange({ ...cookie, path })} + /> + + +
+ { + if (checked) { + onChange({ ...cookie, expires: "SessionEnd" }); + return; + } + + const expiresInput = + cookieExpiresFromInput(expiresInputValue) == null + ? defaultCookieExpiresInputValue() + : expiresInputValue; + + onExpiresInputChange(expiresInput); + onChange({ + ...cookie, + expires: cookieExpiresFromInput(expiresInput)!, + }); + }} + /> + { + onExpiresInputChange(value); + + const expires = cookieExpiresFromInput(value); + if (expires != null) { + onChange({ ...cookie, expires }); + } + }} + /> +
+
+ {cookieSize(cookie)} + + onChange({ ...cookie, httpOnly })} + /> + + + onChange({ ...cookie, secure })} + /> + + + onChange(event.target.value)} + pattern={pattern} + placeholder={placeholder} + required={required} + type="text" + value={value} + /> + ); +} + +function CookieTextarea({ onChange, value }: { onChange: (value: string) => void; value: string }) { + return ( +