Address cookie editing PR feedback

This commit is contained in:
Gregory Schier
2026-05-17 08:58:55 -07:00
parent 1a705ff244
commit 0c97036864
5 changed files with 132 additions and 42 deletions
+88 -35
View File
@@ -24,6 +24,7 @@ import { EventDetailHeader } from "./core/EventViewer";
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow"; import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
import { EmptyStateText } from "./EmptyStateText"; import { EmptyStateText } from "./EmptyStateText";
import { PlainInput } from "./core/PlainInput"; import { PlainInput } from "./core/PlainInput";
import { Select } from "./core/Select";
import { showAlert } from "../lib/alert"; import { showAlert } from "../lib/alert";
interface Props { interface Props {
@@ -38,6 +39,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
const [selectedCookieKey, setSelectedCookieKey] = useState<string | null>(null); const [selectedCookieKey, setSelectedCookieKey] = useState<string | null>(null);
const [editingCookieKey, setEditingCookieKey] = useState<string | null>(null); const [editingCookieKey, setEditingCookieKey] = useState<string | null>(null);
const [draftCookie, setDraftCookie] = useState<Cookie | null>(null); const [draftCookie, setDraftCookie] = useState<Cookie | null>(null);
const [draftExpiresInput, setDraftExpiresInput] = useState("");
const filteredCookies = useMemo(() => { const filteredCookies = useMemo(() => {
return cookieJar?.cookies.filter((cookie) => cookieMatchesFilter(cookie, filter)) ?? []; return cookieJar?.cookies.filter((cookie) => cookieMatchesFilter(cookie, filter)) ?? [];
}, [cookieJar?.cookies, filter]); }, [cookieJar?.cookies, filter]);
@@ -56,6 +58,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
setSelectedCookieKey(null); setSelectedCookieKey(null);
setEditingCookieKey(NEW_COOKIE_KEY); setEditingCookieKey(NEW_COOKIE_KEY);
setDraftCookie(newCookieDraft()); setDraftCookie(newCookieDraft());
setDraftExpiresInput("");
}; };
const handleEditCookie = () => { const handleEditCookie = () => {
@@ -65,6 +68,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
setEditingCookieKey(cookieKey(selectedCookie)); setEditingCookieKey(cookieKey(selectedCookie));
setDraftCookie(selectedCookie); setDraftCookie(selectedCookie);
setDraftExpiresInput(cookieExpiresInputValue(selectedCookie));
}; };
const handleCancelEdit = () => { const handleCancelEdit = () => {
@@ -73,6 +77,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
} }
setEditingCookieKey(null); setEditingCookieKey(null);
setDraftCookie(null); setDraftCookie(null);
setDraftExpiresInput("");
}; };
const handleCloseDetails = () => { const handleCloseDetails = () => {
@@ -89,7 +94,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
return; return;
} }
const nextCookie = normalizeCookie(draftCookie); let nextCookie = normalizeCookie(draftCookie);
if (nextCookie.name.trim().length === 0) { if (nextCookie.name.trim().length === 0) {
showAlert({ showAlert({
id: "invalid-cookie-name", id: "invalid-cookie-name",
@@ -99,6 +104,20 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
return; return;
} }
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 nextCookieKey = cookieKey(nextCookie);
const nextCookies = cookieJar.cookies.filter((cookie) => { const nextCookies = cookieJar.cookies.filter((cookie) => {
const key = cookieKey(cookie); const key = cookieKey(cookie);
@@ -112,6 +131,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
setSelectedCookieKey(nextCookieKey); setSelectedCookieKey(nextCookieKey);
setEditingCookieKey(null); setEditingCookieKey(null);
setDraftCookie(null); setDraftCookie(null);
setDraftExpiresInput("");
}; };
if (cookieJar == null) { if (cookieJar == null) {
@@ -156,6 +176,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
layout="vertical" layout="vertical"
storageKey="cookie-dialog-details" storageKey="cookie-dialog-details"
defaultRatio={0.65} defaultRatio={0.65}
className="-mx-2"
minHeightPx={10} minHeightPx={10}
firstSlot={({ style }) => firstSlot={({ style }) =>
filteredCookies.length === 0 ? ( filteredCookies.length === 0 ? (
@@ -163,7 +184,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
<EmptyStateText>No cookies match the current filter.</EmptyStateText> <EmptyStateText>No cookies match the current filter.</EmptyStateText>
</div> </div>
) : ( ) : (
<Table scrollable style={style}> <Table scrollable style={style} className="pr-0.5">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableHeaderCell>Name</TableHeaderCell> <TableHeaderCell>Name</TableHeaderCell>
@@ -185,6 +206,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
setSelectedCookieKey(null); setSelectedCookieKey(null);
setEditingCookieKey(null); setEditingCookieKey(null);
setDraftCookie(null); setDraftCookie(null);
setDraftExpiresInput("");
patchModel(cookieJar, { cookies: [] }); patchModel(cookieJar, { cookies: [] });
}} }}
/> />
@@ -208,9 +230,12 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
setSelectedCookieKey(key); setSelectedCookieKey(key);
setEditingCookieKey(null); setEditingCookieKey(null);
setDraftCookie(null); setDraftCookie(null);
setDraftExpiresInput("");
}} }}
> >
<TableCell>{c.name}</TableCell> <TableCell className={classNames("pl-2", isSelected && "rounded-l")}>
{c.name}
</TableCell>
<TruncatedWideTableCell className="min-w-[10rem]"> <TruncatedWideTableCell className="min-w-[10rem]">
{c.value} {c.value}
</TruncatedWideTableCell> </TruncatedWideTableCell>
@@ -231,7 +256,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
/> />
</TableCell> </TableCell>
<TableCell>{c.sameSite}</TableCell> <TableCell>{c.sameSite}</TableCell>
<TableCell> <TableCell className="rounded-r pr-2">
<IconButton <IconButton
icon="trash" icon="trash"
size="xs" size="xs"
@@ -246,6 +271,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
if (editingCookieKey === key) { if (editingCookieKey === key) {
setEditingCookieKey(null); setEditingCookieKey(null);
setDraftCookie(null); setDraftCookie(null);
setDraftExpiresInput("");
} }
patchModel(cookieJar, { patchModel(cookieJar, {
cookies: cookieJar.cookies.filter( cookies: cookieJar.cookies.filter(
@@ -298,7 +324,12 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
onClose={handleCloseDetails} onClose={handleCloseDetails}
/> />
{isEditingCookie ? ( {isEditingCookie ? (
<CookieEditor cookie={detailCookie} onChange={setDraftCookie} /> <CookieEditor
cookie={detailCookie}
expiresInputValue={draftExpiresInput}
onChange={setDraftCookie}
onExpiresInputChange={setDraftExpiresInput}
/>
) : ( ) : (
<CookieDetails cookie={detailCookie} /> <CookieDetails cookie={detailCookie} />
)} )}
@@ -354,17 +385,21 @@ function CookieDetails({ cookie }: { cookie: Cookie }) {
function CookieEditor({ function CookieEditor({
cookie, cookie,
expiresInputValue,
onChange, onChange,
onExpiresInputChange,
}: { }: {
cookie: Cookie; cookie: Cookie;
expiresInputValue: string;
onChange: (cookie: Cookie) => void; onChange: (cookie: Cookie) => void;
onExpiresInputChange: (value: string) => void;
}) { }) {
const sessionCookie = cookie.expires === "SessionEnd"; const sessionCookie = cookie.expires === "SessionEnd";
return ( return (
<div className="overflow-y-auto"> <div className="overflow-y-auto">
<KeyValueRows> <KeyValueRows>
<CookieKeyValueRow label="Name"> <CookieKeyValueRow align="middle" label="Name">
<CookieTextInput <CookieTextInput
required required
autoFocus autoFocus
@@ -378,14 +413,14 @@ function CookieEditor({
onChange={(value) => onChange({ ...cookie, value })} onChange={(value) => onChange({ ...cookie, value })}
/> />
</CookieKeyValueRow> </CookieKeyValueRow>
<CookieKeyValueRow label="Domain"> <CookieKeyValueRow align="middle" label="Domain">
<CookieTextInput <CookieTextInput
value={cookieDomainInputValue(cookie)} value={cookieDomainInputValue(cookie)}
placeholder="n/a" placeholder="n/a"
onChange={(domain) => onChange(cookieWithDomain(cookie, domain))} onChange={(domain) => onChange(cookieWithDomain(cookie, domain))}
/> />
</CookieKeyValueRow> </CookieKeyValueRow>
<CookieKeyValueRow label="Path"> <CookieKeyValueRow align="middle" label="Path">
<CookieTextInput <CookieTextInput
value={cookie.path} value={cookie.path}
placeholder="/" placeholder="/"
@@ -397,24 +432,40 @@ function CookieEditor({
<Checkbox <Checkbox
checked={sessionCookie} checked={sessionCookie}
title="Session cookie" title="Session cookie"
onChange={(checked) => onChange={(checked) => {
if (checked) {
onChange({ ...cookie, expires: "SessionEnd" });
return;
}
const expiresInput =
cookieExpiresFromInput(expiresInputValue) == null
? defaultCookieExpiresInputValue()
: expiresInputValue;
onExpiresInputChange(expiresInput);
onChange({ onChange({
...cookie, ...cookie,
expires: checked expires: cookieExpiresFromInput(expiresInput)!,
? "SessionEnd" });
: cookieExpiresFromInput(defaultCookieExpiresInputValue()), }}
})
}
/> />
<CookieTextInput <CookieTextInput
value={sessionCookie ? "" : cookieExpiresInputValue(cookie)} value={sessionCookie ? "" : expiresInputValue}
disabled={sessionCookie} disabled={sessionCookie}
onChange={(value) => onChange({ ...cookie, expires: cookieExpiresFromInput(value) })} onChange={(value) => {
onExpiresInputChange(value);
const expires = cookieExpiresFromInput(value);
if (expires != null) {
onChange({ ...cookie, expires });
}
}}
/> />
</div> </div>
</CookieKeyValueRow> </CookieKeyValueRow>
<CookieKeyValueRow label="Size">{cookieSize(cookie)}</CookieKeyValueRow> <CookieKeyValueRow label="Size">{cookieSize(cookie)}</CookieKeyValueRow>
<CookieKeyValueRow label="HTTP Only"> <CookieKeyValueRow align="middle" label="HTTP Only">
<Checkbox <Checkbox
hideLabel hideLabel
title="HTTP Only" title="HTTP Only"
@@ -422,7 +473,7 @@ function CookieEditor({
onChange={(httpOnly) => onChange({ ...cookie, httpOnly })} onChange={(httpOnly) => onChange({ ...cookie, httpOnly })}
/> />
</CookieKeyValueRow> </CookieKeyValueRow>
<CookieKeyValueRow label="Secure"> <CookieKeyValueRow align="middle" label="Secure">
<Checkbox <Checkbox
hideLabel hideLabel
title="Secure" title="Secure"
@@ -430,23 +481,27 @@ function CookieEditor({
onChange={(secure) => onChange({ ...cookie, secure })} onChange={(secure) => onChange({ ...cookie, secure })}
/> />
</CookieKeyValueRow> </CookieKeyValueRow>
<CookieKeyValueRow label="Same Site"> <CookieKeyValueRow align="middle" label="Same Site">
<select <Select
hideLabel
name="cookie-same-site"
label="Same Site"
value={cookie.sameSite ?? ""} value={cookie.sameSite ?? ""}
className={cookieInputClassName} size="xs"
onChange={(event) => className="w-full"
options={[
{ label: "n/a", value: "" },
{ label: "Lax", value: "Lax" },
{ label: "Strict", value: "Strict" },
{ label: "None", value: "None" },
]}
onChange={(sameSite) =>
onChange({ onChange({
...cookie, ...cookie,
sameSite: sameSite: sameSite === "" ? null : (sameSite as Cookie["sameSite"]),
event.target.value === "" ? null : (event.target.value as Cookie["sameSite"]),
}) })
} }
> />
<option value="">n/a</option>
<option value="Lax">Lax</option>
<option value="Strict">Strict</option>
<option value="None">None</option>
</select>
</CookieKeyValueRow> </CookieKeyValueRow>
</KeyValueRows> </KeyValueRows>
</div> </div>
@@ -454,9 +509,7 @@ function CookieEditor({
} }
function CookieKeyValueRow({ labelClassName, ...props }: ComponentProps<typeof KeyValueRow>) { function CookieKeyValueRow({ labelClassName, ...props }: ComponentProps<typeof KeyValueRow>) {
return ( return <KeyValueRow labelClassName={classNames("w-[7rem]", labelClassName)} {...props} />;
<KeyValueRow labelClassName={classNames("w-[7rem] min-w-[7rem]", labelClassName)} {...props} />
);
} }
function CookieTextInput({ function CookieTextInput({
@@ -581,10 +634,10 @@ function defaultCookieExpiresInputValue() {
return new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); return new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
} }
function cookieExpiresFromInput(value: string): Cookie["expires"] { function cookieExpiresFromInput(value: string): Cookie["expires"] | null {
const time = new Date(value).getTime(); const time = new Date(value).getTime();
if (!Number.isFinite(time)) { if (!Number.isFinite(time)) {
return "SessionEnd"; return null;
} }
return { AtUtc: `${Math.floor(time / 1000)}` }; return { AtUtc: `${Math.floor(time / 1000)}` };
@@ -34,6 +34,7 @@ interface KeyValueRowProps {
children: ReactNode; children: ReactNode;
rightSlot?: ReactNode; rightSlot?: ReactNode;
leftSlot?: ReactNode; leftSlot?: ReactNode;
align?: "top" | "middle";
labelClassName?: string; labelClassName?: string;
labelColor?: "secondary" | "primary" | "info"; labelColor?: "secondary" | "primary" | "info";
enableCopy?: boolean; enableCopy?: boolean;
@@ -45,6 +46,7 @@ export function KeyValueRow({
children, children,
rightSlot, rightSlot,
leftSlot, leftSlot,
align = "top",
labelColor = "secondary", labelColor = "secondary",
labelClassName, labelClassName,
enableCopy, enableCopy,
@@ -69,7 +71,9 @@ export function KeyValueRow({
<> <>
<td <td
className={classNames( className={classNames(
"select-none py-0.5 pr-2 h-full align-top max-w-[10rem]", "select-none py-0.5 pr-2 h-full max-w-[10rem]",
align === "top" && "align-top",
align === "middle" && "align-middle",
labelClassName, labelClassName,
labelColor === "primary" && "text-primary", labelColor === "primary" && "text-primary",
labelColor === "secondary" && "text-text-subtle", labelColor === "secondary" && "text-text-subtle",
@@ -78,7 +82,13 @@ export function KeyValueRow({
> >
<span className="select-text cursor-text">{label}</span> <span className="select-text cursor-text">{label}</span>
</td> </td>
<td className="select-none py-0.5 break-all align-top max-w-[15rem]"> <td
className={classNames(
"select-none py-0.5 break-all max-w-[15rem]",
align === "top" && "align-top",
align === "middle" && "align-middle",
)}
>
<div className="select-text cursor-text max-h-[12rem] overflow-y-auto grid grid-cols-[auto_minmax(0,1fr)_auto]"> <div className="select-text cursor-text max-h-[12rem] overflow-y-auto grid grid-cols-[auto_minmax(0,1fr)_auto]">
{leftSlot ?? <span aria-hidden />} {leftSlot ?? <span aria-hidden />}
{children} {children}
@@ -95,6 +95,7 @@ export function SettingRow({
</div> </div>
); );
} }
export function SettingValue({ export function SettingValue({
actions, actions,
className, className,
+26 -4
View File
@@ -248,11 +248,16 @@ pub async fn cmd_ws_connect<R: Runtime>(
} }
} }
// Add cookies to WS HTTP Upgrade let mut cookie_jar =
if let (true, Some(id)) = (resolved_settings.send_cookies, cookie_jar_id) { match (resolved_settings.send_cookies || resolved_settings.store_cookies, cookie_jar_id) {
let cookie_jar = app_handle.db().get_cookie_jar(&id)?; (true, Some(id)) => Some(app_handle.db().get_cookie_jar(id)?),
let store = CookieStore::from_cookies(cookie_jar.cookies); _ => None,
};
let cookie_store =
cookie_jar.as_ref().map(|jar| CookieStore::from_cookies(jar.cookies.clone()));
// Add cookies to WS HTTP Upgrade
if let (true, Some(store)) = (resolved_settings.send_cookies, cookie_store.as_ref()) {
// Convert WS URL -> HTTP URL because our cookie store matches based on // Convert WS URL -> HTTP URL because our cookie store matches based on
// Path/HttpOnly/Secure attributes even though WS upgrades are HTTP requests // Path/HttpOnly/Secure attributes even though WS upgrades are HTTP requests
let http_url = convert_ws_url_to_http(&url); let http_url = convert_ws_url_to_http(&url);
@@ -329,6 +334,23 @@ pub async fn cmd_ws_connect<R: Runtime>(
}) })
.collect::<Vec<HttpResponseHeader>>(); .collect::<Vec<HttpResponseHeader>>();
if let (true, Some(cookie_jar), Some(store)) =
(resolved_settings.store_cookies, cookie_jar.as_mut(), cookie_store.as_ref())
{
let set_cookie_headers = response
.headers()
.into_iter()
.filter(|(name, _)| name.as_str().eq_ignore_ascii_case("set-cookie"))
.filter_map(|(_, value)| value.to_str().ok().map(ToString::to_string))
.collect::<Vec<_>>();
if !set_cookie_headers.is_empty() {
store.store_cookies_from_response(&convert_ws_url_to_http(&url), &set_cookie_headers);
cookie_jar.cookies = store.get_all_cookies();
app_handle.db().upsert_cookie_jar(cookie_jar, &UpdateSource::Background)?;
}
}
let connection = app_handle.db().upsert_websocket_connection( let connection = app_handle.db().upsert_websocket_connection(
&WebsocketConnection { &WebsocketConnection {
state: WebsocketConnectionState::Connected, state: WebsocketConnectionState::Connected,
+5 -1
View File
@@ -34,9 +34,13 @@ export function TableBody({ children, className }: { children: ReactNode; classN
<tbody <tbody
className={classNames( className={classNames(
className, className,
"[&>tr:not(:last-child)>td]:border-b [&>tr:not(:last-child)>td]:border-b-surface-highlight", "[&>tr:not(:last-child):not([data-table-spacer])>td]:border-b",
"[&>tr:not(:last-child):not([data-table-spacer])>td]:border-b-surface-highlight",
)} )}
> >
<tr aria-hidden data-table-spacer className="h-0.5">
<td className="p-0" colSpan={1000} />
</tr>
{children} {children}
</tbody> </tbody>
); );