mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-05-18 05:37:09 +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 type { WorkspaceSettingsTab } from "../components/WorkspaceSettingsDialog";
|
||||||
import { WorkspaceSettingsDialog } from "../components/WorkspaceSettingsDialog";
|
import { WorkspaceSettingsDialog } from "../components/WorkspaceSettingsDialog";
|
||||||
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
||||||
import { showDialog } from "../lib/dialog";
|
|
||||||
import { jotaiStore } from "../lib/jotai";
|
import { jotaiStore } from "../lib/jotai";
|
||||||
|
|
||||||
export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) {
|
export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) {
|
||||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||||
if (workspaceId == null) return;
|
if (workspaceId == null) return;
|
||||||
showDialog({
|
WorkspaceSettingsDialog.show(workspaceId, tab);
|
||||||
id: "workspace-settings",
|
|
||||||
size: "md",
|
|
||||||
className: "h-[calc(100vh-5rem)] !max-h-[40rem]",
|
|
||||||
noPadding: true,
|
|
||||||
render: ({ hide }) => (
|
|
||||||
<WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { createFolder } from "../commands/commands";
|
import { createFolder } from "../commands/commands";
|
||||||
import { createSubEnvironmentAndActivate } from "../commands/createEnvironment";
|
import { createSubEnvironmentAndActivate } from "../commands/createEnvironment";
|
||||||
import { openSettings } from "../commands/openSettings";
|
import { openSettings } from "../commands/openSettings";
|
||||||
|
import { openWorkspaceSettings } from "../commands/openWorkspaceSettings";
|
||||||
import { switchWorkspace } from "../commands/switchWorkspace";
|
import { switchWorkspace } from "../commands/switchWorkspace";
|
||||||
import { useActiveCookieJar } from "../hooks/useActiveCookieJar";
|
import { useActiveCookieJar } from "../hooks/useActiveCookieJar";
|
||||||
import { useActiveEnvironment } from "../hooks/useActiveEnvironment";
|
import { useActiveEnvironment } from "../hooks/useActiveEnvironment";
|
||||||
@@ -36,7 +37,6 @@ import { appInfo } from "../lib/appInfo";
|
|||||||
import { copyToClipboard } from "../lib/copy";
|
import { copyToClipboard } from "../lib/copy";
|
||||||
import { createRequestAndNavigate } from "../lib/createRequestAndNavigate";
|
import { createRequestAndNavigate } from "../lib/createRequestAndNavigate";
|
||||||
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
||||||
import { showDialog } from "../lib/dialog";
|
|
||||||
import { editEnvironment } from "../lib/editEnvironment";
|
import { editEnvironment } from "../lib/editEnvironment";
|
||||||
import { renameModelWithPrompt } from "../lib/renameModelWithPrompt";
|
import { renameModelWithPrompt } from "../lib/renameModelWithPrompt";
|
||||||
import {
|
import {
|
||||||
@@ -99,6 +99,12 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
|||||||
action: "settings.show",
|
action: "settings.show",
|
||||||
onSelect: () => openSettings.mutate(null),
|
onSelect: () => openSettings.mutate(null),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "workspace_settings.open",
|
||||||
|
label: "Open Workspace Settings",
|
||||||
|
action: "workspace_settings.show",
|
||||||
|
onSelect: () => openWorkspaceSettings(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "app.create",
|
key: "app.create",
|
||||||
label: "Create Workspace",
|
label: "Create Workspace",
|
||||||
@@ -128,12 +134,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
|||||||
key: "cookies.show",
|
key: "cookies.show",
|
||||||
label: "Show Cookies",
|
label: "Show Cookies",
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
showDialog({
|
CookieDialog.show(activeCookieJar?.id ?? null);
|
||||||
id: "cookies",
|
|
||||||
title: "Manage Cookies",
|
|
||||||
size: "full",
|
|
||||||
render: () => <CookieDialog cookieJarId={activeCookieJar?.id ?? null} />,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,190 +1,659 @@
|
|||||||
import type { Cookie, CookieDomain, CookieJar } from "@yaakapp-internal/models";
|
import type { Cookie } from "@yaakapp-internal/models";
|
||||||
import { cookieJarsAtom, patchModelById } from "@yaakapp-internal/models";
|
import { cookieJarsAtom, patchModel } from "@yaakapp-internal/models";
|
||||||
|
import { formatDate } from "date-fns/format";
|
||||||
import { useAtomValue } from "jotai";
|
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 { cookieDomain } from "../lib/model_util";
|
||||||
import { showPromptForm } from "../lib/prompt-form";
|
import {
|
||||||
import { Banner, InlineCode } from "@yaakapp-internal/ui";
|
Icon,
|
||||||
|
SplitLayout,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
TruncatedWideTableCell,
|
||||||
|
} from "@yaakapp-internal/ui";
|
||||||
import { IconButton } from "./core/IconButton";
|
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 {
|
interface Props {
|
||||||
cookieJarId: string | null;
|
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) => {
|
export const CookieDialog = ({ cookieJarId }: Props) => {
|
||||||
const cookieJars = useAtomValue(cookieJarsAtom);
|
const cookieJars = useAtomValue(cookieJarsAtom);
|
||||||
const cookieJar = cookieJars?.find((c) => c.id === cookieJarId);
|
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) {
|
if (cookieJar == null) {
|
||||||
return <div>No cookie jar selected</div>;
|
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 (
|
return (
|
||||||
<div className="pb-2">
|
<div className="pb-2 grid grid-rows-[auto_minmax(0,1fr)] space-y-2">
|
||||||
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
|
||||||
<thead>
|
<PlainInput
|
||||||
<tr>
|
name="cookie-filter"
|
||||||
<th className="py-2 text-left">Domain</th>
|
label="Filter cookies"
|
||||||
<th className="py-2 text-left pl-4">Cookie</th>
|
hideLabel
|
||||||
<th className="py-2 pl-4 w-10">
|
placeholder="Filter cookies"
|
||||||
|
defaultValue={filter}
|
||||||
|
forceUpdateKey={filterUpdateKey}
|
||||||
|
onChange={setFilter}
|
||||||
|
rightSlot={
|
||||||
|
filter.length > 0 && (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="plus"
|
className="!bg-transparent !h-auto min-h-full opacity-50 hover:opacity-100 -mr-1"
|
||||||
size="xs"
|
icon="x"
|
||||||
iconSize="sm"
|
title="Clear filter"
|
||||||
title="Add Cookie"
|
onClick={() => {
|
||||||
className="ml-auto"
|
setFilter("");
|
||||||
onClick={onAddCookie}
|
setFilterUpdateKey((key) => key + 1);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</th>
|
)
|
||||||
</tr>
|
}
|
||||||
</thead>
|
/>
|
||||||
<tbody className="divide-y divide-surface-highlight">{tableBody}</tbody>
|
<IconButton icon="plus" size="sm" title="Add cookie" onClick={handleAddCookie} />
|
||||||
</table>
|
</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>
|
</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 { useActiveCookieJar } from "../hooks/useActiveCookieJar";
|
||||||
import { useCreateCookieJar } from "../hooks/useCreateCookieJar";
|
import { useCreateCookieJar } from "../hooks/useCreateCookieJar";
|
||||||
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
||||||
import { showDialog } from "../lib/dialog";
|
|
||||||
import { showPrompt } from "../lib/prompt";
|
import { showPrompt } from "../lib/prompt";
|
||||||
import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
|
import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
|
||||||
import { CookieDialog } from "./CookieDialog";
|
import { CookieDialog } from "./CookieDialog";
|
||||||
@@ -36,12 +35,7 @@ export const CookieDropdown = memo(function CookieDropdown() {
|
|||||||
leftSlot: <Icon icon="cookie" />,
|
leftSlot: <Icon icon="cookie" />,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
if (activeCookieJar == null) return;
|
if (activeCookieJar == null) return;
|
||||||
showDialog({
|
CookieDialog.show(activeCookieJar.id);
|
||||||
id: "cookies",
|
|
||||||
title: "Manage Cookies",
|
|
||||||
size: "full",
|
|
||||||
render: () => <CookieDialog cookieJarId={activeCookieJar.id} />,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { EnvironmentEditor } from "./EnvironmentEditor";
|
|||||||
import { HeadersEditor } from "./HeadersEditor";
|
import { HeadersEditor } from "./HeadersEditor";
|
||||||
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
||||||
import { MarkdownEditor } from "./MarkdownEditor";
|
import { MarkdownEditor } from "./MarkdownEditor";
|
||||||
|
import { countOverriddenSettings, ModelSettingsEditor } from "./ModelSettingsEditor";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
folderId: string | null;
|
folderId: string | null;
|
||||||
@@ -29,6 +30,7 @@ interface Props {
|
|||||||
|
|
||||||
const TAB_AUTH = "auth";
|
const TAB_AUTH = "auth";
|
||||||
const TAB_HEADERS = "headers";
|
const TAB_HEADERS = "headers";
|
||||||
|
const TAB_SETTINGS = "settings";
|
||||||
const TAB_VARIABLES = "variables";
|
const TAB_VARIABLES = "variables";
|
||||||
const TAB_GENERAL = "general";
|
const TAB_GENERAL = "general";
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ export type FolderSettingsTab =
|
|||||||
| typeof TAB_AUTH
|
| typeof TAB_AUTH
|
||||||
| typeof TAB_HEADERS
|
| typeof TAB_HEADERS
|
||||||
| typeof TAB_GENERAL
|
| typeof TAB_GENERAL
|
||||||
|
| typeof TAB_SETTINGS
|
||||||
| typeof TAB_VARIABLES;
|
| typeof TAB_VARIABLES;
|
||||||
|
|
||||||
export function FolderSettingsDialog({ folderId, tab }: Props) {
|
export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||||
@@ -51,6 +54,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
(e) => e.parentModel === "folder" && e.parentId === folderId,
|
(e) => e.parentModel === "folder" && e.parentId === folderId,
|
||||||
);
|
);
|
||||||
const numVars = (folderEnvironment?.variables ?? []).filter((v) => v.name).length;
|
const numVars = (folderEnvironment?.variables ?? []).filter((v) => v.name).length;
|
||||||
|
const numSettingsOverrides = folder == null ? 0 : countOverriddenSettings(folder);
|
||||||
|
|
||||||
const tabs = useMemo<TabItem[]>(() => {
|
const tabs = useMemo<TabItem[]>(() => {
|
||||||
if (folder == null) return [];
|
if (folder == null) return [];
|
||||||
@@ -60,6 +64,11 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
value: TAB_GENERAL,
|
value: TAB_GENERAL,
|
||||||
label: "General",
|
label: "General",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: TAB_SETTINGS,
|
||||||
|
label: "Settings",
|
||||||
|
rightSlot: <CountBadge count={numSettingsOverrides} />,
|
||||||
|
},
|
||||||
...headersTab,
|
...headersTab,
|
||||||
...authTab,
|
...authTab,
|
||||||
{
|
{
|
||||||
@@ -68,7 +77,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
rightSlot: numVars > 0 ? <CountBadge count={numVars} /> : null,
|
rightSlot: numVars > 0 ? <CountBadge count={numVars} /> : null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [authTab, folder, headersTab, numVars]);
|
}, [authTab, folder, headersTab, numSettingsOverrides, numVars]);
|
||||||
|
|
||||||
if (folder == null) return null;
|
if (folder == null) return null;
|
||||||
|
|
||||||
@@ -159,6 +168,9 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
stateKey={`headers.${folder.id}`}
|
stateKey={`headers.${folder.id}`}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</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">
|
<TabContent value={TAB_VARIABLES} className="overflow-y-auto h-full px-4">
|
||||||
{folderEnvironment == null ? (
|
{folderEnvironment == null ? (
|
||||||
<EmptyStateText>
|
<EmptyStateText>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { GrpcEditor } from "./GrpcEditor";
|
|||||||
import { HeadersEditor } from "./HeadersEditor";
|
import { HeadersEditor } from "./HeadersEditor";
|
||||||
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
||||||
import { MarkdownEditor } from "./MarkdownEditor";
|
import { MarkdownEditor } from "./MarkdownEditor";
|
||||||
|
import { countOverriddenSettings, ModelSettingsEditor } from "./ModelSettingsEditor";
|
||||||
import { UrlBar } from "./UrlBar";
|
import { UrlBar } from "./UrlBar";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -47,6 +48,7 @@ interface Props {
|
|||||||
const TAB_MESSAGE = "message";
|
const TAB_MESSAGE = "message";
|
||||||
const TAB_METADATA = "metadata";
|
const TAB_METADATA = "metadata";
|
||||||
const TAB_AUTH = "auth";
|
const TAB_AUTH = "auth";
|
||||||
|
const TAB_SETTINGS = "settings";
|
||||||
const TAB_DESCRIPTION = "description";
|
const TAB_DESCRIPTION = "description";
|
||||||
|
|
||||||
export function GrpcRequestPane({
|
export function GrpcRequestPane({
|
||||||
@@ -66,6 +68,7 @@ export function GrpcRequestPane({
|
|||||||
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||||
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, "Metadata");
|
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, "Metadata");
|
||||||
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
||||||
|
const numSettingsOverrides = countOverriddenSettings(activeRequest);
|
||||||
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
|
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
|
||||||
|
|
||||||
const urlContainerEl = useRef<HTMLDivElement>(null);
|
const urlContainerEl = useRef<HTMLDivElement>(null);
|
||||||
@@ -128,13 +131,18 @@ export function GrpcRequestPane({
|
|||||||
{ value: TAB_MESSAGE, label: "Message" },
|
{ value: TAB_MESSAGE, label: "Message" },
|
||||||
...metadataTab,
|
...metadataTab,
|
||||||
...authTab,
|
...authTab,
|
||||||
|
{
|
||||||
|
value: TAB_SETTINGS,
|
||||||
|
label: "Settings",
|
||||||
|
rightSlot: <CountBadge count={numSettingsOverrides} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: TAB_DESCRIPTION,
|
value: TAB_DESCRIPTION,
|
||||||
label: "Info",
|
label: "Info",
|
||||||
rightSlot: activeRequest.description && <CountBadge count={true} />,
|
rightSlot: activeRequest.description && <CountBadge count={true} />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[activeRequest.description, authTab, metadataTab],
|
[activeRequest.description, authTab, metadataTab, numSettingsOverrides],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMetadataChange = useCallback(
|
const handleMetadataChange = useCallback(
|
||||||
@@ -278,6 +286,9 @@ export function GrpcRequestPane({
|
|||||||
onChange={handleMetadataChange}
|
onChange={handleMetadataChange}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
<TabContent value={TAB_SETTINGS}>
|
||||||
|
<ModelSettingsEditor model={activeRequest} />
|
||||||
|
</TabContent>
|
||||||
<TabContent value={TAB_DESCRIPTION}>
|
<TabContent value={TAB_DESCRIPTION}>
|
||||||
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
|
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
|
||||||
<PlainInput
|
<PlainInput
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
|||||||
import { JsonBodyEditor } from "./JsonBodyEditor";
|
import { JsonBodyEditor } from "./JsonBodyEditor";
|
||||||
import { MarkdownEditor } from "./MarkdownEditor";
|
import { MarkdownEditor } from "./MarkdownEditor";
|
||||||
import { RequestMethodDropdown } from "./RequestMethodDropdown";
|
import { RequestMethodDropdown } from "./RequestMethodDropdown";
|
||||||
|
import { countOverriddenSettings, ModelSettingsEditor } from "./ModelSettingsEditor";
|
||||||
import { UrlBar } from "./UrlBar";
|
import { UrlBar } from "./UrlBar";
|
||||||
import { UrlParametersEditor } from "./UrlParameterEditor";
|
import { UrlParametersEditor } from "./UrlParameterEditor";
|
||||||
|
|
||||||
@@ -69,6 +70,7 @@ const TAB_BODY = "body";
|
|||||||
const TAB_PARAMS = "params";
|
const TAB_PARAMS = "params";
|
||||||
const TAB_HEADERS = "headers";
|
const TAB_HEADERS = "headers";
|
||||||
const TAB_AUTH = "auth";
|
const TAB_AUTH = "auth";
|
||||||
|
const TAB_SETTINGS = "settings";
|
||||||
const TAB_DESCRIPTION = "description";
|
const TAB_DESCRIPTION = "description";
|
||||||
const TABS_STORAGE_KEY = "http_request_tabs";
|
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 authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||||
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
||||||
const inheritedHeaders = useInheritedHeaders(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)
|
// Listen for event to focus the params tab (e.g., when clicking a :param in the URL)
|
||||||
useRequestEditorEvent(
|
useRequestEditorEvent(
|
||||||
@@ -234,6 +237,11 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
},
|
},
|
||||||
...headersTab,
|
...headersTab,
|
||||||
...authTab,
|
...authTab,
|
||||||
|
{
|
||||||
|
value: TAB_SETTINGS,
|
||||||
|
label: "Settings",
|
||||||
|
rightSlot: <CountBadge count={numSettingsOverrides} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: TAB_DESCRIPTION,
|
value: TAB_DESCRIPTION,
|
||||||
label: "Info",
|
label: "Info",
|
||||||
@@ -246,6 +254,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
handleContentTypeChange,
|
handleContentTypeChange,
|
||||||
headersTab,
|
headersTab,
|
||||||
numParams,
|
numParams,
|
||||||
|
numSettingsOverrides,
|
||||||
urlParameterPairs.length,
|
urlParameterPairs.length,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -372,6 +381,9 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
onChange={(urlParameters) => patchModel(activeRequest, { urlParameters })}
|
onChange={(urlParameters) => patchModel(activeRequest, { urlParameters })}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
<TabContent value={TAB_SETTINGS}>
|
||||||
|
<ModelSettingsEditor model={activeRequest} />
|
||||||
|
</TabContent>
|
||||||
<TabContent value={TAB_BODY}>
|
<TabContent value={TAB_BODY}>
|
||||||
<ConfirmLargeRequestBody request={activeRequest}>
|
<ConfirmLargeRequestBody request={activeRequest}>
|
||||||
{activeRequest.bodyType === BODY_TYPE_JSON ? (
|
{activeRequest.bodyType === BODY_TYPE_JSON ? (
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import type {
|
import type {
|
||||||
|
AnyModel,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
HttpResponseEvent,
|
HttpResponseEvent,
|
||||||
HttpResponseEventData,
|
HttpResponseEventData,
|
||||||
} from "@yaakapp-internal/models";
|
} from "@yaakapp-internal/models";
|
||||||
|
import { foldersAtom, workspacesAtom } from "@yaakapp-internal/models";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
import { type ReactNode, useMemo, useState } from "react";
|
import { type ReactNode, useMemo, useState } from "react";
|
||||||
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
|
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
|
||||||
|
import { useAllRequests } from "../hooks/useAllRequests";
|
||||||
|
import { resolvedModelName } from "../lib/resolvedModelName";
|
||||||
import { Editor } from "./core/Editor/LazyEditor";
|
import { Editor } from "./core/Editor/LazyEditor";
|
||||||
import { type EventDetailAction, EventDetailHeader, EventViewer } from "./core/EventViewer";
|
import { type EventDetailAction, EventDetailHeader, EventViewer } from "./core/EventViewer";
|
||||||
import { EventViewerRow } from "./core/EventViewerRow";
|
import { EventViewerRow } from "./core/EventViewerRow";
|
||||||
@@ -95,6 +100,7 @@ function EventDetails({
|
|||||||
}) {
|
}) {
|
||||||
const { label } = getEventDisplay(event.event);
|
const { label } = getEventDisplay(event.event);
|
||||||
const e = event.event;
|
const e = event.event;
|
||||||
|
const settingSourceModels = useSettingSourceModels();
|
||||||
|
|
||||||
const actions: EventDetailAction[] = [
|
const actions: EventDetailAction[] = [
|
||||||
{
|
{
|
||||||
@@ -211,6 +217,9 @@ function EventDetails({
|
|||||||
<KeyValueRows>
|
<KeyValueRows>
|
||||||
<KeyValueRow label="Setting">{e.name}</KeyValueRow>
|
<KeyValueRow label="Setting">{e.name}</KeyValueRow>
|
||||||
<KeyValueRow label="Value">{e.value}</KeyValueRow>
|
<KeyValueRow label="Value">{e.value}</KeyValueRow>
|
||||||
|
{e.source_model != null ? (
|
||||||
|
<KeyValueRow label="Source">{formatSettingSource(e, settingSourceModels)}</KeyValueRow>
|
||||||
|
) : null}
|
||||||
</KeyValueRows>
|
</KeyValueRows>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -315,6 +324,35 @@ function formatEventText(event: HttpResponseEventData, includePrefix: boolean):
|
|||||||
return includePrefix ? `${prefix} ${text}` : text;
|
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 = {
|
type EventDisplay = {
|
||||||
icon: IconProps["icon"];
|
icon: IconProps["icon"];
|
||||||
color: IconProps["color"];
|
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;
|
inline?: boolean;
|
||||||
noun?: string;
|
noun?: string;
|
||||||
help?: ReactNode;
|
help?: ReactNode;
|
||||||
|
hideLabel?: boolean;
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ export function SelectFile({
|
|||||||
size = "sm",
|
size = "sm",
|
||||||
label,
|
label,
|
||||||
help,
|
help,
|
||||||
|
hideLabel,
|
||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
@@ -95,7 +97,7 @@ export function SelectFile({
|
|||||||
return (
|
return (
|
||||||
<div ref={ref} className="w-full">
|
<div ref={ref} className="w-full">
|
||||||
{label && (
|
{label && (
|
||||||
<Label htmlFor={null} help={help}>
|
<Label htmlFor={null} help={help} visuallyHidden={hideLabel}>
|
||||||
{label}
|
{label}
|
||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,12 +7,18 @@ import { useCheckForUpdates } from "../../hooks/useCheckForUpdates";
|
|||||||
import { appInfo } from "../../lib/appInfo";
|
import { appInfo } from "../../lib/appInfo";
|
||||||
import { revealInFinderText } from "../../lib/reveal";
|
import { revealInFinderText } from "../../lib/reveal";
|
||||||
import { CargoFeature } from "../CargoFeature";
|
import { CargoFeature } from "../CargoFeature";
|
||||||
import { Checkbox } from "../core/Checkbox";
|
|
||||||
import { IconButton } from "../core/IconButton";
|
import { IconButton } from "../core/IconButton";
|
||||||
import { KeyValueRow, KeyValueRows } from "../core/KeyValueRow";
|
import {
|
||||||
import { PlainInput } from "../core/PlainInput";
|
ModelSettingRowBoolean,
|
||||||
import { Select } from "../core/Select";
|
ModelSettingRowNumber,
|
||||||
import { Separator } from "../core/Separator";
|
ModelSettingSelectControl,
|
||||||
|
SettingValue,
|
||||||
|
SettingRow,
|
||||||
|
SettingRowBoolean,
|
||||||
|
SettingRowSelect,
|
||||||
|
SettingsList,
|
||||||
|
SettingsSection,
|
||||||
|
} from "../core/SettingRow";
|
||||||
|
|
||||||
export function SettingsGeneral() {
|
export function SettingsGeneral() {
|
||||||
const workspace = useAtomValue(activeWorkspaceAtom);
|
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||||
@@ -29,147 +35,159 @@ export function SettingsGeneral() {
|
|||||||
<Heading>General</Heading>
|
<Heading>General</Heading>
|
||||||
<p className="text-text-subtle">Configure general settings for update behavior and more.</p>
|
<p className="text-text-subtle">Configure general settings for update behavior and more.</p>
|
||||||
</div>
|
</div>
|
||||||
<CargoFeature feature="updater">
|
<SettingsList className="space-y-8">
|
||||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-1">
|
<CargoFeature feature="updater">
|
||||||
<Select
|
<SettingsSection title="Updates">
|
||||||
name="updateChannel"
|
<SettingRow
|
||||||
label="Update Channel"
|
title="Update Channel"
|
||||||
labelPosition="left"
|
description="Choose whether Yaak should use stable releases or beta releases."
|
||||||
labelClassName="w-[14rem]"
|
>
|
||||||
size="sm"
|
<div className="grid grid-cols-[12rem_auto] gap-1">
|
||||||
value={settings.updateChannel}
|
<ModelSettingSelectControl
|
||||||
onChange={(updateChannel) => patchModel(settings, { updateChannel })}
|
model={settings}
|
||||||
options={[
|
modelKey="updateChannel"
|
||||||
{ label: "Stable", value: "stable" },
|
label="Update Channel"
|
||||||
{ label: "Beta (more frequent)", value: "beta" },
|
selectClassName="!w-full"
|
||||||
]}
|
options={[
|
||||||
/>
|
{ label: "Stable", value: "stable" },
|
||||||
<IconButton
|
{ label: "Beta", value: "beta" },
|
||||||
variant="border"
|
]}
|
||||||
size="sm"
|
/>
|
||||||
title="Check for updates"
|
<IconButton
|
||||||
icon="refresh"
|
variant="border"
|
||||||
spin={checkForUpdates.isPending}
|
size="sm"
|
||||||
onClick={() => checkForUpdates.mutateAsync()}
|
title="Check for updates"
|
||||||
/>
|
icon="refresh"
|
||||||
</div>
|
spin={checkForUpdates.isPending}
|
||||||
|
onClick={() => checkForUpdates.mutateAsync()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingRow>
|
||||||
|
|
||||||
<Select
|
<SettingRowSelect
|
||||||
name="autoupdate"
|
title="Update Behavior"
|
||||||
value={settings.autoupdate ? "auto" : "manual"}
|
description="Choose whether updates are installed automatically or manually."
|
||||||
label="Update Behavior"
|
name="autoupdate"
|
||||||
labelPosition="left"
|
value={settings.autoupdate ? "auto" : "manual"}
|
||||||
size="sm"
|
onChange={(v) => patchModel(settings, { autoupdate: v === "auto" })}
|
||||||
labelClassName="w-[14rem]"
|
options={[
|
||||||
onChange={(v) => patchModel(settings, { autoupdate: v === "auto" })}
|
{ label: "Automatic", value: "auto" },
|
||||||
options={[
|
{ label: "Manual", value: "manual" },
|
||||||
{ 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)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<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}
|
<ModelSettingRowNumber
|
||||||
</KeyValueRow>
|
model={workspace}
|
||||||
<KeyValueRow
|
modelKey="settingRequestTimeout"
|
||||||
label="Logs Directory"
|
title="Request Timeout"
|
||||||
rightSlot={
|
description="Maximum request duration in milliseconds. Set to 0 to disable the timeout."
|
||||||
<IconButton
|
placeholder="0"
|
||||||
title={revealInFinderText}
|
required
|
||||||
icon="folder_open"
|
validate={(value) => Number.parseInt(value, 10) >= 0}
|
||||||
size="2xs"
|
/>
|
||||||
onClick={() => revealItemInDir(appInfo.appLogDir)}
|
|
||||||
|
<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),
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
}
|
</SettingRow>
|
||||||
>
|
<SettingRow
|
||||||
{appInfo.appLogDir}
|
title="Logs Directory"
|
||||||
</KeyValueRow>
|
description="Where Yaak writes application logs."
|
||||||
</KeyValueRows>
|
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>
|
</VStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useFonts } from "@yaakapp-internal/fonts";
|
|||||||
import { useLicense } from "@yaakapp-internal/license";
|
import { useLicense } from "@yaakapp-internal/license";
|
||||||
import type { EditorKeymap, Settings } from "@yaakapp-internal/models";
|
import type { EditorKeymap, Settings } from "@yaakapp-internal/models";
|
||||||
import { patchModel, settingsAtom } 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 { useAtomValue } from "jotai";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
|
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
|
||||||
@@ -13,7 +13,16 @@ import { CargoFeature } from "../CargoFeature";
|
|||||||
import { Button } from "../core/Button";
|
import { Button } from "../core/Button";
|
||||||
import { Checkbox } from "../core/Checkbox";
|
import { Checkbox } from "../core/Checkbox";
|
||||||
import { Link } from "../core/Link";
|
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__";
|
const NULL_FONT_VALUE = "__NULL_FONT__";
|
||||||
|
|
||||||
@@ -38,154 +47,172 @@ export function SettingsInterface() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack space={3} className="mb-4">
|
<VStack space={1.5} className="mb-4">
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<Heading>Interface</Heading>
|
<Heading>Interface</Heading>
|
||||||
<p className="text-text-subtle">Tweak settings related to the user interface.</p>
|
<p className="text-text-subtle">Tweak settings related to the user interface.</p>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<SettingsList className="space-y-8">
|
||||||
name="switchWorkspaceBehavior"
|
<SettingsSection title="Workspaces">
|
||||||
label="Open workspace behavior"
|
<SettingRowSelect
|
||||||
size="sm"
|
title="Open workspace behavior"
|
||||||
help="When opening a workspace, should it open in the current window or a new window?"
|
description="Choose what happens when opening another workspace."
|
||||||
value={
|
name="switchWorkspaceBehavior"
|
||||||
settings.openWorkspaceNewWindow === true
|
value={
|
||||||
? "new"
|
settings.openWorkspaceNewWindow === true
|
||||||
: settings.openWorkspaceNewWindow === false
|
? "new"
|
||||||
? "current"
|
: settings.openWorkspaceNewWindow === false
|
||||||
: "ask"
|
? "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,
|
|
||||||
})) ?? []),
|
|
||||||
]}
|
|
||||||
onChange={async (v) => {
|
onChange={async (v) => {
|
||||||
const interfaceFont = v === NULL_FONT_VALUE ? null : v;
|
if (v === "current") await patchModel(settings, { openWorkspaceNewWindow: false });
|
||||||
await patchModel(settings, { interfaceFont });
|
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={[
|
options={[
|
||||||
{ label: "System default", value: NULL_FONT_VALUE },
|
{ label: "Always ask", value: "ask" },
|
||||||
...(fonts.data.editorFonts.map((f) => ({
|
{ label: "Open in current window", value: "current" },
|
||||||
label: f,
|
{ label: "Open in new window", value: "new" },
|
||||||
value: f,
|
|
||||||
})) ?? []),
|
|
||||||
]}
|
]}
|
||||||
onChange={async (v) => {
|
|
||||||
const editorFont = v === NULL_FONT_VALUE ? null : v;
|
|
||||||
await patchModel(settings, { editorFont });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</SettingsSection>
|
||||||
<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>
|
|
||||||
|
|
||||||
<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" && (
|
<SettingRow
|
||||||
<Checkbox
|
title="Editor font"
|
||||||
checked={settings.hideWindowControls}
|
description="Font used in request and response editors."
|
||||||
title="Hide window controls"
|
controlClassName="gap-1"
|
||||||
help="Hide the close/maximize/minimize controls on Windows or Linux"
|
>
|
||||||
onChange={(hideWindowControls) => patchModel(settings, { hideWindowControls })}
|
{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>
|
</VStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NativeTitlebarSetting({ settings }: { settings: Settings }) {
|
function NativeTitlebarSetting({ settings }: { settings: Settings }) {
|
||||||
const [nativeTitlebar, setNativeTitlebar] = useState(settings.useNativeTitlebar);
|
const [nativeTitlebar, setNativeTitlebar] = useState(settings.useNativeTitlebar);
|
||||||
|
|
||||||
return (
|
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
|
<Checkbox
|
||||||
|
hideLabel
|
||||||
|
size="md"
|
||||||
checked={nativeTitlebar}
|
checked={nativeTitlebar}
|
||||||
title="Native title bar"
|
title="Native title bar"
|
||||||
help="Use the operating system's standard title bar and window controls"
|
|
||||||
onChange={setNativeTitlebar}
|
onChange={setNativeTitlebar}
|
||||||
/>
|
/>
|
||||||
{settings.useNativeTitlebar !== nativeTitlebar && (
|
{settings.useNativeTitlebar !== nativeTitlebar && (
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
size="2xs"
|
size="xs"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await patchModel(settings, { useNativeTitlebar: nativeTitlebar });
|
await patchModel(settings, { useNativeTitlebar: nativeTitlebar });
|
||||||
await invokeCmd("cmd_restart");
|
await invokeCmd("cmd_restart");
|
||||||
@@ -194,7 +221,7 @@ function NativeTitlebarSetting({ settings }: { settings: Settings }) {
|
|||||||
Apply and Restart
|
Apply and Restart
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</SettingRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,37 +232,40 @@ function LicenseSettings({ settings }: { settings: Settings }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<SettingsSection title="License">
|
||||||
checked={settings.hideLicenseBadge}
|
<SettingRowBoolean
|
||||||
title="Hide personal use badge"
|
checked={settings.hideLicenseBadge}
|
||||||
onChange={async (hideLicenseBadge) => {
|
title="Hide personal use badge"
|
||||||
if (hideLicenseBadge) {
|
description="Hide the personal-use badge from the interface."
|
||||||
const confirmed = await showConfirm({
|
onChange={async (hideLicenseBadge) => {
|
||||||
id: "hide-license-badge",
|
if (hideLicenseBadge) {
|
||||||
title: "Confirm Personal Use",
|
const confirmed = await showConfirm({
|
||||||
confirmText: "Confirm",
|
id: "hide-license-badge",
|
||||||
description: (
|
title: "Confirm Personal Use",
|
||||||
<VStack space={3}>
|
confirmText: "Confirm",
|
||||||
<p>Hey there 👋🏼</p>
|
description: (
|
||||||
<p>
|
<VStack space={3}>
|
||||||
Yaak is free for personal projects and learning.{" "}
|
<p>Hey there 👋🏼</p>
|
||||||
<strong>If you’re using Yaak at work, a license is required.</strong>
|
<p>
|
||||||
</p>
|
Yaak is free for personal projects and learning.{" "}
|
||||||
<p>
|
<strong>If you’re using Yaak at work, a license is required.</strong>
|
||||||
Licenses help keep Yaak independent and sustainable.{" "}
|
</p>
|
||||||
<Link href="https://yaak.app/pricing?s=badge">Purchase a License →</Link>
|
<p>
|
||||||
</p>
|
Licenses help keep Yaak independent and sustainable.{" "}
|
||||||
</VStack>
|
<Link href="https://yaak.app/pricing?s=badge">Purchase a License →</Link>
|
||||||
),
|
</p>
|
||||||
requireTyping: "Personal Use",
|
</VStack>
|
||||||
color: "info",
|
),
|
||||||
});
|
requireTyping: "Personal Use",
|
||||||
if (!confirmed) {
|
color: "info",
|
||||||
return; // Cancel
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
await patchModel(settings, { hideLicenseBadge });
|
||||||
await patchModel(settings, { hideLicenseBadge });
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
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 { useAtomValue } from "jotai";
|
||||||
import { Checkbox } from "../core/Checkbox";
|
import {
|
||||||
import { PlainInput } from "../core/PlainInput";
|
SettingRowBoolean,
|
||||||
import { Select } from "../core/Select";
|
SettingRowSelect,
|
||||||
import { Separator } from "../core/Separator";
|
SettingRowText,
|
||||||
|
SettingsList,
|
||||||
|
SettingsSection,
|
||||||
|
} from "../core/SettingRow";
|
||||||
|
|
||||||
export function SettingsProxy() {
|
export function SettingsProxy() {
|
||||||
const settings = useAtomValue(settingsAtom);
|
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 (
|
return (
|
||||||
<VStack space={1.5} className="mb-4">
|
<VStack space={1.5} className="mb-4">
|
||||||
@@ -18,188 +33,146 @@ export function SettingsProxy() {
|
|||||||
traffic, or routing through specific infrastructure.
|
traffic, or routing through specific infrastructure.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<SettingsList className="space-y-8">
|
||||||
name="proxy"
|
<SettingsSection title="Proxy">
|
||||||
label="Proxy"
|
<SettingRowSelect
|
||||||
hideLabel
|
title="Proxy"
|
||||||
size="sm"
|
description="Choose how Yaak should discover or use proxy settings."
|
||||||
value={settings.proxy?.type ?? "automatic"}
|
name="proxy"
|
||||||
onChange={async (v) => {
|
value={settings.proxy?.type ?? "automatic"}
|
||||||
if (v === "automatic") {
|
onChange={async (v) => {
|
||||||
await patchModel(settings, { proxy: undefined });
|
if (v === "automatic") {
|
||||||
} else if (v === "enabled") {
|
await patchModel(settings, { proxy: undefined });
|
||||||
await patchModel(settings, {
|
} else if (v === "enabled") {
|
||||||
proxy: {
|
await patchModel(settings, { proxy });
|
||||||
disabled: false,
|
} else {
|
||||||
type: "enabled",
|
await patchModel(settings, { proxy: { type: "disabled" } });
|
||||||
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
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
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 && (
|
{settings.proxy?.type === "enabled" && (
|
||||||
<HStack space={1.5}>
|
<>
|
||||||
<PlainInput
|
<SettingsSection title="Custom Proxy">
|
||||||
required
|
<SettingRowBoolean
|
||||||
size="sm"
|
checked={!settings.proxy.disabled}
|
||||||
label="User"
|
title="Enable proxy"
|
||||||
placeholder="myUser"
|
description="Temporarily disable the proxy without losing the configuration."
|
||||||
defaultValue={settings.proxy.auth.user}
|
onChange={(enabled) => patchProxy({ disabled: !enabled })}
|
||||||
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 },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<PlainInput
|
<SettingRowText
|
||||||
size="sm"
|
name="proxyHttp"
|
||||||
label="Password"
|
title={
|
||||||
type="password"
|
<>
|
||||||
placeholder="s3cretPassw0rd"
|
Proxy for <InlineCode>http://</InlineCode> traffic
|
||||||
defaultValue={settings.proxy.auth.password}
|
</>
|
||||||
onChange={async (password) => {
|
}
|
||||||
const { proxy } = settings;
|
description="Proxy host used for unencrypted HTTP traffic."
|
||||||
const http = proxy?.type === "enabled" ? proxy.http : "";
|
value={settings.proxy.http}
|
||||||
const https = proxy?.type === "enabled" ? proxy.https : "";
|
placeholder="localhost:9090"
|
||||||
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
|
onChange={(http) => patchProxy({ http })}
|
||||||
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 },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</HStack>
|
<SettingRowText
|
||||||
)}
|
name="proxyHttps"
|
||||||
{settings.proxy.type === "enabled" && (
|
title={
|
||||||
<>
|
<>
|
||||||
<Separator className="my-6" />
|
Proxy for <InlineCode>https://</InlineCode> traffic
|
||||||
<PlainInput
|
</>
|
||||||
label="Proxy Bypass"
|
}
|
||||||
help="Comma-separated list to bypass the proxy."
|
description="Proxy host used for HTTPS traffic."
|
||||||
defaultValue={settings.proxy.bypass}
|
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"
|
placeholder="127.0.0.1, *.example.com, localhost:3000"
|
||||||
onChange={async (bypass) => {
|
inputWidthClassName="!w-96"
|
||||||
const { proxy } = settings;
|
onChange={(bypass) => patchProxy({ bypass })}
|
||||||
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 },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</SettingsSection>
|
||||||
)}
|
|
||||||
</VStack>
|
<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>
|
</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 { IconButton } from "../core/IconButton";
|
||||||
import { Link } from "../core/Link";
|
import { Link } from "../core/Link";
|
||||||
import type { SelectProps } from "../core/Select";
|
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 })));
|
const Editor = lazy(() => import("../core/Editor/Editor").then((m) => ({ default: m.Editor })));
|
||||||
|
|
||||||
@@ -67,7 +72,7 @@ export function SettingsTheme() {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack space={3} className="mb-4">
|
<VStack space={1.5} className="mb-4">
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<Heading>Theme</Heading>
|
<Heading>Theme</Heading>
|
||||||
<p className="text-text-subtle">
|
<p className="text-text-subtle">
|
||||||
@@ -77,96 +82,92 @@ export function SettingsTheme() {
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<SettingsList className="space-y-8">
|
||||||
name="appearance"
|
<SettingsSection title="Theme">
|
||||||
label="Appearance"
|
<ModelSettingRowSelect
|
||||||
labelPosition="top"
|
model={settings}
|
||||||
size="sm"
|
modelKey="appearance"
|
||||||
value={settings.appearance}
|
title="Appearance"
|
||||||
onChange={(appearance) => patchModel(settings, { appearance })}
|
description="Choose whether Yaak follows your system appearance or uses a fixed mode."
|
||||||
options={[
|
options={[
|
||||||
{ label: "Automatic", value: "system" },
|
{ label: "Automatic", value: "system" },
|
||||||
{ label: "Light", value: "light" },
|
{ label: "Light", value: "light" },
|
||||||
{ label: "Dark", value: "dark" },
|
{ 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 })}
|
|
||||||
/>
|
/>
|
||||||
)}
|
{(settings.appearance === "system" || settings.appearance === "light") && (
|
||||||
{(settings.appearance === "system" || settings.appearance === "dark") && (
|
<SettingRowSelect
|
||||||
<Select
|
name="lightTheme"
|
||||||
hideLabel
|
title="Light theme"
|
||||||
name="darkTheme"
|
description="Theme used when Yaak is in light mode."
|
||||||
className="flex-1"
|
value={activeTheme.data.light.id}
|
||||||
label="Dark Theme"
|
options={lightThemes}
|
||||||
leftSlot={<Icon icon="moon" color="secondary" />}
|
onChange={(themeLight) => patchModel(settings, { themeLight })}
|
||||||
size="sm"
|
/>
|
||||||
value={activeTheme.data.dark.id}
|
)}
|
||||||
options={darkThemes}
|
{(settings.appearance === "system" || settings.appearance === "dark") && (
|
||||||
onChange={(themeDark) => patchModel(settings, { themeDark })}
|
<SettingRowSelect
|
||||||
/>
|
name="darkTheme"
|
||||||
)}
|
title="Dark theme"
|
||||||
</HStack>
|
description="Theme used when Yaak is in dark mode."
|
||||||
|
value={activeTheme.data.dark.id}
|
||||||
|
options={darkThemes}
|
||||||
|
onChange={(themeDark) => patchModel(settings, { themeDark })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
<VStack
|
<SettingsSection title="Preview">
|
||||||
space={3}
|
<VStack
|
||||||
className="mt-3 w-full bg-surface p-3 border border-dashed border-border-subtle rounded overflow-x-auto"
|
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"} />
|
<HStack className="text" space={1.5}>
|
||||||
<strong>{activeTheme.data.active.label}</strong>
|
<Icon icon={appearance === "dark" ? "moon" : "sun"} />
|
||||||
<em>(preview)</em>
|
<strong>{activeTheme.data.active.label}</strong>
|
||||||
</HStack>
|
<em>(preview)</em>
|
||||||
<HStack space={1.5} className="w-full">
|
</HStack>
|
||||||
{buttonColors.map((c, i) => (
|
<HStack space={1.5} className="w-full">
|
||||||
<IconButton
|
{buttonColors.map((c, i) => (
|
||||||
key={c}
|
<IconButton
|
||||||
color={c}
|
key={c}
|
||||||
size="2xs"
|
color={c}
|
||||||
iconSize="xs"
|
size="2xs"
|
||||||
icon={icons[i % icons.length] ?? "info"}
|
iconSize="xs"
|
||||||
iconClassName="text"
|
icon={icons[i % icons.length] ?? "info"}
|
||||||
title={`${c}`}
|
iconClassName="text"
|
||||||
/>
|
title={`${c}`}
|
||||||
))}
|
/>
|
||||||
{buttonColors.map((c, i) => (
|
))}
|
||||||
<IconButton
|
{buttonColors.map((c, i) => (
|
||||||
key={c}
|
<IconButton
|
||||||
color={c}
|
key={c}
|
||||||
variant="border"
|
color={c}
|
||||||
size="2xs"
|
variant="border"
|
||||||
iconSize="xs"
|
size="2xs"
|
||||||
icon={icons[i % icons.length] ?? "info"}
|
iconSize="xs"
|
||||||
iconClassName="text"
|
icon={icons[i % icons.length] ?? "info"}
|
||||||
title={`${c}`}
|
iconClassName="text"
|
||||||
/>
|
title={`${c}`}
|
||||||
))}
|
/>
|
||||||
</HStack>
|
))}
|
||||||
<Suspense>
|
</HStack>
|
||||||
<Editor
|
<Suspense>
|
||||||
defaultValue={[
|
<Editor
|
||||||
"let foo = { // Demo code editor",
|
defaultValue={[
|
||||||
' foo: ("bar" || "baz" ?? \'qux\'),',
|
"let foo = { // Demo code editor",
|
||||||
" baz: [1, 10.2, null, false, true],",
|
' foo: ("bar" || "baz" ?? \'qux\'),',
|
||||||
"};",
|
" baz: [1, 10.2, null, false, true],",
|
||||||
].join("\n")}
|
"};",
|
||||||
heightMode="auto"
|
].join("\n")}
|
||||||
language="javascript"
|
heightMode="auto"
|
||||||
stateKey={null}
|
language="javascript"
|
||||||
/>
|
stateKey={null}
|
||||||
</Suspense>
|
/>
|
||||||
</VStack>
|
</Suspense>
|
||||||
|
</VStack>
|
||||||
|
</SettingsSection>
|
||||||
|
</SettingsList>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,79 @@ import { useState } from "react";
|
|||||||
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
|
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { Checkbox } from "./core/Checkbox";
|
import { Checkbox } from "./core/Checkbox";
|
||||||
|
import { SettingRowBoolean, SettingRowDirectory } from "./core/SettingRow";
|
||||||
import { SelectFile } from "./SelectFile";
|
import { SelectFile } from "./SelectFile";
|
||||||
|
|
||||||
export interface SyncToFilesystemSettingProps {
|
export interface SyncToFilesystemSettingProps {
|
||||||
|
layout?: "form" | "settings";
|
||||||
onChange: (args: { filePath: string | null; initGit?: boolean }) => void;
|
onChange: (args: { filePath: string | null; initGit?: boolean }) => void;
|
||||||
onCreateNewWorkspace: () => void;
|
onCreateNewWorkspace: () => void;
|
||||||
value: { filePath: string | null; initGit?: boolean };
|
value: { filePath: string | null; initGit?: boolean };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SyncToFilesystemSetting({
|
export function SyncToFilesystemSetting({
|
||||||
|
layout = "form",
|
||||||
onChange,
|
onChange,
|
||||||
onCreateNewWorkspace,
|
onCreateNewWorkspace,
|
||||||
value,
|
value,
|
||||||
}: SyncToFilesystemSettingProps) {
|
}: SyncToFilesystemSettingProps) {
|
||||||
const [syncDir, setSyncDir] = useState<string | null>(null);
|
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 (
|
return (
|
||||||
<VStack className="w-full my-2" space={3}>
|
<VStack className="w-full my-2" space={3}>
|
||||||
{syncDir && (
|
{syncDir && (
|
||||||
@@ -47,18 +106,7 @@ export function SyncToFilesystemSetting({
|
|||||||
noun="Directory"
|
noun="Directory"
|
||||||
help="Sync data to a folder for backup and Git integration."
|
help="Sync data to a folder for backup and Git integration."
|
||||||
filePath={value.filePath}
|
filePath={value.filePath}
|
||||||
onChange={async ({ filePath }) => {
|
onChange={async ({ filePath }) => handleFilePathChange(filePath)}
|
||||||
if (filePath != null) {
|
|
||||||
const files = await readDir(filePath);
|
|
||||||
if (files.length > 0) {
|
|
||||||
setSyncDir(filePath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setSyncDir(null);
|
|
||||||
onChange({ ...value, filePath });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{value.filePath && typeof value.initGit === "boolean" && (
|
{value.filePath && typeof value.initGit === "boolean" && (
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { setActiveTab, TabContent, Tabs } from "./core/Tabs/Tabs";
|
|||||||
import { HeadersEditor } from "./HeadersEditor";
|
import { HeadersEditor } from "./HeadersEditor";
|
||||||
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
||||||
import { MarkdownEditor } from "./MarkdownEditor";
|
import { MarkdownEditor } from "./MarkdownEditor";
|
||||||
|
import { countOverriddenSettings, ModelSettingsEditor } from "./ModelSettingsEditor";
|
||||||
import { UrlBar } from "./UrlBar";
|
import { UrlBar } from "./UrlBar";
|
||||||
import { UrlParametersEditor } from "./UrlParameterEditor";
|
import { UrlParametersEditor } from "./UrlParameterEditor";
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ const TAB_MESSAGE = "message";
|
|||||||
const TAB_PARAMS = "params";
|
const TAB_PARAMS = "params";
|
||||||
const TAB_HEADERS = "headers";
|
const TAB_HEADERS = "headers";
|
||||||
const TAB_AUTH = "auth";
|
const TAB_AUTH = "auth";
|
||||||
|
const TAB_SETTINGS = "settings";
|
||||||
const TAB_DESCRIPTION = "description";
|
const TAB_DESCRIPTION = "description";
|
||||||
const TABS_STORAGE_KEY = "websocket_request_tabs";
|
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 authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||||
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
||||||
const inheritedHeaders = useInheritedHeaders(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)
|
// Listen for event to focus the params tab (e.g., when clicking a :param in the URL)
|
||||||
useRequestEditorEvent(
|
useRequestEditorEvent(
|
||||||
@@ -109,12 +112,17 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
},
|
},
|
||||||
...headersTab,
|
...headersTab,
|
||||||
...authTab,
|
...authTab,
|
||||||
|
{
|
||||||
|
value: TAB_SETTINGS,
|
||||||
|
label: "Settings",
|
||||||
|
rightSlot: <CountBadge count={numSettingsOverrides} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: TAB_DESCRIPTION,
|
value: TAB_DESCRIPTION,
|
||||||
label: "Info",
|
label: "Info",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [authTab, headersTab, urlParameterPairs.length]);
|
}, [authTab, headersTab, numSettingsOverrides, urlParameterPairs.length]);
|
||||||
|
|
||||||
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
||||||
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||||
@@ -266,6 +274,9 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
stateKey={`json.${activeRequest.id}`}
|
stateKey={`json.${activeRequest.id}`}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
<TabContent value={TAB_SETTINGS}>
|
||||||
|
<ModelSettingsEditor model={activeRequest} />
|
||||||
|
</TabContent>
|
||||||
<TabContent value={TAB_DESCRIPTION}>
|
<TabContent value={TAB_DESCRIPTION}>
|
||||||
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
|
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
|
||||||
<PlainInput
|
<PlainInput
|
||||||
|
|||||||
@@ -20,16 +20,24 @@ import { IconButton } from "./core/IconButton";
|
|||||||
import { IconTooltip } from "./core/IconTooltip";
|
import { IconTooltip } from "./core/IconTooltip";
|
||||||
import { Label } from "./core/Label";
|
import { Label } from "./core/Label";
|
||||||
import { PlainInput } from "./core/PlainInput";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
|
import { SettingRow } from "./core/SettingRow";
|
||||||
import { EncryptionHelp } from "./EncryptionHelp";
|
import { EncryptionHelp } from "./EncryptionHelp";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
layout?: "form" | "settings";
|
||||||
size?: ButtonProps["size"];
|
size?: ButtonProps["size"];
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
onDone?: () => void;
|
onDone?: () => void;
|
||||||
onEnabledEncryption?: () => 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 [justEnabledEncryption, setJustEnabledEncryption] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -66,7 +74,7 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
|
|||||||
key.error != null ||
|
key.error != null ||
|
||||||
(workspace.encryptionKeyChallenge && workspaceMeta.encryptionKey == null)
|
(workspace.encryptionKeyChallenge && workspaceMeta.encryptionKey == null)
|
||||||
) {
|
) {
|
||||||
return (
|
const enterKey = (
|
||||||
<EnterWorkspaceKey
|
<EnterWorkspaceKey
|
||||||
workspaceMeta={workspaceMeta}
|
workspaceMeta={workspaceMeta}
|
||||||
error={key.error}
|
error={key.error}
|
||||||
@@ -79,6 +87,8 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return enterKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the key if it exists
|
// Show the key if it exists
|
||||||
@@ -90,7 +100,8 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
|
|||||||
encryptionKey={key.key}
|
encryptionKey={key.key}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return (
|
|
||||||
|
const content = (
|
||||||
<VStack space={2} className="w-full">
|
<VStack space={2} className="w-full">
|
||||||
{justEnabledEncryption && (
|
{justEnabledEncryption && (
|
||||||
<Banner color="success" className="flex flex-col gap-2">
|
<Banner color="success" className="flex flex-col gap-2">
|
||||||
@@ -111,9 +122,43 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
|
|||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show button to enable encryption
|
// 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 (
|
return (
|
||||||
<div className="mb-auto flex flex-col-reverse">
|
<div className="mb-auto flex flex-col-reverse">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -5,16 +5,19 @@ import { useAuthTab } from "../hooks/useAuthTab";
|
|||||||
import { useHeadersTab } from "../hooks/useHeadersTab";
|
import { useHeadersTab } from "../hooks/useHeadersTab";
|
||||||
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
|
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
|
||||||
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
||||||
|
import { showDialog } from "../lib/dialog";
|
||||||
import { router } from "../lib/router";
|
import { router } from "../lib/router";
|
||||||
import { CopyIconButton } from "./CopyIconButton";
|
import { CopyIconButton } from "./CopyIconButton";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { CountBadge } from "./core/CountBadge";
|
import { CountBadge } from "./core/CountBadge";
|
||||||
import { PlainInput } from "./core/PlainInput";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
|
import { SettingsList, SettingsSection } from "./core/SettingRow";
|
||||||
import { TabContent, Tabs } from "./core/Tabs/Tabs";
|
import { TabContent, Tabs } from "./core/Tabs/Tabs";
|
||||||
import { DnsOverridesEditor } from "./DnsOverridesEditor";
|
import { DnsOverridesEditor } from "./DnsOverridesEditor";
|
||||||
import { HeadersEditor } from "./HeadersEditor";
|
import { HeadersEditor } from "./HeadersEditor";
|
||||||
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
||||||
import { MarkdownEditor } from "./MarkdownEditor";
|
import { MarkdownEditor } from "./MarkdownEditor";
|
||||||
|
import { ModelSettingsEditor } from "./ModelSettingsEditor";
|
||||||
import { SyncToFilesystemSetting } from "./SyncToFilesystemSetting";
|
import { SyncToFilesystemSetting } from "./SyncToFilesystemSetting";
|
||||||
import { WorkspaceEncryptionSetting } from "./WorkspaceEncryptionSetting";
|
import { WorkspaceEncryptionSetting } from "./WorkspaceEncryptionSetting";
|
||||||
|
|
||||||
@@ -25,17 +28,17 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TAB_AUTH = "auth";
|
const TAB_AUTH = "auth";
|
||||||
const TAB_DATA = "data";
|
|
||||||
const TAB_DNS = "dns";
|
const TAB_DNS = "dns";
|
||||||
const TAB_HEADERS = "headers";
|
const TAB_HEADERS = "headers";
|
||||||
const TAB_GENERAL = "general";
|
const TAB_GENERAL = "general";
|
||||||
|
const TAB_SETTINGS = "settings";
|
||||||
|
|
||||||
export type WorkspaceSettingsTab =
|
export type WorkspaceSettingsTab =
|
||||||
| typeof TAB_AUTH
|
| typeof TAB_AUTH
|
||||||
| typeof TAB_DNS
|
| typeof TAB_DNS
|
||||||
| typeof TAB_HEADERS
|
| typeof TAB_HEADERS
|
||||||
| typeof TAB_GENERAL
|
| typeof TAB_GENERAL
|
||||||
| typeof TAB_DATA;
|
| typeof TAB_SETTINGS;
|
||||||
|
|
||||||
const DEFAULT_TAB: WorkspaceSettingsTab = TAB_GENERAL;
|
const DEFAULT_TAB: WorkspaceSettingsTab = TAB_GENERAL;
|
||||||
|
|
||||||
@@ -71,8 +74,8 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
|||||||
tabs={[
|
tabs={[
|
||||||
{ value: TAB_GENERAL, label: "Workspace" },
|
{ value: TAB_GENERAL, label: "Workspace" },
|
||||||
{
|
{
|
||||||
value: TAB_DATA,
|
value: TAB_SETTINGS,
|
||||||
label: "Storage",
|
label: "Settings",
|
||||||
},
|
},
|
||||||
...headersTab,
|
...headersTab,
|
||||||
...authTab,
|
...authTab,
|
||||||
@@ -100,6 +103,20 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
|||||||
stateKey={`headers.${workspace.id}`}
|
stateKey={`headers.${workspace.id}`}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</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">
|
<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">
|
<div className="grid grid-rows-[auto_minmax(0,1fr)_auto] gap-4 pb-3 h-full">
|
||||||
<PlainInput
|
<PlainInput
|
||||||
@@ -152,19 +169,21 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
</TabContent>
|
</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">
|
<TabContent value={TAB_DNS} className="overflow-y-auto h-full px-4">
|
||||||
<DnsOverridesEditor workspace={workspace} />
|
<DnsOverridesEditor workspace={workspace} />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
</Tabs>
|
</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;
|
hideLabel?: boolean;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
help?: ReactNode;
|
help?: ReactNode;
|
||||||
|
size?: "sm" | "md";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Checkbox({
|
export function Checkbox({
|
||||||
@@ -25,6 +26,7 @@ export function Checkbox({
|
|||||||
hideLabel,
|
hideLabel,
|
||||||
fullWidth,
|
fullWidth,
|
||||||
help,
|
help,
|
||||||
|
size = "sm",
|
||||||
}: CheckboxProps) {
|
}: CheckboxProps) {
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
@@ -37,7 +39,9 @@ export function Checkbox({
|
|||||||
<input
|
<input
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className={classNames(
|
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",
|
"rounded outline-none ring-0",
|
||||||
!disabled && "hocus:border-border-focus hocus:bg-focus/[5%]",
|
!disabled && "hocus:border-border-focus hocus:bg-focus/[5%]",
|
||||||
disabled && "border-dotted",
|
disabled && "border-dotted",
|
||||||
@@ -50,7 +54,7 @@ export function Checkbox({
|
|||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<Icon
|
<Icon
|
||||||
size="sm"
|
size={size}
|
||||||
className={classNames(disabled && "opacity-disabled")}
|
className={classNames(disabled && "opacity-disabled")}
|
||||||
icon={checked === "indeterminate" ? "minus" : checked ? "check" : "empty"}
|
icon={checked === "indeterminate" ? "minus" : checked ? "check" : "empty"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import type { HTMLAttributes, ReactElement, ReactNode } from "react";
|
import type { HTMLAttributes, ReactElement, ReactNode } from "react";
|
||||||
|
import { CopyIconButton } from "../CopyIconButton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children:
|
children:
|
||||||
| ReactElement<HTMLAttributes<HTMLTableColElement>>
|
| ReactElement<HTMLAttributes<HTMLTableColElement>>
|
||||||
| (ReactElement<HTMLAttributes<HTMLTableColElement>> | null)[];
|
| (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];
|
const childArray = Array.isArray(children) ? children.filter(Boolean) : [children];
|
||||||
return (
|
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">
|
<tbody className="divide-y divide-surface-highlight">
|
||||||
{childArray.map((child, i) => (
|
{childArray.map((child, i) => (
|
||||||
// oxlint-disable-next-line react/no-array-index-key
|
// oxlint-disable-next-line react/no-array-index-key
|
||||||
@@ -26,8 +34,11 @@ interface KeyValueRowProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
rightSlot?: ReactNode;
|
rightSlot?: ReactNode;
|
||||||
leftSlot?: ReactNode;
|
leftSlot?: ReactNode;
|
||||||
|
align?: "top" | "middle";
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
labelColor?: "secondary" | "primary" | "info";
|
labelColor?: "secondary" | "primary" | "info";
|
||||||
|
enableCopy?: boolean;
|
||||||
|
copyText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KeyValueRow({
|
export function KeyValueRow({
|
||||||
@@ -35,14 +46,34 @@ export function KeyValueRow({
|
|||||||
children,
|
children,
|
||||||
rightSlot,
|
rightSlot,
|
||||||
leftSlot,
|
leftSlot,
|
||||||
|
align = "top",
|
||||||
labelColor = "secondary",
|
labelColor = "secondary",
|
||||||
labelClassName,
|
labelClassName,
|
||||||
|
enableCopy,
|
||||||
|
copyText,
|
||||||
}: KeyValueRowProps) {
|
}: 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<td
|
<td
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"select-none py-0.5 pr-2 h-full align-top max-w-[10rem]",
|
"select-none py-0.5 pr-2 h-full max-w-[10rem]",
|
||||||
|
align === "top" && "align-top",
|
||||||
|
align === "middle" && "align-middle",
|
||||||
labelClassName,
|
labelClassName,
|
||||||
labelColor === "primary" && "text-primary",
|
labelColor === "primary" && "text-primary",
|
||||||
labelColor === "secondary" && "text-text-subtle",
|
labelColor === "secondary" && "text-text-subtle",
|
||||||
@@ -51,11 +82,21 @@ export function KeyValueRow({
|
|||||||
>
|
>
|
||||||
<span className="select-text cursor-text">{label}</span>
|
<span className="select-text cursor-text">{label}</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="select-none py-0.5 break-all align-top max-w-[15rem]">
|
<td
|
||||||
|
className={classNames(
|
||||||
|
"select-none py-0.5 break-all max-w-[15rem]",
|
||||||
|
align === "top" && "align-top",
|
||||||
|
align === "middle" && "align-middle",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="select-text cursor-text max-h-[12rem] overflow-y-auto grid grid-cols-[auto_minmax(0,1fr)_auto]">
|
<div className="select-text cursor-text max-h-[12rem] overflow-y-auto grid grid-cols-[auto_minmax(0,1fr)_auto]">
|
||||||
{leftSlot ?? <span aria-hidden />}
|
{leftSlot ?? <span aria-hidden />}
|
||||||
{children}
|
{children}
|
||||||
{rightSlot ? <div className="ml-1.5">{rightSlot}</div> : <span aria-hidden />}
|
{resolvedRightSlot ? (
|
||||||
|
<div className="ml-1.5">{resolvedRightSlot}</div>
|
||||||
|
) : (
|
||||||
|
<span aria-hidden />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
|||||||
className,
|
className,
|
||||||
containerClassName,
|
containerClassName,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
disabled,
|
||||||
forceUpdateKey: forceUpdateKeyFromAbove,
|
forceUpdateKey: forceUpdateKeyFromAbove,
|
||||||
help,
|
help,
|
||||||
hideLabel,
|
hideLabel,
|
||||||
@@ -163,7 +164,8 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
|||||||
"relative w-full rounded-md text",
|
"relative w-full rounded-md text",
|
||||||
"border",
|
"border",
|
||||||
"overflow-hidden",
|
"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
|
hasChanged && "has-[:invalid]:border-danger", // For built-in HTML validation
|
||||||
size === "md" && "min-h-md",
|
size === "md" && "min-h-md",
|
||||||
size === "sm" && "min-h-sm",
|
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
|
// oxlint-disable-next-line jsx-a11y/no-autofocus
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
defaultValue={defaultValue ?? undefined}
|
defaultValue={defaultValue ?? undefined}
|
||||||
|
disabled={disabled}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
onChange={(e) => handleChange(e.target.value)}
|
onChange={(e) => handleChange(e.target.value)}
|
||||||
onPaste={(e) => onPaste?.(e.clipboardData.getData("Text"))}
|
onPaste={(e) => onPaste?.(e.clipboardData.getData("Text"))}
|
||||||
className={classNames(commonClassName, "h-full")}
|
className={classNames(commonClassName, "h-full disabled:opacity-disabled")}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
required={required}
|
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
|
// Use custom "select" component until Tauri can be configured to have select menus not always appear in
|
||||||
// light mode
|
// 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
|
<Button
|
||||||
className="w-full text-sm font-mono"
|
className="w-full text-sm font-mono"
|
||||||
justify="start"
|
justify="start"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function Separator({
|
|||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"h-0 border-t opacity-60",
|
"opacity-60",
|
||||||
color == null && "border-border",
|
color == null && "border-border",
|
||||||
color === "primary" && "border-primary",
|
color === "primary" && "border-primary",
|
||||||
color === "secondary" && "border-secondary",
|
color === "secondary" && "border-secondary",
|
||||||
@@ -34,8 +34,8 @@ export function Separator({
|
|||||||
color === "danger" && "border-danger",
|
color === "danger" && "border-danger",
|
||||||
color === "info" && "border-info",
|
color === "info" && "border-info",
|
||||||
dashed && "border-dashed",
|
dashed && "border-dashed",
|
||||||
orientation === "horizontal" && "w-full h-[1px]",
|
orientation === "horizontal" && "w-full h-0 border-t",
|
||||||
orientation === "vertical" && "h-full w-[1px]",
|
orientation === "vertical" && "h-full w-0 border-l",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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",
|
color: "success",
|
||||||
label: "Open Workspace Settings",
|
label: "Open Workspace Settings",
|
||||||
leftSlot: <Icon icon="settings" />,
|
leftSlot: <Icon icon="settings" />,
|
||||||
onSelect: () => openWorkspaceSettings("data"),
|
onSelect: () => openWorkspaceSettings("settings"),
|
||||||
},
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function useAuthTab<T extends string>(tabValue: T, model: AuthenticatedMo
|
|||||||
{authentication.find((a) => a.name === inheritedAuth.authenticationType)
|
{authentication.find((a) => a.name === inheritedAuth.authenticationType)
|
||||||
?.shortLabel ?? "UNKNOWN"}
|
?.shortLabel ?? "UNKNOWN"}
|
||||||
<IconTooltip
|
<IconTooltip
|
||||||
icon="magic_wand"
|
icon="zap_off"
|
||||||
iconSize="xs"
|
iconSize="xs"
|
||||||
content="Authentication was inherited from an ancestor"
|
content="Authentication was inherited from an ancestor"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -473,7 +473,7 @@ async fn build_plugin_reply(
|
|||||||
let names = cookie_jar
|
let names = cookie_jar
|
||||||
.cookies
|
.cookies
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|c| parse_cookie_name_value(&c.raw_cookie).map(|(name, _)| name))
|
.map(|c| c.name)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Some(InternalEventPayload::ListCookieNamesResponse(ListCookieNamesResponse {
|
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
|
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> {
|
fn copy_text_to_clipboard(text: &str) -> Result<(), String> {
|
||||||
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
|
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
|
||||||
clipboard.set_text(text.to_string()).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::models::HttpRequest;
|
||||||
use yaak_models::util::UpdateSource;
|
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]
|
#[test]
|
||||||
fn top_level_send_folder_sends_http_requests_and_prints_summary() {
|
fn top_level_send_folder_sends_http_requests_and_prints_summary() {
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use crate::{
|
|||||||
workspace_from_window,
|
workspace_from_window,
|
||||||
};
|
};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use cookie::Cookie;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Emitter, Listener, Manager, Runtime};
|
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 window = get_window_from_plugin_context(app_handle, plugin_context)?;
|
||||||
let names = match cookie_jar_from_window(&window) {
|
let names = match cookie_jar_from_window(&window) {
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
Some(j) => j
|
Some(j) => j.cookies.into_iter().map(|c| c.name).collect(),
|
||||||
.cookies
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|c| Cookie::parse(c.raw_cookie).ok().map(|c| c.name().to_string()))
|
|
||||||
.collect(),
|
|
||||||
};
|
};
|
||||||
Ok(Some(InternalEventPayload::ListCookieNamesResponse(ListCookieNamesResponse {
|
Ok(Some(InternalEventPayload::ListCookieNamesResponse(ListCookieNamesResponse {
|
||||||
names,
|
names,
|
||||||
|
|||||||
@@ -134,7 +134,8 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
|||||||
unrendered_request.folder_id.as_deref(),
|
unrendered_request.folder_id.as_deref(),
|
||||||
environment_id,
|
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 settings = app_handle.db().get_settings();
|
||||||
let (resolved_request, auth_context_id) =
|
let (resolved_request, auth_context_id) =
|
||||||
resolve_websocket_request(&window, &unrendered_request)?;
|
resolve_websocket_request(&window, &unrendered_request)?;
|
||||||
@@ -247,11 +248,18 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add cookies to WS HTTP Upgrade
|
let mut cookie_jar = match (
|
||||||
if let Some(id) = cookie_jar_id {
|
resolved_settings.send_cookies.value || resolved_settings.store_cookies.value,
|
||||||
let cookie_jar = app_handle.db().get_cookie_jar(&id)?;
|
cookie_jar_id,
|
||||||
let store = CookieStore::from_cookies(cookie_jar.cookies);
|
) {
|
||||||
|
(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
|
// Convert WS URL -> HTTP URL because our cookie store matches based on
|
||||||
// Path/HttpOnly/Secure attributes even though WS upgrades are HTTP requests
|
// Path/HttpOnly/Secure attributes even though WS upgrades are HTTP requests
|
||||||
let http_url = convert_ws_url_to_http(&url);
|
let http_url = convert_ws_url_to_http(&url);
|
||||||
@@ -289,7 +297,7 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
|||||||
url.as_str(),
|
url.as_str(),
|
||||||
headers,
|
headers,
|
||||||
receive_tx,
|
receive_tx,
|
||||||
workspace.setting_validate_certificates,
|
resolved_settings.validate_certificates.value,
|
||||||
client_cert,
|
client_cert,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -328,6 +336,23 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<HttpResponseHeader>>();
|
.collect::<Vec<HttpResponseHeader>>();
|
||||||
|
|
||||||
|
if let (true, Some(cookie_jar), Some(store)) =
|
||||||
|
(resolved_settings.store_cookies.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(
|
let connection = app_handle.db().upsert_websocket_connection(
|
||||||
&WebsocketConnection {
|
&WebsocketConnection {
|
||||||
state: WebsocketConnectionState::Connected,
|
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.
|
// 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,
|
export type Environment = {
|
||||||
/**
|
model: "environment";
|
||||||
* Variables defined in this environment scope.
|
id: string;
|
||||||
* Child environments override parent variables by name.
|
workspaceId: string;
|
||||||
*/
|
createdAt: string;
|
||||||
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
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,
|
export type GrpcRequest = {
|
||||||
/**
|
model: "grpc_request";
|
||||||
* Server URL (http for plaintext or https for secure)
|
id: string;
|
||||||
*/
|
createdAt: string;
|
||||||
url: 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,
|
export type HttpRequest = {
|
||||||
/**
|
model: "http_request";
|
||||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
id: string;
|
||||||
*/
|
createdAt: string;
|
||||||
urlParameters: Array<HttpUrlParameter>, };
|
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,
|
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
|
* 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, };
|
*/
|
||||||
|
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,
|
export type InheritedIntSetting = { enabled?: boolean; value: number };
|
||||||
/**
|
|
||||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
|
||||||
*/
|
|
||||||
urlParameters: Array<HttpUrlParameter>, };
|
|
||||||
|
|
||||||
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::sync::{Arc, Mutex};
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
use url::Url;
|
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
|
/// A thread-safe cookie store that can be shared across requests
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -45,10 +45,7 @@ impl CookieStore {
|
|||||||
let matching_cookies: Vec<_> = cookies
|
let matching_cookies: Vec<_> = cookies
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|cookie| self.cookie_matches(cookie, url, &now))
|
.filter(|cookie| self.cookie_matches(cookie, url, &now))
|
||||||
.filter_map(|cookie| {
|
.map(|cookie| (cookie.name.clone(), cookie.value.clone()))
|
||||||
// Parse the raw cookie to get name=value
|
|
||||||
parse_cookie_name_value(&cookie.raw_cookie)
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if matching_cookies.is_empty() {
|
if matching_cookies.is_empty() {
|
||||||
@@ -72,13 +69,7 @@ impl CookieStore {
|
|||||||
if let Some(cookie) = parse_set_cookie(header_value, url) {
|
if let Some(cookie) = parse_set_cookie(header_value, url) {
|
||||||
// Remove any existing cookie with the same name and domain
|
// Remove any existing cookie with the same name and domain
|
||||||
cookies.retain(|existing| !cookies_match(existing, &cookie));
|
cookies.retain(|existing| !cookies_match(existing, &cookie));
|
||||||
debug!(
|
debug!("Storing cookie: {} for domain {:?}", cookie.name, cookie.domain);
|
||||||
"Storing cookie: {} for domain {:?}",
|
|
||||||
parse_cookie_name_value(&cookie.raw_cookie)
|
|
||||||
.map(|(n, _)| n)
|
|
||||||
.unwrap_or_else(|| "unknown".to_string()),
|
|
||||||
cookie.domain
|
|
||||||
);
|
|
||||||
cookies.push(cookie);
|
cookies.push(cookie);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,10 +108,9 @@ impl CookieStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check path
|
// Check path
|
||||||
let (cookie_path, _) = &cookie.path;
|
|
||||||
let url_path = url.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);
|
let domain = domain.and_then(normalize_cookie_domain_filter);
|
||||||
|
|
||||||
cookies.into_iter().find_map(|cookie| {
|
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;
|
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)
|
/// Parse name=value from a cookie string (raw_cookie format)
|
||||||
|
#[cfg(test)]
|
||||||
fn parse_cookie_name_value(raw_cookie: &str) -> Option<(String, String)> {
|
fn parse_cookie_name_value(raw_cookie: &str) -> Option<(String, String)> {
|
||||||
// The raw_cookie typically looks like "name=value" or "name=value; attr1; attr2=..."
|
// The raw_cookie typically looks like "name=value" or "name=value; attr1; attr2=..."
|
||||||
let first_part = raw_cookie.split(';').next()?;
|
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> {
|
fn parse_set_cookie(header_value: &str, request_url: &Url) -> Option<Cookie> {
|
||||||
let parsed = cookie::Cookie::parse(header_value).ok()?;
|
let parsed = cookie::Cookie::parse(header_value).ok()?;
|
||||||
|
|
||||||
let raw_cookie = format!("{}={}", parsed.name(), parsed.value());
|
|
||||||
|
|
||||||
// Determine domain
|
// Determine domain
|
||||||
let domain = if let Some(domain_attr) = parsed.domain() {
|
let domain = if let Some(domain_attr) = parsed.domain() {
|
||||||
// Domain attribute present - this is a suffix match
|
// 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
|
// Determine path
|
||||||
let path = if let Some(path_attr) = parsed.path() {
|
let path = if let Some(path_attr) = parsed.path() {
|
||||||
(path_attr.to_string(), true)
|
path_attr.to_string()
|
||||||
} else {
|
} else {
|
||||||
// Default path is the directory of the request URI
|
// Default path is the directory of the request URI
|
||||||
let default_path = default_cookie_path(request_url.path());
|
default_cookie_path(request_url.path())
|
||||||
(default_path, false)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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)
|
/// 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)
|
/// Check if two cookies match (same name and domain)
|
||||||
fn cookies_match(a: &Cookie, b: &Cookie) -> bool {
|
fn cookies_match(a: &Cookie, b: &Cookie) -> bool {
|
||||||
let name_a = parse_cookie_name_value(&a.raw_cookie).map(|(n, _)| n);
|
if a.name != b.name {
|
||||||
let name_b = parse_cookie_name_value(&b.raw_cookie).map(|(n, _)| n);
|
|
||||||
|
|
||||||
if name_a != name_b {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,11 +316,16 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn cookie(raw_cookie: &str, domain: CookieDomain) -> Cookie {
|
fn cookie(raw_cookie: &str, domain: CookieDomain) -> Cookie {
|
||||||
|
let (name, value) = parse_cookie_name_value(raw_cookie).unwrap();
|
||||||
Cookie {
|
Cookie {
|
||||||
raw_cookie: raw_cookie.to_string(),
|
name,
|
||||||
|
value,
|
||||||
domain,
|
domain,
|
||||||
expires: CookieExpires::SessionEnd,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum HttpResponseEvent {
|
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),
|
Info(String),
|
||||||
Redirect {
|
Redirect {
|
||||||
url: String,
|
url: String,
|
||||||
@@ -67,7 +73,9 @@ pub enum HttpResponseEvent {
|
|||||||
impl Display for HttpResponseEvent {
|
impl Display for HttpResponseEvent {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
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::Info(s) => write!(f, "* {}", s),
|
||||||
HttpResponseEvent::Redirect {
|
HttpResponseEvent::Redirect {
|
||||||
url,
|
url,
|
||||||
@@ -146,7 +154,9 @@ impl From<HttpResponseEvent> for yaak_models::models::HttpResponseEventData {
|
|||||||
fn from(event: HttpResponseEvent) -> Self {
|
fn from(event: HttpResponseEvent) -> Self {
|
||||||
use yaak_models::models::HttpResponseEventData as D;
|
use yaak_models::models::HttpResponseEventData as D;
|
||||||
match event {
|
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::Info(message) => D::Info { message },
|
||||||
HttpResponseEvent::Redirect {
|
HttpResponseEvent::Redirect {
|
||||||
url,
|
url,
|
||||||
@@ -483,15 +493,6 @@ impl HttpSender for ReqwestSender {
|
|||||||
|
|
||||||
// Send the request
|
// Send the request
|
||||||
let sendable_req = req_builder.build()?;
|
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 {
|
send_event(HttpResponseEvent::SendUrl {
|
||||||
method: sendable_req.method().to_string(),
|
method: sendable_req.method().to_string(),
|
||||||
scheme: sendable_req.url().scheme().to_string(),
|
scheme: sendable_req.url().scheme().to_string(),
|
||||||
|
|||||||
@@ -12,22 +12,58 @@ pub struct HttpTransaction<S: HttpSender> {
|
|||||||
sender: S,
|
sender: S,
|
||||||
max_redirects: usize,
|
max_redirects: usize,
|
||||||
cookie_store: Option<CookieStore>,
|
cookie_store: Option<CookieStore>,
|
||||||
|
send_cookies: bool,
|
||||||
|
store_cookies: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: HttpSender> HttpTransaction<S> {
|
impl<S: HttpSender> HttpTransaction<S> {
|
||||||
/// Create a new transaction with default settings
|
/// Create a new transaction with default settings
|
||||||
pub fn new(sender: S) -> Self {
|
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
|
/// Create a new transaction with custom max redirects
|
||||||
pub fn with_max_redirects(sender: S, max_redirects: usize) -> Self {
|
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
|
/// Create a new transaction with a cookie store
|
||||||
pub fn with_cookie_store(sender: S, cookie_store: CookieStore) -> Self {
|
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
|
/// Create a new transaction with custom max redirects and a cookie store
|
||||||
@@ -36,7 +72,13 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
max_redirects: usize,
|
max_redirects: usize,
|
||||||
cookie_store: Option<CookieStore>,
|
cookie_store: Option<CookieStore>,
|
||||||
) -> Self {
|
) -> 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.
|
/// 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
|
// 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();
|
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) {
|
if let Some(cookie_header) = cookie_store.get_cookie_header(&url) {
|
||||||
debug!("Injecting Cookie header: {}", cookie_header);
|
debug!("Injecting Cookie header: {}", cookie_header);
|
||||||
// Check if there's already a Cookie header and merge if so
|
// 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(),
|
options: request.options.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the request
|
|
||||||
send_event(HttpResponseEvent::Setting(
|
|
||||||
"redirects".to_string(),
|
|
||||||
request.options.follow_redirects.to_string(),
|
|
||||||
));
|
|
||||||
|
|
||||||
// Execute with cancellation support
|
// Execute with cancellation support
|
||||||
let response = tokio::select! {
|
let response = tokio::select! {
|
||||||
result = self.sender.send(req, event_tx.clone()) => result?,
|
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
|
// Parse Set-Cookie headers and store cookies
|
||||||
if let Some(cookie_store) = &self.cookie_store {
|
if self.store_cookies {
|
||||||
if let Ok(url) = Url::parse(¤t_url) {
|
if let (Some(cookie_store), Ok(url)) =
|
||||||
|
(&self.cookie_store, Url::parse(¤t_url))
|
||||||
|
{
|
||||||
let set_cookie_headers: Vec<String> = response
|
let set_cookie_headers: Vec<String> = response
|
||||||
.headers
|
.headers
|
||||||
.iter()
|
.iter()
|
||||||
@@ -579,10 +619,14 @@ mod tests {
|
|||||||
|
|
||||||
// Create a cookie store with a test cookie
|
// Create a cookie store with a test cookie
|
||||||
let cookie = 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()),
|
domain: CookieDomain::HostOnly("example.com".to_string()),
|
||||||
expires: CookieExpires::SessionEnd,
|
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]);
|
let cookie_store = CookieStore::from_cookies(vec![cookie]);
|
||||||
|
|
||||||
@@ -602,6 +646,67 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
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]
|
#[tokio::test]
|
||||||
async fn test_set_cookie_parsing() {
|
async fn test_set_cookie_parsing() {
|
||||||
// Create a cookie store
|
// Create a cookie store
|
||||||
@@ -655,7 +760,62 @@ mod tests {
|
|||||||
// Verify the cookie was stored
|
// Verify the cookie was stored
|
||||||
let cookies = cookie_store.get_all_cookies();
|
let cookies = cookie_store.get_all_cookies();
|
||||||
assert_eq!(cookies.len(), 1);
|
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]
|
#[tokio::test]
|
||||||
@@ -719,17 +879,15 @@ mod tests {
|
|||||||
let cookies = cookie_store.get_all_cookies();
|
let cookies = cookie_store.get_all_cookies();
|
||||||
assert_eq!(cookies.len(), 3, "All three Set-Cookie headers should be parsed and stored");
|
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!(
|
assert!(
|
||||||
cookie_values.iter().any(|c| c.contains("session=abc123")),
|
cookie_values.iter().any(|c| c == "session=abc123"),
|
||||||
"session cookie should be stored"
|
"session cookie should be stored"
|
||||||
);
|
);
|
||||||
|
assert!(cookie_values.iter().any(|c| c == "user_id=42"), "user_id cookie should be stored");
|
||||||
assert!(
|
assert!(
|
||||||
cookie_values.iter().any(|c| c.contains("user_id=42")),
|
cookie_values.iter().any(|c| c == "preferences=dark"),
|
||||||
"user_id cookie should be stored"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
cookie_values.iter().any(|c| c.contains("preferences=dark")),
|
|
||||||
"preferences cookie should be stored"
|
"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.
|
// 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";
|
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 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,
|
export type Environment = {
|
||||||
/**
|
model: "environment";
|
||||||
* Variables defined in this environment scope.
|
id: string;
|
||||||
* Child environments override parent variables by name.
|
workspaceId: string;
|
||||||
*/
|
createdAt: string;
|
||||||
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
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 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,
|
export type GrpcRequest = {
|
||||||
/**
|
model: "grpc_request";
|
||||||
* Server URL (http for plaintext or https for secure)
|
id: string;
|
||||||
*/
|
createdAt: string;
|
||||||
url: 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,
|
export type HttpRequest = {
|
||||||
/**
|
model: "http_request";
|
||||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
id: string;
|
||||||
*/
|
createdAt: string;
|
||||||
urlParameters: Array<HttpUrlParameter>, };
|
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.
|
* Serializable representation of HTTP response events for DB storage.
|
||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* 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 HttpResponseState = "initialized" | "connected" | "closed";
|
||||||
|
|
||||||
export type HttpUrlParameter = { enabled?: boolean,
|
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
|
* 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, };
|
*/
|
||||||
|
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 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 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 WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
|
||||||
|
|
||||||
export type WebsocketMessageType = "text" | "binary";
|
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,
|
export type WebsocketRequest = {
|
||||||
/**
|
model: "websocket_request";
|
||||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
id: string;
|
||||||
*/
|
createdAt: string;
|
||||||
urlParameters: Array<HttpUrlParameter>, };
|
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::error::Result;
|
||||||
use crate::models::HttpRequestIden::{
|
use crate::models::HttpRequestIden::{
|
||||||
Authentication, AuthenticationType, Body, BodyType, CreatedAt, Description, FolderId, Headers,
|
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 crate::util::generate_prefixed_id;
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
@@ -90,6 +92,84 @@ pub struct DnsOverride {
|
|||||||
pub enabled: bool,
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
@@ -322,6 +402,10 @@ pub struct Workspace {
|
|||||||
pub setting_request_timeout: i32,
|
pub setting_request_timeout: i32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub setting_dns_overrides: Vec<DnsOverride>,
|
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 {
|
impl UpsertModelInfo for Workspace {
|
||||||
@@ -363,6 +447,8 @@ impl UpsertModelInfo for Workspace {
|
|||||||
(SettingRequestTimeout, self.setting_request_timeout.into()),
|
(SettingRequestTimeout, self.setting_request_timeout.into()),
|
||||||
(SettingValidateCertificates, self.setting_validate_certificates.into()),
|
(SettingValidateCertificates, self.setting_validate_certificates.into()),
|
||||||
(SettingDnsOverrides, serde_json::to_string(&self.setting_dns_overrides)?.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::SettingRequestTimeout,
|
||||||
WorkspaceIden::SettingValidateCertificates,
|
WorkspaceIden::SettingValidateCertificates,
|
||||||
WorkspaceIden::SettingDnsOverrides,
|
WorkspaceIden::SettingDnsOverrides,
|
||||||
|
WorkspaceIden::SettingSendCookies,
|
||||||
|
WorkspaceIden::SettingStoreCookies,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,6 +493,8 @@ impl UpsertModelInfo for Workspace {
|
|||||||
setting_request_timeout: row.get("setting_request_timeout")?,
|
setting_request_timeout: row.get("setting_request_timeout")?,
|
||||||
setting_validate_certificates: row.get("setting_validate_certificates")?,
|
setting_validate_certificates: row.get("setting_validate_certificates")?,
|
||||||
setting_dns_overrides: serde_json::from_str(&setting_dns_overrides).unwrap_or_default(),
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
#[ts(export, export_to = "gen_models.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 struct Cookie {
|
||||||
pub raw_cookie: String,
|
pub name: String,
|
||||||
|
pub value: String,
|
||||||
pub domain: CookieDomain,
|
pub domain: CookieDomain,
|
||||||
pub expires: CookieExpires,
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||||
@@ -751,6 +957,11 @@ pub struct Folder {
|
|||||||
pub headers: Vec<HttpRequestHeader>,
|
pub headers: Vec<HttpRequestHeader>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub sort_priority: f64,
|
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 {
|
impl UpsertModelInfo for Folder {
|
||||||
@@ -790,6 +1001,14 @@ impl UpsertModelInfo for Folder {
|
|||||||
(Description, self.description.into()),
|
(Description, self.description.into()),
|
||||||
(Name, self.name.trim().into()),
|
(Name, self.name.trim().into()),
|
||||||
(SortPriority, self.sort_priority.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::Description,
|
||||||
FolderIden::FolderId,
|
FolderIden::FolderId,
|
||||||
FolderIden::SortPriority,
|
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 headers: String = row.get("headers")?;
|
||||||
let authentication: String = row.get("authentication")?;
|
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 {
|
Ok(Self {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
model: row.get("model")?,
|
model: row.get("model")?,
|
||||||
@@ -825,6 +1054,14 @@ impl UpsertModelInfo for Folder {
|
|||||||
headers: serde_json::from_str(&headers).unwrap_or_default(),
|
headers: serde_json::from_str(&headers).unwrap_or_default(),
|
||||||
authentication_type: row.get("authentication_type")?,
|
authentication_type: row.get("authentication_type")?,
|
||||||
authentication: serde_json::from_str(&authentication).unwrap_or_default(),
|
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,
|
pub url: String,
|
||||||
/// URL parameters used for both path placeholders (`:id`) and query string entries.
|
/// URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||||
pub url_parameters: Vec<HttpUrlParameter>,
|
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 {
|
impl UpsertModelInfo for HttpRequest {
|
||||||
@@ -928,6 +1170,14 @@ impl UpsertModelInfo for HttpRequest {
|
|||||||
(AuthenticationType, self.authentication_type.into()),
|
(AuthenticationType, self.authentication_type.into()),
|
||||||
(Headers, serde_json::to_string(&self.headers)?.into()),
|
(Headers, serde_json::to_string(&self.headers)?.into()),
|
||||||
(SortPriority, self.sort_priority.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,
|
Url,
|
||||||
UrlParameters,
|
UrlParameters,
|
||||||
SortPriority,
|
SortPriority,
|
||||||
|
SettingSendCookies,
|
||||||
|
SettingStoreCookies,
|
||||||
|
SettingValidateCertificates,
|
||||||
|
SettingFollowRedirects,
|
||||||
|
SettingRequestTimeout,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -955,6 +1210,11 @@ impl UpsertModelInfo for HttpRequest {
|
|||||||
let body: String = row.get("body")?;
|
let body: String = row.get("body")?;
|
||||||
let authentication: String = row.get("authentication")?;
|
let authentication: String = row.get("authentication")?;
|
||||||
let headers: String = row.get("headers")?;
|
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 {
|
Ok(Self {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
model: row.get("model")?,
|
model: row.get("model")?,
|
||||||
@@ -973,6 +1233,14 @@ impl UpsertModelInfo for HttpRequest {
|
|||||||
sort_priority: row.get("sort_priority")?,
|
sort_priority: row.get("sort_priority")?,
|
||||||
url: row.get("url")?,
|
url: row.get("url")?,
|
||||||
url_parameters: serde_json::from_str(url_parameters.as_str()).unwrap_or_default(),
|
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,
|
pub url: String,
|
||||||
/// URL parameters used for both path placeholders (`:id`) and query string entries.
|
/// URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||||
pub url_parameters: Vec<HttpUrlParameter>,
|
pub url_parameters: Vec<HttpUrlParameter>,
|
||||||
|
pub setting_send_cookies: InheritedBoolSetting,
|
||||||
|
pub setting_store_cookies: InheritedBoolSetting,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for WebsocketRequest {
|
impl UpsertModelInfo for WebsocketRequest {
|
||||||
@@ -1169,6 +1439,8 @@ impl UpsertModelInfo for WebsocketRequest {
|
|||||||
(SortPriority, self.sort_priority.into()),
|
(SortPriority, self.sort_priority.into()),
|
||||||
(Url, self.url.into()),
|
(Url, self.url.into()),
|
||||||
(UrlParameters, serde_json::to_string(&self.url_parameters)?.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::SortPriority,
|
||||||
WebsocketRequestIden::Url,
|
WebsocketRequestIden::Url,
|
||||||
WebsocketRequestIden::UrlParameters,
|
WebsocketRequestIden::UrlParameters,
|
||||||
|
WebsocketRequestIden::SettingSendCookies,
|
||||||
|
WebsocketRequestIden::SettingStoreCookies,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1196,6 +1470,8 @@ impl UpsertModelInfo for WebsocketRequest {
|
|||||||
let url_parameters: String = row.get("url_parameters")?;
|
let url_parameters: String = row.get("url_parameters")?;
|
||||||
let authentication: String = row.get("authentication")?;
|
let authentication: String = row.get("authentication")?;
|
||||||
let headers: String = row.get("headers")?;
|
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 {
|
Ok(Self {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
model: row.get("model")?,
|
model: row.get("model")?,
|
||||||
@@ -1212,6 +1488,8 @@ impl UpsertModelInfo for WebsocketRequest {
|
|||||||
headers: serde_json::from_str(headers.as_str()).unwrap_or_default(),
|
headers: serde_json::from_str(headers.as_str()).unwrap_or_default(),
|
||||||
folder_id: row.get("folder_id")?,
|
folder_id: row.get("folder_id")?,
|
||||||
name: row.get("name")?,
|
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 {
|
Setting {
|
||||||
name: String,
|
name: String,
|
||||||
value: 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 {
|
Info {
|
||||||
message: String,
|
message: String,
|
||||||
@@ -1742,6 +2029,8 @@ pub struct GrpcRequest {
|
|||||||
pub sort_priority: f64,
|
pub sort_priority: f64,
|
||||||
/// Server URL (http for plaintext or https for secure)
|
/// Server URL (http for plaintext or https for secure)
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
pub setting_send_cookies: InheritedBoolSetting,
|
||||||
|
pub setting_store_cookies: InheritedBoolSetting,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for GrpcRequest {
|
impl UpsertModelInfo for GrpcRequest {
|
||||||
@@ -1785,6 +2074,8 @@ impl UpsertModelInfo for GrpcRequest {
|
|||||||
(AuthenticationType, self.authentication_type.into()),
|
(AuthenticationType, self.authentication_type.into()),
|
||||||
(Authentication, serde_json::to_string(&self.authentication)?.into()),
|
(Authentication, serde_json::to_string(&self.authentication)?.into()),
|
||||||
(Metadata, serde_json::to_string(&self.metadata)?.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::AuthenticationType,
|
||||||
GrpcRequestIden::Authentication,
|
GrpcRequestIden::Authentication,
|
||||||
GrpcRequestIden::Metadata,
|
GrpcRequestIden::Metadata,
|
||||||
|
GrpcRequestIden::SettingSendCookies,
|
||||||
|
GrpcRequestIden::SettingStoreCookies,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1812,6 +2105,8 @@ impl UpsertModelInfo for GrpcRequest {
|
|||||||
{
|
{
|
||||||
let authentication: String = row.get("authentication")?;
|
let authentication: String = row.get("authentication")?;
|
||||||
let metadata: String = row.get("metadata")?;
|
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 {
|
Ok(Self {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
model: row.get("model")?,
|
model: row.get("model")?,
|
||||||
@@ -1829,6 +2124,8 @@ impl UpsertModelInfo for GrpcRequest {
|
|||||||
url: row.get("url")?,
|
url: row.get("url")?,
|
||||||
sort_priority: row.get("sort_priority")?,
|
sort_priority: row.get("sort_priority")?,
|
||||||
metadata: serde_json::from_str(metadata.as_str()).unwrap_or_default(),
|
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::connection_or_tx::ConnectionOrTx;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
Environment, EnvironmentIden, Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest,
|
AnyModel, Environment, EnvironmentIden, Folder, FolderIden, GrpcRequest, GrpcRequestIden,
|
||||||
HttpRequestHeader, HttpRequestIden, WebsocketRequest, WebsocketRequestIden,
|
HttpRequest, HttpRequestHeader, HttpRequestIden, ResolvedHttpRequestSettings, ResolvedSetting,
|
||||||
|
WebsocketRequest, WebsocketRequestIden,
|
||||||
};
|
};
|
||||||
use crate::util::UpdateSource;
|
use crate::util::UpdateSource;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -141,4 +142,60 @@ impl<'a> ClientDb<'a> {
|
|||||||
|
|
||||||
Ok(headers)
|
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 super::dedupe_headers;
|
||||||
use crate::client_db::ClientDb;
|
use crate::client_db::ClientDb;
|
||||||
use crate::error::Result;
|
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 crate::util::UpdateSource;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@@ -91,6 +94,62 @@ impl<'a> ClientDb<'a> {
|
|||||||
Ok(dedupe_headers(headers))
|
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(
|
pub fn list_http_requests_for_folder_recursive(
|
||||||
&self,
|
&self,
|
||||||
folder_id: &str,
|
folder_id: &str,
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ use super::dedupe_headers;
|
|||||||
use crate::client_db::ClientDb;
|
use crate::client_db::ClientDb;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
Folder, FolderIden, HttpRequestHeader, WebsocketRequest, WebsocketRequestIden,
|
AnyModel, Folder, FolderIden, HttpRequestHeader, ResolvedHttpRequestSettings, ResolvedSetting,
|
||||||
|
WebsocketRequest, WebsocketRequestIden,
|
||||||
};
|
};
|
||||||
use crate::util::UpdateSource;
|
use crate::util::UpdateSource;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -116,4 +117,37 @@ impl<'a> ClientDb<'a> {
|
|||||||
|
|
||||||
Ok(dedupe_headers(headers))
|
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::client_db::ClientDb;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
EnvironmentIden, FolderIden, GrpcRequestIden, HttpRequestHeader, HttpRequestIden,
|
AnyModel, EnvironmentIden, FolderIden, GrpcRequestIden, HttpRequestHeader, HttpRequestIden,
|
||||||
WebsocketRequestIden, Workspace, WorkspaceIden,
|
ResolvedHttpRequestSettings, ResolvedSetting, WebsocketRequestIden, Workspace, WorkspaceIden,
|
||||||
};
|
};
|
||||||
use crate::util::UpdateSource;
|
use crate::util::UpdateSource;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -84,6 +84,34 @@ impl<'a> ClientDb<'a> {
|
|||||||
headers.extend(workspace.headers.clone());
|
headers.extend(workspace.headers.clone());
|
||||||
headers
|
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.
|
/// 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.
|
// 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 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,
|
export type Environment = {
|
||||||
/**
|
model: "environment";
|
||||||
* Variables defined in this environment scope.
|
id: string;
|
||||||
* Child environments override parent variables by name.
|
workspaceId: string;
|
||||||
*/
|
createdAt: string;
|
||||||
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
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 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,
|
export type GrpcRequest = {
|
||||||
/**
|
model: "grpc_request";
|
||||||
* Server URL (http for plaintext or https for secure)
|
id: string;
|
||||||
*/
|
createdAt: string;
|
||||||
url: 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,
|
export type HttpRequest = {
|
||||||
/**
|
model: "http_request";
|
||||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
id: string;
|
||||||
*/
|
createdAt: string;
|
||||||
urlParameters: Array<HttpUrlParameter>, };
|
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.
|
* Serializable representation of HTTP response events for DB storage.
|
||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* 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 HttpResponseState = "initialized" | "connected" | "closed";
|
||||||
|
|
||||||
export type HttpUrlParameter = { enabled?: boolean,
|
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
|
* 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, };
|
*/
|
||||||
|
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 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 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 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,
|
export type WebsocketRequest = {
|
||||||
/**
|
model: "websocket_request";
|
||||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
id: string;
|
||||||
*/
|
createdAt: string;
|
||||||
urlParameters: Array<HttpUrlParameter>, };
|
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.
|
// 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,
|
export type Environment = {
|
||||||
/**
|
model: "environment";
|
||||||
* Variables defined in this environment scope.
|
id: string;
|
||||||
* Child environments override parent variables by name.
|
workspaceId: string;
|
||||||
*/
|
createdAt: string;
|
||||||
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
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,
|
export type GrpcRequest = {
|
||||||
/**
|
model: "grpc_request";
|
||||||
* Server URL (http for plaintext or https for secure)
|
id: string;
|
||||||
*/
|
createdAt: string;
|
||||||
url: 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,
|
export type HttpRequest = {
|
||||||
/**
|
model: "http_request";
|
||||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
id: string;
|
||||||
*/
|
createdAt: string;
|
||||||
urlParameters: Array<HttpUrlParameter>, };
|
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,
|
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
|
* 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, };
|
*/
|
||||||
|
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,
|
export type SyncModel =
|
||||||
/**
|
| ({ type: "workspace" } & Workspace)
|
||||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
| ({ type: "environment" } & Environment)
|
||||||
*/
|
| ({ type: "folder" } & Folder)
|
||||||
urlParameters: Array<HttpUrlParameter>, };
|
| ({ 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::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicI32, Ordering};
|
use std::sync::atomic::{AtomicI32, Ordering};
|
||||||
use std::time::Instant;
|
use std::time::{Duration, Instant};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
@@ -26,6 +26,7 @@ use yaak_models::blob_manager::{BlobManager, BodyChunk};
|
|||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
ClientCertificate, CookieJar, DnsOverride, Environment, HttpRequest, HttpResponse,
|
ClientCertificate, CookieJar, DnsOverride, Environment, HttpRequest, HttpResponse,
|
||||||
HttpResponseEvent, HttpResponseHeader, HttpResponseState, ProxySetting, ProxySettingAuth,
|
HttpResponseEvent, HttpResponseHeader, HttpResponseState, ProxySetting, ProxySettingAuth,
|
||||||
|
ResolvedSetting,
|
||||||
};
|
};
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::query_manager::QueryManager;
|
||||||
use yaak_models::util::{UpdateSource, generate_prefixed_id};
|
use yaak_models::util::{UpdateSource, generate_prefixed_id};
|
||||||
@@ -115,10 +116,17 @@ pub trait SendRequestExecutor: Send + Sync {
|
|||||||
&self,
|
&self,
|
||||||
sendable_request: SendableHttpRequest,
|
sendable_request: SendableHttpRequest,
|
||||||
event_tx: mpsc::Sender<SenderHttpResponseEvent>,
|
event_tx: mpsc::Sender<SenderHttpResponseEvent>,
|
||||||
cookie_store: Option<CookieStore>,
|
cookie_behavior: CookieBehavior,
|
||||||
) -> yaak_http::error::Result<yaak_http::sender::HttpResponse>;
|
) -> 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;
|
struct DefaultSendRequestExecutor;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -127,11 +135,16 @@ impl SendRequestExecutor for DefaultSendRequestExecutor {
|
|||||||
&self,
|
&self,
|
||||||
sendable_request: SendableHttpRequest,
|
sendable_request: SendableHttpRequest,
|
||||||
event_tx: mpsc::Sender<SenderHttpResponseEvent>,
|
event_tx: mpsc::Sender<SenderHttpResponseEvent>,
|
||||||
cookie_store: Option<CookieStore>,
|
cookie_behavior: CookieBehavior,
|
||||||
) -> yaak_http::error::Result<yaak_http::sender::HttpResponse> {
|
) -> yaak_http::error::Result<yaak_http::sender::HttpResponse> {
|
||||||
let sender = ReqwestSender::new()?;
|
let sender = ReqwestSender::new()?;
|
||||||
let transaction = match cookie_store {
|
let transaction = match cookie_behavior.store {
|
||||||
Some(store) => HttpTransaction::with_cookie_store(sender, store),
|
Some(store) => HttpTransaction::with_cookie_behavior(
|
||||||
|
sender,
|
||||||
|
store,
|
||||||
|
cookie_behavior.send_cookies,
|
||||||
|
cookie_behavior.store_cookies,
|
||||||
|
),
|
||||||
None => HttpTransaction::new(sender),
|
None => HttpTransaction::new(sender),
|
||||||
};
|
};
|
||||||
let (_cancel_tx, cancel_rx) = watch::channel(false);
|
let (_cancel_tx, cancel_rx) = watch::channel(false);
|
||||||
@@ -182,7 +195,7 @@ struct ConnectionManagerSendRequestExecutor<'a> {
|
|||||||
connection_manager: &'a HttpConnectionManager,
|
connection_manager: &'a HttpConnectionManager,
|
||||||
plugin_context_id: String,
|
plugin_context_id: String,
|
||||||
query_manager: QueryManager,
|
query_manager: QueryManager,
|
||||||
workspace_id: String,
|
request: HttpRequest,
|
||||||
cancelled_rx: Option<watch::Receiver<bool>>,
|
cancelled_rx: Option<watch::Receiver<bool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,11 +205,10 @@ impl SendRequestExecutor for ConnectionManagerSendRequestExecutor<'_> {
|
|||||||
&self,
|
&self,
|
||||||
sendable_request: SendableHttpRequest,
|
sendable_request: SendableHttpRequest,
|
||||||
event_tx: mpsc::Sender<SenderHttpResponseEvent>,
|
event_tx: mpsc::Sender<SenderHttpResponseEvent>,
|
||||||
cookie_store: Option<CookieStore>,
|
cookie_behavior: CookieBehavior,
|
||||||
) -> yaak_http::error::Result<yaak_http::sender::HttpResponse> {
|
) -> yaak_http::error::Result<yaak_http::sender::HttpResponse> {
|
||||||
let runtime_config =
|
let runtime_config = resolve_http_send_runtime_config(&self.query_manager, &self.request)
|
||||||
resolve_http_send_runtime_config(&self.query_manager, &self.workspace_id)
|
.map_err(|e| yaak_http::error::Error::RequestError(e.to_string()))?;
|
||||||
.map_err(|e| yaak_http::error::Error::RequestError(e.to_string()))?;
|
|
||||||
let client_certificate =
|
let client_certificate =
|
||||||
find_client_certificate(&sendable_request.url, &runtime_config.client_certificates);
|
find_client_certificate(&sendable_request.url, &runtime_config.client_certificates);
|
||||||
let cached_client = self
|
let cached_client = self
|
||||||
@@ -213,8 +225,13 @@ impl SendRequestExecutor for ConnectionManagerSendRequestExecutor<'_> {
|
|||||||
cached_client.resolver.set_event_sender(Some(event_tx.clone())).await;
|
cached_client.resolver.set_event_sender(Some(event_tx.clone())).await;
|
||||||
|
|
||||||
let sender = ReqwestSender::with_client(cached_client.client);
|
let sender = ReqwestSender::with_client(cached_client.client);
|
||||||
let transaction = match cookie_store {
|
let transaction = match cookie_behavior.store {
|
||||||
Some(cs) => HttpTransaction::with_cookie_store(sender, cs),
|
Some(cs) => HttpTransaction::with_cookie_behavior(
|
||||||
|
sender,
|
||||||
|
cs,
|
||||||
|
cookie_behavior.send_cookies,
|
||||||
|
cookie_behavior.store_cookies,
|
||||||
|
),
|
||||||
None => HttpTransaction::new(sender),
|
None => HttpTransaction::new(sender),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -315,24 +332,28 @@ pub struct HttpSendRuntimeConfig {
|
|||||||
|
|
||||||
pub fn resolve_http_send_runtime_config(
|
pub fn resolve_http_send_runtime_config(
|
||||||
query_manager: &QueryManager,
|
query_manager: &QueryManager,
|
||||||
workspace_id: &str,
|
request: &HttpRequest,
|
||||||
) -> Result<HttpSendRuntimeConfig> {
|
) -> Result<HttpSendRuntimeConfig> {
|
||||||
let db = query_manager.connect();
|
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();
|
let settings = db.get_settings();
|
||||||
|
|
||||||
Ok(HttpSendRuntimeConfig {
|
Ok(HttpSendRuntimeConfig {
|
||||||
send_options: SendableHttpRequestOptions {
|
send_options: SendableHttpRequestOptions {
|
||||||
follow_redirects: workspace.setting_follow_redirects,
|
follow_redirects: resolved_settings.follow_redirects.value,
|
||||||
timeout: if workspace.setting_request_timeout > 0 {
|
timeout: if resolved_settings.request_timeout.value > 0 {
|
||||||
Some(std::time::Duration::from_millis(
|
Some(std::time::Duration::from_millis(
|
||||||
workspace.setting_request_timeout.unsigned_abs() as u64,
|
resolved_settings.request_timeout.value.unsigned_abs() as u64,
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
validate_certificates: workspace.setting_validate_certificates,
|
validate_certificates: resolved_settings.validate_certificates.value,
|
||||||
proxy: proxy_setting_from_settings(settings.proxy),
|
proxy: proxy_setting_from_settings(settings.proxy),
|
||||||
dns_overrides: workspace.setting_dns_overrides,
|
dns_overrides: workspace.setting_dns_overrides,
|
||||||
client_certificates: settings.client_certificates,
|
client_certificates: settings.client_certificates,
|
||||||
@@ -387,7 +408,7 @@ pub async fn send_http_request_with_plugins(
|
|||||||
connection_manager,
|
connection_manager,
|
||||||
plugin_context_id: params.plugin_context.id.clone(),
|
plugin_context_id: params.plugin_context.id.clone(),
|
||||||
query_manager: params.query_manager.clone(),
|
query_manager: params.query_manager.clone(),
|
||||||
workspace_id: params.request.workspace_id.clone(),
|
request: params.request.clone(),
|
||||||
cancelled_rx: params.cancelled_rx.clone(),
|
cancelled_rx: params.cancelled_rx.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -454,12 +475,21 @@ pub async fn send_http_request<T: TemplateCallback>(
|
|||||||
} else {
|
} else {
|
||||||
resolve_inherited_request(params.query_manager, ¶ms.request)?
|
resolve_inherited_request(params.query_manager, ¶ms.request)?
|
||||||
};
|
};
|
||||||
let runtime_config =
|
let runtime_config = resolve_http_send_runtime_config(params.query_manager, ¶ms.request)?;
|
||||||
resolve_http_send_runtime_config(params.query_manager, ¶ms.request.workspace_id)?;
|
|
||||||
let send_options = params.send_options.unwrap_or(runtime_config.send_options);
|
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 mut cookie_jar = load_cookie_jar(params.query_manager, params.cookie_jar_id.as_deref())?;
|
||||||
let cookie_store =
|
let cookie_store =
|
||||||
cookie_jar.as_ref().map(|jar| CookieStore::from_cookies(jar.cookies.clone()));
|
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(
|
let rendered_request = render_http_request(
|
||||||
&resolved_request,
|
&resolved_request,
|
||||||
@@ -585,33 +615,66 @@ pub async fn send_http_request<T: TemplateCallback>(
|
|||||||
let started_at = Instant::now();
|
let started_at = Instant::now();
|
||||||
let request_started_url = sendable_request.url.clone();
|
let request_started_url = sendable_request.url.clone();
|
||||||
|
|
||||||
let mut http_response = match executor
|
send_setting_event(
|
||||||
.send(sendable_request, event_tx, cookie_store.clone())
|
&event_tx,
|
||||||
.await
|
"validate_certificates",
|
||||||
{
|
runtime_config.validate_certificates.to_string(),
|
||||||
Ok(response) => response,
|
&resolved_settings.validate_certificates,
|
||||||
Err(err) => {
|
);
|
||||||
persist_cookie_jar(params.query_manager, cookie_jar.as_mut(), cookie_store.as_ref())?;
|
send_setting_event(
|
||||||
if persist_response {
|
&event_tx,
|
||||||
let _ = persist_response_error(
|
"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.query_manager,
|
||||||
params.blob_manager,
|
cookie_jar.as_mut(),
|
||||||
¶ms.update_source,
|
cookie_behavior.store.as_ref(),
|
||||||
&response,
|
)?;
|
||||||
started_at,
|
if persist_response {
|
||||||
err.to_string(),
|
let _ = persist_response_error(
|
||||||
request_started_url,
|
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());
|
let headers_elapsed = duration_to_i32(started_at.elapsed());
|
||||||
std::fs::create_dir_all(params.response_dir).map_err(|source| {
|
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,
|
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);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,7 +873,7 @@ pub async fn send_http_request<T: TemplateCallback>(
|
|||||||
response = final_response;
|
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 })
|
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 {
|
fn proxy_setting_from_settings(proxy: Option<ProxySetting>) -> HttpConnectionProxySetting {
|
||||||
match proxy {
|
match proxy {
|
||||||
None => HttpConnectionProxySetting::System,
|
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.
|
// 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 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,
|
export type Environment = {
|
||||||
/**
|
model: "environment";
|
||||||
* Variables defined in this environment scope.
|
id: string;
|
||||||
* Child environments override parent variables by name.
|
workspaceId: string;
|
||||||
*/
|
createdAt: string;
|
||||||
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
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 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,
|
export type GrpcRequest = {
|
||||||
/**
|
model: "grpc_request";
|
||||||
* Server URL (http for plaintext or https for secure)
|
id: string;
|
||||||
*/
|
createdAt: string;
|
||||||
url: 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,
|
export type HttpRequest = {
|
||||||
/**
|
model: "http_request";
|
||||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
id: string;
|
||||||
*/
|
createdAt: string;
|
||||||
urlParameters: Array<HttpUrlParameter>, };
|
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.
|
* Serializable representation of HTTP response events for DB storage.
|
||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* 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 HttpResponseState = "initialized" | "connected" | "closed";
|
||||||
|
|
||||||
export type HttpUrlParameter = { enabled?: boolean,
|
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
|
* 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, };
|
*/
|
||||||
|
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 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 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 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,
|
export type WebsocketRequest = {
|
||||||
/**
|
model: "websocket_request";
|
||||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
id: string;
|
||||||
*/
|
createdAt: string;
|
||||||
urlParameters: Array<HttpUrlParameter>, };
|
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,
|
HomeIcon,
|
||||||
ImportIcon,
|
ImportIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
KeyboardIcon,
|
|
||||||
KeyRoundIcon,
|
KeyRoundIcon,
|
||||||
|
KeyboardIcon,
|
||||||
|
ListXIcon,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
LockOpenIcon,
|
LockOpenIcon,
|
||||||
MergeIcon,
|
MergeIcon,
|
||||||
@@ -131,12 +132,15 @@ import {
|
|||||||
SunIcon,
|
SunIcon,
|
||||||
TableIcon,
|
TableIcon,
|
||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
|
Undo2Icon,
|
||||||
UploadIcon,
|
UploadIcon,
|
||||||
VariableIcon,
|
VariableIcon,
|
||||||
Wand2Icon,
|
Wand2Icon,
|
||||||
WifiIcon,
|
WifiIcon,
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
|
ZapIcon,
|
||||||
|
ZapOffIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { CSSProperties, HTMLAttributes } from "react";
|
import type { CSSProperties, HTMLAttributes } from "react";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
@@ -238,6 +242,7 @@ const icons = {
|
|||||||
keyboard: KeyboardIcon,
|
keyboard: KeyboardIcon,
|
||||||
left_panel_hidden: PanelLeftOpenIcon,
|
left_panel_hidden: PanelLeftOpenIcon,
|
||||||
left_panel_visible: PanelLeftCloseIcon,
|
left_panel_visible: PanelLeftCloseIcon,
|
||||||
|
list_x: ListXIcon,
|
||||||
lock: LockIcon,
|
lock: LockIcon,
|
||||||
lock_open: LockOpenIcon,
|
lock_open: LockOpenIcon,
|
||||||
magic_wand: Wand2Icon,
|
magic_wand: Wand2Icon,
|
||||||
@@ -271,6 +276,7 @@ const icons = {
|
|||||||
table: TableIcon,
|
table: TableIcon,
|
||||||
text: FileTextIcon,
|
text: FileTextIcon,
|
||||||
trash: Trash2Icon,
|
trash: Trash2Icon,
|
||||||
|
undo_2: Undo2Icon,
|
||||||
unpin: PinOffIcon,
|
unpin: PinOffIcon,
|
||||||
update: RefreshCcwIcon,
|
update: RefreshCcwIcon,
|
||||||
upload: UploadIcon,
|
upload: UploadIcon,
|
||||||
@@ -278,6 +284,8 @@ const icons = {
|
|||||||
wifi: WifiIcon,
|
wifi: WifiIcon,
|
||||||
wrench: WrenchIcon,
|
wrench: WrenchIcon,
|
||||||
x: XIcon,
|
x: XIcon,
|
||||||
|
zap: ZapIcon,
|
||||||
|
zap_off: ZapOffIcon,
|
||||||
_unknown: ShieldAlertIcon,
|
_unknown: ShieldAlertIcon,
|
||||||
|
|
||||||
empty: (props: HTMLAttributes<HTMLSpanElement>) => <div {...props} />,
|
empty: (props: HTMLAttributes<HTMLSpanElement>) => <div {...props} />,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import type { HTMLAttributes } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
export function Table({
|
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 (
|
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}
|
{children}
|
||||||
</tbody>
|
</tbody>
|
||||||
);
|
);
|
||||||
@@ -49,8 +59,19 @@ export function TableHead({ children, className }: { children: ReactNode; classN
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableRow({ children }: { children: ReactNode }) {
|
export function TableRow({
|
||||||
return <tr>{children}</tr>;
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & HTMLAttributes<HTMLTableRowElement>) {
|
||||||
|
return (
|
||||||
|
<tr className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableCell({
|
export function TableCell({
|
||||||
@@ -98,7 +119,7 @@ export function TableHeaderCell({
|
|||||||
<th
|
<th
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
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}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user