import type { Folder, GrpcRequest, HttpRequest, HttpVersion, InheritedBoolSetting, InheritedHttpVersionSetting, 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_HTTP_VERSION, 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 { Select } from "./core/Select"; import { SettingOverrideRow, SettingRow, SettingRowBoolean, 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 HttpVersionSetting = HttpVersion | InheritedHttpVersionSetting; type CookieSettingsPatch = { settingSendCookies?: ModelWithCookieSettings["settingSendCookies"]; settingStoreCookies?: ModelWithCookieSettings["settingStoreCookies"]; }; type HttpSettingsPatch = { settingFollowRedirects?: ModelWithHttpSettings["settingFollowRedirects"]; settingHttpVersion?: ModelWithHttpSettings["settingHttpVersion"]; 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, }) } /> )} {supportsHttpSettings && ( patchHttpSettings(model, { settingHttpVersion, }) } /> )} )} {supportsCookieSettings && ( patchCookieSettings(model, { settingSendCookies, }) } /> patchCookieSettings(model, { settingStoreCookies, }) } /> )} ); } export function countOverriddenSettings(model: ModelWithSettings) { const settings: (BooleanSetting | IntegerSetting | HttpVersionSetting)[] = []; if (modelSupportsCookieSettings(model)) { settings.push(model.settingSendCookies, model.settingStoreCookies); } settings.push(model.settingValidateCertificates); if (modelSupportsHttpSettings(model)) { settings.push( model.settingFollowRedirects, model.settingRequestTimeout, model.settingHttpVersion, ); } if (modelSupportsMessageSizeSettings(model)) { settings.push(model.settingRequestMessageSize); } return settings.filter((setting) => isInheritedSetting(setting) && setting.enabled === true) .length; } function patchCookieSettings(model: ModelWithCookieSettings, patch: Partial) { switch (model.model) { case "workspace": return patchModel(model, patch as Partial); case "folder": return patchModel(model, patch as Partial); case "http_request": return patchModel(model, patch as Partial); case "websocket_request": return patchModel(model, patch as Partial); } } function patchHttpSettings(model: ModelWithHttpSettings, patch: Partial) { switch (model.model) { case "workspace": return patchModel(model, patch as Partial); case "folder": return patchModel(model, patch as Partial); case "http_request": return patchModel(model, patch as Partial); } } function patchTlsSettings(model: ModelWithTlsSettings, patch: Partial) { switch (model.model) { case "workspace": return patchModel(model, patch as Partial); case "folder": return patchModel(model, patch as Partial); case "http_request": return patchModel(model, patch as Partial); case "websocket_request": return patchModel(model, patch as Partial); case "grpc_request": return patchModel(model, patch as Partial); } } function patchMessageSizeSettings( model: ModelWithMessageSizeSettings, patch: Partial, ) { switch (model.model) { case "workspace": return patchModel(model, patch as Partial); case "folder": return patchModel(model, patch as Partial); case "websocket_request": return patchModel(model, patch as Partial); case "grpc_request": 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 })} /> ); } const HTTP_VERSION_OPTIONS: { label: string; value: HttpVersion }[] = [ { label: "Automatic", value: "auto" }, { label: "HTTP/1.1", value: "http1" }, { label: "HTTP/2", value: "http2" }, ]; function HttpVersionSettingRow({ inheritedValue, setting, settingDefinition, onChange, }: { inheritedValue: HttpVersion; setting: HttpVersionSetting; settingDefinition: RequestSettingDefinition<"settingHttpVersion">; onChange: (setting: HttpVersionSetting) => void; }) { const inherited = isInheritedSetting(setting); const overridden = inherited ? setting.enabled === true : false; const value = inherited ? (overridden ? setting.value : inheritedValue) : setting; if (!inherited) { return ( onChange({ ...setting, enabled: true, value })} /> ); } function IntegerSettingRow({ inheritedValue, setting, settingDefinition, onChange, }: { inheritedValue: number; setting: IntegerSetting; settingDefinition: RequestSettingDefinition<"settingRequestTimeout">; 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 ( onChange(parseInteger(value))} /> ); } return ( onChange({ ...setting, enabled: false })} > onChange({ ...setting, enabled: true, value: parseInteger(value), }) } /> ); } 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 = "0"; 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 ( ); } function NumberUnitInput({ inputMode, label, name, onChange, placeholder, step, unit, validate, value, }: { inputMode?: "decimal" | "numeric"; label: string; name: string; onChange: (value: string) => void; placeholder: string; step?: number | "any"; unit: string; validate: (value: string) => boolean; value: string; }) { return ( {unit} } 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: "settingHttpVersion", fallback: HttpVersionSetting, ): HttpVersion; function resolveInheritedValue( ancestors: (Folder | Workspace)[], key: keyof WorkspaceSettings, fallback: BooleanSetting | IntegerSetting | HttpVersionSetting, ) { for (const ancestor of ancestors) { const setting = ancestor[key] as BooleanSetting | IntegerSetting | HttpVersionSetting; if (isInheritedSetting(setting)) { if (setting.enabled === true) { return setting.value; } continue; } return setting; } return isInheritedSetting(fallback) ? fallback.value : fallback; } type WorkspaceSettings = Pick< Workspace, | "settingFollowRedirects" | "settingHttpVersion" | "settingRequestMessageSize" | "settingRequestTimeout" | "settingSendCookies" | "settingStoreCookies" | "settingValidateCertificates" >; type BooleanWorkspaceSettingKey = Exclude< keyof WorkspaceSettings, "settingRequestTimeout" | "settingRequestMessageSize" | "settingHttpVersion" >; 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 parseInteger(value: string) { const parsed = Number(value); return Number.isFinite(parsed) ? Math.trunc(parsed) : 0; } function isValidInteger(value: string) { const parsed = Number(value); return value === "" || (Number.isInteger(parsed) && parsed >= 0); } function isValidMegabytes(value: string) { if (value === "") return true; const megabytes = Number(value); return Number.isFinite(megabytes) && megabytes >= 0 && megabytes <= MAX_MESSAGE_SIZE_MB; }