Files
yaak/apps/yaak-client/components/ModelSettingsEditor.tsx
T
2026-05-17 07:58:12 -07:00

322 lines
9.8 KiB
TypeScript

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">;