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 { modelSupportsSetting, type RequestSettingDefinition, SETTING_FOLLOW_REDIRECTS, SETTING_REQUEST_MESSAGE_SIZE, SETTING_REQUEST_TIMEOUT, SETTING_SEND_COOKIES, SETTING_STORE_COOKIES, SETTING_VALIDATE_CERTIFICATES, } from "../lib/requestSettings"; import { Checkbox } from "./core/Checkbox"; import { PlainInput } from "./core/PlainInput"; import { SettingOverrideRow, SettingRow, SettingRowBoolean, SettingRowNumber, SettingsList, SettingsSection, } from "./core/SettingRow"; const BYTES_PER_MB = 1024 * 1024; const MAX_REQUEST_MESSAGE_SIZE_BYTES = 2_147_483_647; const MAX_MESSAGE_SIZE_MB = MAX_REQUEST_MESSAGE_SIZE_BYTES / BYTES_PER_MB; interface Props { showSectionTitles?: boolean; model: ModelWithSettings; } type ModelWithSettings = | Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest; type ModelWithHttpSettings = Workspace | Folder | HttpRequest; type ModelWithTlsSettings = | Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest; type ModelWithCookieSettings = | Workspace | Folder | HttpRequest | WebsocketRequest; type ModelWithMessageSizeSettings = | Workspace | Folder | WebsocketRequest | GrpcRequest; type BooleanSetting = boolean | InheritedBoolSetting; type IntegerSetting = number | InheritedIntSetting; type CookieSettingsPatch = { settingSendCookies?: ModelWithCookieSettings["settingSendCookies"]; settingStoreCookies?: ModelWithCookieSettings["settingStoreCookies"]; }; type HttpSettingsPatch = { settingFollowRedirects?: ModelWithHttpSettings["settingFollowRedirects"]; settingRequestTimeout?: ModelWithHttpSettings["settingRequestTimeout"]; }; type TlsSettingsPatch = { settingValidateCertificates?: ModelWithTlsSettings["settingValidateCertificates"]; }; type MessageSizeSettingsPatch = { settingRequestMessageSize?: ModelWithMessageSizeSettings["settingRequestMessageSize"]; }; export function ModelSettingsEditor({ model, showSectionTitles = false, }: Props) { const ancestors = useModelAncestors(model); const supportsHttpSettings = modelSupportsHttpSettings(model); const supportsCookieSettings = modelSupportsCookieSettings(model); const supportsTlsSettings = modelSupportsTlsSettings(model); const supportsMessageSizeSettings = modelSupportsMessageSizeSettings(model); return ( {supportsTlsSettings && ( {supportsHttpSettings && ( patchHttpSettings(model, { settingRequestTimeout, }) } /> )} {supportsMessageSizeSettings && ( patchMessageSizeSettings(model, { settingRequestMessageSize, }) } /> )} patchTlsSettings(model, { settingValidateCertificates, }) } /> {supportsHttpSettings && ( patchHttpSettings(model, { settingFollowRedirects, }) } /> )} )} {supportsCookieSettings && ( patchCookieSettings(model, { settingSendCookies, }) } /> patchCookieSettings(model, { settingStoreCookies, }) } /> )} ); } export function countOverriddenSettings(model: ModelWithSettings) { const settings: (BooleanSetting | IntegerSetting)[] = []; if (modelSupportsCookieSettings(model)) { settings.push(model.settingSendCookies, model.settingStoreCookies); } settings.push(model.settingValidateCertificates); if (modelSupportsHttpSettings(model)) { settings.push(model.settingFollowRedirects, model.settingRequestTimeout); } if (modelSupportsMessageSizeSettings(model)) { settings.push(model.settingRequestMessageSize); } return settings.filter( (setting) => isInheritedSetting(setting) && setting.enabled === true, ).length; } function patchCookieSettings( model: ModelWithCookieSettings, patch: Partial, ) { if (model.model === "workspace") return patchModel(model, patch as Partial); if (model.model === "folder") return patchModel(model, patch as Partial); if (model.model === "http_request") return patchModel(model, patch as Partial); if (model.model === "websocket_request") return patchModel(model, patch as Partial); throw new Error("Unsupported cookie settings model"); } function patchHttpSettings( model: ModelWithHttpSettings, patch: Partial, ) { if (model.model === "workspace") return patchModel(model, patch as Partial); if (model.model === "folder") return patchModel(model, patch as Partial); return patchModel(model, patch as Partial); } function patchTlsSettings( model: ModelWithTlsSettings, patch: Partial, ) { if (model.model === "workspace") return patchModel(model, patch as Partial); if (model.model === "folder") return patchModel(model, patch as Partial); if (model.model === "http_request") return patchModel(model, patch as Partial); if (model.model === "websocket_request") return patchModel(model, patch as Partial); return patchModel(model, patch as Partial); } function patchMessageSizeSettings( model: ModelWithMessageSizeSettings, patch: Partial, ) { if (model.model === "workspace") return patchModel(model, patch as Partial); if (model.model === "folder") return patchModel(model, patch as Partial); if (model.model === "websocket_request") return patchModel(model, patch as Partial); return patchModel(model, patch as Partial); } function modelSupportsHttpSettings( model: ModelWithSettings, ): model is ModelWithHttpSettings { return modelSupportsSetting(model, SETTING_REQUEST_TIMEOUT); } function modelSupportsCookieSettings( model: ModelWithSettings, ): model is ModelWithCookieSettings { return modelSupportsSetting(model, SETTING_SEND_COOKIES); } function modelSupportsTlsSettings( model: ModelWithSettings, ): model is ModelWithTlsSettings { return modelSupportsSetting(model, SETTING_VALIDATE_CERTIFICATES); } function modelSupportsMessageSizeSettings( model: ModelWithSettings, ): model is ModelWithMessageSizeSettings { return modelSupportsSetting(model, SETTING_REQUEST_MESSAGE_SIZE); } function BooleanSettingRow({ inheritedValue, setting, settingDefinition, onChange, }: { inheritedValue: boolean; setting: BooleanSetting; settingDefinition: RequestSettingDefinition; 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 ( onChange(value)} /> ); } return ( onChange({ ...setting, enabled: false })} > onChange({ ...setting, enabled: true, value })} /> ); } function IntegerSettingRow({ inheritedValue, setting, settingDefinition, onChange, }: { inheritedValue: number; setting: IntegerSetting; settingDefinition: RequestSettingDefinition< "settingRequestTimeout" | "settingRequestMessageSize" >; onChange: (setting: IntegerSetting) => void; }) { const inherited = isInheritedSetting(setting); const overridden = inherited ? setting.enabled === true : false; const value = inherited ? overridden ? setting.value : inheritedValue : setting; if (!inherited) { return ( value === "" || Number.parseInt(value, 10) >= 0} onChange={(value) => onChange(value)} /> ); } return ( onChange({ ...setting, enabled: false })} > value === "" || Number.parseInt(value, 10) >= 0} onChange={(value) => onChange({ ...setting, enabled: true, value: Number.parseInt(value, 10) || 0, }) } /> ); } function MessageSizeSettingRow({ inheritedValue, setting, settingDefinition, onChange, }: { inheritedValue: number; setting: IntegerSetting; settingDefinition: RequestSettingDefinition<"settingRequestMessageSize">; onChange: (setting: IntegerSetting) => void; }) { const inherited = isInheritedSetting(setting); const overridden = inherited ? setting.enabled === true : false; const value = inherited ? overridden ? setting.value : inheritedValue : setting; const displayValue = formatMegabytes(value); const placeholder = formatMegabytes(settingDefinition.defaultValue); if (!inherited) { return ( onChange(parseMegabytes(value))} /> ); } return ( onChange({ ...setting, enabled: false })} > onChange({ ...setting, enabled: true, value: parseMegabytes(value), }) } /> ); } function MessageSizeInput({ label, name, onChange, placeholder, value, }: { label: string; name: string; onChange: (value: string) => void; placeholder: string; value: string; }) { return ( MB} onChange={onChange} /> ); } function isInheritedSetting( 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" | "settingRequestMessageSize", 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" | "settingRequestMessageSize" | "settingRequestTimeout" | "settingSendCookies" | "settingStoreCookies" | "settingValidateCertificates" >; type BooleanWorkspaceSettingKey = Exclude< keyof WorkspaceSettings, "settingRequestTimeout" | "settingRequestMessageSize" >; function formatMegabytes(bytes: number) { const megabytes = bytes / BYTES_PER_MB; return Number.isInteger(megabytes) ? `${megabytes}` : megabytes.toFixed(3).replace(/\.?0+$/, ""); } function parseMegabytes(value: string) { const megabytes = Number(value); return Number.isFinite(megabytes) ? Math.round(megabytes * BYTES_PER_MB) : 0; } function isValidMegabytes(value: string) { if (value === "") return true; const megabytes = Number(value); return ( Number.isFinite(megabytes) && megabytes >= 0 && megabytes <= MAX_MESSAGE_SIZE_MB ); }