mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-05-17 21:27:05 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c0835031b | |||
| 0a6aed6cc6 | |||
| 0c97036864 | |||
| 1a705ff244 | |||
| dc47b54b1c |
@@ -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 workspaceId={workspaceId} hide={hide} tab={tab} />
|
||||
),
|
||||
});
|
||||
WorkspaceSettingsDialog.show(workspaceId, tab);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -128,12 +134,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
key: "cookies.show",
|
||||
label: "Show Cookies",
|
||||
onSelect: async () => {
|
||||
showDialog({
|
||||
id: "cookies",
|
||||
title: "Manage Cookies",
|
||||
size: "full",
|
||||
render: () => <CookieDialog cookieJarId={activeCookieJar?.id ?? null} />,
|
||||
});
|
||||
CookieDialog.show(activeCookieJar?.id ?? null);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,190 +1,659 @@
|
||||
import type { Cookie, CookieDomain, CookieJar } from "@yaakapp-internal/models";
|
||||
import { cookieJarsAtom, patchModelById } from "@yaakapp-internal/models";
|
||||
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, useMemo, useState } from "react";
|
||||
import { showDialog } from "../lib/dialog";
|
||||
import { jotaiStore } from "../lib/jotai";
|
||||
import { cookieDomain } from "../lib/model_util";
|
||||
import { showPromptForm } from "../lib/prompt-form";
|
||||
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;
|
||||
}
|
||||
|
||||
async function showAddCookieForm(cookieJarId: string): Promise<void> {
|
||||
const result = await showPromptForm({
|
||||
id: "add-cookie",
|
||||
title: "Add Cookie",
|
||||
size: "md",
|
||||
inputs: [
|
||||
{
|
||||
name: "cookie_pairs",
|
||||
label: "Cookie Attributes",
|
||||
type: "key_value",
|
||||
description:
|
||||
"Add key-value pairs for the cookie. These will be combined into the cookie string.",
|
||||
},
|
||||
{
|
||||
name: "domain_value",
|
||||
label: "Domain",
|
||||
type: "text",
|
||||
placeholder: "example.com",
|
||||
},
|
||||
{
|
||||
name: "hostOnly",
|
||||
label: "Host Only",
|
||||
type: "checkbox",
|
||||
defaultValue: "true",
|
||||
description:
|
||||
"If enabled, cookie is restricted to the exact host. Otherwise, it applies to the domain and its subdomains.",
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
label: "Path",
|
||||
type: "text",
|
||||
placeholder: "/",
|
||||
defaultValue: "/",
|
||||
},
|
||||
{
|
||||
name: "secure",
|
||||
label: "Secure",
|
||||
type: "checkbox",
|
||||
defaultValue: "true",
|
||||
description: "If enabled, cookie will only be sent over HTTPS connections.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (result == null) return;
|
||||
|
||||
// Parse the form results
|
||||
const cookie_pairs_raw = result.cookie_pairs;
|
||||
const domain_value = (result.domain_value as string) ?? "";
|
||||
const path = (result.path as string) ?? "/";
|
||||
const hostOnly = (result.hostOnly as string) === "true";
|
||||
const secure = (result.secure as string) === "true";
|
||||
|
||||
// Convert key-value pairs to raw_cookie string format: key1=value1;key2=value2
|
||||
// Parse cookie_pairs - it comes as a JSON string from the key_value input
|
||||
let parsedPairs: Array<{ name: string; value: string }> = [];
|
||||
try {
|
||||
// Handle null, undefined, or string value
|
||||
const pairsStr =
|
||||
typeof cookie_pairs_raw === "string"
|
||||
? cookie_pairs_raw
|
||||
: cookie_pairs_raw != null
|
||||
? JSON.stringify(cookie_pairs_raw)
|
||||
: "[]";
|
||||
if (pairsStr && pairsStr !== "") {
|
||||
parsedPairs = JSON.parse(pairsStr);
|
||||
}
|
||||
} catch {
|
||||
parsedPairs = [];
|
||||
}
|
||||
|
||||
const validPairs = parsedPairs.filter((p) => p?.name?.trim());
|
||||
// Ensure at least one valid pair exists
|
||||
if (validPairs.length === 0) {
|
||||
console.log("No valid cookie pairs provided");
|
||||
return;
|
||||
}
|
||||
|
||||
const raw_cookie = validPairs.map((p) => `${p.name}=${p.value}`).join(";");
|
||||
|
||||
const domain: CookieDomain = hostOnly
|
||||
? { HostOnly: domain_value ?? "" }
|
||||
: { Suffix: domain_value ?? "" };
|
||||
|
||||
// Build the new cookie with explicit tuple type for path
|
||||
const newCookie: Cookie = {
|
||||
raw_cookie,
|
||||
domain,
|
||||
expires: "SessionEnd",
|
||||
path: [path, secure] as [string, boolean],
|
||||
};
|
||||
|
||||
try {
|
||||
await patchModelById<"cookie_jar", CookieJar>("cookie_jar", cookieJarId, (prev) => ({
|
||||
...prev,
|
||||
cookies: [...prev.cookies, newCookie],
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Failed to add cookie:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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<string | null>(null);
|
||||
const [editingCookieKey, setEditingCookieKey] = useState<string | null>(null);
|
||||
const [draftCookie, setDraftCookie] = useState<Cookie | null>(null);
|
||||
const [draftExpiresInput, setDraftExpiresInput] = useState("");
|
||||
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 = () => {
|
||||
if (cookieJar == null || draftCookie == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextCookie = normalizeCookie(draftCookie);
|
||||
if (nextCookie.name.trim().length === 0) {
|
||||
showAlert({
|
||||
id: "invalid-cookie-name",
|
||||
title: "Invalid Cookie",
|
||||
body: "Cookie name is required.",
|
||||
});
|
||||
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 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 <div>No cookie jar selected</div>;
|
||||
}
|
||||
|
||||
const onAddCookie = () => showAddCookieForm(cookieJar.id);
|
||||
|
||||
let tableBody;
|
||||
if (cookieJar.cookies.length === 0) {
|
||||
tableBody = (
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<Banner>
|
||||
Cookies will appear when a response contains the <InlineCode>Set-Cookie</InlineCode>{" "}
|
||||
header
|
||||
</Banner>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
// );
|
||||
} else {
|
||||
tableBody = cookieJar?.cookies.map((c: Cookie) => (
|
||||
<tr key={JSON.stringify(c)}>
|
||||
<td className="py-2 select-text cursor-text font-mono font-semibold max-w-0">
|
||||
{cookieDomain(c)}
|
||||
</td>
|
||||
<td className="py-2 pl-4 select-text cursor-text font-mono text-text-subtle whitespace-nowrap overflow-x-auto max-w-[200px] hide-scrollbars">
|
||||
{c.raw_cookie}
|
||||
</td>
|
||||
<td className="max-w-0 w-10">
|
||||
<IconButton
|
||||
icon="trash"
|
||||
size="xs"
|
||||
iconSize="sm"
|
||||
title="Delete"
|
||||
className="ml-auto"
|
||||
onClick={async () =>
|
||||
await patchModelById<"cookie_jar", CookieJar>("cookie_jar", cookieJar.id, (prev) => ({
|
||||
...prev,
|
||||
cookies: prev.cookies.filter((c2: Cookie) => c2 !== c),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pb-2">
|
||||
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-2 text-left">Domain</th>
|
||||
<th className="py-2 text-left pl-4">Cookie</th>
|
||||
<th className="py-2 pl-4 w-10">
|
||||
<div className="pb-2 grid grid-rows-[auto_minmax(0,1fr)] space-y-2">
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
|
||||
<PlainInput
|
||||
name="cookie-filter"
|
||||
label="Filter cookies"
|
||||
hideLabel
|
||||
placeholder="Filter cookies"
|
||||
defaultValue={filter}
|
||||
forceUpdateKey={filterUpdateKey}
|
||||
onChange={setFilter}
|
||||
rightSlot={
|
||||
filter.length > 0 && (
|
||||
<IconButton
|
||||
icon="plus"
|
||||
size="xs"
|
||||
iconSize="sm"
|
||||
title="Add Cookie"
|
||||
className="ml-auto"
|
||||
onClick={onAddCookie}
|
||||
className="!bg-transparent !h-auto min-h-full opacity-50 hover:opacity-100 -mr-1"
|
||||
icon="x"
|
||||
title="Clear filter"
|
||||
onClick={() => {
|
||||
setFilter("");
|
||||
setFilterUpdateKey((key) => key + 1);
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-surface-highlight">{tableBody}</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<IconButton icon="plus" size="sm" title="Add cookie" onClick={handleAddCookie} />
|
||||
</div>
|
||||
{cookieJar.cookies.length === 0 && detailCookie == null ? (
|
||||
<EmptyStateText>
|
||||
Cookies will appear when a response includes a Set-Cookie header.
|
||||
</EmptyStateText>
|
||||
) : filteredCookies.length === 0 && detailCookie == null ? (
|
||||
<EmptyStateText>No cookies match the current filter.</EmptyStateText>
|
||||
) : (
|
||||
<SplitLayout
|
||||
layout="vertical"
|
||||
storageKey="cookie-dialog-details"
|
||||
defaultRatio={0.65}
|
||||
className="-mx-2"
|
||||
minHeightPx={10}
|
||||
firstSlot={({ style }) =>
|
||||
filteredCookies.length === 0 ? (
|
||||
<div style={style}>
|
||||
<EmptyStateText>No cookies match the current filter.</EmptyStateText>
|
||||
</div>
|
||||
) : (
|
||||
<Table scrollable style={style} className="pr-0.5">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
<TableHeaderCell>Value</TableHeaderCell>
|
||||
<TableHeaderCell>Domain</TableHeaderCell>
|
||||
<TableHeaderCell>Path</TableHeaderCell>
|
||||
<TableHeaderCell>Expires</TableHeaderCell>
|
||||
<TableHeaderCell>Size</TableHeaderCell>
|
||||
<TableHeaderCell>HTTP Only</TableHeaderCell>
|
||||
<TableHeaderCell>Secure</TableHeaderCell>
|
||||
<TableHeaderCell>Same Site</TableHeaderCell>
|
||||
<TableHeaderCell>
|
||||
<IconButton
|
||||
icon="list_x"
|
||||
size="sm"
|
||||
className="text-text-subtle"
|
||||
title="Clear all cookies"
|
||||
onClick={() => {
|
||||
setSelectedCookieKey(null);
|
||||
setEditingCookieKey(null);
|
||||
setDraftCookie(null);
|
||||
setDraftExpiresInput("");
|
||||
patchModel(cookieJar, { cookies: [] });
|
||||
}}
|
||||
/>
|
||||
</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody className="[&_td]:select-auto [&_td]:cursor-auto">
|
||||
{filteredCookies.map((c: Cookie) => {
|
||||
const key = cookieKey(c);
|
||||
const isSelected = key === selectedCookieKey;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={key}
|
||||
className={classNames(
|
||||
"group/tr cursor-default",
|
||||
isSelected && "[&_td]:bg-surface-highlight",
|
||||
!isSelected && "hover:[&_td]:bg-surface-hover",
|
||||
)}
|
||||
onClick={() => {
|
||||
setSelectedCookieKey(key);
|
||||
setEditingCookieKey(null);
|
||||
setDraftCookie(null);
|
||||
setDraftExpiresInput("");
|
||||
}}
|
||||
>
|
||||
<TableCell className={classNames("pl-2", isSelected && "rounded-l")}>
|
||||
{c.name}
|
||||
</TableCell>
|
||||
<TruncatedWideTableCell className="min-w-[10rem]">
|
||||
{c.value}
|
||||
</TruncatedWideTableCell>
|
||||
<TableCell>{cookieDomain(c)}</TableCell>
|
||||
<TableCell>{c.path}</TableCell>
|
||||
<TableCell>{cookieExpires(c)}</TableCell>
|
||||
<TableCell>{cookieSize(c)}</TableCell>
|
||||
<TableCell>
|
||||
<Icon
|
||||
icon={c.httpOnly ? "check" : "x"}
|
||||
className={classNames(!c.httpOnly && "opacity-10")}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Icon
|
||||
icon={c.secure ? "check" : "x"}
|
||||
className={classNames(!c.secure && "opacity-10")}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{c.sameSite}</TableCell>
|
||||
<TableCell className="rounded-r pr-2">
|
||||
<IconButton
|
||||
icon="trash"
|
||||
size="xs"
|
||||
iconSize="sm"
|
||||
title="Delete"
|
||||
className="text-text-subtlest ml-auto group-hover/tr:text-text transition-colors"
|
||||
onClick={(event) => {
|
||||
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,
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
secondSlot={
|
||||
detailCookie == null
|
||||
? null
|
||||
: ({ style }) => (
|
||||
<div
|
||||
style={style}
|
||||
className="grid grid-rows-[auto_minmax(0,1fr)] bg-surface border-t border-border pt-2"
|
||||
>
|
||||
<EventDetailHeader
|
||||
title={isCreatingCookie ? "New Cookie" : detailCookie.name || "Cookie"}
|
||||
copyText={isEditingCookie ? undefined : detailCookie.value}
|
||||
actions={
|
||||
isEditingCookie
|
||||
? [
|
||||
{
|
||||
key: "save",
|
||||
label: isCreatingCookie ? "Create" : "Save",
|
||||
onClick: handleSaveCookie,
|
||||
},
|
||||
{
|
||||
key: "cancel",
|
||||
label: "Cancel",
|
||||
onClick: handleCancelEdit,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
key: "edit",
|
||||
label: "Edit",
|
||||
onClick: handleEditCookie,
|
||||
},
|
||||
]
|
||||
}
|
||||
onClose={handleCloseDetails}
|
||||
/>
|
||||
{isEditingCookie ? (
|
||||
<CookieEditor
|
||||
cookie={detailCookie}
|
||||
expiresInputValue={draftExpiresInput}
|
||||
onChange={setDraftCookie}
|
||||
onExpiresInputChange={setDraftExpiresInput}
|
||||
/>
|
||||
) : (
|
||||
<CookieDetails cookie={detailCookie} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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: () => <CookieDialog cookieJarId={cookieJarId} />,
|
||||
});
|
||||
};
|
||||
|
||||
function CookieDetails({ cookie }: { cookie: Cookie }) {
|
||||
return (
|
||||
<div className="overflow-y-auto">
|
||||
<KeyValueRows selectable>
|
||||
<CookieKeyValueRow label="Name">{cookie.name}</CookieKeyValueRow>
|
||||
<CookieKeyValueRow label="Value" enableCopy copyText={cookie.value}>
|
||||
<pre className="whitespace-pre-wrap break-all">{cookie.value}</pre>
|
||||
</CookieKeyValueRow>
|
||||
<CookieKeyValueRow label="Domain">{cookieDomain(cookie)}</CookieKeyValueRow>
|
||||
<CookieKeyValueRow label="Path">{cookie.path}</CookieKeyValueRow>
|
||||
<CookieKeyValueRow label="Expires">{cookieExpires(cookie)}</CookieKeyValueRow>
|
||||
<CookieKeyValueRow label="Size">{cookieSize(cookie)}</CookieKeyValueRow>
|
||||
<CookieKeyValueRow label="HTTP Only">{cookie.httpOnly ? "Yes" : "No"}</CookieKeyValueRow>
|
||||
<CookieKeyValueRow label="Secure">{cookie.secure ? "Yes" : "No"}</CookieKeyValueRow>
|
||||
{cookie.sameSite && (
|
||||
<CookieKeyValueRow label="Same Site">{cookie.sameSite}</CookieKeyValueRow>
|
||||
)}
|
||||
</KeyValueRows>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CookieEditor({
|
||||
cookie,
|
||||
expiresInputValue,
|
||||
onChange,
|
||||
onExpiresInputChange,
|
||||
}: {
|
||||
cookie: Cookie;
|
||||
expiresInputValue: string;
|
||||
onChange: (cookie: Cookie) => void;
|
||||
onExpiresInputChange: (value: string) => void;
|
||||
}) {
|
||||
const sessionCookie = cookie.expires === "SessionEnd";
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto">
|
||||
<KeyValueRows>
|
||||
<CookieKeyValueRow align="middle" label="Name">
|
||||
<CookieTextInput
|
||||
required
|
||||
autoFocus
|
||||
value={cookie.name}
|
||||
onChange={(name) => onChange({ ...cookie, name })}
|
||||
/>
|
||||
</CookieKeyValueRow>
|
||||
<CookieKeyValueRow label="Value">
|
||||
<CookieTextarea
|
||||
value={cookie.value}
|
||||
onChange={(value) => onChange({ ...cookie, value })}
|
||||
/>
|
||||
</CookieKeyValueRow>
|
||||
<CookieKeyValueRow align="middle" label="Domain">
|
||||
<CookieTextInput
|
||||
value={cookieDomainInputValue(cookie)}
|
||||
placeholder="n/a"
|
||||
onChange={(domain) => onChange(cookieWithDomain(cookie, domain))}
|
||||
/>
|
||||
</CookieKeyValueRow>
|
||||
<CookieKeyValueRow align="middle" label="Path">
|
||||
<CookieTextInput
|
||||
value={cookie.path}
|
||||
placeholder="/"
|
||||
onChange={(path) => onChange({ ...cookie, path })}
|
||||
/>
|
||||
</CookieKeyValueRow>
|
||||
<CookieKeyValueRow label="Expires">
|
||||
<div className="grid gap-1">
|
||||
<Checkbox
|
||||
checked={sessionCookie}
|
||||
title="Session cookie"
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
onChange({ ...cookie, expires: "SessionEnd" });
|
||||
return;
|
||||
}
|
||||
|
||||
const expiresInput =
|
||||
cookieExpiresFromInput(expiresInputValue) == null
|
||||
? defaultCookieExpiresInputValue()
|
||||
: expiresInputValue;
|
||||
|
||||
onExpiresInputChange(expiresInput);
|
||||
onChange({
|
||||
...cookie,
|
||||
expires: cookieExpiresFromInput(expiresInput)!,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<CookieTextInput
|
||||
value={sessionCookie ? "" : expiresInputValue}
|
||||
disabled={sessionCookie}
|
||||
onChange={(value) => {
|
||||
onExpiresInputChange(value);
|
||||
|
||||
const expires = cookieExpiresFromInput(value);
|
||||
if (expires != null) {
|
||||
onChange({ ...cookie, expires });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CookieKeyValueRow>
|
||||
<CookieKeyValueRow label="Size">{cookieSize(cookie)}</CookieKeyValueRow>
|
||||
<CookieKeyValueRow align="middle" label="HTTP Only">
|
||||
<Checkbox
|
||||
hideLabel
|
||||
title="HTTP Only"
|
||||
checked={cookie.httpOnly}
|
||||
onChange={(httpOnly) => onChange({ ...cookie, httpOnly })}
|
||||
/>
|
||||
</CookieKeyValueRow>
|
||||
<CookieKeyValueRow align="middle" label="Secure">
|
||||
<Checkbox
|
||||
hideLabel
|
||||
title="Secure"
|
||||
checked={cookie.secure}
|
||||
onChange={(secure) => onChange({ ...cookie, secure })}
|
||||
/>
|
||||
</CookieKeyValueRow>
|
||||
<CookieKeyValueRow align="middle" label="Same Site">
|
||||
<Select
|
||||
hideLabel
|
||||
name="cookie-same-site"
|
||||
label="Same Site"
|
||||
value={cookie.sameSite ?? ""}
|
||||
size="xs"
|
||||
className="w-full"
|
||||
options={[
|
||||
{ label: "n/a", value: "" },
|
||||
{ label: "Lax", value: "Lax" },
|
||||
{ label: "Strict", value: "Strict" },
|
||||
{ label: "None", value: "None" },
|
||||
]}
|
||||
onChange={(sameSite) =>
|
||||
onChange({
|
||||
...cookie,
|
||||
sameSite: sameSite === "" ? null : (sameSite as Cookie["sameSite"]),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</CookieKeyValueRow>
|
||||
</KeyValueRows>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CookieKeyValueRow({ labelClassName, ...props }: ComponentProps<typeof KeyValueRow>) {
|
||||
return <KeyValueRow labelClassName={classNames("w-[7rem]", labelClassName)} {...props} />;
|
||||
}
|
||||
|
||||
function CookieTextInput({
|
||||
autoFocus,
|
||||
disabled,
|
||||
onChange,
|
||||
placeholder,
|
||||
required,
|
||||
value,
|
||||
}: {
|
||||
autoFocus?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
value: string;
|
||||
}) {
|
||||
return (
|
||||
<input
|
||||
autoFocus={autoFocus}
|
||||
className={cookieInputClassName}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
type="text"
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CookieTextarea({ onChange, value }: { onChange: (value: string) => void; value: string }) {
|
||||
return (
|
||||
<textarea
|
||||
className={classNames(cookieInputClassName, "min-h-[5rem] resize-y")}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const NEW_COOKIE_KEY = "__new-cookie__";
|
||||
const cookieInputClassName = classNames(
|
||||
"w-full min-w-0 rounded bg-transparent px-1 py-0.5",
|
||||
"border border-transparent outline-none",
|
||||
"hover:border-border-subtle focus:border-border-focus",
|
||||
"disabled:opacity-disabled disabled:border-dotted",
|
||||
);
|
||||
|
||||
function cookieSize(cookie: Cookie) {
|
||||
const encoder = new TextEncoder();
|
||||
return encoder.encode(cookie.name).length + encoder.encode(cookie.value).length;
|
||||
}
|
||||
|
||||
function newCookieDraft(): Cookie {
|
||||
return {
|
||||
name: "",
|
||||
value: "",
|
||||
domain: "NotPresent",
|
||||
expires: "SessionEnd",
|
||||
path: "/",
|
||||
secure: false,
|
||||
httpOnly: false,
|
||||
sameSite: null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCookie(cookie: Cookie): Cookie {
|
||||
return {
|
||||
...cookie,
|
||||
name: cookie.name.trim(),
|
||||
path: cookie.path.trim() || "/",
|
||||
};
|
||||
}
|
||||
|
||||
function cookieDomainInputValue(cookie: Cookie) {
|
||||
const domain = cookieDomain(cookie);
|
||||
return domain === "n/a" ? "" : domain;
|
||||
}
|
||||
|
||||
function cookieWithDomain(cookie: Cookie, domain: string): Cookie {
|
||||
const trimmedDomain = domain.trim();
|
||||
if (trimmedDomain.length === 0) {
|
||||
return { ...cookie, domain: "NotPresent" };
|
||||
}
|
||||
|
||||
if (cookie.domain !== "NotPresent" && cookie.domain !== "Empty" && "Suffix" in cookie.domain) {
|
||||
return { ...cookie, domain: { Suffix: trimmedDomain } };
|
||||
}
|
||||
|
||||
return { ...cookie, domain: { HostOnly: trimmedDomain } };
|
||||
}
|
||||
|
||||
function cookieExpires(cookie: Cookie) {
|
||||
if (cookie.expires === "SessionEnd") {
|
||||
return "Session";
|
||||
}
|
||||
|
||||
const expiresSeconds = Number(cookie.expires.AtUtc);
|
||||
if (!Number.isFinite(expiresSeconds)) {
|
||||
return cookie.expires.AtUtc;
|
||||
}
|
||||
|
||||
const date = new Date(expiresSeconds * 1000);
|
||||
return formatDate(date, "MMM d, yyyy, h:mm:ss a");
|
||||
}
|
||||
|
||||
function cookieExpiresInputValue(cookie: Cookie) {
|
||||
if (cookie.expires === "SessionEnd") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const expiresSeconds = Number(cookie.expires.AtUtc);
|
||||
if (!Number.isFinite(expiresSeconds)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return new Date(expiresSeconds * 1000).toISOString();
|
||||
}
|
||||
|
||||
function defaultCookieExpiresInputValue() {
|
||||
return new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||
}
|
||||
|
||||
function cookieExpiresFromInput(value: string): Cookie["expires"] | null {
|
||||
const time = new Date(value).getTime();
|
||||
if (!Number.isFinite(time)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { AtUtc: `${Math.floor(time / 1000)}` };
|
||||
}
|
||||
|
||||
function cookieMatchesFilter(cookie: Cookie, filter: string) {
|
||||
const query = filter.trim().toLowerCase();
|
||||
if (query.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [cookie.name, cookie.value, cookieDomain(cookie)].some((value) =>
|
||||
value.toLowerCase().includes(query),
|
||||
);
|
||||
}
|
||||
|
||||
function cookieKey(cookie: Cookie) {
|
||||
return JSON.stringify([cookie.name, cookieDomain(cookie), cookie.path]);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { memo, useMemo } from "react";
|
||||
import { useActiveCookieJar } from "../hooks/useActiveCookieJar";
|
||||
import { useCreateCookieJar } from "../hooks/useCreateCookieJar";
|
||||
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
||||
import { showDialog } from "../lib/dialog";
|
||||
import { showPrompt } from "../lib/prompt";
|
||||
import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
|
||||
import { CookieDialog } from "./CookieDialog";
|
||||
@@ -36,12 +35,7 @@ export const CookieDropdown = memo(function CookieDropdown() {
|
||||
leftSlot: <Icon icon="cookie" />,
|
||||
onSelect: () => {
|
||||
if (activeCookieJar == null) return;
|
||||
showDialog({
|
||||
id: "cookies",
|
||||
title: "Manage Cookies",
|
||||
size: "full",
|
||||
render: () => <CookieDialog cookieJarId={activeCookieJar.id} />,
|
||||
});
|
||||
CookieDialog.show(activeCookieJar.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ import { EnvironmentEditor } from "./EnvironmentEditor";
|
||||
import { HeadersEditor } from "./HeadersEditor";
|
||||
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
||||
import { MarkdownEditor } from "./MarkdownEditor";
|
||||
import { countOverriddenSettings, ModelSettingsEditor } from "./ModelSettingsEditor";
|
||||
|
||||
interface Props {
|
||||
folderId: string | null;
|
||||
@@ -29,6 +30,7 @@ interface Props {
|
||||
|
||||
const TAB_AUTH = "auth";
|
||||
const TAB_HEADERS = "headers";
|
||||
const TAB_SETTINGS = "settings";
|
||||
const TAB_VARIABLES = "variables";
|
||||
const TAB_GENERAL = "general";
|
||||
|
||||
@@ -36,6 +38,7 @@ export type FolderSettingsTab =
|
||||
| typeof TAB_AUTH
|
||||
| typeof TAB_HEADERS
|
||||
| typeof TAB_GENERAL
|
||||
| typeof TAB_SETTINGS
|
||||
| typeof TAB_VARIABLES;
|
||||
|
||||
export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||
@@ -51,6 +54,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||
(e) => e.parentModel === "folder" && e.parentId === folderId,
|
||||
);
|
||||
const numVars = (folderEnvironment?.variables ?? []).filter((v) => v.name).length;
|
||||
const numSettingsOverrides = folder == null ? 0 : countOverriddenSettings(folder);
|
||||
|
||||
const tabs = useMemo<TabItem[]>(() => {
|
||||
if (folder == null) return [];
|
||||
@@ -60,6 +64,11 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||
value: TAB_GENERAL,
|
||||
label: "General",
|
||||
},
|
||||
{
|
||||
value: TAB_SETTINGS,
|
||||
label: "Settings",
|
||||
rightSlot: <CountBadge count={numSettingsOverrides} />,
|
||||
},
|
||||
...headersTab,
|
||||
...authTab,
|
||||
{
|
||||
@@ -68,7 +77,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||
rightSlot: numVars > 0 ? <CountBadge count={numVars} /> : null,
|
||||
},
|
||||
];
|
||||
}, [authTab, folder, headersTab, numVars]);
|
||||
}, [authTab, folder, headersTab, numSettingsOverrides, numVars]);
|
||||
|
||||
if (folder == null) return null;
|
||||
|
||||
@@ -159,6 +168,9 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||
stateKey={`headers.${folder.id}`}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_SETTINGS} className="overflow-y-auto h-full px-4">
|
||||
<ModelSettingsEditor model={folder} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_VARIABLES} className="overflow-y-auto h-full px-4">
|
||||
{folderEnvironment == null ? (
|
||||
<EmptyStateText>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { GrpcEditor } from "./GrpcEditor";
|
||||
import { HeadersEditor } from "./HeadersEditor";
|
||||
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
||||
import { MarkdownEditor } from "./MarkdownEditor";
|
||||
import { countOverriddenSettings, ModelSettingsEditor } from "./ModelSettingsEditor";
|
||||
import { UrlBar } from "./UrlBar";
|
||||
|
||||
interface Props {
|
||||
@@ -47,6 +48,7 @@ interface Props {
|
||||
const TAB_MESSAGE = "message";
|
||||
const TAB_METADATA = "metadata";
|
||||
const TAB_AUTH = "auth";
|
||||
const TAB_SETTINGS = "settings";
|
||||
const TAB_DESCRIPTION = "description";
|
||||
|
||||
export function GrpcRequestPane({
|
||||
@@ -66,6 +68,7 @@ export function GrpcRequestPane({
|
||||
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, "Metadata");
|
||||
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
||||
const numSettingsOverrides = countOverriddenSettings(activeRequest);
|
||||
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
|
||||
|
||||
const urlContainerEl = useRef<HTMLDivElement>(null);
|
||||
@@ -128,13 +131,18 @@ export function GrpcRequestPane({
|
||||
{ value: TAB_MESSAGE, label: "Message" },
|
||||
...metadataTab,
|
||||
...authTab,
|
||||
{
|
||||
value: TAB_SETTINGS,
|
||||
label: "Settings",
|
||||
rightSlot: <CountBadge count={numSettingsOverrides} />,
|
||||
},
|
||||
{
|
||||
value: TAB_DESCRIPTION,
|
||||
label: "Info",
|
||||
rightSlot: activeRequest.description && <CountBadge count={true} />,
|
||||
},
|
||||
],
|
||||
[activeRequest.description, authTab, metadataTab],
|
||||
[activeRequest.description, authTab, metadataTab, numSettingsOverrides],
|
||||
);
|
||||
|
||||
const handleMetadataChange = useCallback(
|
||||
@@ -278,6 +286,9 @@ export function GrpcRequestPane({
|
||||
onChange={handleMetadataChange}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_SETTINGS}>
|
||||
<ModelSettingsEditor model={activeRequest} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_DESCRIPTION}>
|
||||
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
|
||||
<PlainInput
|
||||
|
||||
@@ -51,6 +51,7 @@ import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
||||
import { JsonBodyEditor } from "./JsonBodyEditor";
|
||||
import { MarkdownEditor } from "./MarkdownEditor";
|
||||
import { RequestMethodDropdown } from "./RequestMethodDropdown";
|
||||
import { countOverriddenSettings, ModelSettingsEditor } from "./ModelSettingsEditor";
|
||||
import { UrlBar } from "./UrlBar";
|
||||
import { UrlParametersEditor } from "./UrlParameterEditor";
|
||||
|
||||
@@ -69,6 +70,7 @@ const TAB_BODY = "body";
|
||||
const TAB_PARAMS = "params";
|
||||
const TAB_HEADERS = "headers";
|
||||
const TAB_AUTH = "auth";
|
||||
const TAB_SETTINGS = "settings";
|
||||
const TAB_DESCRIPTION = "description";
|
||||
const TABS_STORAGE_KEY = "http_request_tabs";
|
||||
|
||||
@@ -92,6 +94,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
||||
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
||||
const numSettingsOverrides = countOverriddenSettings(activeRequest);
|
||||
|
||||
// Listen for event to focus the params tab (e.g., when clicking a :param in the URL)
|
||||
useRequestEditorEvent(
|
||||
@@ -234,6 +237,11 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
},
|
||||
...headersTab,
|
||||
...authTab,
|
||||
{
|
||||
value: TAB_SETTINGS,
|
||||
label: "Settings",
|
||||
rightSlot: <CountBadge count={numSettingsOverrides} />,
|
||||
},
|
||||
{
|
||||
value: TAB_DESCRIPTION,
|
||||
label: "Info",
|
||||
@@ -246,6 +254,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
handleContentTypeChange,
|
||||
headersTab,
|
||||
numParams,
|
||||
numSettingsOverrides,
|
||||
urlParameterPairs.length,
|
||||
],
|
||||
);
|
||||
@@ -372,6 +381,9 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
onChange={(urlParameters) => patchModel(activeRequest, { urlParameters })}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_SETTINGS}>
|
||||
<ModelSettingsEditor model={activeRequest} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_BODY}>
|
||||
<ConfirmLargeRequestBody request={activeRequest}>
|
||||
{activeRequest.bodyType === BODY_TYPE_JSON ? (
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import type {
|
||||
AnyModel,
|
||||
HttpResponse,
|
||||
HttpResponseEvent,
|
||||
HttpResponseEventData,
|
||||
} from "@yaakapp-internal/models";
|
||||
import { foldersAtom, workspacesAtom } from "@yaakapp-internal/models";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { type ReactNode, useMemo, useState } from "react";
|
||||
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
|
||||
import { useAllRequests } from "../hooks/useAllRequests";
|
||||
import { resolvedModelName } from "../lib/resolvedModelName";
|
||||
import { Editor } from "./core/Editor/LazyEditor";
|
||||
import { type EventDetailAction, EventDetailHeader, EventViewer } from "./core/EventViewer";
|
||||
import { EventViewerRow } from "./core/EventViewerRow";
|
||||
@@ -95,6 +100,7 @@ function EventDetails({
|
||||
}) {
|
||||
const { label } = getEventDisplay(event.event);
|
||||
const e = event.event;
|
||||
const settingSourceModels = useSettingSourceModels();
|
||||
|
||||
const actions: EventDetailAction[] = [
|
||||
{
|
||||
@@ -211,6 +217,9 @@ function EventDetails({
|
||||
<KeyValueRows>
|
||||
<KeyValueRow label="Setting">{e.name}</KeyValueRow>
|
||||
<KeyValueRow label="Value">{e.value}</KeyValueRow>
|
||||
{e.source_model != null ? (
|
||||
<KeyValueRow label="Source">{formatSettingSource(e, settingSourceModels)}</KeyValueRow>
|
||||
) : null}
|
||||
</KeyValueRows>
|
||||
);
|
||||
}
|
||||
@@ -315,6 +324,35 @@ function formatEventText(event: HttpResponseEventData, includePrefix: boolean):
|
||||
return includePrefix ? `${prefix} ${text}` : text;
|
||||
}
|
||||
|
||||
function useSettingSourceModels() {
|
||||
const requests = useAllRequests();
|
||||
const folders = useAtomValue(foldersAtom);
|
||||
const workspaces = useAtomValue(workspacesAtom);
|
||||
|
||||
return useMemo<AnyModel[]>(
|
||||
() => [...requests, ...folders, ...workspaces],
|
||||
[requests, folders, workspaces],
|
||||
);
|
||||
}
|
||||
|
||||
function formatSettingSource(
|
||||
event: Extract<HttpResponseEventData, { type: "setting" }>,
|
||||
models: AnyModel[],
|
||||
): string {
|
||||
const sourceModel = event.source_model;
|
||||
if (sourceModel == null || sourceModel === "default") {
|
||||
return "Default";
|
||||
}
|
||||
|
||||
const model =
|
||||
event.source_id == null
|
||||
? null
|
||||
: (models.find((m) => m.model === sourceModel && m.id === event.source_id) ?? null);
|
||||
const name = model == null ? event.source_name : resolvedModelName(model);
|
||||
const label = sourceModel.replaceAll("_", " ");
|
||||
return name == null || name.length === 0 ? label : `${name} (${label})`;
|
||||
}
|
||||
|
||||
type EventDisplay = {
|
||||
icon: IconProps["icon"];
|
||||
color: IconProps["color"];
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
import type {
|
||||
Folder,
|
||||
GrpcRequest,
|
||||
HttpRequest,
|
||||
InheritedBoolSetting,
|
||||
InheritedIntSetting,
|
||||
WebsocketRequest,
|
||||
Workspace,
|
||||
} from "@yaakapp-internal/models";
|
||||
import { patchModel } from "@yaakapp-internal/models";
|
||||
import { useModelAncestors } from "../hooks/useModelAncestors";
|
||||
import { Checkbox } from "./core/Checkbox";
|
||||
import { PlainInput } from "./core/PlainInput";
|
||||
import {
|
||||
SettingOverrideRow,
|
||||
SettingRowBoolean,
|
||||
SettingRowNumber,
|
||||
SettingsList,
|
||||
SettingsSection,
|
||||
} from "./core/SettingRow";
|
||||
|
||||
interface Props {
|
||||
showSectionTitles?: boolean;
|
||||
model: ModelWithSettings;
|
||||
}
|
||||
|
||||
type ModelWithSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest;
|
||||
type ModelWithHttpSettings = Workspace | Folder | HttpRequest;
|
||||
type ModelWithCookieSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest;
|
||||
type BooleanSetting = boolean | InheritedBoolSetting;
|
||||
type IntegerSetting = number | InheritedIntSetting;
|
||||
type CookieSettingsPatch = {
|
||||
settingSendCookies?: ModelWithCookieSettings["settingSendCookies"];
|
||||
settingStoreCookies?: ModelWithCookieSettings["settingStoreCookies"];
|
||||
};
|
||||
type HttpSettingsPatch = {
|
||||
settingValidateCertificates?: ModelWithHttpSettings["settingValidateCertificates"];
|
||||
settingFollowRedirects?: ModelWithHttpSettings["settingFollowRedirects"];
|
||||
settingRequestTimeout?: ModelWithHttpSettings["settingRequestTimeout"];
|
||||
};
|
||||
|
||||
export function ModelSettingsEditor({ model, showSectionTitles = false }: Props) {
|
||||
const ancestors = useModelAncestors(model);
|
||||
const supportsHttpSettings =
|
||||
model.model === "workspace" || model.model === "folder" || model.model === "http_request";
|
||||
|
||||
return (
|
||||
<SettingsList className="space-y-8">
|
||||
{supportsHttpSettings && (
|
||||
<SettingsSection title={showSectionTitles ? "Requests" : null}>
|
||||
<IntegerSettingRow
|
||||
title="Request Timeout"
|
||||
description="Maximum request duration in milliseconds. Set to 0 to disable the timeout."
|
||||
name="settingRequestTimeout"
|
||||
setting={model.settingRequestTimeout}
|
||||
inheritedValue={resolveInheritedValue(
|
||||
ancestors,
|
||||
"settingRequestTimeout",
|
||||
model.settingRequestTimeout,
|
||||
)}
|
||||
onChange={(settingRequestTimeout) =>
|
||||
patchHttpSettings(model, {
|
||||
settingRequestTimeout,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<BooleanSettingRow
|
||||
title="Validate TLS certificates"
|
||||
description="When disabled, skip validation of server certificates."
|
||||
setting={model.settingValidateCertificates}
|
||||
inheritedValue={resolveInheritedValue(
|
||||
ancestors,
|
||||
"settingValidateCertificates",
|
||||
model.settingValidateCertificates,
|
||||
)}
|
||||
onChange={(settingValidateCertificates) =>
|
||||
patchHttpSettings(model, {
|
||||
settingValidateCertificates,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<BooleanSettingRow
|
||||
title="Follow redirects"
|
||||
description="Follow HTTP redirects automatically."
|
||||
setting={model.settingFollowRedirects}
|
||||
inheritedValue={resolveInheritedValue(
|
||||
ancestors,
|
||||
"settingFollowRedirects",
|
||||
model.settingFollowRedirects,
|
||||
)}
|
||||
onChange={(settingFollowRedirects) =>
|
||||
patchHttpSettings(model, {
|
||||
settingFollowRedirects,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SettingsSection>
|
||||
)}
|
||||
<SettingsSection title={supportsHttpSettings || showSectionTitles ? "Cookies" : null}>
|
||||
<BooleanSettingRow
|
||||
title="Automatically send cookies"
|
||||
description="Attach matching cookies from the active cookie jar to outgoing requests."
|
||||
setting={model.settingSendCookies}
|
||||
inheritedValue={resolveInheritedValue(
|
||||
ancestors,
|
||||
"settingSendCookies",
|
||||
model.settingSendCookies,
|
||||
)}
|
||||
onChange={(settingSendCookies) =>
|
||||
patchCookieSettings(model, {
|
||||
settingSendCookies,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<BooleanSettingRow
|
||||
title="Automatically store cookies"
|
||||
description="Save cookies from Set-Cookie response headers to the active cookie jar."
|
||||
setting={model.settingStoreCookies}
|
||||
inheritedValue={resolveInheritedValue(
|
||||
ancestors,
|
||||
"settingStoreCookies",
|
||||
model.settingStoreCookies,
|
||||
)}
|
||||
onChange={(settingStoreCookies) =>
|
||||
patchCookieSettings(model, {
|
||||
settingStoreCookies,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SettingsSection>
|
||||
</SettingsList>
|
||||
);
|
||||
}
|
||||
|
||||
export function countOverriddenSettings(model: ModelWithSettings) {
|
||||
const settings: (BooleanSetting | IntegerSetting)[] = [
|
||||
model.settingSendCookies,
|
||||
model.settingStoreCookies,
|
||||
];
|
||||
|
||||
if (model.model === "workspace" || model.model === "folder" || model.model === "http_request") {
|
||||
settings.push(
|
||||
model.settingValidateCertificates,
|
||||
model.settingFollowRedirects,
|
||||
model.settingRequestTimeout,
|
||||
);
|
||||
}
|
||||
|
||||
return settings.filter((setting) => isInheritedSetting(setting) && setting.enabled === true)
|
||||
.length;
|
||||
}
|
||||
|
||||
function patchCookieSettings(model: ModelWithCookieSettings, patch: Partial<CookieSettingsPatch>) {
|
||||
if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>);
|
||||
if (model.model === "folder") return patchModel(model, patch as Partial<Folder>);
|
||||
if (model.model === "http_request") return patchModel(model, patch as Partial<HttpRequest>);
|
||||
if (model.model === "websocket_request")
|
||||
return patchModel(model, patch as Partial<WebsocketRequest>);
|
||||
return patchModel(model, patch as Partial<GrpcRequest>);
|
||||
}
|
||||
|
||||
function patchHttpSettings(model: ModelWithHttpSettings, patch: Partial<HttpSettingsPatch>) {
|
||||
if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>);
|
||||
if (model.model === "folder") return patchModel(model, patch as Partial<Folder>);
|
||||
return patchModel(model, patch as Partial<HttpRequest>);
|
||||
}
|
||||
|
||||
function BooleanSettingRow({
|
||||
description,
|
||||
inheritedValue,
|
||||
setting,
|
||||
title,
|
||||
onChange,
|
||||
}: {
|
||||
description: string;
|
||||
inheritedValue: boolean;
|
||||
setting: BooleanSetting;
|
||||
title: string;
|
||||
onChange: (setting: BooleanSetting) => void;
|
||||
}) {
|
||||
const inherited = isInheritedSetting(setting);
|
||||
const overridden = inherited ? setting.enabled === true : false;
|
||||
const value = inherited ? (overridden ? setting.value : inheritedValue) : setting;
|
||||
|
||||
if (!inherited) {
|
||||
return (
|
||||
<SettingRowBoolean
|
||||
checked={value}
|
||||
title={title}
|
||||
description={description}
|
||||
onChange={(value) => onChange(value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingOverrideRow
|
||||
title={title}
|
||||
description={description}
|
||||
overridden={overridden}
|
||||
onResetOverride={() => onChange({ ...setting, enabled: false })}
|
||||
>
|
||||
<Checkbox
|
||||
hideLabel
|
||||
size="md"
|
||||
title={title}
|
||||
checked={value}
|
||||
onChange={(value) => onChange({ ...setting, enabled: true, value })}
|
||||
/>
|
||||
</SettingOverrideRow>
|
||||
);
|
||||
}
|
||||
|
||||
function IntegerSettingRow({
|
||||
description,
|
||||
inheritedValue,
|
||||
name,
|
||||
setting,
|
||||
title,
|
||||
onChange,
|
||||
}: {
|
||||
description: string;
|
||||
inheritedValue: number;
|
||||
name: string;
|
||||
setting: IntegerSetting;
|
||||
title: string;
|
||||
onChange: (setting: IntegerSetting) => void;
|
||||
}) {
|
||||
const inherited = isInheritedSetting(setting);
|
||||
const overridden = inherited ? setting.enabled === true : false;
|
||||
const value = inherited ? (overridden ? setting.value : inheritedValue) : setting;
|
||||
const showReset = overridden && value !== inheritedValue;
|
||||
|
||||
if (!inherited) {
|
||||
return (
|
||||
<SettingRowNumber
|
||||
name={name}
|
||||
title={title}
|
||||
description={description}
|
||||
value={value}
|
||||
placeholder="0"
|
||||
validate={(value) => value === "" || Number.parseInt(value, 10) >= 0}
|
||||
onChange={(value) => onChange(value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingOverrideRow
|
||||
title={title}
|
||||
description={description}
|
||||
overridden={showReset}
|
||||
onResetOverride={() => onChange({ ...setting, enabled: false })}
|
||||
>
|
||||
<PlainInput
|
||||
hideLabel
|
||||
name={name}
|
||||
label={title}
|
||||
size="sm"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
defaultValue={`${value}`}
|
||||
containerClassName="!w-48"
|
||||
validate={(value) => value === "" || Number.parseInt(value, 10) >= 0}
|
||||
onChange={(value) =>
|
||||
onChange({
|
||||
...setting,
|
||||
enabled: true,
|
||||
value: Number.parseInt(value, 10) || 0,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SettingOverrideRow>
|
||||
);
|
||||
}
|
||||
|
||||
function isInheritedSetting<T>(
|
||||
setting: T | { enabled?: boolean; value: T },
|
||||
): setting is { enabled?: boolean; value: T } {
|
||||
return typeof setting === "object" && setting != null && "value" in setting;
|
||||
}
|
||||
|
||||
function resolveInheritedValue(
|
||||
ancestors: (Folder | Workspace)[],
|
||||
key: "settingRequestTimeout",
|
||||
fallback: IntegerSetting,
|
||||
): number;
|
||||
function resolveInheritedValue(
|
||||
ancestors: (Folder | Workspace)[],
|
||||
key: BooleanWorkspaceSettingKey,
|
||||
fallback: BooleanSetting,
|
||||
): boolean;
|
||||
function resolveInheritedValue(
|
||||
ancestors: (Folder | Workspace)[],
|
||||
key: keyof WorkspaceSettings,
|
||||
fallback: BooleanSetting | IntegerSetting,
|
||||
) {
|
||||
for (const ancestor of ancestors) {
|
||||
const setting = ancestor[key] as BooleanSetting | IntegerSetting;
|
||||
if (isInheritedSetting(setting)) {
|
||||
if (setting.enabled === true) {
|
||||
return setting.value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return setting;
|
||||
}
|
||||
|
||||
return isInheritedSetting(fallback) ? fallback.value : fallback;
|
||||
}
|
||||
|
||||
type WorkspaceSettings = Pick<
|
||||
Workspace,
|
||||
| "settingFollowRedirects"
|
||||
| "settingRequestTimeout"
|
||||
| "settingSendCookies"
|
||||
| "settingStoreCookies"
|
||||
| "settingValidateCertificates"
|
||||
>;
|
||||
|
||||
type BooleanWorkspaceSettingKey = Exclude<keyof WorkspaceSettings, "settingRequestTimeout">;
|
||||
@@ -19,6 +19,7 @@ type Props = Omit<ButtonProps, "type"> & {
|
||||
inline?: boolean;
|
||||
noun?: string;
|
||||
help?: ReactNode;
|
||||
hideLabel?: boolean;
|
||||
label?: ReactNode;
|
||||
};
|
||||
|
||||
@@ -36,6 +37,7 @@ export function SelectFile({
|
||||
size = "sm",
|
||||
label,
|
||||
help,
|
||||
hideLabel,
|
||||
...props
|
||||
}: Props) {
|
||||
const handleClick = async () => {
|
||||
@@ -95,7 +97,7 @@ export function SelectFile({
|
||||
return (
|
||||
<div ref={ref} className="w-full">
|
||||
{label && (
|
||||
<Label htmlFor={null} help={help}>
|
||||
<Label htmlFor={null} help={help} visuallyHidden={hideLabel}>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
|
||||
@@ -7,12 +7,18 @@ import { useCheckForUpdates } from "../../hooks/useCheckForUpdates";
|
||||
import { appInfo } from "../../lib/appInfo";
|
||||
import { revealInFinderText } from "../../lib/reveal";
|
||||
import { CargoFeature } from "../CargoFeature";
|
||||
import { Checkbox } from "../core/Checkbox";
|
||||
import { IconButton } from "../core/IconButton";
|
||||
import { KeyValueRow, KeyValueRows } from "../core/KeyValueRow";
|
||||
import { PlainInput } from "../core/PlainInput";
|
||||
import { Select } from "../core/Select";
|
||||
import { Separator } from "../core/Separator";
|
||||
import {
|
||||
ModelSettingRowBoolean,
|
||||
ModelSettingRowNumber,
|
||||
ModelSettingSelectControl,
|
||||
SettingValue,
|
||||
SettingRow,
|
||||
SettingRowBoolean,
|
||||
SettingRowSelect,
|
||||
SettingsList,
|
||||
SettingsSection,
|
||||
} from "../core/SettingRow";
|
||||
|
||||
export function SettingsGeneral() {
|
||||
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||
@@ -29,147 +35,159 @@ export function SettingsGeneral() {
|
||||
<Heading>General</Heading>
|
||||
<p className="text-text-subtle">Configure general settings for update behavior and more.</p>
|
||||
</div>
|
||||
<CargoFeature feature="updater">
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-1">
|
||||
<Select
|
||||
name="updateChannel"
|
||||
label="Update Channel"
|
||||
labelPosition="left"
|
||||
labelClassName="w-[14rem]"
|
||||
size="sm"
|
||||
value={settings.updateChannel}
|
||||
onChange={(updateChannel) => patchModel(settings, { updateChannel })}
|
||||
options={[
|
||||
{ label: "Stable", value: "stable" },
|
||||
{ label: "Beta (more frequent)", value: "beta" },
|
||||
]}
|
||||
/>
|
||||
<IconButton
|
||||
variant="border"
|
||||
size="sm"
|
||||
title="Check for updates"
|
||||
icon="refresh"
|
||||
spin={checkForUpdates.isPending}
|
||||
onClick={() => checkForUpdates.mutateAsync()}
|
||||
/>
|
||||
</div>
|
||||
<SettingsList className="space-y-8">
|
||||
<CargoFeature feature="updater">
|
||||
<SettingsSection title="Updates">
|
||||
<SettingRow
|
||||
title="Update Channel"
|
||||
description="Choose whether Yaak should use stable releases or beta releases."
|
||||
>
|
||||
<div className="grid grid-cols-[12rem_auto] gap-1">
|
||||
<ModelSettingSelectControl
|
||||
model={settings}
|
||||
modelKey="updateChannel"
|
||||
label="Update Channel"
|
||||
selectClassName="!w-full"
|
||||
options={[
|
||||
{ label: "Stable", value: "stable" },
|
||||
{ label: "Beta", value: "beta" },
|
||||
]}
|
||||
/>
|
||||
<IconButton
|
||||
variant="border"
|
||||
size="sm"
|
||||
title="Check for updates"
|
||||
icon="refresh"
|
||||
spin={checkForUpdates.isPending}
|
||||
onClick={() => checkForUpdates.mutateAsync()}
|
||||
/>
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<Select
|
||||
name="autoupdate"
|
||||
value={settings.autoupdate ? "auto" : "manual"}
|
||||
label="Update Behavior"
|
||||
labelPosition="left"
|
||||
size="sm"
|
||||
labelClassName="w-[14rem]"
|
||||
onChange={(v) => patchModel(settings, { autoupdate: v === "auto" })}
|
||||
options={[
|
||||
{ label: "Automatic", value: "auto" },
|
||||
{ label: "Manual", value: "manual" },
|
||||
]}
|
||||
/>
|
||||
<Checkbox
|
||||
className="pl-2 mt-1 ml-[14rem]"
|
||||
checked={settings.autoDownloadUpdates}
|
||||
disabled={!settings.autoupdate}
|
||||
help="Automatically download Yaak updates (!50MB) in the background, so they will be immediately ready to install."
|
||||
title="Automatically download updates"
|
||||
onChange={(autoDownloadUpdates) => patchModel(settings, { autoDownloadUpdates })}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
className="pl-2 mt-1 ml-[14rem]"
|
||||
checked={settings.checkNotifications}
|
||||
title="Check for notifications"
|
||||
help="Periodically ping Yaak servers to check for relevant notifications."
|
||||
onChange={(checkNotifications) => patchModel(settings, { checkNotifications })}
|
||||
/>
|
||||
<Checkbox
|
||||
disabled
|
||||
className="pl-2 mt-1 ml-[14rem]"
|
||||
checked={false}
|
||||
title="Send anonymous usage statistics"
|
||||
help="Yaak is local-first and does not collect analytics or usage data 🔐"
|
||||
onChange={(checkNotifications) => patchModel(settings, { checkNotifications })}
|
||||
/>
|
||||
</CargoFeature>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<Heading level={2}>
|
||||
Workspace{" "}
|
||||
<div className="inline-block ml-1 bg-surface-highlight px-2 py-0.5 rounded text text-shrink">
|
||||
{workspace.name}
|
||||
</div>
|
||||
</Heading>
|
||||
<VStack className="mt-1 w-full" space={3}>
|
||||
<PlainInput
|
||||
required
|
||||
size="sm"
|
||||
name="requestTimeout"
|
||||
label="Request Timeout (ms)"
|
||||
labelClassName="w-[14rem]"
|
||||
placeholder="0"
|
||||
labelPosition="left"
|
||||
defaultValue={`${workspace.settingRequestTimeout}`}
|
||||
validate={(value) => Number.parseInt(value, 10) >= 0}
|
||||
onChange={(v) =>
|
||||
patchModel(workspace, { settingRequestTimeout: Number.parseInt(v, 10) || 0 })
|
||||
}
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={workspace.settingValidateCertificates}
|
||||
help="When disabled, skip validation of server certificates, useful when interacting with self-signed certs."
|
||||
title="Validate TLS certificates"
|
||||
onChange={(settingValidateCertificates) =>
|
||||
patchModel(workspace, { settingValidateCertificates })
|
||||
}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={workspace.settingFollowRedirects}
|
||||
title="Follow redirects"
|
||||
onChange={(settingFollowRedirects) =>
|
||||
patchModel(workspace, {
|
||||
settingFollowRedirects,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</VStack>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<Heading level={2}>App Info</Heading>
|
||||
<KeyValueRows>
|
||||
<KeyValueRow label="Version">{appInfo.version}</KeyValueRow>
|
||||
<KeyValueRow
|
||||
label="Data Directory"
|
||||
rightSlot={
|
||||
<IconButton
|
||||
title={revealInFinderText}
|
||||
icon="folder_open"
|
||||
size="2xs"
|
||||
onClick={() => revealItemInDir(appInfo.appDataDir)}
|
||||
<SettingRowSelect
|
||||
title="Update Behavior"
|
||||
description="Choose whether updates are installed automatically or manually."
|
||||
name="autoupdate"
|
||||
value={settings.autoupdate ? "auto" : "manual"}
|
||||
onChange={(v) => patchModel(settings, { autoupdate: v === "auto" })}
|
||||
options={[
|
||||
{ label: "Automatic", value: "auto" },
|
||||
{ label: "Manual", value: "manual" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<ModelSettingRowBoolean
|
||||
model={settings}
|
||||
modelKey="autoDownloadUpdates"
|
||||
title="Automatically download updates"
|
||||
description="Download Yaak updates in the background so they are ready to install."
|
||||
disabled={!settings.autoupdate}
|
||||
/>
|
||||
|
||||
<ModelSettingRowBoolean
|
||||
model={settings}
|
||||
modelKey="checkNotifications"
|
||||
title="Check for notifications"
|
||||
description="Periodically ping Yaak servers to check for relevant notifications."
|
||||
/>
|
||||
|
||||
<SettingRowBoolean
|
||||
title="Send anonymous usage statistics"
|
||||
description="Yaak is local-first and does not collect analytics or usage data."
|
||||
disabled
|
||||
checked={false}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</SettingsSection>
|
||||
</CargoFeature>
|
||||
|
||||
<SettingsSection
|
||||
title={
|
||||
<>
|
||||
Workspace{" "}
|
||||
<span className="inline-block bg-surface-highlight px-2 py-0.5 rounded text">
|
||||
{workspace.name}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{appInfo.appDataDir}
|
||||
</KeyValueRow>
|
||||
<KeyValueRow
|
||||
label="Logs Directory"
|
||||
rightSlot={
|
||||
<IconButton
|
||||
title={revealInFinderText}
|
||||
icon="folder_open"
|
||||
size="2xs"
|
||||
onClick={() => revealItemInDir(appInfo.appLogDir)}
|
||||
<ModelSettingRowNumber
|
||||
model={workspace}
|
||||
modelKey="settingRequestTimeout"
|
||||
title="Request Timeout"
|
||||
description="Maximum request duration in milliseconds. Set to 0 to disable the timeout."
|
||||
placeholder="0"
|
||||
required
|
||||
validate={(value) => Number.parseInt(value, 10) >= 0}
|
||||
/>
|
||||
|
||||
<ModelSettingRowBoolean
|
||||
model={workspace}
|
||||
modelKey="settingValidateCertificates"
|
||||
title="Validate TLS certificates"
|
||||
description="When disabled, skip validation of server certificates."
|
||||
/>
|
||||
|
||||
<ModelSettingRowBoolean
|
||||
model={workspace}
|
||||
modelKey="settingFollowRedirects"
|
||||
title="Follow redirects"
|
||||
description="Follow HTTP redirects automatically."
|
||||
/>
|
||||
|
||||
<ModelSettingRowBoolean
|
||||
model={workspace}
|
||||
modelKey="settingSendCookies"
|
||||
title="Automatically send cookies"
|
||||
description="Attach matching cookies from the active cookie jar to outgoing requests."
|
||||
/>
|
||||
|
||||
<ModelSettingRowBoolean
|
||||
model={workspace}
|
||||
modelKey="settingStoreCookies"
|
||||
title="Automatically store cookies"
|
||||
description="Save cookies from Set-Cookie response headers to the active cookie jar."
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="App Info">
|
||||
<SettingRow title="Version" description="Current Yaak version.">
|
||||
<SettingValue value={appInfo.version} />
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
title="Data Directory"
|
||||
description="Where Yaak stores application data."
|
||||
controlClassName="min-w-0 max-w-[min(42rem,55vw)] gap-2"
|
||||
>
|
||||
<SettingValue
|
||||
value={appInfo.appDataDir}
|
||||
actions={[
|
||||
{
|
||||
title: revealInFinderText,
|
||||
icon: "folder_open",
|
||||
onClick: () => revealItemInDir(appInfo.appDataDir),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{appInfo.appLogDir}
|
||||
</KeyValueRow>
|
||||
</KeyValueRows>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
title="Logs Directory"
|
||||
description="Where Yaak writes application logs."
|
||||
controlClassName="min-w-0 max-w-[min(42rem,55vw)] gap-2"
|
||||
>
|
||||
<SettingValue
|
||||
value={appInfo.appLogDir}
|
||||
actions={[
|
||||
{
|
||||
title: revealInFinderText,
|
||||
icon: "folder_open",
|
||||
onClick: () => revealItemInDir(appInfo.appLogDir),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingsSection>
|
||||
</SettingsList>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useFonts } from "@yaakapp-internal/fonts";
|
||||
import { useLicense } from "@yaakapp-internal/license";
|
||||
import type { EditorKeymap, Settings } from "@yaakapp-internal/models";
|
||||
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
||||
import { clamp, Heading, HStack, Icon, VStack } from "@yaakapp-internal/ui";
|
||||
import { clamp, Heading, VStack } from "@yaakapp-internal/ui";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useState } from "react";
|
||||
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
|
||||
@@ -13,7 +13,16 @@ import { CargoFeature } from "../CargoFeature";
|
||||
import { Button } from "../core/Button";
|
||||
import { Checkbox } from "../core/Checkbox";
|
||||
import { Link } from "../core/Link";
|
||||
import { Select } from "../core/Select";
|
||||
import {
|
||||
ModelSettingRowBoolean,
|
||||
ModelSettingRowSelect,
|
||||
SettingRow,
|
||||
SettingRowBoolean,
|
||||
SettingRowSelect,
|
||||
SettingSelectControl,
|
||||
SettingsList,
|
||||
SettingsSection,
|
||||
} from "../core/SettingRow";
|
||||
|
||||
const NULL_FONT_VALUE = "__NULL_FONT__";
|
||||
|
||||
@@ -38,154 +47,172 @@ export function SettingsInterface() {
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack space={3} className="mb-4">
|
||||
<VStack space={1.5} className="mb-4">
|
||||
<div className="mb-3">
|
||||
<Heading>Interface</Heading>
|
||||
<p className="text-text-subtle">Tweak settings related to the user interface.</p>
|
||||
</div>
|
||||
<Select
|
||||
name="switchWorkspaceBehavior"
|
||||
label="Open workspace behavior"
|
||||
size="sm"
|
||||
help="When opening a workspace, should it open in the current window or a new window?"
|
||||
value={
|
||||
settings.openWorkspaceNewWindow === true
|
||||
? "new"
|
||||
: settings.openWorkspaceNewWindow === false
|
||||
? "current"
|
||||
: "ask"
|
||||
}
|
||||
onChange={async (v) => {
|
||||
if (v === "current") await patchModel(settings, { openWorkspaceNewWindow: false });
|
||||
else if (v === "new") await patchModel(settings, { openWorkspaceNewWindow: true });
|
||||
else await patchModel(settings, { openWorkspaceNewWindow: null });
|
||||
}}
|
||||
options={[
|
||||
{ label: "Always ask", value: "ask" },
|
||||
{ label: "Open in current window", value: "current" },
|
||||
{ label: "Open in new window", value: "new" },
|
||||
]}
|
||||
/>
|
||||
<HStack space={2} alignItems="end">
|
||||
{fonts.data && (
|
||||
<Select
|
||||
size="sm"
|
||||
name="uiFont"
|
||||
label="Interface font"
|
||||
value={settings.interfaceFont ?? NULL_FONT_VALUE}
|
||||
options={[
|
||||
{ label: "System default", value: NULL_FONT_VALUE },
|
||||
...(fonts.data.uiFonts.map((f) => ({
|
||||
label: f,
|
||||
value: f,
|
||||
})) ?? []),
|
||||
// Some people like monospace fonts for the UI
|
||||
...(fonts.data.editorFonts.map((f) => ({
|
||||
label: f,
|
||||
value: f,
|
||||
})) ?? []),
|
||||
]}
|
||||
<SettingsList className="space-y-8">
|
||||
<SettingsSection title="Workspaces">
|
||||
<SettingRowSelect
|
||||
title="Open workspace behavior"
|
||||
description="Choose what happens when opening another workspace."
|
||||
name="switchWorkspaceBehavior"
|
||||
value={
|
||||
settings.openWorkspaceNewWindow === true
|
||||
? "new"
|
||||
: settings.openWorkspaceNewWindow === false
|
||||
? "current"
|
||||
: "ask"
|
||||
}
|
||||
onChange={async (v) => {
|
||||
const interfaceFont = v === NULL_FONT_VALUE ? null : v;
|
||||
await patchModel(settings, { interfaceFont });
|
||||
if (v === "current") await patchModel(settings, { openWorkspaceNewWindow: false });
|
||||
else if (v === "new") await patchModel(settings, { openWorkspaceNewWindow: true });
|
||||
else await patchModel(settings, { openWorkspaceNewWindow: null });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Select
|
||||
hideLabel
|
||||
size="sm"
|
||||
name="interfaceFontSize"
|
||||
label="Interface Font Size"
|
||||
defaultValue="14"
|
||||
value={`${settings.interfaceFontSize}`}
|
||||
options={fontSizeOptions}
|
||||
onChange={(v) => patchModel(settings, { interfaceFontSize: Number.parseInt(v, 10) })}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack space={2} alignItems="end">
|
||||
{fonts.data && (
|
||||
<Select
|
||||
size="sm"
|
||||
name="editorFont"
|
||||
label="Editor font"
|
||||
value={settings.editorFont ?? NULL_FONT_VALUE}
|
||||
options={[
|
||||
{ label: "System default", value: NULL_FONT_VALUE },
|
||||
...(fonts.data.editorFonts.map((f) => ({
|
||||
label: f,
|
||||
value: f,
|
||||
})) ?? []),
|
||||
{ label: "Always ask", value: "ask" },
|
||||
{ label: "Open in current window", value: "current" },
|
||||
{ label: "Open in new window", value: "new" },
|
||||
]}
|
||||
onChange={async (v) => {
|
||||
const editorFont = v === NULL_FONT_VALUE ? null : v;
|
||||
await patchModel(settings, { editorFont });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Select
|
||||
hideLabel
|
||||
size="sm"
|
||||
name="editorFontSize"
|
||||
label="Editor Font Size"
|
||||
defaultValue="12"
|
||||
value={`${settings.editorFontSize}`}
|
||||
options={fontSizeOptions}
|
||||
onChange={(v) =>
|
||||
patchModel(settings, { editorFontSize: clamp(Number.parseInt(v, 10) || 14, 8, 30) })
|
||||
}
|
||||
/>
|
||||
</HStack>
|
||||
<Select
|
||||
leftSlot={<Icon icon="keyboard" color="secondary" />}
|
||||
size="sm"
|
||||
name="editorKeymap"
|
||||
label="Editor keymap"
|
||||
value={`${settings.editorKeymap}`}
|
||||
options={keymaps}
|
||||
onChange={(v) => patchModel(settings, { editorKeymap: v })}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={settings.editorSoftWrap}
|
||||
title="Wrap editor lines"
|
||||
onChange={(editorSoftWrap) => patchModel(settings, { editorSoftWrap })}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={settings.coloredMethods}
|
||||
title="Colorize request methods"
|
||||
onChange={(coloredMethods) => patchModel(settings, { coloredMethods })}
|
||||
/>
|
||||
<CargoFeature feature="license">
|
||||
<LicenseSettings settings={settings} />
|
||||
</CargoFeature>
|
||||
</SettingsSection>
|
||||
|
||||
<NativeTitlebarSetting settings={settings} />
|
||||
<SettingsSection title="Fonts">
|
||||
<SettingRow
|
||||
title="Interface font"
|
||||
description="Font used for Yaak interface controls."
|
||||
controlClassName="gap-1"
|
||||
>
|
||||
{fonts.data && (
|
||||
<SettingSelectControl
|
||||
name="uiFont"
|
||||
label="Interface font"
|
||||
selectClassName="!w-72"
|
||||
value={settings.interfaceFont ?? NULL_FONT_VALUE}
|
||||
defaultValue={NULL_FONT_VALUE}
|
||||
options={[
|
||||
{ label: "System default", value: NULL_FONT_VALUE },
|
||||
...fonts.data.uiFonts.map((f) => ({ label: f, value: f })),
|
||||
...fonts.data.editorFonts.map((f) => ({ label: f, value: f })),
|
||||
]}
|
||||
onChange={async (v) => {
|
||||
const interfaceFont = v === NULL_FONT_VALUE ? null : v;
|
||||
await patchModel(settings, { interfaceFont });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SettingSelectControl
|
||||
name="interfaceFontSize"
|
||||
label="Interface Font Size"
|
||||
selectClassName="!w-20"
|
||||
value={`${settings.interfaceFontSize}`}
|
||||
defaultValue="14"
|
||||
options={fontSizeOptions}
|
||||
onChange={(v) => patchModel(settings, { interfaceFontSize: Number.parseInt(v, 10) })}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
{type() !== "macos" && (
|
||||
<Checkbox
|
||||
checked={settings.hideWindowControls}
|
||||
title="Hide window controls"
|
||||
help="Hide the close/maximize/minimize controls on Windows or Linux"
|
||||
onChange={(hideWindowControls) => patchModel(settings, { hideWindowControls })}
|
||||
/>
|
||||
)}
|
||||
<SettingRow
|
||||
title="Editor font"
|
||||
description="Font used in request and response editors."
|
||||
controlClassName="gap-1"
|
||||
>
|
||||
{fonts.data && (
|
||||
<SettingSelectControl
|
||||
name="editorFont"
|
||||
label="Editor font"
|
||||
selectClassName="!w-72"
|
||||
value={settings.editorFont ?? NULL_FONT_VALUE}
|
||||
defaultValue={NULL_FONT_VALUE}
|
||||
options={[
|
||||
{ label: "System default", value: NULL_FONT_VALUE },
|
||||
...fonts.data.editorFonts.map((f) => ({ label: f, value: f })),
|
||||
]}
|
||||
onChange={async (v) => {
|
||||
const editorFont = v === NULL_FONT_VALUE ? null : v;
|
||||
await patchModel(settings, { editorFont });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SettingSelectControl
|
||||
name="editorFontSize"
|
||||
label="Editor Font Size"
|
||||
selectClassName="!w-20"
|
||||
value={`${settings.editorFontSize}`}
|
||||
defaultValue="12"
|
||||
options={fontSizeOptions}
|
||||
onChange={(v) =>
|
||||
patchModel(settings, {
|
||||
editorFontSize: clamp(Number.parseInt(v, 10) || 14, 8, 30),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Editor">
|
||||
<ModelSettingRowSelect
|
||||
model={settings}
|
||||
modelKey="editorKeymap"
|
||||
title="Editor keymap"
|
||||
description="Keyboard shortcut preset used by text editors."
|
||||
options={keymaps}
|
||||
/>
|
||||
<ModelSettingRowBoolean
|
||||
model={settings}
|
||||
modelKey="editorSoftWrap"
|
||||
title="Wrap editor lines"
|
||||
description="Wrap long lines in request and response editors."
|
||||
/>
|
||||
<ModelSettingRowBoolean
|
||||
model={settings}
|
||||
modelKey="coloredMethods"
|
||||
title="Colorize request methods"
|
||||
description="Use method-specific colors for HTTP request methods."
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Window">
|
||||
<NativeTitlebarSetting settings={settings} />
|
||||
{type() !== "macos" && (
|
||||
<ModelSettingRowBoolean
|
||||
model={settings}
|
||||
modelKey="hideWindowControls"
|
||||
title="Hide window controls"
|
||||
description="Hide the close, maximize, and minimize controls on Windows or Linux."
|
||||
/>
|
||||
)}
|
||||
</SettingsSection>
|
||||
|
||||
<CargoFeature feature="license">
|
||||
<LicenseSettings settings={settings} />
|
||||
</CargoFeature>
|
||||
</SettingsList>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
function NativeTitlebarSetting({ settings }: { settings: Settings }) {
|
||||
const [nativeTitlebar, setNativeTitlebar] = useState(settings.useNativeTitlebar);
|
||||
|
||||
return (
|
||||
<div className="flex gap-1 overflow-hidden h-2xs">
|
||||
<SettingRow
|
||||
title="Native title bar"
|
||||
description="Use the operating system's standard title bar and window controls."
|
||||
controlClassName="gap-2"
|
||||
>
|
||||
<Checkbox
|
||||
hideLabel
|
||||
size="md"
|
||||
checked={nativeTitlebar}
|
||||
title="Native title bar"
|
||||
help="Use the operating system's standard title bar and window controls"
|
||||
onChange={setNativeTitlebar}
|
||||
/>
|
||||
{settings.useNativeTitlebar !== nativeTitlebar && (
|
||||
<Button
|
||||
color="primary"
|
||||
size="2xs"
|
||||
size="xs"
|
||||
onClick={async () => {
|
||||
await patchModel(settings, { useNativeTitlebar: nativeTitlebar });
|
||||
await invokeCmd("cmd_restart");
|
||||
@@ -194,7 +221,7 @@ function NativeTitlebarSetting({ settings }: { settings: Settings }) {
|
||||
Apply and Restart
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -205,37 +232,40 @@ function LicenseSettings({ settings }: { settings: Settings }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
checked={settings.hideLicenseBadge}
|
||||
title="Hide personal use badge"
|
||||
onChange={async (hideLicenseBadge) => {
|
||||
if (hideLicenseBadge) {
|
||||
const confirmed = await showConfirm({
|
||||
id: "hide-license-badge",
|
||||
title: "Confirm Personal Use",
|
||||
confirmText: "Confirm",
|
||||
description: (
|
||||
<VStack space={3}>
|
||||
<p>Hey there 👋🏼</p>
|
||||
<p>
|
||||
Yaak is free for personal projects and learning.{" "}
|
||||
<strong>If you’re using Yaak at work, a license is required.</strong>
|
||||
</p>
|
||||
<p>
|
||||
Licenses help keep Yaak independent and sustainable.{" "}
|
||||
<Link href="https://yaak.app/pricing?s=badge">Purchase a License →</Link>
|
||||
</p>
|
||||
</VStack>
|
||||
),
|
||||
requireTyping: "Personal Use",
|
||||
color: "info",
|
||||
});
|
||||
if (!confirmed) {
|
||||
return; // Cancel
|
||||
<SettingsSection title="License">
|
||||
<SettingRowBoolean
|
||||
checked={settings.hideLicenseBadge}
|
||||
title="Hide personal use badge"
|
||||
description="Hide the personal-use badge from the interface."
|
||||
onChange={async (hideLicenseBadge) => {
|
||||
if (hideLicenseBadge) {
|
||||
const confirmed = await showConfirm({
|
||||
id: "hide-license-badge",
|
||||
title: "Confirm Personal Use",
|
||||
confirmText: "Confirm",
|
||||
description: (
|
||||
<VStack space={3}>
|
||||
<p>Hey there 👋🏼</p>
|
||||
<p>
|
||||
Yaak is free for personal projects and learning.{" "}
|
||||
<strong>If you’re using Yaak at work, a license is required.</strong>
|
||||
</p>
|
||||
<p>
|
||||
Licenses help keep Yaak independent and sustainable.{" "}
|
||||
<Link href="https://yaak.app/pricing?s=badge">Purchase a License →</Link>
|
||||
</p>
|
||||
</VStack>
|
||||
),
|
||||
requireTyping: "Personal Use",
|
||||
color: "info",
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
await patchModel(settings, { hideLicenseBadge });
|
||||
}}
|
||||
/>
|
||||
await patchModel(settings, { hideLicenseBadge });
|
||||
}}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
||||
import { Heading, HStack, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||
import type { ProxySetting } from "@yaakapp-internal/models";
|
||||
import { Heading, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { Checkbox } from "../core/Checkbox";
|
||||
import { PlainInput } from "../core/PlainInput";
|
||||
import { Select } from "../core/Select";
|
||||
import { Separator } from "../core/Separator";
|
||||
import {
|
||||
SettingRowBoolean,
|
||||
SettingRowSelect,
|
||||
SettingRowText,
|
||||
SettingsList,
|
||||
SettingsSection,
|
||||
} from "../core/SettingRow";
|
||||
|
||||
export function SettingsProxy() {
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const proxy = enabledProxyOrDefault(settings.proxy);
|
||||
|
||||
const patchProxy = async (patch: Partial<EnabledProxySetting>) => {
|
||||
await patchModel(settings, {
|
||||
proxy: {
|
||||
...proxy,
|
||||
...patch,
|
||||
auth: Object.hasOwn(patch, "auth") ? (patch.auth ?? null) : proxy.auth,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack space={1.5} className="mb-4">
|
||||
@@ -18,188 +33,146 @@ export function SettingsProxy() {
|
||||
traffic, or routing through specific infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
name="proxy"
|
||||
label="Proxy"
|
||||
hideLabel
|
||||
size="sm"
|
||||
value={settings.proxy?.type ?? "automatic"}
|
||||
onChange={async (v) => {
|
||||
if (v === "automatic") {
|
||||
await patchModel(settings, { proxy: undefined });
|
||||
} else if (v === "enabled") {
|
||||
await patchModel(settings, {
|
||||
proxy: {
|
||||
disabled: false,
|
||||
type: "enabled",
|
||||
http: "",
|
||||
https: "",
|
||||
auth: { user: "", password: "" },
|
||||
bypass: "",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await patchModel(settings, { proxy: { type: "disabled" } });
|
||||
}
|
||||
}}
|
||||
options={[
|
||||
{ label: "Automatic proxy detection", value: "automatic" },
|
||||
{ label: "Custom proxy configuration", value: "enabled" },
|
||||
{ label: "No proxy", value: "disabled" },
|
||||
]}
|
||||
/>
|
||||
{settings.proxy?.type === "enabled" && (
|
||||
<VStack space={1.5}>
|
||||
<Checkbox
|
||||
className="my-3"
|
||||
checked={!settings.proxy.disabled}
|
||||
title="Enable proxy"
|
||||
help="Use this to temporarily disable the proxy without losing the configuration"
|
||||
onChange={async (enabled) => {
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === "enabled" ? proxy.http : "";
|
||||
const https = proxy?.type === "enabled" ? proxy.https : "";
|
||||
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
|
||||
const auth = proxy?.type === "enabled" ? proxy.auth : null;
|
||||
const disabled = !enabled;
|
||||
await patchModel(settings, {
|
||||
proxy: { type: "enabled", http, https, auth, disabled, bypass },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<HStack space={1.5}>
|
||||
<PlainInput
|
||||
size="sm"
|
||||
label={
|
||||
<>
|
||||
Proxy for <InlineCode>http://</InlineCode> traffic
|
||||
</>
|
||||
<SettingsList className="space-y-8">
|
||||
<SettingsSection title="Proxy">
|
||||
<SettingRowSelect
|
||||
title="Proxy"
|
||||
description="Choose how Yaak should discover or use proxy settings."
|
||||
name="proxy"
|
||||
value={settings.proxy?.type ?? "automatic"}
|
||||
onChange={async (v) => {
|
||||
if (v === "automatic") {
|
||||
await patchModel(settings, { proxy: undefined });
|
||||
} else if (v === "enabled") {
|
||||
await patchModel(settings, { proxy });
|
||||
} else {
|
||||
await patchModel(settings, { proxy: { type: "disabled" } });
|
||||
}
|
||||
placeholder="localhost:9090"
|
||||
defaultValue={settings.proxy?.http}
|
||||
onChange={async (http) => {
|
||||
const { proxy } = settings;
|
||||
const https = proxy?.type === "enabled" ? proxy.https : "";
|
||||
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
|
||||
const auth = proxy?.type === "enabled" ? proxy.auth : null;
|
||||
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
|
||||
await patchModel(settings, {
|
||||
proxy: {
|
||||
type: "enabled",
|
||||
http,
|
||||
https,
|
||||
auth,
|
||||
disabled,
|
||||
bypass,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<PlainInput
|
||||
size="sm"
|
||||
label={
|
||||
<>
|
||||
Proxy for <InlineCode>https://</InlineCode> traffic
|
||||
</>
|
||||
}
|
||||
placeholder="localhost:9090"
|
||||
defaultValue={settings.proxy?.https}
|
||||
onChange={async (https) => {
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === "enabled" ? proxy.http : "";
|
||||
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
|
||||
const auth = proxy?.type === "enabled" ? proxy.auth : null;
|
||||
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
|
||||
await patchModel(settings, {
|
||||
proxy: { type: "enabled", http, https, auth, disabled, bypass },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
<Separator className="my-6" />
|
||||
<Checkbox
|
||||
checked={settings.proxy.auth != null}
|
||||
title="Enable authentication"
|
||||
onChange={async (enabled) => {
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === "enabled" ? proxy.http : "";
|
||||
const https = proxy?.type === "enabled" ? proxy.https : "";
|
||||
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
|
||||
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
|
||||
const auth = enabled ? { user: "", password: "" } : null;
|
||||
await patchModel(settings, {
|
||||
proxy: { type: "enabled", http, https, auth, disabled, bypass },
|
||||
});
|
||||
}}
|
||||
options={[
|
||||
{ label: "Automatic proxy detection", value: "automatic" },
|
||||
{ label: "Custom proxy configuration", value: "enabled" },
|
||||
{ label: "No proxy", value: "disabled" },
|
||||
]}
|
||||
selectClassName="!w-64"
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
{settings.proxy.auth != null && (
|
||||
<HStack space={1.5}>
|
||||
<PlainInput
|
||||
required
|
||||
size="sm"
|
||||
label="User"
|
||||
placeholder="myUser"
|
||||
defaultValue={settings.proxy.auth.user}
|
||||
onChange={async (user) => {
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === "enabled" ? proxy.http : "";
|
||||
const https = proxy?.type === "enabled" ? proxy.https : "";
|
||||
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
|
||||
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
|
||||
const password = proxy?.type === "enabled" ? (proxy.auth?.password ?? "") : "";
|
||||
const auth = { user, password };
|
||||
await patchModel(settings, {
|
||||
proxy: { type: "enabled", http, https, auth, disabled, bypass },
|
||||
});
|
||||
}}
|
||||
{settings.proxy?.type === "enabled" && (
|
||||
<>
|
||||
<SettingsSection title="Custom Proxy">
|
||||
<SettingRowBoolean
|
||||
checked={!settings.proxy.disabled}
|
||||
title="Enable proxy"
|
||||
description="Temporarily disable the proxy without losing the configuration."
|
||||
onChange={(enabled) => patchProxy({ disabled: !enabled })}
|
||||
/>
|
||||
<PlainInput
|
||||
size="sm"
|
||||
label="Password"
|
||||
type="password"
|
||||
placeholder="s3cretPassw0rd"
|
||||
defaultValue={settings.proxy.auth.password}
|
||||
onChange={async (password) => {
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === "enabled" ? proxy.http : "";
|
||||
const https = proxy?.type === "enabled" ? proxy.https : "";
|
||||
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
|
||||
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
|
||||
const user = proxy?.type === "enabled" ? (proxy.auth?.user ?? "") : "";
|
||||
const auth = { user, password };
|
||||
await patchModel(settings, {
|
||||
proxy: { type: "enabled", http, https, auth, disabled, bypass },
|
||||
});
|
||||
}}
|
||||
<SettingRowText
|
||||
name="proxyHttp"
|
||||
title={
|
||||
<>
|
||||
Proxy for <InlineCode>http://</InlineCode> traffic
|
||||
</>
|
||||
}
|
||||
description="Proxy host used for unencrypted HTTP traffic."
|
||||
value={settings.proxy.http}
|
||||
placeholder="localhost:9090"
|
||||
onChange={(http) => patchProxy({ http })}
|
||||
/>
|
||||
</HStack>
|
||||
)}
|
||||
{settings.proxy.type === "enabled" && (
|
||||
<>
|
||||
<Separator className="my-6" />
|
||||
<PlainInput
|
||||
label="Proxy Bypass"
|
||||
help="Comma-separated list to bypass the proxy."
|
||||
defaultValue={settings.proxy.bypass}
|
||||
<SettingRowText
|
||||
name="proxyHttps"
|
||||
title={
|
||||
<>
|
||||
Proxy for <InlineCode>https://</InlineCode> traffic
|
||||
</>
|
||||
}
|
||||
description="Proxy host used for HTTPS traffic."
|
||||
value={settings.proxy.https}
|
||||
placeholder="localhost:9090"
|
||||
onChange={(https) => patchProxy({ https })}
|
||||
/>
|
||||
<SettingRowText
|
||||
name="proxyBypass"
|
||||
title="Proxy Bypass"
|
||||
description="Comma-separated list of hosts that should bypass the proxy."
|
||||
value={settings.proxy.bypass}
|
||||
placeholder="127.0.0.1, *.example.com, localhost:3000"
|
||||
onChange={async (bypass) => {
|
||||
const { proxy } = settings;
|
||||
const http = proxy?.type === "enabled" ? proxy.http : "";
|
||||
const https = proxy?.type === "enabled" ? proxy.https : "";
|
||||
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
|
||||
const user = proxy?.type === "enabled" ? (proxy.auth?.user ?? "") : "";
|
||||
const password = proxy?.type === "enabled" ? (proxy.auth?.password ?? "") : "";
|
||||
const auth = { user, password };
|
||||
await patchModel(settings, {
|
||||
proxy: { type: "enabled", http, https, auth, disabled, bypass },
|
||||
});
|
||||
}}
|
||||
inputWidthClassName="!w-96"
|
||||
onChange={(bypass) => patchProxy({ bypass })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
)}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Authentication">
|
||||
<SettingRowBoolean
|
||||
checked={settings.proxy.auth != null}
|
||||
title="Enable authentication"
|
||||
description="Send proxy credentials with proxied requests."
|
||||
onChange={(enabled) =>
|
||||
patchProxy({ auth: enabled ? { user: "", password: "" } : null })
|
||||
}
|
||||
/>
|
||||
|
||||
{settings.proxy.auth != null && (
|
||||
<>
|
||||
<SettingRowText
|
||||
required
|
||||
name="proxyUser"
|
||||
title="User"
|
||||
description="Username for proxy authentication."
|
||||
value={settings.proxy.auth.user}
|
||||
placeholder="myUser"
|
||||
onChange={(user) =>
|
||||
patchProxy({
|
||||
auth: {
|
||||
user,
|
||||
password:
|
||||
settings.proxy?.type === "enabled"
|
||||
? (settings.proxy.auth?.password ?? "")
|
||||
: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SettingRowText
|
||||
name="proxyPassword"
|
||||
title="Password"
|
||||
description="Password for proxy authentication."
|
||||
value={settings.proxy.auth.password}
|
||||
placeholder="s3cretPassw0rd"
|
||||
type="password"
|
||||
onChange={(password) =>
|
||||
patchProxy({
|
||||
auth: {
|
||||
user:
|
||||
settings.proxy?.type === "enabled"
|
||||
? (settings.proxy.auth?.user ?? "")
|
||||
: "",
|
||||
password,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsSection>
|
||||
</>
|
||||
)}
|
||||
</SettingsList>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
type EnabledProxySetting = Extract<ProxySetting, { type: "enabled" }>;
|
||||
|
||||
function enabledProxyOrDefault(proxy: ProxySetting | null): EnabledProxySetting {
|
||||
if (proxy?.type === "enabled") return proxy;
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
type: "enabled",
|
||||
http: "",
|
||||
https: "",
|
||||
auth: { user: "", password: "" },
|
||||
bypass: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,12 @@ import type { ButtonProps } from "../core/Button";
|
||||
import { IconButton } from "../core/IconButton";
|
||||
import { Link } from "../core/Link";
|
||||
import type { SelectProps } from "../core/Select";
|
||||
import { Select } from "../core/Select";
|
||||
import {
|
||||
ModelSettingRowSelect,
|
||||
SettingRowSelect,
|
||||
SettingsList,
|
||||
SettingsSection,
|
||||
} from "../core/SettingRow";
|
||||
|
||||
const Editor = lazy(() => import("../core/Editor/Editor").then((m) => ({ default: m.Editor })));
|
||||
|
||||
@@ -67,7 +72,7 @@ export function SettingsTheme() {
|
||||
}));
|
||||
|
||||
return (
|
||||
<VStack space={3} className="mb-4">
|
||||
<VStack space={1.5} className="mb-4">
|
||||
<div className="mb-3">
|
||||
<Heading>Theme</Heading>
|
||||
<p className="text-text-subtle">
|
||||
@@ -77,96 +82,92 @@ export function SettingsTheme() {
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
name="appearance"
|
||||
label="Appearance"
|
||||
labelPosition="top"
|
||||
size="sm"
|
||||
value={settings.appearance}
|
||||
onChange={(appearance) => patchModel(settings, { appearance })}
|
||||
options={[
|
||||
{ label: "Automatic", value: "system" },
|
||||
{ label: "Light", value: "light" },
|
||||
{ label: "Dark", value: "dark" },
|
||||
]}
|
||||
/>
|
||||
<HStack space={2}>
|
||||
{(settings.appearance === "system" || settings.appearance === "light") && (
|
||||
<Select
|
||||
hideLabel
|
||||
leftSlot={<Icon icon="sun" color="secondary" />}
|
||||
name="lightTheme"
|
||||
label="Light Theme"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
value={activeTheme.data.light.id}
|
||||
options={lightThemes}
|
||||
onChange={(themeLight) => patchModel(settings, { themeLight })}
|
||||
<SettingsList className="space-y-8">
|
||||
<SettingsSection title="Theme">
|
||||
<ModelSettingRowSelect
|
||||
model={settings}
|
||||
modelKey="appearance"
|
||||
title="Appearance"
|
||||
description="Choose whether Yaak follows your system appearance or uses a fixed mode."
|
||||
options={[
|
||||
{ label: "Automatic", value: "system" },
|
||||
{ label: "Light", value: "light" },
|
||||
{ label: "Dark", value: "dark" },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{(settings.appearance === "system" || settings.appearance === "dark") && (
|
||||
<Select
|
||||
hideLabel
|
||||
name="darkTheme"
|
||||
className="flex-1"
|
||||
label="Dark Theme"
|
||||
leftSlot={<Icon icon="moon" color="secondary" />}
|
||||
size="sm"
|
||||
value={activeTheme.data.dark.id}
|
||||
options={darkThemes}
|
||||
onChange={(themeDark) => patchModel(settings, { themeDark })}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
{(settings.appearance === "system" || settings.appearance === "light") && (
|
||||
<SettingRowSelect
|
||||
name="lightTheme"
|
||||
title="Light theme"
|
||||
description="Theme used when Yaak is in light mode."
|
||||
value={activeTheme.data.light.id}
|
||||
options={lightThemes}
|
||||
onChange={(themeLight) => patchModel(settings, { themeLight })}
|
||||
/>
|
||||
)}
|
||||
{(settings.appearance === "system" || settings.appearance === "dark") && (
|
||||
<SettingRowSelect
|
||||
name="darkTheme"
|
||||
title="Dark theme"
|
||||
description="Theme used when Yaak is in dark mode."
|
||||
value={activeTheme.data.dark.id}
|
||||
options={darkThemes}
|
||||
onChange={(themeDark) => patchModel(settings, { themeDark })}
|
||||
/>
|
||||
)}
|
||||
</SettingsSection>
|
||||
|
||||
<VStack
|
||||
space={3}
|
||||
className="mt-3 w-full bg-surface p-3 border border-dashed border-border-subtle rounded overflow-x-auto"
|
||||
>
|
||||
<HStack className="text" space={1.5}>
|
||||
<Icon icon={appearance === "dark" ? "moon" : "sun"} />
|
||||
<strong>{activeTheme.data.active.label}</strong>
|
||||
<em>(preview)</em>
|
||||
</HStack>
|
||||
<HStack space={1.5} className="w-full">
|
||||
{buttonColors.map((c, i) => (
|
||||
<IconButton
|
||||
key={c}
|
||||
color={c}
|
||||
size="2xs"
|
||||
iconSize="xs"
|
||||
icon={icons[i % icons.length] ?? "info"}
|
||||
iconClassName="text"
|
||||
title={`${c}`}
|
||||
/>
|
||||
))}
|
||||
{buttonColors.map((c, i) => (
|
||||
<IconButton
|
||||
key={c}
|
||||
color={c}
|
||||
variant="border"
|
||||
size="2xs"
|
||||
iconSize="xs"
|
||||
icon={icons[i % icons.length] ?? "info"}
|
||||
iconClassName="text"
|
||||
title={`${c}`}
|
||||
/>
|
||||
))}
|
||||
</HStack>
|
||||
<Suspense>
|
||||
<Editor
|
||||
defaultValue={[
|
||||
"let foo = { // Demo code editor",
|
||||
' foo: ("bar" || "baz" ?? \'qux\'),',
|
||||
" baz: [1, 10.2, null, false, true],",
|
||||
"};",
|
||||
].join("\n")}
|
||||
heightMode="auto"
|
||||
language="javascript"
|
||||
stateKey={null}
|
||||
/>
|
||||
</Suspense>
|
||||
</VStack>
|
||||
<SettingsSection title="Preview">
|
||||
<VStack
|
||||
space={3}
|
||||
className="mt-4 w-full bg-surface p-3 border border-dashed border-border-subtle rounded overflow-x-auto"
|
||||
>
|
||||
<HStack className="text" space={1.5}>
|
||||
<Icon icon={appearance === "dark" ? "moon" : "sun"} />
|
||||
<strong>{activeTheme.data.active.label}</strong>
|
||||
<em>(preview)</em>
|
||||
</HStack>
|
||||
<HStack space={1.5} className="w-full">
|
||||
{buttonColors.map((c, i) => (
|
||||
<IconButton
|
||||
key={c}
|
||||
color={c}
|
||||
size="2xs"
|
||||
iconSize="xs"
|
||||
icon={icons[i % icons.length] ?? "info"}
|
||||
iconClassName="text"
|
||||
title={`${c}`}
|
||||
/>
|
||||
))}
|
||||
{buttonColors.map((c, i) => (
|
||||
<IconButton
|
||||
key={c}
|
||||
color={c}
|
||||
variant="border"
|
||||
size="2xs"
|
||||
iconSize="xs"
|
||||
icon={icons[i % icons.length] ?? "info"}
|
||||
iconClassName="text"
|
||||
title={`${c}`}
|
||||
/>
|
||||
))}
|
||||
</HStack>
|
||||
<Suspense>
|
||||
<Editor
|
||||
defaultValue={[
|
||||
"let foo = { // Demo code editor",
|
||||
' foo: ("bar" || "baz" ?? \'qux\'),',
|
||||
" baz: [1, 10.2, null, false, true],",
|
||||
"};",
|
||||
].join("\n")}
|
||||
heightMode="auto"
|
||||
language="javascript"
|
||||
stateKey={null}
|
||||
/>
|
||||
</Suspense>
|
||||
</VStack>
|
||||
</SettingsSection>
|
||||
</SettingsList>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,20 +4,79 @@ import { useState } from "react";
|
||||
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
|
||||
import { Button } from "./core/Button";
|
||||
import { Checkbox } from "./core/Checkbox";
|
||||
import { SettingRowBoolean, SettingRowDirectory } from "./core/SettingRow";
|
||||
import { SelectFile } from "./SelectFile";
|
||||
|
||||
export interface SyncToFilesystemSettingProps {
|
||||
layout?: "form" | "settings";
|
||||
onChange: (args: { filePath: string | null; initGit?: boolean }) => void;
|
||||
onCreateNewWorkspace: () => void;
|
||||
value: { filePath: string | null; initGit?: boolean };
|
||||
}
|
||||
|
||||
export function SyncToFilesystemSetting({
|
||||
layout = "form",
|
||||
onChange,
|
||||
onCreateNewWorkspace,
|
||||
value,
|
||||
}: SyncToFilesystemSettingProps) {
|
||||
const [syncDir, setSyncDir] = useState<string | null>(null);
|
||||
|
||||
const handleFilePathChange = async (filePath: string | null) => {
|
||||
if (filePath != null) {
|
||||
const files = await readDir(filePath);
|
||||
if (files.length > 0) {
|
||||
setSyncDir(filePath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setSyncDir(null);
|
||||
onChange({ ...value, filePath });
|
||||
};
|
||||
|
||||
if (layout === "settings") {
|
||||
return (
|
||||
<VStack className="w-full" space={0}>
|
||||
{syncDir && (
|
||||
<Banner color="notice" className="mb-3 flex flex-col gap-1.5">
|
||||
<p>Directory is not empty. Do you want to open it instead?</p>
|
||||
<div>
|
||||
<Button
|
||||
variant="border"
|
||||
color="notice"
|
||||
size="xs"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
openWorkspaceFromSyncDir.mutate(syncDir);
|
||||
onCreateNewWorkspace();
|
||||
}}
|
||||
>
|
||||
Open Workspace
|
||||
</Button>
|
||||
</div>
|
||||
</Banner>
|
||||
)}
|
||||
|
||||
<SettingRowDirectory
|
||||
title="Local directory sync"
|
||||
description="Sync data to a folder for backup and Git integration."
|
||||
filePath={value.filePath}
|
||||
onChange={handleFilePathChange}
|
||||
/>
|
||||
|
||||
{value.filePath && typeof value.initGit === "boolean" && (
|
||||
<SettingRowBoolean
|
||||
checked={value.initGit}
|
||||
title="Initialize Git Repo"
|
||||
description="Create a Git repository in the selected sync directory."
|
||||
onChange={(initGit) => onChange({ ...value, initGit })}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack className="w-full my-2" space={3}>
|
||||
{syncDir && (
|
||||
@@ -47,18 +106,7 @@ export function SyncToFilesystemSetting({
|
||||
noun="Directory"
|
||||
help="Sync data to a folder for backup and Git integration."
|
||||
filePath={value.filePath}
|
||||
onChange={async ({ filePath }) => {
|
||||
if (filePath != null) {
|
||||
const files = await readDir(filePath);
|
||||
if (files.length > 0) {
|
||||
setSyncDir(filePath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setSyncDir(null);
|
||||
onChange({ ...value, filePath });
|
||||
}}
|
||||
onChange={async ({ filePath }) => handleFilePathChange(filePath)}
|
||||
/>
|
||||
|
||||
{value.filePath && typeof value.initGit === "boolean" && (
|
||||
|
||||
@@ -34,6 +34,7 @@ import { setActiveTab, TabContent, Tabs } from "./core/Tabs/Tabs";
|
||||
import { HeadersEditor } from "./HeadersEditor";
|
||||
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
||||
import { MarkdownEditor } from "./MarkdownEditor";
|
||||
import { countOverriddenSettings, ModelSettingsEditor } from "./ModelSettingsEditor";
|
||||
import { UrlBar } from "./UrlBar";
|
||||
import { UrlParametersEditor } from "./UrlParameterEditor";
|
||||
|
||||
@@ -48,6 +49,7 @@ const TAB_MESSAGE = "message";
|
||||
const TAB_PARAMS = "params";
|
||||
const TAB_HEADERS = "headers";
|
||||
const TAB_AUTH = "auth";
|
||||
const TAB_SETTINGS = "settings";
|
||||
const TAB_DESCRIPTION = "description";
|
||||
const TABS_STORAGE_KEY = "websocket_request_tabs";
|
||||
|
||||
@@ -69,6 +71,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
||||
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
||||
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
||||
const numSettingsOverrides = countOverriddenSettings(activeRequest);
|
||||
|
||||
// Listen for event to focus the params tab (e.g., when clicking a :param in the URL)
|
||||
useRequestEditorEvent(
|
||||
@@ -109,12 +112,17 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
||||
},
|
||||
...headersTab,
|
||||
...authTab,
|
||||
{
|
||||
value: TAB_SETTINGS,
|
||||
label: "Settings",
|
||||
rightSlot: <CountBadge count={numSettingsOverrides} />,
|
||||
},
|
||||
{
|
||||
value: TAB_DESCRIPTION,
|
||||
label: "Info",
|
||||
},
|
||||
];
|
||||
}, [authTab, headersTab, urlParameterPairs.length]);
|
||||
}, [authTab, headersTab, numSettingsOverrides, urlParameterPairs.length]);
|
||||
|
||||
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
||||
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||
@@ -266,6 +274,9 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
||||
stateKey={`json.${activeRequest.id}`}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_SETTINGS}>
|
||||
<ModelSettingsEditor model={activeRequest} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_DESCRIPTION}>
|
||||
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
|
||||
<PlainInput
|
||||
|
||||
@@ -20,16 +20,24 @@ import { IconButton } from "./core/IconButton";
|
||||
import { IconTooltip } from "./core/IconTooltip";
|
||||
import { Label } from "./core/Label";
|
||||
import { PlainInput } from "./core/PlainInput";
|
||||
import { SettingRow } from "./core/SettingRow";
|
||||
import { EncryptionHelp } from "./EncryptionHelp";
|
||||
|
||||
interface Props {
|
||||
layout?: "form" | "settings";
|
||||
size?: ButtonProps["size"];
|
||||
expanded?: boolean;
|
||||
onDone?: () => void;
|
||||
onEnabledEncryption?: () => void;
|
||||
}
|
||||
|
||||
export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEncryption }: Props) {
|
||||
export function WorkspaceEncryptionSetting({
|
||||
layout = "form",
|
||||
size,
|
||||
expanded,
|
||||
onDone,
|
||||
onEnabledEncryption,
|
||||
}: Props) {
|
||||
const [justEnabledEncryption, setJustEnabledEncryption] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -66,7 +74,7 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
|
||||
key.error != null ||
|
||||
(workspace.encryptionKeyChallenge && workspaceMeta.encryptionKey == null)
|
||||
) {
|
||||
return (
|
||||
const enterKey = (
|
||||
<EnterWorkspaceKey
|
||||
workspaceMeta={workspaceMeta}
|
||||
error={key.error}
|
||||
@@ -79,6 +87,8 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return enterKey;
|
||||
}
|
||||
|
||||
// Show the key if it exists
|
||||
@@ -90,7 +100,8 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
|
||||
encryptionKey={key.key}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
|
||||
const content = (
|
||||
<VStack space={2} className="w-full">
|
||||
{justEnabledEncryption && (
|
||||
<Banner color="success" className="flex flex-col gap-2">
|
||||
@@ -111,9 +122,43 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// Show button to enable encryption
|
||||
if (layout === "settings") {
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<Banner color="danger" className="mb-3">
|
||||
{error}
|
||||
</Banner>
|
||||
)}
|
||||
<SettingRow
|
||||
title="Workspace encryption"
|
||||
description="Encrypt workspace secrets and sensitive values at rest."
|
||||
>
|
||||
<Button
|
||||
color="secondary"
|
||||
size={size}
|
||||
onClick={async () => {
|
||||
setError(null);
|
||||
try {
|
||||
await enableEncryption(workspaceMeta.workspaceId);
|
||||
setJustEnabledEncryption(true);
|
||||
} catch (err) {
|
||||
setError(`Failed to enable encryption: ${String(err)}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Enable Encryption
|
||||
</Button>
|
||||
</SettingRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-auto flex flex-col-reverse">
|
||||
<Button
|
||||
|
||||
@@ -5,16 +5,19 @@ import { useAuthTab } from "../hooks/useAuthTab";
|
||||
import { useHeadersTab } from "../hooks/useHeadersTab";
|
||||
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
|
||||
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
||||
import { showDialog } from "../lib/dialog";
|
||||
import { router } from "../lib/router";
|
||||
import { CopyIconButton } from "./CopyIconButton";
|
||||
import { Button } from "./core/Button";
|
||||
import { CountBadge } from "./core/CountBadge";
|
||||
import { PlainInput } from "./core/PlainInput";
|
||||
import { SettingsList, SettingsSection } from "./core/SettingRow";
|
||||
import { TabContent, Tabs } from "./core/Tabs/Tabs";
|
||||
import { DnsOverridesEditor } from "./DnsOverridesEditor";
|
||||
import { HeadersEditor } from "./HeadersEditor";
|
||||
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
||||
import { MarkdownEditor } from "./MarkdownEditor";
|
||||
import { ModelSettingsEditor } from "./ModelSettingsEditor";
|
||||
import { SyncToFilesystemSetting } from "./SyncToFilesystemSetting";
|
||||
import { WorkspaceEncryptionSetting } from "./WorkspaceEncryptionSetting";
|
||||
|
||||
@@ -25,17 +28,17 @@ interface Props {
|
||||
}
|
||||
|
||||
const TAB_AUTH = "auth";
|
||||
const TAB_DATA = "data";
|
||||
const TAB_DNS = "dns";
|
||||
const TAB_HEADERS = "headers";
|
||||
const TAB_GENERAL = "general";
|
||||
const TAB_SETTINGS = "settings";
|
||||
|
||||
export type WorkspaceSettingsTab =
|
||||
| typeof TAB_AUTH
|
||||
| typeof TAB_DNS
|
||||
| typeof TAB_HEADERS
|
||||
| typeof TAB_GENERAL
|
||||
| typeof TAB_DATA;
|
||||
| typeof TAB_SETTINGS;
|
||||
|
||||
const DEFAULT_TAB: WorkspaceSettingsTab = TAB_GENERAL;
|
||||
|
||||
@@ -71,8 +74,8 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
||||
tabs={[
|
||||
{ value: TAB_GENERAL, label: "Workspace" },
|
||||
{
|
||||
value: TAB_DATA,
|
||||
label: "Storage",
|
||||
value: TAB_SETTINGS,
|
||||
label: "Settings",
|
||||
},
|
||||
...headersTab,
|
||||
...authTab,
|
||||
@@ -100,6 +103,20 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
||||
stateKey={`headers.${workspace.id}`}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_SETTINGS} className="overflow-y-auto h-full px-4">
|
||||
<SettingsList className="space-y-8 pb-3">
|
||||
<SettingsSection title={null}>
|
||||
<SyncToFilesystemSetting
|
||||
layout="settings"
|
||||
value={{ filePath: workspaceMeta.settingSyncDir }}
|
||||
onCreateNewWorkspace={hide}
|
||||
onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })}
|
||||
/>
|
||||
<WorkspaceEncryptionSetting layout="settings" size="xs" />
|
||||
</SettingsSection>
|
||||
<ModelSettingsEditor model={workspace} showSectionTitles />
|
||||
</SettingsList>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_GENERAL} className="overflow-y-auto h-full px-4">
|
||||
<div className="grid grid-rows-[auto_minmax(0,1fr)_auto] gap-4 pb-3 h-full">
|
||||
<PlainInput
|
||||
@@ -152,19 +169,21 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
||||
</HStack>
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_DATA} className="overflow-y-auto h-full px-4">
|
||||
<VStack space={4} alignItems="start" className="pb-3 h-full">
|
||||
<SyncToFilesystemSetting
|
||||
value={{ filePath: workspaceMeta.settingSyncDir }}
|
||||
onCreateNewWorkspace={hide}
|
||||
onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })}
|
||||
/>
|
||||
<WorkspaceEncryptionSetting size="xs" />
|
||||
</VStack>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_DNS} className="overflow-y-auto h-full px-4">
|
||||
<DnsOverridesEditor workspace={workspace} />
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
WorkspaceSettingsDialog.show = (workspaceId: string, tab?: WorkspaceSettingsTab) => {
|
||||
showDialog({
|
||||
id: "workspace-settings",
|
||||
size: "lg",
|
||||
className: "h-[calc(100vh-5rem)] !max-h-[50rem]",
|
||||
noPadding: true,
|
||||
render: ({ hide }) => (
|
||||
<WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface CheckboxProps {
|
||||
hideLabel?: boolean;
|
||||
fullWidth?: boolean;
|
||||
help?: ReactNode;
|
||||
size?: "sm" | "md";
|
||||
}
|
||||
|
||||
export function Checkbox({
|
||||
@@ -25,6 +26,7 @@ export function Checkbox({
|
||||
hideLabel,
|
||||
fullWidth,
|
||||
help,
|
||||
size = "sm",
|
||||
}: CheckboxProps) {
|
||||
return (
|
||||
<HStack
|
||||
@@ -37,7 +39,9 @@ export function Checkbox({
|
||||
<input
|
||||
aria-hidden
|
||||
className={classNames(
|
||||
"appearance-none w-4 h-4 flex-shrink-0 border border-border",
|
||||
"appearance-none flex-shrink-0 border border-border",
|
||||
size === "sm" && "w-4 h-4",
|
||||
size === "md" && "w-5 h-5",
|
||||
"rounded outline-none ring-0",
|
||||
!disabled && "hocus:border-border-focus hocus:bg-focus/[5%]",
|
||||
disabled && "border-dotted",
|
||||
@@ -50,7 +54,7 @@ export function Checkbox({
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Icon
|
||||
size="sm"
|
||||
size={size}
|
||||
className={classNames(disabled && "opacity-disabled")}
|
||||
icon={checked === "indeterminate" ? "minus" : checked ? "check" : "empty"}
|
||||
/>
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import classNames from "classnames";
|
||||
import type { HTMLAttributes, ReactElement, ReactNode } from "react";
|
||||
import { CopyIconButton } from "../CopyIconButton";
|
||||
|
||||
interface Props {
|
||||
children:
|
||||
| ReactElement<HTMLAttributes<HTMLTableColElement>>
|
||||
| (ReactElement<HTMLAttributes<HTMLTableColElement>> | null)[];
|
||||
selectable?: boolean;
|
||||
}
|
||||
|
||||
export function KeyValueRows({ children }: Props) {
|
||||
export function KeyValueRows({ children, selectable }: Props) {
|
||||
const childArray = Array.isArray(children) ? children.filter(Boolean) : [children];
|
||||
return (
|
||||
<table className="text-editor font-mono min-w-0 w-full mb-auto">
|
||||
<table
|
||||
className={classNames(
|
||||
"text-editor font-mono min-w-0 w-full mb-auto",
|
||||
selectable &&
|
||||
"[&_td]:select-auto [&_td]:cursor-auto [&_td_*]:select-auto [&_td_*]:cursor-auto",
|
||||
)}
|
||||
>
|
||||
<tbody className="divide-y divide-surface-highlight">
|
||||
{childArray.map((child, i) => (
|
||||
// oxlint-disable-next-line react/no-array-index-key
|
||||
@@ -26,8 +34,11 @@ interface KeyValueRowProps {
|
||||
children: ReactNode;
|
||||
rightSlot?: ReactNode;
|
||||
leftSlot?: ReactNode;
|
||||
align?: "top" | "middle";
|
||||
labelClassName?: string;
|
||||
labelColor?: "secondary" | "primary" | "info";
|
||||
enableCopy?: boolean;
|
||||
copyText?: string;
|
||||
}
|
||||
|
||||
export function KeyValueRow({
|
||||
@@ -35,14 +46,34 @@ export function KeyValueRow({
|
||||
children,
|
||||
rightSlot,
|
||||
leftSlot,
|
||||
align = "top",
|
||||
labelColor = "secondary",
|
||||
labelClassName,
|
||||
enableCopy,
|
||||
copyText,
|
||||
}: KeyValueRowProps) {
|
||||
const textToCopy =
|
||||
copyText ??
|
||||
(typeof children === "string" || typeof children === "number" ? `${children}` : null);
|
||||
const resolvedRightSlot =
|
||||
rightSlot ??
|
||||
(enableCopy && textToCopy != null ? (
|
||||
<CopyIconButton
|
||||
text={textToCopy}
|
||||
className="text-text-subtle"
|
||||
size="2xs"
|
||||
title={`Copy ${label}`}
|
||||
iconSize="sm"
|
||||
/>
|
||||
) : null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<td
|
||||
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,
|
||||
labelColor === "primary" && "text-primary",
|
||||
labelColor === "secondary" && "text-text-subtle",
|
||||
@@ -51,11 +82,21 @@ export function KeyValueRow({
|
||||
>
|
||||
<span className="select-text cursor-text">{label}</span>
|
||||
</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]">
|
||||
{leftSlot ?? <span aria-hidden />}
|
||||
{children}
|
||||
{rightSlot ? <div className="ml-1.5">{rightSlot}</div> : <span aria-hidden />}
|
||||
{resolvedRightSlot ? (
|
||||
<div className="ml-1.5">{resolvedRightSlot}</div>
|
||||
) : (
|
||||
<span aria-hidden />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</>
|
||||
|
||||
@@ -43,6 +43,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
className,
|
||||
containerClassName,
|
||||
defaultValue,
|
||||
disabled,
|
||||
forceUpdateKey: forceUpdateKeyFromAbove,
|
||||
help,
|
||||
hideLabel,
|
||||
@@ -163,7 +164,8 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
"relative w-full rounded-md text",
|
||||
"border",
|
||||
"overflow-hidden",
|
||||
focused ? "border-border-focus" : "border-border-subtle",
|
||||
focused && !disabled ? "border-border-focus" : "border-border-subtle",
|
||||
disabled && "border-dotted",
|
||||
hasChanged && "has-[:invalid]:border-danger", // For built-in HTML validation
|
||||
size === "md" && "min-h-md",
|
||||
size === "sm" && "min-h-sm",
|
||||
@@ -198,12 +200,13 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
// oxlint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={autoFocus}
|
||||
defaultValue={defaultValue ?? undefined}
|
||||
disabled={disabled}
|
||||
autoComplete="off"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
onPaste={(e) => onPaste?.(e.clipboardData.getData("Text"))}
|
||||
className={classNames(commonClassName, "h-full")}
|
||||
className={classNames(commonClassName, "h-full disabled:opacity-disabled")}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
required={required}
|
||||
|
||||
@@ -109,7 +109,15 @@ export function Select<T extends string>({
|
||||
) : (
|
||||
// Use custom "select" component until Tauri can be configured to have select menus not always appear in
|
||||
// light mode
|
||||
<RadioDropdown value={value} onChange={handleChange} items={options}>
|
||||
<RadioDropdown
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
items={options.map((o) =>
|
||||
o.type === "separator" || o.value !== defaultValue
|
||||
? o
|
||||
: { ...o, label: <>{o.label} (default)</> },
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
className="w-full text-sm font-mono"
|
||||
justify="start"
|
||||
|
||||
@@ -24,7 +24,7 @@ export function Separator({
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
"h-0 border-t opacity-60",
|
||||
"opacity-60",
|
||||
color == null && "border-border",
|
||||
color === "primary" && "border-primary",
|
||||
color === "secondary" && "border-secondary",
|
||||
@@ -34,8 +34,8 @@ export function Separator({
|
||||
color === "danger" && "border-danger",
|
||||
color === "info" && "border-info",
|
||||
dashed && "border-dashed",
|
||||
orientation === "horizontal" && "w-full h-[1px]",
|
||||
orientation === "vertical" && "h-full w-[1px]",
|
||||
orientation === "horizontal" && "w-full h-0 border-t",
|
||||
orientation === "vertical" && "h-full w-0 border-l",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
import type { AnyModel } from "@yaakapp-internal/models";
|
||||
import { patchModel } from "@yaakapp-internal/models";
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
import { CopyIconButton } from "../CopyIconButton";
|
||||
import { Checkbox } from "./Checkbox";
|
||||
import { IconButton, type IconButtonProps } from "./IconButton";
|
||||
import { PlainInput } from "./PlainInput";
|
||||
import type { RadioDropdownItem } from "./RadioDropdown";
|
||||
import { Select } from "./Select";
|
||||
import { SelectFile } from "../SelectFile";
|
||||
|
||||
type ModelKeyOfValue<T, V> = {
|
||||
[K in keyof T]-?: T[K] extends V ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
type SettingRowBaseProps = {
|
||||
className?: string;
|
||||
controlClassName?: string;
|
||||
description?: ReactNode;
|
||||
disabled?: boolean;
|
||||
title: ReactNode;
|
||||
};
|
||||
|
||||
export function SettingsList({ children, className }: { children: ReactNode; className?: string }) {
|
||||
return <div className={classNames("w-full", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function SettingsSection({
|
||||
children,
|
||||
className,
|
||||
description,
|
||||
title,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
description?: ReactNode;
|
||||
title: ReactNode | null;
|
||||
}) {
|
||||
const showHeader = title != null || description != null;
|
||||
|
||||
return (
|
||||
<section className={classNames(className, "w-full")}>
|
||||
{showHeader && (
|
||||
<div className="border-b border-border-subtle pb-2">
|
||||
{title != null && <div className="text-text-subtle">{title}</div>}
|
||||
{description != null && <p className="mt-1 text-sm text-text-subtlest">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
<div className="[&>*:last-child]:border-b-0">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingRow({
|
||||
children,
|
||||
className,
|
||||
controlClassName,
|
||||
description,
|
||||
disabled,
|
||||
title,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
} & SettingRowBaseProps) {
|
||||
return (
|
||||
<div
|
||||
aria-disabled={disabled || undefined}
|
||||
className={classNames(
|
||||
className,
|
||||
"@container border-b border-border-subtle py-4",
|
||||
disabled && "opacity-disabled",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
"grid grid-cols-1 gap-2",
|
||||
"@[30rem]:grid-cols-[minmax(0,1fr)_auto] items-center",
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="text text-text">{title}</div>
|
||||
{description != null && (
|
||||
<div className="mt-1 max-w-2xl text-sm text-text-subtle">{description}</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"flex min-w-0 items-center justify-start @[40rem]:justify-end",
|
||||
controlClassName,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingValue({
|
||||
actions,
|
||||
className,
|
||||
copyText,
|
||||
enableCopy = true,
|
||||
value,
|
||||
}: {
|
||||
actions?: SettingValueAction[];
|
||||
className?: string;
|
||||
copyText?: string;
|
||||
enableCopy?: boolean;
|
||||
value: ReactNode;
|
||||
}) {
|
||||
const textValue = typeof value === "string" || typeof value === "number" ? `${value}` : null;
|
||||
const textToCopy = copyText ?? textValue;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
"cursor-text select-text truncate font-mono text-editor text-text-subtle pr-1.5",
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
{actions?.map((action) => (
|
||||
<IconButton
|
||||
key={action.title}
|
||||
icon={action.icon}
|
||||
title={action.title}
|
||||
size="2xs"
|
||||
iconSize="sm"
|
||||
onClick={action.onClick}
|
||||
/>
|
||||
))}
|
||||
{enableCopy && textToCopy != null && (
|
||||
<CopyIconButton size="2xs" text={textToCopy} title="Copy value" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type SettingValueAction = {
|
||||
icon: IconButtonProps["icon"];
|
||||
onClick: () => void;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export function SettingRowBoolean({
|
||||
checked,
|
||||
checkboxSize = "md",
|
||||
onChange,
|
||||
title,
|
||||
...props
|
||||
}: {
|
||||
checked: boolean;
|
||||
checkboxSize?: "sm" | "md";
|
||||
onChange: (checked: boolean) => void;
|
||||
} & SettingRowBaseProps) {
|
||||
return (
|
||||
<SettingRow title={title} {...props}>
|
||||
<Checkbox
|
||||
hideLabel
|
||||
size={checkboxSize}
|
||||
checked={checked}
|
||||
disabled={props.disabled}
|
||||
title={title}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModelSettingRowBoolean<M extends AnyModel, K extends ModelKeyOfValue<M, boolean>>({
|
||||
model,
|
||||
modelKey,
|
||||
...props
|
||||
}: {
|
||||
model: M;
|
||||
modelKey: K;
|
||||
} & Omit<Parameters<typeof SettingRowBoolean>[0], "checked" | "onChange">) {
|
||||
return (
|
||||
<SettingRowBoolean
|
||||
checked={model[modelKey] as boolean}
|
||||
onChange={(value) => patchModel(model, { [modelKey]: value } as Partial<M>)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingRowNumber({
|
||||
inputClassName,
|
||||
inputWidthClassName = "!w-48",
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
required,
|
||||
title,
|
||||
type = "number",
|
||||
validate,
|
||||
value,
|
||||
...props
|
||||
}: {
|
||||
inputClassName?: string;
|
||||
inputWidthClassName?: string;
|
||||
name: string;
|
||||
onChange: (value: number) => void;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
type?: "number";
|
||||
validate?: (value: string) => boolean;
|
||||
value: number;
|
||||
} & SettingRowBaseProps) {
|
||||
return (
|
||||
<SettingRow title={title} {...props}>
|
||||
<PlainInput
|
||||
required={required}
|
||||
hideLabel
|
||||
size="sm"
|
||||
name={name}
|
||||
label={typeof title === "string" ? title : name}
|
||||
placeholder={placeholder}
|
||||
defaultValue={`${value}`}
|
||||
validate={validate}
|
||||
onChange={(value) => onChange(Number.parseInt(value, 10) || 0)}
|
||||
type={type}
|
||||
className={inputClassName}
|
||||
containerClassName={inputWidthClassName}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModelSettingRowNumber<M extends AnyModel, K extends ModelKeyOfValue<M, number>>({
|
||||
model,
|
||||
modelKey,
|
||||
name = String(modelKey),
|
||||
...props
|
||||
}: {
|
||||
model: M;
|
||||
modelKey: K;
|
||||
name?: string;
|
||||
} & Omit<Parameters<typeof SettingRowNumber>[0], "name" | "onChange" | "value">) {
|
||||
return (
|
||||
<SettingRowNumber
|
||||
name={name}
|
||||
value={model[modelKey] as number}
|
||||
onChange={(value) => patchModel(model, { [modelKey]: value } as Partial<M>)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingRowText({
|
||||
inputClassName,
|
||||
inputWidthClassName = "!w-80",
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
required,
|
||||
title,
|
||||
type = "text",
|
||||
value,
|
||||
...props
|
||||
}: {
|
||||
inputClassName?: string;
|
||||
inputWidthClassName?: string;
|
||||
name: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
type?: "text" | "password";
|
||||
value: string;
|
||||
} & SettingRowBaseProps) {
|
||||
return (
|
||||
<SettingRow title={title} {...props}>
|
||||
<PlainInput
|
||||
required={required}
|
||||
hideLabel
|
||||
size="sm"
|
||||
name={name}
|
||||
label={typeof title === "string" ? title : name}
|
||||
placeholder={placeholder}
|
||||
defaultValue={value}
|
||||
onChange={onChange}
|
||||
type={type}
|
||||
className={inputClassName}
|
||||
containerClassName={inputWidthClassName}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModelSettingRowText<M extends AnyModel, K extends ModelKeyOfValue<M, string>>({
|
||||
model,
|
||||
modelKey,
|
||||
name = String(modelKey),
|
||||
...props
|
||||
}: {
|
||||
model: M;
|
||||
modelKey: K;
|
||||
name?: string;
|
||||
} & Omit<Parameters<typeof SettingRowText>[0], "name" | "onChange" | "value">) {
|
||||
return (
|
||||
<SettingRowText
|
||||
name={name}
|
||||
value={model[modelKey] as string}
|
||||
onChange={(value) => patchModel(model, { [modelKey]: value } as Partial<M>)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingRowFile({
|
||||
buttonClassName,
|
||||
controlClassName = "min-w-0 max-w-[min(32rem,45vw)]",
|
||||
directory,
|
||||
filePath,
|
||||
nameOverride,
|
||||
noun,
|
||||
onChange,
|
||||
size = "xs",
|
||||
title,
|
||||
...props
|
||||
}: {
|
||||
buttonClassName?: string;
|
||||
directory?: boolean;
|
||||
filePath: string | null;
|
||||
nameOverride?: string | null;
|
||||
noun?: string;
|
||||
onChange: (filePath: string | null) => void | Promise<void>;
|
||||
size?: Parameters<typeof SelectFile>[0]["size"];
|
||||
} & SettingRowBaseProps) {
|
||||
return (
|
||||
<SettingRow title={title} controlClassName={controlClassName} {...props}>
|
||||
<SelectFile
|
||||
directory={directory}
|
||||
inline
|
||||
hideLabel
|
||||
label={typeof title === "string" ? title : noun}
|
||||
size={size}
|
||||
noun={noun}
|
||||
nameOverride={nameOverride}
|
||||
filePath={filePath}
|
||||
className={buttonClassName}
|
||||
onChange={({ filePath }) => onChange(filePath)}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingRowDirectory({
|
||||
noun = "Directory",
|
||||
...props
|
||||
}: Omit<Parameters<typeof SettingRowFile>[0], "directory">) {
|
||||
return <SettingRowFile directory noun={noun} {...props} />;
|
||||
}
|
||||
|
||||
export function SettingRowSelect<T extends string>({
|
||||
defaultValue,
|
||||
name,
|
||||
onChange,
|
||||
options,
|
||||
selectClassName = "!w-48",
|
||||
title,
|
||||
value,
|
||||
...props
|
||||
}: {
|
||||
defaultValue?: T;
|
||||
name: string;
|
||||
onChange: (value: T) => void;
|
||||
options: RadioDropdownItem<T>[];
|
||||
selectClassName?: string;
|
||||
value: T;
|
||||
} & SettingRowBaseProps) {
|
||||
return (
|
||||
<SettingRow title={title} {...props}>
|
||||
<SettingSelectControl
|
||||
name={name}
|
||||
label={typeof title === "string" ? title : name}
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
selectClassName={selectClassName}
|
||||
disabled={props.disabled}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingSelectControl<T extends string>({
|
||||
defaultValue,
|
||||
disabled,
|
||||
label,
|
||||
name,
|
||||
onChange,
|
||||
options,
|
||||
selectClassName = "!w-48",
|
||||
value,
|
||||
}: {
|
||||
defaultValue?: T;
|
||||
disabled?: boolean;
|
||||
label: string;
|
||||
name: string;
|
||||
onChange: (value: T) => void;
|
||||
options: RadioDropdownItem<T>[];
|
||||
selectClassName?: string;
|
||||
value: T;
|
||||
}) {
|
||||
return (
|
||||
<Select
|
||||
hideLabel
|
||||
name={name}
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
label={label}
|
||||
size="sm"
|
||||
className={selectClassName}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModelSettingSelectControl<
|
||||
M extends AnyModel,
|
||||
K extends ModelKeyOfValue<M, string>,
|
||||
V extends M[K] & string,
|
||||
>({
|
||||
model,
|
||||
modelKey,
|
||||
name = String(modelKey),
|
||||
...props
|
||||
}: {
|
||||
model: M;
|
||||
modelKey: K;
|
||||
name?: string;
|
||||
} & Omit<Parameters<typeof SettingSelectControl<V>>[0], "name" | "onChange" | "value">) {
|
||||
return (
|
||||
<SettingSelectControl
|
||||
name={name}
|
||||
value={model[modelKey] as V}
|
||||
onChange={(value) => patchModel(model, { [modelKey]: value } as Partial<M>)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModelSettingRowSelect<
|
||||
M extends AnyModel,
|
||||
K extends ModelKeyOfValue<M, string>,
|
||||
V extends M[K] & string,
|
||||
>({
|
||||
model,
|
||||
modelKey,
|
||||
name = String(modelKey),
|
||||
...props
|
||||
}: {
|
||||
model: M;
|
||||
modelKey: K;
|
||||
name?: string;
|
||||
} & Omit<Parameters<typeof SettingRowSelect<V>>[0], "name" | "onChange" | "value">) {
|
||||
return (
|
||||
<SettingRowSelect
|
||||
name={name}
|
||||
value={model[modelKey] as V}
|
||||
onChange={(value) => patchModel(model, { [modelKey]: value } as Partial<M>)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingOverrideRow({
|
||||
children,
|
||||
className,
|
||||
controlClassName,
|
||||
description,
|
||||
disabled,
|
||||
onResetOverride,
|
||||
overridden,
|
||||
resetTitle = "Reset override",
|
||||
title,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
controlClassName?: string;
|
||||
description?: ReactNode;
|
||||
disabled?: boolean;
|
||||
onResetOverride: () => void;
|
||||
overridden: boolean;
|
||||
resetTitle?: string;
|
||||
title: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<SettingRow
|
||||
className={className}
|
||||
controlClassName={controlClassName}
|
||||
description={description}
|
||||
disabled={disabled}
|
||||
title={
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
{title}
|
||||
{overridden && (
|
||||
<IconButton
|
||||
icon="undo_2"
|
||||
size="2xs"
|
||||
iconSize="sm"
|
||||
title={resetTitle}
|
||||
className="text-text-subtle"
|
||||
onClick={onResetOverride}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
@@ -596,7 +596,7 @@ function SetupSyncDropdown({ workspaceMeta }: { workspaceMeta: WorkspaceMeta })
|
||||
color: "success",
|
||||
label: "Open Workspace Settings",
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
onSelect: () => openWorkspaceSettings("data"),
|
||||
onSelect: () => openWorkspaceSettings("settings"),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
|
||||
@@ -43,7 +43,7 @@ export function useAuthTab<T extends string>(tabValue: T, model: AuthenticatedMo
|
||||
{authentication.find((a) => a.name === inheritedAuth.authenticationType)
|
||||
?.shortLabel ?? "UNKNOWN"}
|
||||
<IconTooltip
|
||||
icon="magic_wand"
|
||||
icon="zap_off"
|
||||
iconSize="xs"
|
||||
content="Authentication was inherited from an ancestor"
|
||||
/>
|
||||
|
||||
@@ -473,7 +473,7 @@ async fn build_plugin_reply(
|
||||
let names = cookie_jar
|
||||
.cookies
|
||||
.into_iter()
|
||||
.filter_map(|c| parse_cookie_name_value(&c.raw_cookie).map(|(name, _)| name))
|
||||
.map(|c| c.name)
|
||||
.collect();
|
||||
|
||||
Some(InternalEventPayload::ListCookieNamesResponse(ListCookieNamesResponse {
|
||||
@@ -531,12 +531,6 @@ async fn render_json_value_for_cli<T: TemplateCallback>(
|
||||
render_json_value_raw(value, vars, cb, opt).await
|
||||
}
|
||||
|
||||
fn parse_cookie_name_value(raw_cookie: &str) -> Option<(String, String)> {
|
||||
let first_part = raw_cookie.split(';').next()?.trim();
|
||||
let (name, value) = first_part.split_once('=')?;
|
||||
Some((name.trim().to_string(), value.to_string()))
|
||||
}
|
||||
|
||||
fn copy_text_to_clipboard(text: &str) -> Result<(), String> {
|
||||
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
|
||||
clipboard.set_text(text.to_string()).map_err(|e| e.to_string())
|
||||
|
||||
@@ -7,34 +7,6 @@ use tempfile::TempDir;
|
||||
use yaak_models::models::HttpRequest;
|
||||
use yaak_models::util::UpdateSource;
|
||||
|
||||
#[test]
|
||||
fn top_level_send_workspace_sends_http_requests_and_prints_summary() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let data_dir = temp_dir.path();
|
||||
seed_workspace(data_dir, "wk_test");
|
||||
|
||||
let server = TestHttpServer::spawn_ok("workspace bulk send");
|
||||
let request = HttpRequest {
|
||||
id: "rq_workspace_send".to_string(),
|
||||
workspace_id: "wk_test".to_string(),
|
||||
name: "Workspace Send".to_string(),
|
||||
method: "GET".to_string(),
|
||||
url: server.url.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
query_manager(data_dir)
|
||||
.connect()
|
||||
.upsert_http_request(&request, &UpdateSource::Sync)
|
||||
.expect("Failed to seed workspace request");
|
||||
|
||||
cli_cmd(data_dir)
|
||||
.args(["send", "wk_test"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("workspace bulk send"))
|
||||
.stdout(contains("Send summary: 1 succeeded, 0 failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_send_folder_sends_http_requests_and_prints_summary() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::{
|
||||
workspace_from_window,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use cookie::Cookie;
|
||||
use log::error;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter, Listener, Manager, Runtime};
|
||||
@@ -409,11 +408,7 @@ async fn handle_host_plugin_request<R: Runtime>(
|
||||
let window = get_window_from_plugin_context(app_handle, plugin_context)?;
|
||||
let names = match cookie_jar_from_window(&window) {
|
||||
None => Vec::new(),
|
||||
Some(j) => j
|
||||
.cookies
|
||||
.into_iter()
|
||||
.filter_map(|c| Cookie::parse(c.raw_cookie).ok().map(|c| c.name().to_string()))
|
||||
.collect(),
|
||||
Some(j) => j.cookies.into_iter().map(|c| c.name).collect(),
|
||||
};
|
||||
Ok(Some(InternalEventPayload::ListCookieNamesResponse(ListCookieNamesResponse {
|
||||
names,
|
||||
|
||||
@@ -134,7 +134,8 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
unrendered_request.folder_id.as_deref(),
|
||||
environment_id,
|
||||
)?;
|
||||
let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?;
|
||||
let resolved_settings =
|
||||
app_handle.db().resolve_settings_for_websocket_request(&unrendered_request)?;
|
||||
let settings = app_handle.db().get_settings();
|
||||
let (resolved_request, auth_context_id) =
|
||||
resolve_websocket_request(&window, &unrendered_request)?;
|
||||
@@ -247,11 +248,18 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
}
|
||||
}
|
||||
|
||||
// Add cookies to WS HTTP Upgrade
|
||||
if let Some(id) = cookie_jar_id {
|
||||
let cookie_jar = app_handle.db().get_cookie_jar(&id)?;
|
||||
let store = CookieStore::from_cookies(cookie_jar.cookies);
|
||||
let mut cookie_jar = match (
|
||||
resolved_settings.send_cookies.value || resolved_settings.store_cookies.value,
|
||||
cookie_jar_id,
|
||||
) {
|
||||
(true, Some(id)) => Some(app_handle.db().get_cookie_jar(id)?),
|
||||
_ => 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.value, cookie_store.as_ref()) {
|
||||
// Convert WS URL -> HTTP URL because our cookie store matches based on
|
||||
// Path/HttpOnly/Secure attributes even though WS upgrades are HTTP requests
|
||||
let http_url = convert_ws_url_to_http(&url);
|
||||
@@ -289,7 +297,7 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
url.as_str(),
|
||||
headers,
|
||||
receive_tx,
|
||||
workspace.setting_validate_certificates,
|
||||
resolved_settings.validate_certificates.value,
|
||||
client_cert,
|
||||
)
|
||||
.await
|
||||
@@ -328,6 +336,23 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
})
|
||||
.collect::<Vec<HttpResponseHeader>>();
|
||||
|
||||
if let (true, Some(cookie_jar), Some(store)) =
|
||||
(resolved_settings.store_cookies.value, 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(
|
||||
&WebsocketConnection {
|
||||
state: WebsocketConnectionState::Connected,
|
||||
|
||||
Generated
+156
-33
@@ -1,45 +1,168 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
||||
export type DnsOverride = {
|
||||
hostname: string;
|
||||
ipv4: Array<string>;
|
||||
ipv6: Array<string>;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
|
||||
/**
|
||||
* Variables defined in this environment scope.
|
||||
* Child environments override parent variables by name.
|
||||
*/
|
||||
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||
export type Environment = {
|
||||
model: "environment";
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
name: string;
|
||||
public: boolean;
|
||||
parentModel: string;
|
||||
parentId: string | null;
|
||||
/**
|
||||
* Variables defined in this environment scope.
|
||||
* Child environments override parent variables by name.
|
||||
*/
|
||||
variables: Array<EnvironmentVariable>;
|
||||
color: string | null;
|
||||
sortPriority: number;
|
||||
};
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string };
|
||||
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
|
||||
export type Folder = {
|
||||
model: "folder";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
settingValidateCertificates: InheritedBoolSetting;
|
||||
settingFollowRedirects: InheritedBoolSetting;
|
||||
settingRequestTimeout: InheritedIntSetting;
|
||||
};
|
||||
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number,
|
||||
/**
|
||||
* Server URL (http for plaintext or https for secure)
|
||||
*/
|
||||
url: string, };
|
||||
export type GrpcRequest = {
|
||||
model: "grpc_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authenticationType: string | null;
|
||||
authentication: Record<string, any>;
|
||||
description: string;
|
||||
message: string;
|
||||
metadata: Array<HttpRequestHeader>;
|
||||
method: string | null;
|
||||
name: string;
|
||||
service: string | null;
|
||||
sortPriority: number;
|
||||
/**
|
||||
* Server URL (http for plaintext or https for secure)
|
||||
*/
|
||||
url: string;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
};
|
||||
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>, };
|
||||
export type HttpRequest = {
|
||||
model: "http_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
body: Record<string, any>;
|
||||
bodyType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
method: string;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
url: string;
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
settingValidateCertificates: InheritedBoolSetting;
|
||||
settingFollowRedirects: InheritedBoolSetting;
|
||||
settingRequestTimeout: InheritedIntSetting;
|
||||
};
|
||||
|
||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string };
|
||||
|
||||
export type HttpUrlParameter = { enabled?: boolean,
|
||||
/**
|
||||
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||
* Other entries are appended as query parameters
|
||||
*/
|
||||
name: string, value: string, id?: string, };
|
||||
export type HttpUrlParameter = {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||
* Other entries are appended as query parameters
|
||||
*/
|
||||
name: string;
|
||||
value: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environment" } & Environment | { "type": "folder" } & Folder | { "type": "http_request" } & HttpRequest | { "type": "grpc_request" } & GrpcRequest | { "type": "websocket_request" } & WebsocketRequest;
|
||||
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string,
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>, };
|
||||
export type InheritedIntSetting = { enabled?: boolean; value: number };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||
export type SyncModel =
|
||||
| ({ type: "workspace" } & Workspace)
|
||||
| ({ type: "environment" } & Environment)
|
||||
| ({ type: "folder" } & Folder)
|
||||
| ({ type: "http_request" } & HttpRequest)
|
||||
| ({ type: "grpc_request" } & GrpcRequest)
|
||||
| ({ type: "websocket_request" } & WebsocketRequest);
|
||||
|
||||
export type WebsocketRequest = {
|
||||
model: "websocket_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
message: string;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
url: string;
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
};
|
||||
|
||||
export type Workspace = {
|
||||
model: "workspace";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
name: string;
|
||||
encryptionKeyChallenge: string | null;
|
||||
settingValidateCertificates: boolean;
|
||||
settingFollowRedirects: boolean;
|
||||
settingRequestTimeout: number;
|
||||
settingDnsOverrides: Array<DnsOverride>;
|
||||
settingSendCookies: boolean;
|
||||
settingStoreCookies: boolean;
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ use log::debug;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use url::Url;
|
||||
use yaak_models::models::{Cookie, CookieDomain, CookieExpires};
|
||||
use yaak_models::models::{Cookie, CookieDomain, CookieExpires, CookieSameSite};
|
||||
|
||||
/// A thread-safe cookie store that can be shared across requests
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -45,10 +45,7 @@ impl CookieStore {
|
||||
let matching_cookies: Vec<_> = cookies
|
||||
.iter()
|
||||
.filter(|cookie| self.cookie_matches(cookie, url, &now))
|
||||
.filter_map(|cookie| {
|
||||
// Parse the raw cookie to get name=value
|
||||
parse_cookie_name_value(&cookie.raw_cookie)
|
||||
})
|
||||
.map(|cookie| (cookie.name.clone(), cookie.value.clone()))
|
||||
.collect();
|
||||
|
||||
if matching_cookies.is_empty() {
|
||||
@@ -72,13 +69,7 @@ impl CookieStore {
|
||||
if let Some(cookie) = parse_set_cookie(header_value, url) {
|
||||
// Remove any existing cookie with the same name and domain
|
||||
cookies.retain(|existing| !cookies_match(existing, &cookie));
|
||||
debug!(
|
||||
"Storing cookie: {} for domain {:?}",
|
||||
parse_cookie_name_value(&cookie.raw_cookie)
|
||||
.map(|(n, _)| n)
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
cookie.domain
|
||||
);
|
||||
debug!("Storing cookie: {} for domain {:?}", cookie.name, cookie.domain);
|
||||
cookies.push(cookie);
|
||||
}
|
||||
}
|
||||
@@ -117,10 +108,9 @@ impl CookieStore {
|
||||
}
|
||||
|
||||
// Check path
|
||||
let (cookie_path, _) = &cookie.path;
|
||||
let url_path = url.path();
|
||||
|
||||
path_matches(url_path, cookie_path)
|
||||
path_matches(url_path, &cookie.path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,8 +123,7 @@ pub fn get_cookie_value_from_jar(
|
||||
let domain = domain.and_then(normalize_cookie_domain_filter);
|
||||
|
||||
cookies.into_iter().find_map(|cookie| {
|
||||
let (cookie_name, value) = parse_cookie_name_value(&cookie.raw_cookie)?;
|
||||
if cookie_name != name {
|
||||
if cookie.name != name {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -144,11 +133,12 @@ pub fn get_cookie_value_from_jar(
|
||||
}
|
||||
}
|
||||
|
||||
Some(value)
|
||||
Some(cookie.value)
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse name=value from a cookie string (raw_cookie format)
|
||||
#[cfg(test)]
|
||||
fn parse_cookie_name_value(raw_cookie: &str) -> Option<(String, String)> {
|
||||
// The raw_cookie typically looks like "name=value" or "name=value; attr1; attr2=..."
|
||||
let first_part = raw_cookie.split(';').next()?;
|
||||
@@ -177,8 +167,6 @@ fn cookie_domain_matches_filter(cookie_domain: &CookieDomain, domain: &str) -> b
|
||||
fn parse_set_cookie(header_value: &str, request_url: &Url) -> Option<Cookie> {
|
||||
let parsed = cookie::Cookie::parse(header_value).ok()?;
|
||||
|
||||
let raw_cookie = format!("{}={}", parsed.name(), parsed.value());
|
||||
|
||||
// Determine domain
|
||||
let domain = if let Some(domain_attr) = parsed.domain() {
|
||||
// Domain attribute present - this is a suffix match
|
||||
@@ -216,14 +204,28 @@ fn parse_set_cookie(header_value: &str, request_url: &Url) -> Option<Cookie> {
|
||||
|
||||
// Determine path
|
||||
let path = if let Some(path_attr) = parsed.path() {
|
||||
(path_attr.to_string(), true)
|
||||
path_attr.to_string()
|
||||
} else {
|
||||
// Default path is the directory of the request URI
|
||||
let default_path = default_cookie_path(request_url.path());
|
||||
(default_path, false)
|
||||
default_cookie_path(request_url.path())
|
||||
};
|
||||
|
||||
Some(Cookie { raw_cookie, domain, expires, path })
|
||||
let same_site = parsed.same_site().map(|same_site| match same_site {
|
||||
cookie::SameSite::Strict => CookieSameSite::Strict,
|
||||
cookie::SameSite::Lax => CookieSameSite::Lax,
|
||||
cookie::SameSite::None => CookieSameSite::None,
|
||||
});
|
||||
|
||||
Some(Cookie {
|
||||
name: parsed.name().to_string(),
|
||||
value: parsed.value().to_string(),
|
||||
domain,
|
||||
expires,
|
||||
path,
|
||||
secure: parsed.secure().unwrap_or(false),
|
||||
http_only: parsed.http_only().unwrap_or(false),
|
||||
same_site,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the default cookie path from a request path (RFC 6265 Section 5.1.4)
|
||||
@@ -261,10 +263,7 @@ fn path_matches(request_path: &str, cookie_path: &str) -> bool {
|
||||
|
||||
/// Check if two cookies match (same name and domain)
|
||||
fn cookies_match(a: &Cookie, b: &Cookie) -> bool {
|
||||
let name_a = parse_cookie_name_value(&a.raw_cookie).map(|(n, _)| n);
|
||||
let name_b = parse_cookie_name_value(&b.raw_cookie).map(|(n, _)| n);
|
||||
|
||||
if name_a != name_b {
|
||||
if a.name != b.name {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -317,11 +316,16 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cookie(raw_cookie: &str, domain: CookieDomain) -> Cookie {
|
||||
let (name, value) = parse_cookie_name_value(raw_cookie).unwrap();
|
||||
Cookie {
|
||||
raw_cookie: raw_cookie.to_string(),
|
||||
name,
|
||||
value,
|
||||
domain,
|
||||
expires: CookieExpires::SessionEnd,
|
||||
path: ("/".to_string(), false),
|
||||
path: "/".to_string(),
|
||||
secure: false,
|
||||
http_only: false,
|
||||
same_site: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,13 @@ pub enum RedirectBehavior {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HttpResponseEvent {
|
||||
Setting(String, String),
|
||||
Setting {
|
||||
name: String,
|
||||
value: String,
|
||||
source_model: Option<String>,
|
||||
source_id: Option<String>,
|
||||
source_name: Option<String>,
|
||||
},
|
||||
Info(String),
|
||||
Redirect {
|
||||
url: String,
|
||||
@@ -67,7 +73,9 @@ pub enum HttpResponseEvent {
|
||||
impl Display for HttpResponseEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
HttpResponseEvent::Setting(name, value) => write!(f, "* Setting {}={}", name, value),
|
||||
HttpResponseEvent::Setting { name, value, .. } => {
|
||||
write!(f, "* Setting {}={}", name, value)
|
||||
}
|
||||
HttpResponseEvent::Info(s) => write!(f, "* {}", s),
|
||||
HttpResponseEvent::Redirect {
|
||||
url,
|
||||
@@ -146,7 +154,9 @@ impl From<HttpResponseEvent> for yaak_models::models::HttpResponseEventData {
|
||||
fn from(event: HttpResponseEvent) -> Self {
|
||||
use yaak_models::models::HttpResponseEventData as D;
|
||||
match event {
|
||||
HttpResponseEvent::Setting(name, value) => D::Setting { name, value },
|
||||
HttpResponseEvent::Setting { name, value, source_model, source_id, source_name } => {
|
||||
D::Setting { name, value, source_model, source_id, source_name }
|
||||
}
|
||||
HttpResponseEvent::Info(message) => D::Info { message },
|
||||
HttpResponseEvent::Redirect {
|
||||
url,
|
||||
@@ -483,15 +493,6 @@ impl HttpSender for ReqwestSender {
|
||||
|
||||
// Send the request
|
||||
let sendable_req = req_builder.build()?;
|
||||
send_event(HttpResponseEvent::Setting(
|
||||
"timeout".to_string(),
|
||||
if request.options.timeout.unwrap_or_default().is_zero() {
|
||||
"Infinity".to_string()
|
||||
} else {
|
||||
format!("{:?}", request.options.timeout)
|
||||
},
|
||||
));
|
||||
|
||||
send_event(HttpResponseEvent::SendUrl {
|
||||
method: sendable_req.method().to_string(),
|
||||
scheme: sendable_req.url().scheme().to_string(),
|
||||
|
||||
@@ -12,22 +12,58 @@ pub struct HttpTransaction<S: HttpSender> {
|
||||
sender: S,
|
||||
max_redirects: usize,
|
||||
cookie_store: Option<CookieStore>,
|
||||
send_cookies: bool,
|
||||
store_cookies: bool,
|
||||
}
|
||||
|
||||
impl<S: HttpSender> HttpTransaction<S> {
|
||||
/// Create a new transaction with default settings
|
||||
pub fn new(sender: S) -> Self {
|
||||
Self { sender, max_redirects: 10, cookie_store: None }
|
||||
Self {
|
||||
sender,
|
||||
max_redirects: 10,
|
||||
cookie_store: None,
|
||||
send_cookies: false,
|
||||
store_cookies: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new transaction with custom max redirects
|
||||
pub fn with_max_redirects(sender: S, max_redirects: usize) -> Self {
|
||||
Self { sender, max_redirects, cookie_store: None }
|
||||
Self {
|
||||
sender,
|
||||
max_redirects,
|
||||
cookie_store: None,
|
||||
send_cookies: false,
|
||||
store_cookies: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new transaction with a cookie store
|
||||
pub fn with_cookie_store(sender: S, cookie_store: CookieStore) -> Self {
|
||||
Self { sender, max_redirects: 10, cookie_store: Some(cookie_store) }
|
||||
Self {
|
||||
sender,
|
||||
max_redirects: 10,
|
||||
cookie_store: Some(cookie_store),
|
||||
send_cookies: true,
|
||||
store_cookies: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new transaction with a cookie store and explicit send/store behavior
|
||||
pub fn with_cookie_behavior(
|
||||
sender: S,
|
||||
cookie_store: CookieStore,
|
||||
send_cookies: bool,
|
||||
store_cookies: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
sender,
|
||||
max_redirects: 10,
|
||||
cookie_store: Some(cookie_store),
|
||||
send_cookies,
|
||||
store_cookies,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new transaction with custom max redirects and a cookie store
|
||||
@@ -36,7 +72,13 @@ impl<S: HttpSender> HttpTransaction<S> {
|
||||
max_redirects: usize,
|
||||
cookie_store: Option<CookieStore>,
|
||||
) -> Self {
|
||||
Self { sender, max_redirects, cookie_store }
|
||||
Self {
|
||||
sender,
|
||||
max_redirects,
|
||||
send_cookies: cookie_store.is_some(),
|
||||
store_cookies: cookie_store.is_some(),
|
||||
cookie_store,
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute the request with cancellation support.
|
||||
@@ -66,9 +108,11 @@ impl<S: HttpSender> HttpTransaction<S> {
|
||||
}
|
||||
|
||||
// Inject cookies into headers if we have a cookie store
|
||||
let headers_with_cookies = if let Some(cookie_store) = &self.cookie_store {
|
||||
let headers_with_cookies = if self.send_cookies {
|
||||
let mut headers = current_headers.clone();
|
||||
if let Ok(url) = Url::parse(¤t_url) {
|
||||
if let (Some(cookie_store), Ok(url)) =
|
||||
(&self.cookie_store, Url::parse(¤t_url))
|
||||
{
|
||||
if let Some(cookie_header) = cookie_store.get_cookie_header(&url) {
|
||||
debug!("Injecting Cookie header: {}", cookie_header);
|
||||
// Check if there's already a Cookie header and merge if so
|
||||
@@ -100,12 +144,6 @@ impl<S: HttpSender> HttpTransaction<S> {
|
||||
options: request.options.clone(),
|
||||
};
|
||||
|
||||
// Send the request
|
||||
send_event(HttpResponseEvent::Setting(
|
||||
"redirects".to_string(),
|
||||
request.options.follow_redirects.to_string(),
|
||||
));
|
||||
|
||||
// Execute with cancellation support
|
||||
let response = tokio::select! {
|
||||
result = self.sender.send(req, event_tx.clone()) => result?,
|
||||
@@ -115,8 +153,10 @@ impl<S: HttpSender> HttpTransaction<S> {
|
||||
};
|
||||
|
||||
// Parse Set-Cookie headers and store cookies
|
||||
if let Some(cookie_store) = &self.cookie_store {
|
||||
if let Ok(url) = Url::parse(¤t_url) {
|
||||
if self.store_cookies {
|
||||
if let (Some(cookie_store), Ok(url)) =
|
||||
(&self.cookie_store, Url::parse(¤t_url))
|
||||
{
|
||||
let set_cookie_headers: Vec<String> = response
|
||||
.headers
|
||||
.iter()
|
||||
@@ -579,10 +619,14 @@ mod tests {
|
||||
|
||||
// Create a cookie store with a test cookie
|
||||
let cookie = Cookie {
|
||||
raw_cookie: "session=abc123".to_string(),
|
||||
name: "session".to_string(),
|
||||
value: "abc123".to_string(),
|
||||
domain: CookieDomain::HostOnly("example.com".to_string()),
|
||||
expires: CookieExpires::SessionEnd,
|
||||
path: ("/".to_string(), false),
|
||||
path: "/".to_string(),
|
||||
secure: false,
|
||||
http_only: false,
|
||||
same_site: None,
|
||||
};
|
||||
let cookie_store = CookieStore::from_cookies(vec![cookie]);
|
||||
|
||||
@@ -602,6 +646,67 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cookie_injection_can_be_disabled() {
|
||||
struct CookieRejectingSender;
|
||||
|
||||
#[async_trait]
|
||||
impl HttpSender for CookieRejectingSender {
|
||||
async fn send(
|
||||
&self,
|
||||
request: SendableHttpRequest,
|
||||
_event_tx: mpsc::Sender<HttpResponseEvent>,
|
||||
) -> Result<HttpResponse> {
|
||||
let cookie_header =
|
||||
request.headers.iter().find(|(k, _)| k.eq_ignore_ascii_case("cookie"));
|
||||
assert!(cookie_header.is_none(), "Cookie header should not be present");
|
||||
|
||||
let body_stream: Pin<Box<dyn AsyncRead + Send>> =
|
||||
Box::pin(std::io::Cursor::new(vec![]));
|
||||
Ok(HttpResponse::new(
|
||||
200,
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
"https://example.com".to_string(),
|
||||
None,
|
||||
Some("HTTP/1.1".to_string()),
|
||||
body_stream,
|
||||
ContentEncoding::Identity,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
use yaak_models::models::{Cookie, CookieDomain, CookieExpires};
|
||||
|
||||
let cookie = Cookie {
|
||||
name: "session".to_string(),
|
||||
value: "abc123".to_string(),
|
||||
domain: CookieDomain::HostOnly("example.com".to_string()),
|
||||
expires: CookieExpires::SessionEnd,
|
||||
path: "/".to_string(),
|
||||
secure: false,
|
||||
http_only: false,
|
||||
same_site: None,
|
||||
};
|
||||
let cookie_store = CookieStore::from_cookies(vec![cookie]);
|
||||
let transaction =
|
||||
HttpTransaction::with_cookie_behavior(CookieRejectingSender, cookie_store, false, true);
|
||||
|
||||
let request = SendableHttpRequest {
|
||||
url: "https://example.com/api".to_string(),
|
||||
method: "GET".to_string(),
|
||||
headers: vec![],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (_tx, rx) = tokio::sync::watch::channel(false);
|
||||
let (event_tx, _event_rx) = mpsc::channel(100);
|
||||
let result = transaction.execute_with_cancellation(request, rx, event_tx).await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_cookie_parsing() {
|
||||
// Create a cookie store
|
||||
@@ -655,7 +760,62 @@ mod tests {
|
||||
// Verify the cookie was stored
|
||||
let cookies = cookie_store.get_all_cookies();
|
||||
assert_eq!(cookies.len(), 1);
|
||||
assert!(cookies[0].raw_cookie.contains("session=xyz789"));
|
||||
assert_eq!(cookies[0].name, "session");
|
||||
assert_eq!(cookies[0].value, "xyz789");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_cookie_storage_can_be_disabled() {
|
||||
let cookie_store = CookieStore::new();
|
||||
|
||||
struct SetCookieSender;
|
||||
|
||||
#[async_trait]
|
||||
impl HttpSender for SetCookieSender {
|
||||
async fn send(
|
||||
&self,
|
||||
_request: SendableHttpRequest,
|
||||
_event_tx: mpsc::Sender<HttpResponseEvent>,
|
||||
) -> Result<HttpResponse> {
|
||||
let headers =
|
||||
vec![("set-cookie".to_string(), "session=xyz789; Path=/".to_string())];
|
||||
|
||||
let body_stream: Pin<Box<dyn AsyncRead + Send>> =
|
||||
Box::pin(std::io::Cursor::new(vec![]));
|
||||
Ok(HttpResponse::new(
|
||||
200,
|
||||
None,
|
||||
headers,
|
||||
Vec::new(),
|
||||
None,
|
||||
"https://example.com".to_string(),
|
||||
None,
|
||||
Some("HTTP/1.1".to_string()),
|
||||
body_stream,
|
||||
ContentEncoding::Identity,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
let transaction = HttpTransaction::with_cookie_behavior(
|
||||
SetCookieSender,
|
||||
cookie_store.clone(),
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
let request = SendableHttpRequest {
|
||||
url: "https://example.com/login".to_string(),
|
||||
method: "POST".to_string(),
|
||||
headers: vec![],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (_tx, rx) = tokio::sync::watch::channel(false);
|
||||
let (event_tx, _event_rx) = mpsc::channel(100);
|
||||
let result = transaction.execute_with_cancellation(request, rx, event_tx).await;
|
||||
assert!(result.is_ok());
|
||||
assert!(cookie_store.get_all_cookies().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -719,17 +879,15 @@ mod tests {
|
||||
let cookies = cookie_store.get_all_cookies();
|
||||
assert_eq!(cookies.len(), 3, "All three Set-Cookie headers should be parsed and stored");
|
||||
|
||||
let cookie_values: Vec<&str> = cookies.iter().map(|c| c.raw_cookie.as_str()).collect();
|
||||
let cookie_values: Vec<_> =
|
||||
cookies.iter().map(|c| format!("{}={}", c.name, c.value)).collect();
|
||||
assert!(
|
||||
cookie_values.iter().any(|c| c.contains("session=abc123")),
|
||||
cookie_values.iter().any(|c| c == "session=abc123"),
|
||||
"session cookie should be stored"
|
||||
);
|
||||
assert!(cookie_values.iter().any(|c| c == "user_id=42"), "user_id cookie should be stored");
|
||||
assert!(
|
||||
cookie_values.iter().any(|c| c.contains("user_id=42")),
|
||||
"user_id cookie should be stored"
|
||||
);
|
||||
assert!(
|
||||
cookie_values.iter().any(|c| c.contains("preferences=dark")),
|
||||
cookie_values.iter().any(|c| c == "preferences=dark"),
|
||||
"preferences cookie should be stored"
|
||||
);
|
||||
}
|
||||
|
||||
+454
-61
@@ -1,121 +1,514 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ModelChangeEvent } from "./ModelChangeEvent";
|
||||
|
||||
export type AnyModel = CookieJar | Environment | Folder | GraphQlIntrospection | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | HttpResponseEvent | KeyValue | Plugin | Settings | SyncState | WebsocketConnection | WebsocketEvent | WebsocketRequest | Workspace | WorkspaceMeta;
|
||||
export type AnyModel =
|
||||
| CookieJar
|
||||
| Environment
|
||||
| Folder
|
||||
| GraphQlIntrospection
|
||||
| GrpcConnection
|
||||
| GrpcEvent
|
||||
| GrpcRequest
|
||||
| HttpRequest
|
||||
| HttpResponse
|
||||
| HttpResponseEvent
|
||||
| KeyValue
|
||||
| Plugin
|
||||
| Settings
|
||||
| SyncState
|
||||
| WebsocketConnection
|
||||
| WebsocketEvent
|
||||
| WebsocketRequest
|
||||
| Workspace
|
||||
| WorkspaceMeta;
|
||||
|
||||
export type ClientCertificate = { host: string, port: number | null, crtFile: string | null, keyFile: string | null, pfxFile: string | null, passphrase: string | null, enabled?: boolean, };
|
||||
export type ClientCertificate = {
|
||||
host: string;
|
||||
port: number | null;
|
||||
crtFile: string | null;
|
||||
keyFile: string | null;
|
||||
pfxFile: string | null;
|
||||
passphrase: string | null;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type Cookie = { raw_cookie: string, domain: CookieDomain, expires: CookieExpires, path: [string, boolean], };
|
||||
export type Cookie = {
|
||||
name: string;
|
||||
value: string;
|
||||
domain: CookieDomain;
|
||||
expires: CookieExpires;
|
||||
path: string;
|
||||
secure: boolean;
|
||||
httpOnly: boolean;
|
||||
sameSite: CookieSameSite | null;
|
||||
};
|
||||
|
||||
export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty";
|
||||
export type CookieDomain = { HostOnly: string } | { Suffix: string } | "NotPresent" | "Empty";
|
||||
|
||||
export type CookieExpires = { "AtUtc": string } | "SessionEnd";
|
||||
export type CookieExpires = { AtUtc: string } | "SessionEnd";
|
||||
|
||||
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
||||
export type CookieJar = {
|
||||
model: "cookie_jar";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
cookies: Array<Cookie>;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
||||
export type CookieSameSite = "Strict" | "Lax" | "None";
|
||||
|
||||
export type DnsOverride = {
|
||||
hostname: string;
|
||||
ipv4: Array<string>;
|
||||
ipv6: Array<string>;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||
|
||||
export type EncryptedKey = { encryptedKey: string, };
|
||||
export type EncryptedKey = { encryptedKey: string };
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
|
||||
/**
|
||||
* Variables defined in this environment scope.
|
||||
* Child environments override parent variables by name.
|
||||
*/
|
||||
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||
export type Environment = {
|
||||
model: "environment";
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
name: string;
|
||||
public: boolean;
|
||||
parentModel: string;
|
||||
parentId: string | null;
|
||||
/**
|
||||
* Variables defined in this environment scope.
|
||||
* Child environments override parent variables by name.
|
||||
*/
|
||||
variables: Array<EnvironmentVariable>;
|
||||
color: string | null;
|
||||
sortPriority: number;
|
||||
};
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string };
|
||||
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
|
||||
export type Folder = {
|
||||
model: "folder";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
settingValidateCertificates: InheritedBoolSetting;
|
||||
settingFollowRedirects: InheritedBoolSetting;
|
||||
settingRequestTimeout: InheritedIntSetting;
|
||||
};
|
||||
|
||||
export type GraphQlIntrospection = { model: "graphql_introspection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, content: string | null, };
|
||||
export type GraphQlIntrospection = {
|
||||
model: "graphql_introspection";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
content: string | null;
|
||||
};
|
||||
|
||||
export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, };
|
||||
export type GrpcConnection = {
|
||||
model: "grpc_connection";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
elapsed: number;
|
||||
error: string | null;
|
||||
method: string;
|
||||
service: string;
|
||||
status: number;
|
||||
state: GrpcConnectionState;
|
||||
trailers: { [key in string]?: string };
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type GrpcConnectionState = "initialized" | "connected" | "closed";
|
||||
|
||||
export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, content: string, error: string | null, eventType: GrpcEventType, metadata: { [key in string]?: string }, status: number | null, };
|
||||
export type GrpcEvent = {
|
||||
model: "grpc_event";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
connectionId: string;
|
||||
content: string;
|
||||
error: string | null;
|
||||
eventType: GrpcEventType;
|
||||
metadata: { [key in string]?: string };
|
||||
status: number | null;
|
||||
};
|
||||
|
||||
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
|
||||
export type GrpcEventType =
|
||||
| "info"
|
||||
| "error"
|
||||
| "client_message"
|
||||
| "server_message"
|
||||
| "connection_start"
|
||||
| "connection_end";
|
||||
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number,
|
||||
/**
|
||||
* Server URL (http for plaintext or https for secure)
|
||||
*/
|
||||
url: string, };
|
||||
export type GrpcRequest = {
|
||||
model: "grpc_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authenticationType: string | null;
|
||||
authentication: Record<string, any>;
|
||||
description: string;
|
||||
message: string;
|
||||
metadata: Array<HttpRequestHeader>;
|
||||
method: string | null;
|
||||
name: string;
|
||||
service: string | null;
|
||||
sortPriority: number;
|
||||
/**
|
||||
* Server URL (http for plaintext or https for secure)
|
||||
*/
|
||||
url: string;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
};
|
||||
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>, };
|
||||
export type HttpRequest = {
|
||||
model: "http_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
body: Record<string, any>;
|
||||
bodyType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
method: string;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
url: string;
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
settingValidateCertificates: InheritedBoolSetting;
|
||||
settingFollowRedirects: InheritedBoolSetting;
|
||||
settingRequestTimeout: InheritedIntSetting;
|
||||
};
|
||||
|
||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string };
|
||||
|
||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||
export type HttpResponse = {
|
||||
model: "http_response";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
bodyPath: string | null;
|
||||
contentLength: number | null;
|
||||
contentLengthCompressed: number | null;
|
||||
elapsed: number;
|
||||
elapsedHeaders: number;
|
||||
elapsedDns: number;
|
||||
error: string | null;
|
||||
headers: Array<HttpResponseHeader>;
|
||||
remoteAddr: string | null;
|
||||
requestContentLength: number | null;
|
||||
requestHeaders: Array<HttpResponseHeader>;
|
||||
status: number;
|
||||
statusReason: string | null;
|
||||
state: HttpResponseState;
|
||||
url: string;
|
||||
version: string | null;
|
||||
};
|
||||
|
||||
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
||||
export type HttpResponseEvent = {
|
||||
model: "http_response_event";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
responseId: string;
|
||||
event: HttpResponseEventData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializable representation of HTTP response events for DB storage.
|
||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||
*/
|
||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||
export type HttpResponseEventData =
|
||||
| {
|
||||
type: "setting";
|
||||
name: string;
|
||||
value: string;
|
||||
source_model?: string;
|
||||
source_id?: string;
|
||||
source_name?: string;
|
||||
}
|
||||
| { type: "info"; message: string }
|
||||
| {
|
||||
type: "redirect";
|
||||
url: string;
|
||||
status: number;
|
||||
behavior: string;
|
||||
dropped_body: boolean;
|
||||
dropped_headers: Array<string>;
|
||||
}
|
||||
| {
|
||||
type: "send_url";
|
||||
method: string;
|
||||
scheme: string;
|
||||
username: string;
|
||||
password: string;
|
||||
host: string;
|
||||
port: number;
|
||||
path: string;
|
||||
query: string;
|
||||
fragment: string;
|
||||
}
|
||||
| { type: "receive_url"; version: string; status: string }
|
||||
| { type: "header_up"; name: string; value: string }
|
||||
| { type: "header_down"; name: string; value: string }
|
||||
| { type: "chunk_sent"; bytes: number }
|
||||
| { type: "chunk_received"; bytes: number }
|
||||
| {
|
||||
type: "dns_resolved";
|
||||
hostname: string;
|
||||
addresses: Array<string>;
|
||||
duration: bigint;
|
||||
overridden: boolean;
|
||||
};
|
||||
|
||||
export type HttpResponseHeader = { name: string, value: string, };
|
||||
export type HttpResponseHeader = { name: string; value: string };
|
||||
|
||||
export type HttpResponseState = "initialized" | "connected" | "closed";
|
||||
|
||||
export type HttpUrlParameter = { enabled?: boolean,
|
||||
/**
|
||||
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||
* Other entries are appended as query parameters
|
||||
*/
|
||||
name: string, value: string, id?: string, };
|
||||
export type HttpUrlParameter = {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||
* Other entries are appended as query parameters
|
||||
*/
|
||||
name: string;
|
||||
value: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
|
||||
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
|
||||
|
||||
export type ModelPayload = { model: AnyModel, updateSource: UpdateSource, change: ModelChangeEvent, };
|
||||
export type InheritedIntSetting = { enabled?: boolean; value: number };
|
||||
|
||||
export type ParentAuthentication = { authentication: Record<string, any>, authenticationType: string | null, };
|
||||
export type KeyValue = {
|
||||
model: "key_value";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
key: string;
|
||||
namespace: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type ParentHeaders = { headers: Array<HttpRequestHeader>, };
|
||||
export type ModelPayload = {
|
||||
model: AnyModel;
|
||||
updateSource: UpdateSource;
|
||||
change: ModelChangeEvent;
|
||||
};
|
||||
|
||||
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
|
||||
export type ParentAuthentication = {
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
};
|
||||
|
||||
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
|
||||
export type ParentHeaders = { headers: Array<HttpRequestHeader> };
|
||||
|
||||
export type Plugin = {
|
||||
model: "plugin";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
checkedAt: string | null;
|
||||
directory: string;
|
||||
enabled: boolean;
|
||||
url: string | null;
|
||||
source: PluginSource;
|
||||
};
|
||||
|
||||
export type PluginKeyValue = {
|
||||
model: "plugin_key_value";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
pluginName: string;
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type PluginSource = "bundled" | "filesystem" | "registry";
|
||||
|
||||
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
|
||||
export type ProxySetting =
|
||||
| {
|
||||
type: "enabled";
|
||||
http: string;
|
||||
https: string;
|
||||
auth: ProxySettingAuth | null;
|
||||
bypass: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
| { type: "disabled" };
|
||||
|
||||
export type ProxySettingAuth = { user: string, password: string, };
|
||||
export type ProxySettingAuth = { user: string; password: string };
|
||||
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, clientCertificates: Array<ClientCertificate>, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, useNativeTitlebar: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, checkNotifications: boolean, hotkeys: { [key in string]?: Array<string> }, };
|
||||
export type Settings = {
|
||||
model: "settings";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
appearance: string;
|
||||
clientCertificates: Array<ClientCertificate>;
|
||||
coloredMethods: boolean;
|
||||
editorFont: string | null;
|
||||
editorFontSize: number;
|
||||
editorKeymap: EditorKeymap;
|
||||
editorSoftWrap: boolean;
|
||||
hideWindowControls: boolean;
|
||||
useNativeTitlebar: boolean;
|
||||
interfaceFont: string | null;
|
||||
interfaceFontSize: number;
|
||||
interfaceScale: number;
|
||||
openWorkspaceNewWindow: boolean | null;
|
||||
proxy: ProxySetting | null;
|
||||
themeDark: string;
|
||||
themeLight: string;
|
||||
updateChannel: string;
|
||||
hideLicenseBadge: boolean;
|
||||
autoupdate: boolean;
|
||||
autoDownloadUpdates: boolean;
|
||||
checkNotifications: boolean;
|
||||
hotkeys: { [key in string]?: Array<string> };
|
||||
};
|
||||
|
||||
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
|
||||
export type SyncState = {
|
||||
model: "sync_state";
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
flushedAt: string;
|
||||
modelId: string;
|
||||
checksum: string;
|
||||
relPath: string;
|
||||
syncDir: string;
|
||||
};
|
||||
|
||||
export type UpdateSource = { "type": "background" } | { "type": "import" } | { "type": "plugin" } | { "type": "sync" } | { "type": "window", label: string, };
|
||||
export type UpdateSource =
|
||||
| { type: "background" }
|
||||
| { type: "import" }
|
||||
| { type: "plugin" }
|
||||
| { type: "sync" }
|
||||
| { type: "window"; label: string };
|
||||
|
||||
export type WebsocketConnection = { model: "websocket_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, headers: Array<HttpResponseHeader>, state: WebsocketConnectionState, status: number, url: string, };
|
||||
export type WebsocketConnection = {
|
||||
model: "websocket_connection";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
elapsed: number;
|
||||
error: string | null;
|
||||
headers: Array<HttpResponseHeader>;
|
||||
state: WebsocketConnectionState;
|
||||
status: number;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed";
|
||||
|
||||
export type WebsocketEvent = { model: "websocket_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, isServer: boolean, message: Array<number>, messageType: WebsocketEventType, };
|
||||
export type WebsocketEvent = {
|
||||
model: "websocket_event";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
connectionId: string;
|
||||
isServer: boolean;
|
||||
message: Array<number>;
|
||||
messageType: WebsocketEventType;
|
||||
};
|
||||
|
||||
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
|
||||
|
||||
export type WebsocketMessageType = "text" | "binary";
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string,
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>, };
|
||||
export type WebsocketRequest = {
|
||||
model: "websocket_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
message: string;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
url: string;
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
};
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||
export type Workspace = {
|
||||
model: "workspace";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
name: string;
|
||||
encryptionKeyChallenge: string | null;
|
||||
settingValidateCertificates: boolean;
|
||||
settingFollowRedirects: boolean;
|
||||
settingRequestTimeout: number;
|
||||
settingDnsOverrides: Array<DnsOverride>;
|
||||
settingSendCookies: boolean;
|
||||
settingStoreCookies: boolean;
|
||||
};
|
||||
|
||||
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
||||
export type WorkspaceMeta = {
|
||||
model: "workspace_meta";
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
encryptionKey: EncryptedKey | null;
|
||||
settingSyncDir: string | null;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
ALTER TABLE workspaces ADD COLUMN setting_send_cookies BOOLEAN DEFAULT TRUE NOT NULL;
|
||||
ALTER TABLE workspaces ADD COLUMN setting_store_cookies BOOLEAN DEFAULT TRUE NOT NULL;
|
||||
|
||||
ALTER TABLE folders ADD COLUMN setting_send_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
|
||||
ALTER TABLE folders ADD COLUMN setting_store_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
|
||||
ALTER TABLE folders ADD COLUMN setting_request_timeout TEXT DEFAULT '{"enabled":false,"value":0}' NOT NULL;
|
||||
ALTER TABLE folders ADD COLUMN setting_validate_certificates TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
|
||||
ALTER TABLE folders ADD COLUMN setting_follow_redirects TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
|
||||
|
||||
ALTER TABLE http_requests ADD COLUMN setting_send_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
|
||||
ALTER TABLE http_requests ADD COLUMN setting_store_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
|
||||
ALTER TABLE http_requests ADD COLUMN setting_request_timeout TEXT DEFAULT '{"enabled":false,"value":0}' NOT NULL;
|
||||
ALTER TABLE http_requests ADD COLUMN setting_validate_certificates TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
|
||||
ALTER TABLE http_requests ADD COLUMN setting_follow_redirects TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
|
||||
|
||||
ALTER TABLE websocket_requests ADD COLUMN setting_send_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
|
||||
ALTER TABLE websocket_requests ADD COLUMN setting_store_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
|
||||
|
||||
ALTER TABLE grpc_requests ADD COLUMN setting_send_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
|
||||
ALTER TABLE grpc_requests ADD COLUMN setting_store_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::error::Result;
|
||||
use crate::models::HttpRequestIden::{
|
||||
Authentication, AuthenticationType, Body, BodyType, CreatedAt, Description, FolderId, Headers,
|
||||
Method, Name, SortPriority, UpdatedAt, Url, UrlParameters, WorkspaceId,
|
||||
Method, Name, SettingFollowRedirects, SettingRequestTimeout, SettingSendCookies,
|
||||
SettingStoreCookies, SettingValidateCertificates, SortPriority, UpdatedAt, Url, UrlParameters,
|
||||
WorkspaceId,
|
||||
};
|
||||
use crate::util::generate_prefixed_id;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
@@ -90,6 +92,84 @@ pub struct DnsOverride {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct ResolvedSetting<T> {
|
||||
pub value: T,
|
||||
pub source_model: String,
|
||||
pub source_id: Option<String>,
|
||||
pub source_name: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> ResolvedSetting<T> {
|
||||
pub fn from_model(value: T, model: AnyModel) -> Self {
|
||||
Self {
|
||||
value,
|
||||
source_model: model.model().to_string(),
|
||||
source_id: Some(model.id().to_string()),
|
||||
source_name: Some(model.resolved_name()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_source(value: T) -> Self {
|
||||
Self { value, source_model: "default".to_string(), source_id: None, source_name: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ResolvedHttpRequestSettings {
|
||||
pub validate_certificates: ResolvedSetting<bool>,
|
||||
pub follow_redirects: ResolvedSetting<bool>,
|
||||
pub request_timeout: ResolvedSetting<i32>,
|
||||
pub send_cookies: ResolvedSetting<bool>,
|
||||
pub store_cookies: ResolvedSetting<bool>,
|
||||
}
|
||||
|
||||
impl Default for ResolvedHttpRequestSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
validate_certificates: ResolvedSetting::default_source(true),
|
||||
follow_redirects: ResolvedSetting::default_source(true),
|
||||
request_timeout: ResolvedSetting::default_source(0),
|
||||
send_cookies: ResolvedSetting::default_source(true),
|
||||
store_cookies: ResolvedSetting::default_source(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct InheritedBoolSetting {
|
||||
#[serde(default)]
|
||||
#[ts(optional, as = "Option<bool>")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub value: bool,
|
||||
}
|
||||
|
||||
impl Default for InheritedBoolSetting {
|
||||
fn default() -> Self {
|
||||
Self { enabled: false, value: true }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct InheritedIntSetting {
|
||||
#[serde(default)]
|
||||
#[ts(optional, as = "Option<bool>")]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub value: i32,
|
||||
}
|
||||
|
||||
impl Default for InheritedIntSetting {
|
||||
fn default() -> Self {
|
||||
Self { enabled: false, value: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
@@ -322,6 +402,10 @@ pub struct Workspace {
|
||||
pub setting_request_timeout: i32,
|
||||
#[serde(default)]
|
||||
pub setting_dns_overrides: Vec<DnsOverride>,
|
||||
#[serde(default = "default_true")]
|
||||
pub setting_send_cookies: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub setting_store_cookies: bool,
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for Workspace {
|
||||
@@ -363,6 +447,8 @@ impl UpsertModelInfo for Workspace {
|
||||
(SettingRequestTimeout, self.setting_request_timeout.into()),
|
||||
(SettingValidateCertificates, self.setting_validate_certificates.into()),
|
||||
(SettingDnsOverrides, serde_json::to_string(&self.setting_dns_overrides)?.into()),
|
||||
(SettingSendCookies, self.setting_send_cookies.into()),
|
||||
(SettingStoreCookies, self.setting_store_cookies.into()),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -380,6 +466,8 @@ impl UpsertModelInfo for Workspace {
|
||||
WorkspaceIden::SettingRequestTimeout,
|
||||
WorkspaceIden::SettingValidateCertificates,
|
||||
WorkspaceIden::SettingDnsOverrides,
|
||||
WorkspaceIden::SettingSendCookies,
|
||||
WorkspaceIden::SettingStoreCookies,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -405,6 +493,8 @@ impl UpsertModelInfo for Workspace {
|
||||
setting_request_timeout: row.get("setting_request_timeout")?,
|
||||
setting_validate_certificates: row.get("setting_validate_certificates")?,
|
||||
setting_dns_overrides: serde_json::from_str(&setting_dns_overrides).unwrap_or_default(),
|
||||
setting_send_cookies: row.get("setting_send_cookies")?,
|
||||
setting_store_cookies: row.get("setting_store_cookies")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -509,11 +599,127 @@ pub enum CookieExpires {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub enum CookieSameSite {
|
||||
Strict,
|
||||
Lax,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct Cookie {
|
||||
pub raw_cookie: String,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
pub domain: CookieDomain,
|
||||
pub expires: CookieExpires,
|
||||
pub path: (String, bool),
|
||||
pub path: String,
|
||||
pub secure: bool,
|
||||
pub http_only: bool,
|
||||
pub same_site: Option<CookieSameSite>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CookieFields {
|
||||
name: String,
|
||||
value: String,
|
||||
domain: CookieDomain,
|
||||
expires: CookieExpires,
|
||||
path: String,
|
||||
#[serde(default)]
|
||||
secure: bool,
|
||||
#[serde(default)]
|
||||
http_only: bool,
|
||||
#[serde(default)]
|
||||
same_site: Option<CookieSameSite>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LegacyCookie {
|
||||
raw_cookie: String,
|
||||
domain: CookieDomain,
|
||||
expires: CookieExpires,
|
||||
path: (String, bool),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum CookieCompat {
|
||||
New(CookieFields),
|
||||
Legacy(LegacyCookie),
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Cookie {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(match CookieCompat::deserialize(deserializer)? {
|
||||
CookieCompat::New(cookie) => Self {
|
||||
name: cookie.name,
|
||||
value: cookie.value,
|
||||
domain: cookie.domain,
|
||||
expires: cookie.expires,
|
||||
path: cookie.path,
|
||||
secure: cookie.secure,
|
||||
http_only: cookie.http_only,
|
||||
same_site: cookie.same_site,
|
||||
},
|
||||
CookieCompat::Legacy(cookie) => {
|
||||
let (name, value, secure, http_only, same_site) =
|
||||
parse_legacy_cookie_parts(&cookie.raw_cookie);
|
||||
Self {
|
||||
name,
|
||||
value,
|
||||
domain: cookie.domain,
|
||||
expires: cookie.expires,
|
||||
path: cookie.path.0,
|
||||
secure,
|
||||
http_only,
|
||||
same_site,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_legacy_cookie_parts(
|
||||
raw_cookie: &str,
|
||||
) -> (String, String, bool, bool, Option<CookieSameSite>) {
|
||||
let mut parts = raw_cookie.split(';').map(str::trim);
|
||||
let (name, value) = parts
|
||||
.next()
|
||||
.and_then(|part| {
|
||||
let mut nv = part.splitn(2, '=');
|
||||
Some((nv.next()?.trim().to_string(), nv.next().unwrap_or("").trim().to_string()))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut secure = false;
|
||||
let mut http_only = false;
|
||||
let mut same_site = None;
|
||||
|
||||
for part in parts {
|
||||
let mut attr = part.splitn(2, '=');
|
||||
let key = attr.next().unwrap_or("").trim().to_lowercase();
|
||||
let value = attr.next().unwrap_or("").trim().to_lowercase();
|
||||
match key.as_str() {
|
||||
"secure" => secure = true,
|
||||
"httponly" => http_only = true,
|
||||
"samesite" => {
|
||||
same_site = match value.as_str() {
|
||||
"strict" => Some(CookieSameSite::Strict),
|
||||
"lax" => Some(CookieSameSite::Lax),
|
||||
"none" => Some(CookieSameSite::None),
|
||||
_ => same_site,
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
(name, value, secure, http_only, same_site)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||
@@ -751,6 +957,11 @@ pub struct Folder {
|
||||
pub headers: Vec<HttpRequestHeader>,
|
||||
pub name: String,
|
||||
pub sort_priority: f64,
|
||||
pub setting_send_cookies: InheritedBoolSetting,
|
||||
pub setting_store_cookies: InheritedBoolSetting,
|
||||
pub setting_validate_certificates: InheritedBoolSetting,
|
||||
pub setting_follow_redirects: InheritedBoolSetting,
|
||||
pub setting_request_timeout: InheritedIntSetting,
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for Folder {
|
||||
@@ -790,6 +1001,14 @@ impl UpsertModelInfo for Folder {
|
||||
(Description, self.description.into()),
|
||||
(Name, self.name.trim().into()),
|
||||
(SortPriority, self.sort_priority.into()),
|
||||
(SettingSendCookies, serde_json::to_string(&self.setting_send_cookies)?.into()),
|
||||
(SettingStoreCookies, serde_json::to_string(&self.setting_store_cookies)?.into()),
|
||||
(
|
||||
SettingValidateCertificates,
|
||||
serde_json::to_string(&self.setting_validate_certificates)?.into(),
|
||||
),
|
||||
(SettingFollowRedirects, serde_json::to_string(&self.setting_follow_redirects)?.into()),
|
||||
(SettingRequestTimeout, serde_json::to_string(&self.setting_request_timeout)?.into()),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -803,6 +1022,11 @@ impl UpsertModelInfo for Folder {
|
||||
FolderIden::Description,
|
||||
FolderIden::FolderId,
|
||||
FolderIden::SortPriority,
|
||||
FolderIden::SettingSendCookies,
|
||||
FolderIden::SettingStoreCookies,
|
||||
FolderIden::SettingValidateCertificates,
|
||||
FolderIden::SettingFollowRedirects,
|
||||
FolderIden::SettingRequestTimeout,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -812,6 +1036,11 @@ impl UpsertModelInfo for Folder {
|
||||
{
|
||||
let headers: String = row.get("headers")?;
|
||||
let authentication: String = row.get("authentication")?;
|
||||
let setting_send_cookies: String = row.get("setting_send_cookies")?;
|
||||
let setting_store_cookies: String = row.get("setting_store_cookies")?;
|
||||
let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
|
||||
let setting_follow_redirects: String = row.get("setting_follow_redirects")?;
|
||||
let setting_request_timeout: String = row.get("setting_request_timeout")?;
|
||||
Ok(Self {
|
||||
id: row.get("id")?,
|
||||
model: row.get("model")?,
|
||||
@@ -825,6 +1054,14 @@ impl UpsertModelInfo for Folder {
|
||||
headers: serde_json::from_str(&headers).unwrap_or_default(),
|
||||
authentication_type: row.get("authentication_type")?,
|
||||
authentication: serde_json::from_str(&authentication).unwrap_or_default(),
|
||||
setting_send_cookies: serde_json::from_str(&setting_send_cookies).unwrap_or_default(),
|
||||
setting_store_cookies: serde_json::from_str(&setting_store_cookies).unwrap_or_default(),
|
||||
setting_validate_certificates: serde_json::from_str(&setting_validate_certificates)
|
||||
.unwrap_or_default(),
|
||||
setting_follow_redirects: serde_json::from_str(&setting_follow_redirects)
|
||||
.unwrap_or_default(),
|
||||
setting_request_timeout: serde_json::from_str(&setting_request_timeout)
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -885,6 +1122,11 @@ pub struct HttpRequest {
|
||||
pub url: String,
|
||||
/// URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
pub url_parameters: Vec<HttpUrlParameter>,
|
||||
pub setting_send_cookies: InheritedBoolSetting,
|
||||
pub setting_store_cookies: InheritedBoolSetting,
|
||||
pub setting_validate_certificates: InheritedBoolSetting,
|
||||
pub setting_follow_redirects: InheritedBoolSetting,
|
||||
pub setting_request_timeout: InheritedIntSetting,
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for HttpRequest {
|
||||
@@ -928,6 +1170,14 @@ impl UpsertModelInfo for HttpRequest {
|
||||
(AuthenticationType, self.authentication_type.into()),
|
||||
(Headers, serde_json::to_string(&self.headers)?.into()),
|
||||
(SortPriority, self.sort_priority.into()),
|
||||
(SettingSendCookies, serde_json::to_string(&self.setting_send_cookies)?.into()),
|
||||
(SettingStoreCookies, serde_json::to_string(&self.setting_store_cookies)?.into()),
|
||||
(
|
||||
SettingValidateCertificates,
|
||||
serde_json::to_string(&self.setting_validate_certificates)?.into(),
|
||||
),
|
||||
(SettingFollowRedirects, serde_json::to_string(&self.setting_follow_redirects)?.into()),
|
||||
(SettingRequestTimeout, serde_json::to_string(&self.setting_request_timeout)?.into()),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -947,6 +1197,11 @@ impl UpsertModelInfo for HttpRequest {
|
||||
Url,
|
||||
UrlParameters,
|
||||
SortPriority,
|
||||
SettingSendCookies,
|
||||
SettingStoreCookies,
|
||||
SettingValidateCertificates,
|
||||
SettingFollowRedirects,
|
||||
SettingRequestTimeout,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -955,6 +1210,11 @@ impl UpsertModelInfo for HttpRequest {
|
||||
let body: String = row.get("body")?;
|
||||
let authentication: String = row.get("authentication")?;
|
||||
let headers: String = row.get("headers")?;
|
||||
let setting_send_cookies: String = row.get("setting_send_cookies")?;
|
||||
let setting_store_cookies: String = row.get("setting_store_cookies")?;
|
||||
let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
|
||||
let setting_follow_redirects: String = row.get("setting_follow_redirects")?;
|
||||
let setting_request_timeout: String = row.get("setting_request_timeout")?;
|
||||
Ok(Self {
|
||||
id: row.get("id")?,
|
||||
model: row.get("model")?,
|
||||
@@ -973,6 +1233,14 @@ impl UpsertModelInfo for HttpRequest {
|
||||
sort_priority: row.get("sort_priority")?,
|
||||
url: row.get("url")?,
|
||||
url_parameters: serde_json::from_str(url_parameters.as_str()).unwrap_or_default(),
|
||||
setting_send_cookies: serde_json::from_str(&setting_send_cookies).unwrap_or_default(),
|
||||
setting_store_cookies: serde_json::from_str(&setting_store_cookies).unwrap_or_default(),
|
||||
setting_validate_certificates: serde_json::from_str(&setting_validate_certificates)
|
||||
.unwrap_or_default(),
|
||||
setting_follow_redirects: serde_json::from_str(&setting_follow_redirects)
|
||||
.unwrap_or_default(),
|
||||
setting_request_timeout: serde_json::from_str(&setting_request_timeout)
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1127,6 +1395,8 @@ pub struct WebsocketRequest {
|
||||
pub url: String,
|
||||
/// URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
pub url_parameters: Vec<HttpUrlParameter>,
|
||||
pub setting_send_cookies: InheritedBoolSetting,
|
||||
pub setting_store_cookies: InheritedBoolSetting,
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for WebsocketRequest {
|
||||
@@ -1169,6 +1439,8 @@ impl UpsertModelInfo for WebsocketRequest {
|
||||
(SortPriority, self.sort_priority.into()),
|
||||
(Url, self.url.into()),
|
||||
(UrlParameters, serde_json::to_string(&self.url_parameters)?.into()),
|
||||
(SettingSendCookies, serde_json::to_string(&self.setting_send_cookies)?.into()),
|
||||
(SettingStoreCookies, serde_json::to_string(&self.setting_store_cookies)?.into()),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -1186,6 +1458,8 @@ impl UpsertModelInfo for WebsocketRequest {
|
||||
WebsocketRequestIden::SortPriority,
|
||||
WebsocketRequestIden::Url,
|
||||
WebsocketRequestIden::UrlParameters,
|
||||
WebsocketRequestIden::SettingSendCookies,
|
||||
WebsocketRequestIden::SettingStoreCookies,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1196,6 +1470,8 @@ impl UpsertModelInfo for WebsocketRequest {
|
||||
let url_parameters: String = row.get("url_parameters")?;
|
||||
let authentication: String = row.get("authentication")?;
|
||||
let headers: String = row.get("headers")?;
|
||||
let setting_send_cookies: String = row.get("setting_send_cookies")?;
|
||||
let setting_store_cookies: String = row.get("setting_store_cookies")?;
|
||||
Ok(Self {
|
||||
id: row.get("id")?,
|
||||
model: row.get("model")?,
|
||||
@@ -1212,6 +1488,8 @@ impl UpsertModelInfo for WebsocketRequest {
|
||||
headers: serde_json::from_str(headers.as_str()).unwrap_or_default(),
|
||||
folder_id: row.get("folder_id")?,
|
||||
name: row.get("name")?,
|
||||
setting_send_cookies: serde_json::from_str(&setting_send_cookies).unwrap_or_default(),
|
||||
setting_store_cookies: serde_json::from_str(&setting_store_cookies).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1493,6 +1771,15 @@ pub enum HttpResponseEventData {
|
||||
Setting {
|
||||
name: String,
|
||||
value: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional, as = "Option<String>")]
|
||||
source_model: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional, as = "Option<String>")]
|
||||
source_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional, as = "Option<String>")]
|
||||
source_name: Option<String>,
|
||||
},
|
||||
Info {
|
||||
message: String,
|
||||
@@ -1742,6 +2029,8 @@ pub struct GrpcRequest {
|
||||
pub sort_priority: f64,
|
||||
/// Server URL (http for plaintext or https for secure)
|
||||
pub url: String,
|
||||
pub setting_send_cookies: InheritedBoolSetting,
|
||||
pub setting_store_cookies: InheritedBoolSetting,
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for GrpcRequest {
|
||||
@@ -1785,6 +2074,8 @@ impl UpsertModelInfo for GrpcRequest {
|
||||
(AuthenticationType, self.authentication_type.into()),
|
||||
(Authentication, serde_json::to_string(&self.authentication)?.into()),
|
||||
(Metadata, serde_json::to_string(&self.metadata)?.into()),
|
||||
(SettingSendCookies, serde_json::to_string(&self.setting_send_cookies)?.into()),
|
||||
(SettingStoreCookies, serde_json::to_string(&self.setting_store_cookies)?.into()),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -1803,6 +2094,8 @@ impl UpsertModelInfo for GrpcRequest {
|
||||
GrpcRequestIden::AuthenticationType,
|
||||
GrpcRequestIden::Authentication,
|
||||
GrpcRequestIden::Metadata,
|
||||
GrpcRequestIden::SettingSendCookies,
|
||||
GrpcRequestIden::SettingStoreCookies,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1812,6 +2105,8 @@ impl UpsertModelInfo for GrpcRequest {
|
||||
{
|
||||
let authentication: String = row.get("authentication")?;
|
||||
let metadata: String = row.get("metadata")?;
|
||||
let setting_send_cookies: String = row.get("setting_send_cookies")?;
|
||||
let setting_store_cookies: String = row.get("setting_store_cookies")?;
|
||||
Ok(Self {
|
||||
id: row.get("id")?,
|
||||
model: row.get("model")?,
|
||||
@@ -1829,6 +2124,8 @@ impl UpsertModelInfo for GrpcRequest {
|
||||
url: row.get("url")?,
|
||||
sort_priority: row.get("sort_priority")?,
|
||||
metadata: serde_json::from_str(metadata.as_str()).unwrap_or_default(),
|
||||
setting_send_cookies: serde_json::from_str(&setting_send_cookies).unwrap_or_default(),
|
||||
setting_store_cookies: serde_json::from_str(&setting_store_cookies).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ use crate::client_db::ClientDb;
|
||||
use crate::connection_or_tx::ConnectionOrTx;
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
Environment, EnvironmentIden, Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest,
|
||||
HttpRequestHeader, HttpRequestIden, WebsocketRequest, WebsocketRequestIden,
|
||||
AnyModel, Environment, EnvironmentIden, Folder, FolderIden, GrpcRequest, GrpcRequestIden,
|
||||
HttpRequest, HttpRequestHeader, HttpRequestIden, ResolvedHttpRequestSettings, ResolvedSetting,
|
||||
WebsocketRequest, WebsocketRequestIden,
|
||||
};
|
||||
use crate::util::UpdateSource;
|
||||
use serde_json::Value;
|
||||
@@ -141,4 +142,60 @@ impl<'a> ClientDb<'a> {
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
pub fn resolve_settings_for_folder(
|
||||
&self,
|
||||
folder: &Folder,
|
||||
) -> Result<ResolvedHttpRequestSettings> {
|
||||
let parent = if let Some(folder_id) = folder.folder_id.clone() {
|
||||
let parent_folder = self.get_folder(&folder_id)?;
|
||||
self.resolve_settings_for_folder(&parent_folder)?
|
||||
} else {
|
||||
let workspace = self.get_workspace(&folder.workspace_id)?;
|
||||
self.resolve_settings_for_workspace(&workspace)
|
||||
};
|
||||
|
||||
Ok(ResolvedHttpRequestSettings {
|
||||
validate_certificates: if folder.setting_validate_certificates.enabled {
|
||||
ResolvedSetting::from_model(
|
||||
folder.setting_validate_certificates.value,
|
||||
AnyModel::Folder(folder.clone()),
|
||||
)
|
||||
} else {
|
||||
parent.validate_certificates
|
||||
},
|
||||
follow_redirects: if folder.setting_follow_redirects.enabled {
|
||||
ResolvedSetting::from_model(
|
||||
folder.setting_follow_redirects.value,
|
||||
AnyModel::Folder(folder.clone()),
|
||||
)
|
||||
} else {
|
||||
parent.follow_redirects
|
||||
},
|
||||
request_timeout: if folder.setting_request_timeout.enabled {
|
||||
ResolvedSetting::from_model(
|
||||
folder.setting_request_timeout.value,
|
||||
AnyModel::Folder(folder.clone()),
|
||||
)
|
||||
} else {
|
||||
parent.request_timeout
|
||||
},
|
||||
send_cookies: if folder.setting_send_cookies.enabled {
|
||||
ResolvedSetting::from_model(
|
||||
folder.setting_send_cookies.value,
|
||||
AnyModel::Folder(folder.clone()),
|
||||
)
|
||||
} else {
|
||||
parent.send_cookies
|
||||
},
|
||||
store_cookies: if folder.setting_store_cookies.enabled {
|
||||
ResolvedSetting::from_model(
|
||||
folder.setting_store_cookies.value,
|
||||
AnyModel::Folder(folder.clone()),
|
||||
)
|
||||
} else {
|
||||
parent.store_cookies
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use super::dedupe_headers;
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::error::Result;
|
||||
use crate::models::{Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden};
|
||||
use crate::models::{
|
||||
AnyModel, Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden,
|
||||
ResolvedHttpRequestSettings, ResolvedSetting,
|
||||
};
|
||||
use crate::util::UpdateSource;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -91,6 +94,62 @@ impl<'a> ClientDb<'a> {
|
||||
Ok(dedupe_headers(headers))
|
||||
}
|
||||
|
||||
pub fn resolve_settings_for_http_request(
|
||||
&self,
|
||||
http_request: &HttpRequest,
|
||||
) -> Result<ResolvedHttpRequestSettings> {
|
||||
let parent = if let Some(folder_id) = http_request.folder_id.clone() {
|
||||
let folder = self.get_folder(&folder_id)?;
|
||||
self.resolve_settings_for_folder(&folder)?
|
||||
} else {
|
||||
let workspace = self.get_workspace(&http_request.workspace_id)?;
|
||||
self.resolve_settings_for_workspace(&workspace)
|
||||
};
|
||||
|
||||
Ok(ResolvedHttpRequestSettings {
|
||||
validate_certificates: if http_request.setting_validate_certificates.enabled {
|
||||
ResolvedSetting::from_model(
|
||||
http_request.setting_validate_certificates.value,
|
||||
AnyModel::HttpRequest(http_request.clone()),
|
||||
)
|
||||
} else {
|
||||
parent.validate_certificates
|
||||
},
|
||||
follow_redirects: if http_request.setting_follow_redirects.enabled {
|
||||
ResolvedSetting::from_model(
|
||||
http_request.setting_follow_redirects.value,
|
||||
AnyModel::HttpRequest(http_request.clone()),
|
||||
)
|
||||
} else {
|
||||
parent.follow_redirects
|
||||
},
|
||||
request_timeout: if http_request.setting_request_timeout.enabled {
|
||||
ResolvedSetting::from_model(
|
||||
http_request.setting_request_timeout.value,
|
||||
AnyModel::HttpRequest(http_request.clone()),
|
||||
)
|
||||
} else {
|
||||
parent.request_timeout
|
||||
},
|
||||
send_cookies: if http_request.setting_send_cookies.enabled {
|
||||
ResolvedSetting::from_model(
|
||||
http_request.setting_send_cookies.value,
|
||||
AnyModel::HttpRequest(http_request.clone()),
|
||||
)
|
||||
} else {
|
||||
parent.send_cookies
|
||||
},
|
||||
store_cookies: if http_request.setting_store_cookies.enabled {
|
||||
ResolvedSetting::from_model(
|
||||
http_request.setting_store_cookies.value,
|
||||
AnyModel::HttpRequest(http_request.clone()),
|
||||
)
|
||||
} else {
|
||||
parent.store_cookies
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_http_requests_for_folder_recursive(
|
||||
&self,
|
||||
folder_id: &str,
|
||||
|
||||
@@ -2,7 +2,8 @@ use super::dedupe_headers;
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
Folder, FolderIden, HttpRequestHeader, WebsocketRequest, WebsocketRequestIden,
|
||||
AnyModel, Folder, FolderIden, HttpRequestHeader, ResolvedHttpRequestSettings, ResolvedSetting,
|
||||
WebsocketRequest, WebsocketRequestIden,
|
||||
};
|
||||
use crate::util::UpdateSource;
|
||||
use serde_json::Value;
|
||||
@@ -116,4 +117,37 @@ impl<'a> ClientDb<'a> {
|
||||
|
||||
Ok(dedupe_headers(headers))
|
||||
}
|
||||
|
||||
pub fn resolve_settings_for_websocket_request(
|
||||
&self,
|
||||
websocket_request: &WebsocketRequest,
|
||||
) -> Result<ResolvedHttpRequestSettings> {
|
||||
let parent = if let Some(folder_id) = websocket_request.folder_id.clone() {
|
||||
let folder = self.get_folder(&folder_id)?;
|
||||
self.resolve_settings_for_folder(&folder)?
|
||||
} else {
|
||||
let workspace = self.get_workspace(&websocket_request.workspace_id)?;
|
||||
self.resolve_settings_for_workspace(&workspace)
|
||||
};
|
||||
|
||||
Ok(ResolvedHttpRequestSettings {
|
||||
send_cookies: if websocket_request.setting_send_cookies.enabled {
|
||||
ResolvedSetting::from_model(
|
||||
websocket_request.setting_send_cookies.value,
|
||||
AnyModel::WebsocketRequest(websocket_request.clone()),
|
||||
)
|
||||
} else {
|
||||
parent.send_cookies
|
||||
},
|
||||
store_cookies: if websocket_request.setting_store_cookies.enabled {
|
||||
ResolvedSetting::from_model(
|
||||
websocket_request.setting_store_cookies.value,
|
||||
AnyModel::WebsocketRequest(websocket_request.clone()),
|
||||
)
|
||||
} else {
|
||||
parent.store_cookies
|
||||
},
|
||||
..parent
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
EnvironmentIden, FolderIden, GrpcRequestIden, HttpRequestHeader, HttpRequestIden,
|
||||
WebsocketRequestIden, Workspace, WorkspaceIden,
|
||||
AnyModel, EnvironmentIden, FolderIden, GrpcRequestIden, HttpRequestHeader, HttpRequestIden,
|
||||
ResolvedHttpRequestSettings, ResolvedSetting, WebsocketRequestIden, Workspace, WorkspaceIden,
|
||||
};
|
||||
use crate::util::UpdateSource;
|
||||
use serde_json::Value;
|
||||
@@ -84,6 +84,34 @@ impl<'a> ClientDb<'a> {
|
||||
headers.extend(workspace.headers.clone());
|
||||
headers
|
||||
}
|
||||
|
||||
pub fn resolve_settings_for_workspace(
|
||||
&self,
|
||||
workspace: &Workspace,
|
||||
) -> ResolvedHttpRequestSettings {
|
||||
ResolvedHttpRequestSettings {
|
||||
validate_certificates: ResolvedSetting::from_model(
|
||||
workspace.setting_validate_certificates,
|
||||
AnyModel::Workspace(workspace.clone()),
|
||||
),
|
||||
follow_redirects: ResolvedSetting::from_model(
|
||||
workspace.setting_follow_redirects,
|
||||
AnyModel::Workspace(workspace.clone()),
|
||||
),
|
||||
request_timeout: ResolvedSetting::from_model(
|
||||
workspace.setting_request_timeout,
|
||||
AnyModel::Workspace(workspace.clone()),
|
||||
),
|
||||
send_cookies: ResolvedSetting::from_model(
|
||||
workspace.setting_send_cookies,
|
||||
AnyModel::Workspace(workspace.clone()),
|
||||
),
|
||||
store_cookies: ResolvedSetting::from_model(
|
||||
workspace.setting_store_cookies,
|
||||
AnyModel::Workspace(workspace.clone()),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Global default headers that are always sent with requests unless overridden.
|
||||
|
||||
+430
-56
@@ -1,108 +1,482 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AnyModel = CookieJar | Environment | Folder | GraphQlIntrospection | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | HttpResponseEvent | KeyValue | Plugin | Settings | SyncState | WebsocketConnection | WebsocketEvent | WebsocketRequest | Workspace | WorkspaceMeta;
|
||||
export type AnyModel =
|
||||
| CookieJar
|
||||
| Environment
|
||||
| Folder
|
||||
| GraphQlIntrospection
|
||||
| GrpcConnection
|
||||
| GrpcEvent
|
||||
| GrpcRequest
|
||||
| HttpRequest
|
||||
| HttpResponse
|
||||
| HttpResponseEvent
|
||||
| KeyValue
|
||||
| Plugin
|
||||
| Settings
|
||||
| SyncState
|
||||
| WebsocketConnection
|
||||
| WebsocketEvent
|
||||
| WebsocketRequest
|
||||
| Workspace
|
||||
| WorkspaceMeta;
|
||||
|
||||
export type ClientCertificate = { host: string, port: number | null, crtFile: string | null, keyFile: string | null, pfxFile: string | null, passphrase: string | null, enabled?: boolean, };
|
||||
export type ClientCertificate = {
|
||||
host: string;
|
||||
port: number | null;
|
||||
crtFile: string | null;
|
||||
keyFile: string | null;
|
||||
pfxFile: string | null;
|
||||
passphrase: string | null;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type Cookie = { raw_cookie: string, domain: CookieDomain, expires: CookieExpires, path: [string, boolean], };
|
||||
export type Cookie = {
|
||||
name: string;
|
||||
value: string;
|
||||
domain: CookieDomain;
|
||||
expires: CookieExpires;
|
||||
path: string;
|
||||
secure: boolean;
|
||||
httpOnly: boolean;
|
||||
sameSite: CookieSameSite | null;
|
||||
};
|
||||
|
||||
export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty";
|
||||
export type CookieDomain = { HostOnly: string } | { Suffix: string } | "NotPresent" | "Empty";
|
||||
|
||||
export type CookieExpires = { "AtUtc": string } | "SessionEnd";
|
||||
export type CookieExpires = { AtUtc: string } | "SessionEnd";
|
||||
|
||||
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
||||
export type CookieJar = {
|
||||
model: "cookie_jar";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
cookies: Array<Cookie>;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
||||
export type CookieSameSite = "Strict" | "Lax" | "None";
|
||||
|
||||
export type DnsOverride = {
|
||||
hostname: string;
|
||||
ipv4: Array<string>;
|
||||
ipv6: Array<string>;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||
|
||||
export type EncryptedKey = { encryptedKey: string, };
|
||||
export type EncryptedKey = { encryptedKey: string };
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
|
||||
/**
|
||||
* Variables defined in this environment scope.
|
||||
* Child environments override parent variables by name.
|
||||
*/
|
||||
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||
export type Environment = {
|
||||
model: "environment";
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
name: string;
|
||||
public: boolean;
|
||||
parentModel: string;
|
||||
parentId: string | null;
|
||||
/**
|
||||
* Variables defined in this environment scope.
|
||||
* Child environments override parent variables by name.
|
||||
*/
|
||||
variables: Array<EnvironmentVariable>;
|
||||
color: string | null;
|
||||
sortPriority: number;
|
||||
};
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string };
|
||||
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
|
||||
export type Folder = {
|
||||
model: "folder";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
settingValidateCertificates: InheritedBoolSetting;
|
||||
settingFollowRedirects: InheritedBoolSetting;
|
||||
settingRequestTimeout: InheritedIntSetting;
|
||||
};
|
||||
|
||||
export type GraphQlIntrospection = { model: "graphql_introspection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, content: string | null, };
|
||||
export type GraphQlIntrospection = {
|
||||
model: "graphql_introspection";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
content: string | null;
|
||||
};
|
||||
|
||||
export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, };
|
||||
export type GrpcConnection = {
|
||||
model: "grpc_connection";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
elapsed: number;
|
||||
error: string | null;
|
||||
method: string;
|
||||
service: string;
|
||||
status: number;
|
||||
state: GrpcConnectionState;
|
||||
trailers: { [key in string]?: string };
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type GrpcConnectionState = "initialized" | "connected" | "closed";
|
||||
|
||||
export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, content: string, error: string | null, eventType: GrpcEventType, metadata: { [key in string]?: string }, status: number | null, };
|
||||
export type GrpcEvent = {
|
||||
model: "grpc_event";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
connectionId: string;
|
||||
content: string;
|
||||
error: string | null;
|
||||
eventType: GrpcEventType;
|
||||
metadata: { [key in string]?: string };
|
||||
status: number | null;
|
||||
};
|
||||
|
||||
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
|
||||
export type GrpcEventType =
|
||||
| "info"
|
||||
| "error"
|
||||
| "client_message"
|
||||
| "server_message"
|
||||
| "connection_start"
|
||||
| "connection_end";
|
||||
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number,
|
||||
/**
|
||||
* Server URL (http for plaintext or https for secure)
|
||||
*/
|
||||
url: string, };
|
||||
export type GrpcRequest = {
|
||||
model: "grpc_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authenticationType: string | null;
|
||||
authentication: Record<string, any>;
|
||||
description: string;
|
||||
message: string;
|
||||
metadata: Array<HttpRequestHeader>;
|
||||
method: string | null;
|
||||
name: string;
|
||||
service: string | null;
|
||||
sortPriority: number;
|
||||
/**
|
||||
* Server URL (http for plaintext or https for secure)
|
||||
*/
|
||||
url: string;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
};
|
||||
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>, };
|
||||
export type HttpRequest = {
|
||||
model: "http_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
body: Record<string, any>;
|
||||
bodyType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
method: string;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
url: string;
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
settingValidateCertificates: InheritedBoolSetting;
|
||||
settingFollowRedirects: InheritedBoolSetting;
|
||||
settingRequestTimeout: InheritedIntSetting;
|
||||
};
|
||||
|
||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string };
|
||||
|
||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||
export type HttpResponse = {
|
||||
model: "http_response";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
bodyPath: string | null;
|
||||
contentLength: number | null;
|
||||
contentLengthCompressed: number | null;
|
||||
elapsed: number;
|
||||
elapsedHeaders: number;
|
||||
elapsedDns: number;
|
||||
error: string | null;
|
||||
headers: Array<HttpResponseHeader>;
|
||||
remoteAddr: string | null;
|
||||
requestContentLength: number | null;
|
||||
requestHeaders: Array<HttpResponseHeader>;
|
||||
status: number;
|
||||
statusReason: string | null;
|
||||
state: HttpResponseState;
|
||||
url: string;
|
||||
version: string | null;
|
||||
};
|
||||
|
||||
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
||||
export type HttpResponseEvent = {
|
||||
model: "http_response_event";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
responseId: string;
|
||||
event: HttpResponseEventData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializable representation of HTTP response events for DB storage.
|
||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||
*/
|
||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||
export type HttpResponseEventData =
|
||||
| {
|
||||
type: "setting";
|
||||
name: string;
|
||||
value: string;
|
||||
source_model?: string;
|
||||
source_id?: string;
|
||||
source_name?: string;
|
||||
}
|
||||
| { type: "info"; message: string }
|
||||
| {
|
||||
type: "redirect";
|
||||
url: string;
|
||||
status: number;
|
||||
behavior: string;
|
||||
dropped_body: boolean;
|
||||
dropped_headers: Array<string>;
|
||||
}
|
||||
| {
|
||||
type: "send_url";
|
||||
method: string;
|
||||
scheme: string;
|
||||
username: string;
|
||||
password: string;
|
||||
host: string;
|
||||
port: number;
|
||||
path: string;
|
||||
query: string;
|
||||
fragment: string;
|
||||
}
|
||||
| { type: "receive_url"; version: string; status: string }
|
||||
| { type: "header_up"; name: string; value: string }
|
||||
| { type: "header_down"; name: string; value: string }
|
||||
| { type: "chunk_sent"; bytes: number }
|
||||
| { type: "chunk_received"; bytes: number }
|
||||
| {
|
||||
type: "dns_resolved";
|
||||
hostname: string;
|
||||
addresses: Array<string>;
|
||||
duration: bigint;
|
||||
overridden: boolean;
|
||||
};
|
||||
|
||||
export type HttpResponseHeader = { name: string, value: string, };
|
||||
export type HttpResponseHeader = { name: string; value: string };
|
||||
|
||||
export type HttpResponseState = "initialized" | "connected" | "closed";
|
||||
|
||||
export type HttpUrlParameter = { enabled?: boolean,
|
||||
/**
|
||||
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||
* Other entries are appended as query parameters
|
||||
*/
|
||||
name: string, value: string, id?: string, };
|
||||
export type HttpUrlParameter = {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||
* Other entries are appended as query parameters
|
||||
*/
|
||||
name: string;
|
||||
value: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
|
||||
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
|
||||
|
||||
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
|
||||
export type InheritedIntSetting = { enabled?: boolean; value: number };
|
||||
|
||||
export type KeyValue = {
|
||||
model: "key_value";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
key: string;
|
||||
namespace: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type Plugin = {
|
||||
model: "plugin";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
checkedAt: string | null;
|
||||
directory: string;
|
||||
enabled: boolean;
|
||||
url: string | null;
|
||||
source: PluginSource;
|
||||
};
|
||||
|
||||
export type PluginSource = "bundled" | "filesystem" | "registry";
|
||||
|
||||
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
|
||||
export type ProxySetting =
|
||||
| {
|
||||
type: "enabled";
|
||||
http: string;
|
||||
https: string;
|
||||
auth: ProxySettingAuth | null;
|
||||
bypass: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
| { type: "disabled" };
|
||||
|
||||
export type ProxySettingAuth = { user: string, password: string, };
|
||||
export type ProxySettingAuth = { user: string; password: string };
|
||||
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, clientCertificates: Array<ClientCertificate>, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, useNativeTitlebar: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, checkNotifications: boolean, hotkeys: { [key in string]?: Array<string> }, };
|
||||
export type Settings = {
|
||||
model: "settings";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
appearance: string;
|
||||
clientCertificates: Array<ClientCertificate>;
|
||||
coloredMethods: boolean;
|
||||
editorFont: string | null;
|
||||
editorFontSize: number;
|
||||
editorKeymap: EditorKeymap;
|
||||
editorSoftWrap: boolean;
|
||||
hideWindowControls: boolean;
|
||||
useNativeTitlebar: boolean;
|
||||
interfaceFont: string | null;
|
||||
interfaceFontSize: number;
|
||||
interfaceScale: number;
|
||||
openWorkspaceNewWindow: boolean | null;
|
||||
proxy: ProxySetting | null;
|
||||
themeDark: string;
|
||||
themeLight: string;
|
||||
updateChannel: string;
|
||||
hideLicenseBadge: boolean;
|
||||
autoupdate: boolean;
|
||||
autoDownloadUpdates: boolean;
|
||||
checkNotifications: boolean;
|
||||
hotkeys: { [key in string]?: Array<string> };
|
||||
};
|
||||
|
||||
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
|
||||
export type SyncState = {
|
||||
model: "sync_state";
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
flushedAt: string;
|
||||
modelId: string;
|
||||
checksum: string;
|
||||
relPath: string;
|
||||
syncDir: string;
|
||||
};
|
||||
|
||||
export type WebsocketConnection = { model: "websocket_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, headers: Array<HttpResponseHeader>, state: WebsocketConnectionState, status: number, url: string, };
|
||||
export type WebsocketConnection = {
|
||||
model: "websocket_connection";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
elapsed: number;
|
||||
error: string | null;
|
||||
headers: Array<HttpResponseHeader>;
|
||||
state: WebsocketConnectionState;
|
||||
status: number;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed";
|
||||
|
||||
export type WebsocketEvent = { model: "websocket_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, isServer: boolean, message: Array<number>, messageType: WebsocketEventType, };
|
||||
export type WebsocketEvent = {
|
||||
model: "websocket_event";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
connectionId: string;
|
||||
isServer: boolean;
|
||||
message: Array<number>;
|
||||
messageType: WebsocketEventType;
|
||||
};
|
||||
|
||||
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string,
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>, };
|
||||
export type WebsocketRequest = {
|
||||
model: "websocket_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
message: string;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
url: string;
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
};
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||
export type Workspace = {
|
||||
model: "workspace";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
name: string;
|
||||
encryptionKeyChallenge: string | null;
|
||||
settingValidateCertificates: boolean;
|
||||
settingFollowRedirects: boolean;
|
||||
settingRequestTimeout: number;
|
||||
settingDnsOverrides: Array<DnsOverride>;
|
||||
settingSendCookies: boolean;
|
||||
settingStoreCookies: boolean;
|
||||
};
|
||||
|
||||
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
||||
export type WorkspaceMeta = {
|
||||
model: "workspace_meta";
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
encryptionKey: EncryptedKey | null;
|
||||
settingSyncDir: string | null;
|
||||
};
|
||||
|
||||
Generated
+168
-34
@@ -1,47 +1,181 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
||||
export type DnsOverride = {
|
||||
hostname: string;
|
||||
ipv4: Array<string>;
|
||||
ipv6: Array<string>;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
|
||||
/**
|
||||
* Variables defined in this environment scope.
|
||||
* Child environments override parent variables by name.
|
||||
*/
|
||||
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||
export type Environment = {
|
||||
model: "environment";
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
name: string;
|
||||
public: boolean;
|
||||
parentModel: string;
|
||||
parentId: string | null;
|
||||
/**
|
||||
* Variables defined in this environment scope.
|
||||
* Child environments override parent variables by name.
|
||||
*/
|
||||
variables: Array<EnvironmentVariable>;
|
||||
color: string | null;
|
||||
sortPriority: number;
|
||||
};
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string };
|
||||
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
|
||||
export type Folder = {
|
||||
model: "folder";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
settingValidateCertificates: InheritedBoolSetting;
|
||||
settingFollowRedirects: InheritedBoolSetting;
|
||||
settingRequestTimeout: InheritedIntSetting;
|
||||
};
|
||||
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number,
|
||||
/**
|
||||
* Server URL (http for plaintext or https for secure)
|
||||
*/
|
||||
url: string, };
|
||||
export type GrpcRequest = {
|
||||
model: "grpc_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authenticationType: string | null;
|
||||
authentication: Record<string, any>;
|
||||
description: string;
|
||||
message: string;
|
||||
metadata: Array<HttpRequestHeader>;
|
||||
method: string | null;
|
||||
name: string;
|
||||
service: string | null;
|
||||
sortPriority: number;
|
||||
/**
|
||||
* Server URL (http for plaintext or https for secure)
|
||||
*/
|
||||
url: string;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
};
|
||||
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>, };
|
||||
export type HttpRequest = {
|
||||
model: "http_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
body: Record<string, any>;
|
||||
bodyType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
method: string;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
url: string;
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
settingValidateCertificates: InheritedBoolSetting;
|
||||
settingFollowRedirects: InheritedBoolSetting;
|
||||
settingRequestTimeout: InheritedIntSetting;
|
||||
};
|
||||
|
||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string };
|
||||
|
||||
export type HttpUrlParameter = { enabled?: boolean,
|
||||
/**
|
||||
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||
* Other entries are appended as query parameters
|
||||
*/
|
||||
name: string, value: string, id?: string, };
|
||||
export type HttpUrlParameter = {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||
* Other entries are appended as query parameters
|
||||
*/
|
||||
name: string;
|
||||
value: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environment" } & Environment | { "type": "folder" } & Folder | { "type": "http_request" } & HttpRequest | { "type": "grpc_request" } & GrpcRequest | { "type": "websocket_request" } & WebsocketRequest;
|
||||
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
|
||||
|
||||
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
|
||||
export type InheritedIntSetting = { enabled?: boolean; value: number };
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string,
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>, };
|
||||
export type SyncModel =
|
||||
| ({ type: "workspace" } & Workspace)
|
||||
| ({ type: "environment" } & Environment)
|
||||
| ({ type: "folder" } & Folder)
|
||||
| ({ type: "http_request" } & HttpRequest)
|
||||
| ({ type: "grpc_request" } & GrpcRequest)
|
||||
| ({ type: "websocket_request" } & WebsocketRequest);
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||
export type SyncState = {
|
||||
model: "sync_state";
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
flushedAt: string;
|
||||
modelId: string;
|
||||
checksum: string;
|
||||
relPath: string;
|
||||
syncDir: string;
|
||||
};
|
||||
|
||||
export type WebsocketRequest = {
|
||||
model: "websocket_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
message: string;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
url: string;
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
};
|
||||
|
||||
export type Workspace = {
|
||||
model: "workspace";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
name: string;
|
||||
encryptionKeyChallenge: string | null;
|
||||
settingValidateCertificates: boolean;
|
||||
settingFollowRedirects: boolean;
|
||||
settingRequestTimeout: number;
|
||||
settingDnsOverrides: Array<DnsOverride>;
|
||||
settingSendCookies: boolean;
|
||||
settingStoreCookies: boolean;
|
||||
};
|
||||
|
||||
+137
-48
@@ -4,7 +4,7 @@ use log::warn;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::time::Instant;
|
||||
use std::time::{Duration, Instant};
|
||||
use thiserror::Error;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
@@ -26,6 +26,7 @@ use yaak_models::blob_manager::{BlobManager, BodyChunk};
|
||||
use yaak_models::models::{
|
||||
ClientCertificate, CookieJar, DnsOverride, Environment, HttpRequest, HttpResponse,
|
||||
HttpResponseEvent, HttpResponseHeader, HttpResponseState, ProxySetting, ProxySettingAuth,
|
||||
ResolvedSetting,
|
||||
};
|
||||
use yaak_models::query_manager::QueryManager;
|
||||
use yaak_models::util::{UpdateSource, generate_prefixed_id};
|
||||
@@ -115,10 +116,17 @@ pub trait SendRequestExecutor: Send + Sync {
|
||||
&self,
|
||||
sendable_request: SendableHttpRequest,
|
||||
event_tx: mpsc::Sender<SenderHttpResponseEvent>,
|
||||
cookie_store: Option<CookieStore>,
|
||||
cookie_behavior: CookieBehavior,
|
||||
) -> yaak_http::error::Result<yaak_http::sender::HttpResponse>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CookieBehavior {
|
||||
pub store: Option<CookieStore>,
|
||||
pub send_cookies: bool,
|
||||
pub store_cookies: bool,
|
||||
}
|
||||
|
||||
struct DefaultSendRequestExecutor;
|
||||
|
||||
#[async_trait]
|
||||
@@ -127,11 +135,16 @@ impl SendRequestExecutor for DefaultSendRequestExecutor {
|
||||
&self,
|
||||
sendable_request: SendableHttpRequest,
|
||||
event_tx: mpsc::Sender<SenderHttpResponseEvent>,
|
||||
cookie_store: Option<CookieStore>,
|
||||
cookie_behavior: CookieBehavior,
|
||||
) -> yaak_http::error::Result<yaak_http::sender::HttpResponse> {
|
||||
let sender = ReqwestSender::new()?;
|
||||
let transaction = match cookie_store {
|
||||
Some(store) => HttpTransaction::with_cookie_store(sender, store),
|
||||
let transaction = match cookie_behavior.store {
|
||||
Some(store) => HttpTransaction::with_cookie_behavior(
|
||||
sender,
|
||||
store,
|
||||
cookie_behavior.send_cookies,
|
||||
cookie_behavior.store_cookies,
|
||||
),
|
||||
None => HttpTransaction::new(sender),
|
||||
};
|
||||
let (_cancel_tx, cancel_rx) = watch::channel(false);
|
||||
@@ -182,7 +195,7 @@ struct ConnectionManagerSendRequestExecutor<'a> {
|
||||
connection_manager: &'a HttpConnectionManager,
|
||||
plugin_context_id: String,
|
||||
query_manager: QueryManager,
|
||||
workspace_id: String,
|
||||
request: HttpRequest,
|
||||
cancelled_rx: Option<watch::Receiver<bool>>,
|
||||
}
|
||||
|
||||
@@ -192,11 +205,10 @@ impl SendRequestExecutor for ConnectionManagerSendRequestExecutor<'_> {
|
||||
&self,
|
||||
sendable_request: SendableHttpRequest,
|
||||
event_tx: mpsc::Sender<SenderHttpResponseEvent>,
|
||||
cookie_store: Option<CookieStore>,
|
||||
cookie_behavior: CookieBehavior,
|
||||
) -> yaak_http::error::Result<yaak_http::sender::HttpResponse> {
|
||||
let runtime_config =
|
||||
resolve_http_send_runtime_config(&self.query_manager, &self.workspace_id)
|
||||
.map_err(|e| yaak_http::error::Error::RequestError(e.to_string()))?;
|
||||
let runtime_config = resolve_http_send_runtime_config(&self.query_manager, &self.request)
|
||||
.map_err(|e| yaak_http::error::Error::RequestError(e.to_string()))?;
|
||||
let client_certificate =
|
||||
find_client_certificate(&sendable_request.url, &runtime_config.client_certificates);
|
||||
let cached_client = self
|
||||
@@ -213,8 +225,13 @@ impl SendRequestExecutor for ConnectionManagerSendRequestExecutor<'_> {
|
||||
cached_client.resolver.set_event_sender(Some(event_tx.clone())).await;
|
||||
|
||||
let sender = ReqwestSender::with_client(cached_client.client);
|
||||
let transaction = match cookie_store {
|
||||
Some(cs) => HttpTransaction::with_cookie_store(sender, cs),
|
||||
let transaction = match cookie_behavior.store {
|
||||
Some(cs) => HttpTransaction::with_cookie_behavior(
|
||||
sender,
|
||||
cs,
|
||||
cookie_behavior.send_cookies,
|
||||
cookie_behavior.store_cookies,
|
||||
),
|
||||
None => HttpTransaction::new(sender),
|
||||
};
|
||||
|
||||
@@ -315,24 +332,28 @@ pub struct HttpSendRuntimeConfig {
|
||||
|
||||
pub fn resolve_http_send_runtime_config(
|
||||
query_manager: &QueryManager,
|
||||
workspace_id: &str,
|
||||
request: &HttpRequest,
|
||||
) -> Result<HttpSendRuntimeConfig> {
|
||||
let db = query_manager.connect();
|
||||
let workspace = db.get_workspace(workspace_id).map_err(SendHttpRequestError::LoadWorkspace)?;
|
||||
let workspace =
|
||||
db.get_workspace(&request.workspace_id).map_err(SendHttpRequestError::LoadWorkspace)?;
|
||||
let resolved_settings = db
|
||||
.resolve_settings_for_http_request(request)
|
||||
.map_err(SendHttpRequestError::ResolveRequestInheritance)?;
|
||||
let settings = db.get_settings();
|
||||
|
||||
Ok(HttpSendRuntimeConfig {
|
||||
send_options: SendableHttpRequestOptions {
|
||||
follow_redirects: workspace.setting_follow_redirects,
|
||||
timeout: if workspace.setting_request_timeout > 0 {
|
||||
follow_redirects: resolved_settings.follow_redirects.value,
|
||||
timeout: if resolved_settings.request_timeout.value > 0 {
|
||||
Some(std::time::Duration::from_millis(
|
||||
workspace.setting_request_timeout.unsigned_abs() as u64,
|
||||
resolved_settings.request_timeout.value.unsigned_abs() as u64,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
validate_certificates: workspace.setting_validate_certificates,
|
||||
validate_certificates: resolved_settings.validate_certificates.value,
|
||||
proxy: proxy_setting_from_settings(settings.proxy),
|
||||
dns_overrides: workspace.setting_dns_overrides,
|
||||
client_certificates: settings.client_certificates,
|
||||
@@ -387,7 +408,7 @@ pub async fn send_http_request_with_plugins(
|
||||
connection_manager,
|
||||
plugin_context_id: params.plugin_context.id.clone(),
|
||||
query_manager: params.query_manager.clone(),
|
||||
workspace_id: params.request.workspace_id.clone(),
|
||||
request: params.request.clone(),
|
||||
cancelled_rx: params.cancelled_rx.clone(),
|
||||
});
|
||||
|
||||
@@ -454,12 +475,21 @@ pub async fn send_http_request<T: TemplateCallback>(
|
||||
} else {
|
||||
resolve_inherited_request(params.query_manager, ¶ms.request)?
|
||||
};
|
||||
let runtime_config =
|
||||
resolve_http_send_runtime_config(params.query_manager, ¶ms.request.workspace_id)?;
|
||||
let runtime_config = resolve_http_send_runtime_config(params.query_manager, ¶ms.request)?;
|
||||
let send_options = params.send_options.unwrap_or(runtime_config.send_options);
|
||||
let resolved_settings = params
|
||||
.query_manager
|
||||
.connect()
|
||||
.resolve_settings_for_http_request(¶ms.request)
|
||||
.map_err(SendHttpRequestError::ResolveRequestInheritance)?;
|
||||
let mut cookie_jar = load_cookie_jar(params.query_manager, params.cookie_jar_id.as_deref())?;
|
||||
let cookie_store =
|
||||
cookie_jar.as_ref().map(|jar| CookieStore::from_cookies(jar.cookies.clone()));
|
||||
let cookie_behavior = CookieBehavior {
|
||||
store: cookie_store,
|
||||
send_cookies: resolved_settings.send_cookies.value,
|
||||
store_cookies: resolved_settings.store_cookies.value,
|
||||
};
|
||||
|
||||
let rendered_request = render_http_request(
|
||||
&resolved_request,
|
||||
@@ -585,33 +615,66 @@ pub async fn send_http_request<T: TemplateCallback>(
|
||||
let started_at = Instant::now();
|
||||
let request_started_url = sendable_request.url.clone();
|
||||
|
||||
let mut http_response = match executor
|
||||
.send(sendable_request, event_tx, cookie_store.clone())
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
persist_cookie_jar(params.query_manager, cookie_jar.as_mut(), cookie_store.as_ref())?;
|
||||
if persist_response {
|
||||
let _ = persist_response_error(
|
||||
send_setting_event(
|
||||
&event_tx,
|
||||
"validate_certificates",
|
||||
runtime_config.validate_certificates.to_string(),
|
||||
&resolved_settings.validate_certificates,
|
||||
);
|
||||
send_setting_event(
|
||||
&event_tx,
|
||||
"redirects",
|
||||
sendable_request.options.follow_redirects.to_string(),
|
||||
&resolved_settings.follow_redirects,
|
||||
);
|
||||
send_setting_event(
|
||||
&event_tx,
|
||||
"timeout",
|
||||
timeout_setting_value(sendable_request.options.timeout),
|
||||
&resolved_settings.request_timeout,
|
||||
);
|
||||
send_setting_event(
|
||||
&event_tx,
|
||||
"send_cookies",
|
||||
cookie_behavior.send_cookies.to_string(),
|
||||
&resolved_settings.send_cookies,
|
||||
);
|
||||
send_setting_event(
|
||||
&event_tx,
|
||||
"store_cookies",
|
||||
cookie_behavior.store_cookies.to_string(),
|
||||
&resolved_settings.store_cookies,
|
||||
);
|
||||
|
||||
let mut http_response =
|
||||
match executor.send(sendable_request, event_tx, cookie_behavior.clone()).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
persist_cookie_jar(
|
||||
params.query_manager,
|
||||
params.blob_manager,
|
||||
¶ms.update_source,
|
||||
&response,
|
||||
started_at,
|
||||
err.to_string(),
|
||||
request_started_url,
|
||||
);
|
||||
cookie_jar.as_mut(),
|
||||
cookie_behavior.store.as_ref(),
|
||||
)?;
|
||||
if persist_response {
|
||||
let _ = persist_response_error(
|
||||
params.query_manager,
|
||||
params.blob_manager,
|
||||
¶ms.update_source,
|
||||
&response,
|
||||
started_at,
|
||||
err.to_string(),
|
||||
request_started_url,
|
||||
);
|
||||
}
|
||||
if let Err(join_err) = event_handle.await {
|
||||
warn!("Failed to join response event task: {}", join_err);
|
||||
}
|
||||
if let Some(task) = request_body_capture_task.take() {
|
||||
let _ = task.await;
|
||||
}
|
||||
return Err(SendHttpRequestError::SendRequest(err));
|
||||
}
|
||||
if let Err(join_err) = event_handle.await {
|
||||
warn!("Failed to join response event task: {}", join_err);
|
||||
}
|
||||
if let Some(task) = request_body_capture_task.take() {
|
||||
let _ = task.await;
|
||||
}
|
||||
return Err(SendHttpRequestError::SendRequest(err));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let headers_elapsed = duration_to_i32(started_at.elapsed());
|
||||
std::fs::create_dir_all(params.response_dir).map_err(|source| {
|
||||
@@ -781,7 +844,11 @@ pub async fn send_http_request<T: TemplateCallback>(
|
||||
request_started_url,
|
||||
);
|
||||
}
|
||||
persist_cookie_jar(params.query_manager, cookie_jar.as_mut(), cookie_store.as_ref())?;
|
||||
persist_cookie_jar(
|
||||
params.query_manager,
|
||||
cookie_jar.as_mut(),
|
||||
cookie_behavior.store.as_ref(),
|
||||
)?;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
@@ -806,7 +873,7 @@ pub async fn send_http_request<T: TemplateCallback>(
|
||||
response = final_response;
|
||||
}
|
||||
|
||||
persist_cookie_jar(params.query_manager, cookie_jar.as_mut(), cookie_store.as_ref())?;
|
||||
persist_cookie_jar(params.query_manager, cookie_jar.as_mut(), cookie_behavior.store.as_ref())?;
|
||||
|
||||
Ok(SendHttpRequestResult { rendered_request, response, response_body })
|
||||
}
|
||||
@@ -923,6 +990,28 @@ fn persist_cookie_jar(
|
||||
}
|
||||
}
|
||||
|
||||
fn send_setting_event<T>(
|
||||
event_tx: &mpsc::Sender<SenderHttpResponseEvent>,
|
||||
name: impl Into<String>,
|
||||
value: impl Into<String>,
|
||||
setting: &ResolvedSetting<T>,
|
||||
) {
|
||||
let _ = event_tx.try_send(SenderHttpResponseEvent::Setting {
|
||||
name: name.into(),
|
||||
value: value.into(),
|
||||
source_model: Some(setting.source_model.clone()),
|
||||
source_id: setting.source_id.clone(),
|
||||
source_name: setting.source_name.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
fn timeout_setting_value(timeout: Option<Duration>) -> String {
|
||||
match timeout {
|
||||
Some(timeout) if !timeout.is_zero() => format!("{timeout:?}"),
|
||||
_ => "Infinity".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy_setting_from_settings(proxy: Option<ProxySetting>) -> HttpConnectionProxySetting {
|
||||
match proxy {
|
||||
None => HttpConnectionProxySetting::System,
|
||||
|
||||
+430
-56
@@ -1,108 +1,482 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AnyModel = CookieJar | Environment | Folder | GraphQlIntrospection | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | HttpResponseEvent | KeyValue | Plugin | Settings | SyncState | WebsocketConnection | WebsocketEvent | WebsocketRequest | Workspace | WorkspaceMeta;
|
||||
export type AnyModel =
|
||||
| CookieJar
|
||||
| Environment
|
||||
| Folder
|
||||
| GraphQlIntrospection
|
||||
| GrpcConnection
|
||||
| GrpcEvent
|
||||
| GrpcRequest
|
||||
| HttpRequest
|
||||
| HttpResponse
|
||||
| HttpResponseEvent
|
||||
| KeyValue
|
||||
| Plugin
|
||||
| Settings
|
||||
| SyncState
|
||||
| WebsocketConnection
|
||||
| WebsocketEvent
|
||||
| WebsocketRequest
|
||||
| Workspace
|
||||
| WorkspaceMeta;
|
||||
|
||||
export type ClientCertificate = { host: string, port: number | null, crtFile: string | null, keyFile: string | null, pfxFile: string | null, passphrase: string | null, enabled?: boolean, };
|
||||
export type ClientCertificate = {
|
||||
host: string;
|
||||
port: number | null;
|
||||
crtFile: string | null;
|
||||
keyFile: string | null;
|
||||
pfxFile: string | null;
|
||||
passphrase: string | null;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type Cookie = { raw_cookie: string, domain: CookieDomain, expires: CookieExpires, path: [string, boolean], };
|
||||
export type Cookie = {
|
||||
name: string;
|
||||
value: string;
|
||||
domain: CookieDomain;
|
||||
expires: CookieExpires;
|
||||
path: string;
|
||||
secure: boolean;
|
||||
httpOnly: boolean;
|
||||
sameSite: CookieSameSite | null;
|
||||
};
|
||||
|
||||
export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty";
|
||||
export type CookieDomain = { HostOnly: string } | { Suffix: string } | "NotPresent" | "Empty";
|
||||
|
||||
export type CookieExpires = { "AtUtc": string } | "SessionEnd";
|
||||
export type CookieExpires = { AtUtc: string } | "SessionEnd";
|
||||
|
||||
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
||||
export type CookieJar = {
|
||||
model: "cookie_jar";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
cookies: Array<Cookie>;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
||||
export type CookieSameSite = "Strict" | "Lax" | "None";
|
||||
|
||||
export type DnsOverride = {
|
||||
hostname: string;
|
||||
ipv4: Array<string>;
|
||||
ipv6: Array<string>;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||
|
||||
export type EncryptedKey = { encryptedKey: string, };
|
||||
export type EncryptedKey = { encryptedKey: string };
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
|
||||
/**
|
||||
* Variables defined in this environment scope.
|
||||
* Child environments override parent variables by name.
|
||||
*/
|
||||
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||
export type Environment = {
|
||||
model: "environment";
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
name: string;
|
||||
public: boolean;
|
||||
parentModel: string;
|
||||
parentId: string | null;
|
||||
/**
|
||||
* Variables defined in this environment scope.
|
||||
* Child environments override parent variables by name.
|
||||
*/
|
||||
variables: Array<EnvironmentVariable>;
|
||||
color: string | null;
|
||||
sortPriority: number;
|
||||
};
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string };
|
||||
|
||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
|
||||
export type Folder = {
|
||||
model: "folder";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
settingValidateCertificates: InheritedBoolSetting;
|
||||
settingFollowRedirects: InheritedBoolSetting;
|
||||
settingRequestTimeout: InheritedIntSetting;
|
||||
};
|
||||
|
||||
export type GraphQlIntrospection = { model: "graphql_introspection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, content: string | null, };
|
||||
export type GraphQlIntrospection = {
|
||||
model: "graphql_introspection";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
content: string | null;
|
||||
};
|
||||
|
||||
export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, };
|
||||
export type GrpcConnection = {
|
||||
model: "grpc_connection";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
elapsed: number;
|
||||
error: string | null;
|
||||
method: string;
|
||||
service: string;
|
||||
status: number;
|
||||
state: GrpcConnectionState;
|
||||
trailers: { [key in string]?: string };
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type GrpcConnectionState = "initialized" | "connected" | "closed";
|
||||
|
||||
export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, content: string, error: string | null, eventType: GrpcEventType, metadata: { [key in string]?: string }, status: number | null, };
|
||||
export type GrpcEvent = {
|
||||
model: "grpc_event";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
connectionId: string;
|
||||
content: string;
|
||||
error: string | null;
|
||||
eventType: GrpcEventType;
|
||||
metadata: { [key in string]?: string };
|
||||
status: number | null;
|
||||
};
|
||||
|
||||
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
|
||||
export type GrpcEventType =
|
||||
| "info"
|
||||
| "error"
|
||||
| "client_message"
|
||||
| "server_message"
|
||||
| "connection_start"
|
||||
| "connection_end";
|
||||
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number,
|
||||
/**
|
||||
* Server URL (http for plaintext or https for secure)
|
||||
*/
|
||||
url: string, };
|
||||
export type GrpcRequest = {
|
||||
model: "grpc_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authenticationType: string | null;
|
||||
authentication: Record<string, any>;
|
||||
description: string;
|
||||
message: string;
|
||||
metadata: Array<HttpRequestHeader>;
|
||||
method: string | null;
|
||||
name: string;
|
||||
service: string | null;
|
||||
sortPriority: number;
|
||||
/**
|
||||
* Server URL (http for plaintext or https for secure)
|
||||
*/
|
||||
url: string;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
};
|
||||
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>, };
|
||||
export type HttpRequest = {
|
||||
model: "http_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
body: Record<string, any>;
|
||||
bodyType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
method: string;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
url: string;
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
settingValidateCertificates: InheritedBoolSetting;
|
||||
settingFollowRedirects: InheritedBoolSetting;
|
||||
settingRequestTimeout: InheritedIntSetting;
|
||||
};
|
||||
|
||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string };
|
||||
|
||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||
export type HttpResponse = {
|
||||
model: "http_response";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
bodyPath: string | null;
|
||||
contentLength: number | null;
|
||||
contentLengthCompressed: number | null;
|
||||
elapsed: number;
|
||||
elapsedHeaders: number;
|
||||
elapsedDns: number;
|
||||
error: string | null;
|
||||
headers: Array<HttpResponseHeader>;
|
||||
remoteAddr: string | null;
|
||||
requestContentLength: number | null;
|
||||
requestHeaders: Array<HttpResponseHeader>;
|
||||
status: number;
|
||||
statusReason: string | null;
|
||||
state: HttpResponseState;
|
||||
url: string;
|
||||
version: string | null;
|
||||
};
|
||||
|
||||
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
||||
export type HttpResponseEvent = {
|
||||
model: "http_response_event";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
responseId: string;
|
||||
event: HttpResponseEventData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializable representation of HTTP response events for DB storage.
|
||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||
*/
|
||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||
export type HttpResponseEventData =
|
||||
| {
|
||||
type: "setting";
|
||||
name: string;
|
||||
value: string;
|
||||
source_model?: string;
|
||||
source_id?: string;
|
||||
source_name?: string;
|
||||
}
|
||||
| { type: "info"; message: string }
|
||||
| {
|
||||
type: "redirect";
|
||||
url: string;
|
||||
status: number;
|
||||
behavior: string;
|
||||
dropped_body: boolean;
|
||||
dropped_headers: Array<string>;
|
||||
}
|
||||
| {
|
||||
type: "send_url";
|
||||
method: string;
|
||||
scheme: string;
|
||||
username: string;
|
||||
password: string;
|
||||
host: string;
|
||||
port: number;
|
||||
path: string;
|
||||
query: string;
|
||||
fragment: string;
|
||||
}
|
||||
| { type: "receive_url"; version: string; status: string }
|
||||
| { type: "header_up"; name: string; value: string }
|
||||
| { type: "header_down"; name: string; value: string }
|
||||
| { type: "chunk_sent"; bytes: number }
|
||||
| { type: "chunk_received"; bytes: number }
|
||||
| {
|
||||
type: "dns_resolved";
|
||||
hostname: string;
|
||||
addresses: Array<string>;
|
||||
duration: bigint;
|
||||
overridden: boolean;
|
||||
};
|
||||
|
||||
export type HttpResponseHeader = { name: string, value: string, };
|
||||
export type HttpResponseHeader = { name: string; value: string };
|
||||
|
||||
export type HttpResponseState = "initialized" | "connected" | "closed";
|
||||
|
||||
export type HttpUrlParameter = { enabled?: boolean,
|
||||
/**
|
||||
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||
* Other entries are appended as query parameters
|
||||
*/
|
||||
name: string, value: string, id?: string, };
|
||||
export type HttpUrlParameter = {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||
* Other entries are appended as query parameters
|
||||
*/
|
||||
name: string;
|
||||
value: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
|
||||
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
|
||||
|
||||
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
|
||||
export type InheritedIntSetting = { enabled?: boolean; value: number };
|
||||
|
||||
export type KeyValue = {
|
||||
model: "key_value";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
key: string;
|
||||
namespace: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type Plugin = {
|
||||
model: "plugin";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
checkedAt: string | null;
|
||||
directory: string;
|
||||
enabled: boolean;
|
||||
url: string | null;
|
||||
source: PluginSource;
|
||||
};
|
||||
|
||||
export type PluginSource = "bundled" | "filesystem" | "registry";
|
||||
|
||||
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
|
||||
export type ProxySetting =
|
||||
| {
|
||||
type: "enabled";
|
||||
http: string;
|
||||
https: string;
|
||||
auth: ProxySettingAuth | null;
|
||||
bypass: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
| { type: "disabled" };
|
||||
|
||||
export type ProxySettingAuth = { user: string, password: string, };
|
||||
export type ProxySettingAuth = { user: string; password: string };
|
||||
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, clientCertificates: Array<ClientCertificate>, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, useNativeTitlebar: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, checkNotifications: boolean, hotkeys: { [key in string]?: Array<string> }, };
|
||||
export type Settings = {
|
||||
model: "settings";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
appearance: string;
|
||||
clientCertificates: Array<ClientCertificate>;
|
||||
coloredMethods: boolean;
|
||||
editorFont: string | null;
|
||||
editorFontSize: number;
|
||||
editorKeymap: EditorKeymap;
|
||||
editorSoftWrap: boolean;
|
||||
hideWindowControls: boolean;
|
||||
useNativeTitlebar: boolean;
|
||||
interfaceFont: string | null;
|
||||
interfaceFontSize: number;
|
||||
interfaceScale: number;
|
||||
openWorkspaceNewWindow: boolean | null;
|
||||
proxy: ProxySetting | null;
|
||||
themeDark: string;
|
||||
themeLight: string;
|
||||
updateChannel: string;
|
||||
hideLicenseBadge: boolean;
|
||||
autoupdate: boolean;
|
||||
autoDownloadUpdates: boolean;
|
||||
checkNotifications: boolean;
|
||||
hotkeys: { [key in string]?: Array<string> };
|
||||
};
|
||||
|
||||
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
|
||||
export type SyncState = {
|
||||
model: "sync_state";
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
flushedAt: string;
|
||||
modelId: string;
|
||||
checksum: string;
|
||||
relPath: string;
|
||||
syncDir: string;
|
||||
};
|
||||
|
||||
export type WebsocketConnection = { model: "websocket_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, headers: Array<HttpResponseHeader>, state: WebsocketConnectionState, status: number, url: string, };
|
||||
export type WebsocketConnection = {
|
||||
model: "websocket_connection";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
elapsed: number;
|
||||
error: string | null;
|
||||
headers: Array<HttpResponseHeader>;
|
||||
state: WebsocketConnectionState;
|
||||
status: number;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed";
|
||||
|
||||
export type WebsocketEvent = { model: "websocket_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, isServer: boolean, message: Array<number>, messageType: WebsocketEventType, };
|
||||
export type WebsocketEvent = {
|
||||
model: "websocket_event";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
connectionId: string;
|
||||
isServer: boolean;
|
||||
message: Array<number>;
|
||||
messageType: WebsocketEventType;
|
||||
};
|
||||
|
||||
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string,
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>, };
|
||||
export type WebsocketRequest = {
|
||||
model: "websocket_request";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
folderId: string | null;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
message: string;
|
||||
name: string;
|
||||
sortPriority: number;
|
||||
url: string;
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>;
|
||||
settingSendCookies: InheritedBoolSetting;
|
||||
settingStoreCookies: InheritedBoolSetting;
|
||||
};
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||
export type Workspace = {
|
||||
model: "workspace";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
description: string;
|
||||
headers: Array<HttpRequestHeader>;
|
||||
name: string;
|
||||
encryptionKeyChallenge: string | null;
|
||||
settingValidateCertificates: boolean;
|
||||
settingFollowRedirects: boolean;
|
||||
settingRequestTimeout: number;
|
||||
settingDnsOverrides: Array<DnsOverride>;
|
||||
settingSendCookies: boolean;
|
||||
settingStoreCookies: boolean;
|
||||
};
|
||||
|
||||
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
||||
export type WorkspaceMeta = {
|
||||
model: "workspace_meta";
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
encryptionKey: EncryptedKey | null;
|
||||
settingSyncDir: string | null;
|
||||
};
|
||||
|
||||
@@ -91,8 +91,9 @@ import {
|
||||
HomeIcon,
|
||||
ImportIcon,
|
||||
InfoIcon,
|
||||
KeyboardIcon,
|
||||
KeyRoundIcon,
|
||||
KeyboardIcon,
|
||||
ListXIcon,
|
||||
LockIcon,
|
||||
LockOpenIcon,
|
||||
MergeIcon,
|
||||
@@ -131,12 +132,15 @@ import {
|
||||
SunIcon,
|
||||
TableIcon,
|
||||
Trash2Icon,
|
||||
Undo2Icon,
|
||||
UploadIcon,
|
||||
VariableIcon,
|
||||
Wand2Icon,
|
||||
WifiIcon,
|
||||
WrenchIcon,
|
||||
XIcon,
|
||||
ZapIcon,
|
||||
ZapOffIcon,
|
||||
} from "lucide-react";
|
||||
import type { CSSProperties, HTMLAttributes } from "react";
|
||||
import { memo } from "react";
|
||||
@@ -238,6 +242,7 @@ const icons = {
|
||||
keyboard: KeyboardIcon,
|
||||
left_panel_hidden: PanelLeftOpenIcon,
|
||||
left_panel_visible: PanelLeftCloseIcon,
|
||||
list_x: ListXIcon,
|
||||
lock: LockIcon,
|
||||
lock_open: LockOpenIcon,
|
||||
magic_wand: Wand2Icon,
|
||||
@@ -271,6 +276,7 @@ const icons = {
|
||||
table: TableIcon,
|
||||
text: FileTextIcon,
|
||||
trash: Trash2Icon,
|
||||
undo_2: Undo2Icon,
|
||||
unpin: PinOffIcon,
|
||||
update: RefreshCcwIcon,
|
||||
upload: UploadIcon,
|
||||
@@ -278,6 +284,8 @@ const icons = {
|
||||
wifi: WifiIcon,
|
||||
wrench: WrenchIcon,
|
||||
x: XIcon,
|
||||
zap: ZapIcon,
|
||||
zap_off: ZapOffIcon,
|
||||
_unknown: ShieldAlertIcon,
|
||||
|
||||
empty: (props: HTMLAttributes<HTMLSpanElement>) => <div {...props} />,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import classNames from "classnames";
|
||||
import type { HTMLAttributes } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function Table({
|
||||
@@ -28,9 +29,18 @@ export function Table({
|
||||
);
|
||||
}
|
||||
|
||||
export function TableBody({ children }: { children: ReactNode }) {
|
||||
export function TableBody({ children, className }: { children: ReactNode; className?: string }) {
|
||||
return (
|
||||
<tbody className="[&>tr:not(:last-child)>td]:border-b [&>tr:not(:last-child)>td]:border-b-surface-highlight">
|
||||
<tbody
|
||||
className={classNames(
|
||||
className,
|
||||
"[&>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}
|
||||
</tbody>
|
||||
);
|
||||
@@ -49,8 +59,19 @@ export function TableHead({ children, className }: { children: ReactNode; classN
|
||||
);
|
||||
}
|
||||
|
||||
export function TableRow({ children }: { children: ReactNode }) {
|
||||
return <tr>{children}</tr>;
|
||||
export function TableRow({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
} & HTMLAttributes<HTMLTableRowElement>) {
|
||||
return (
|
||||
<tr className={className} {...props}>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableCell({
|
||||
@@ -98,7 +119,7 @@ export function TableHeaderCell({
|
||||
<th
|
||||
className={classNames(
|
||||
className,
|
||||
"py-2 [&:not(:first-child)]:pl-4 text-left text-text-subtle",
|
||||
"whitespace-nowrap py-2 [&:not(:first-child)]:pl-4 text-left text-text-subtle",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user