blob: 9b1efb854860a5672e979a3b82dea3654cd74c8d [file]
import { HStack, Text, VStack } from '@astryxdesign/core';
import {
normalizeOptionalRequestBodyOverlay,
normalizeRequestHeaderUpdates,
normalizeRequestHeaders,
type JsonObject,
type RequestHeaderUpdate,
} from '@maka/core';
import { Button, IconButton, TextArea, TextInput } from '@maka/ui';
import { Trash2 } from '@maka/ui/icons';
import { PasswordInput } from './password-input';
export interface RequestHeaderDraft {
readonly id: number;
readonly name: string;
readonly value: string;
readonly retained: boolean;
}
export function savedRequestHeaderDrafts(names: readonly string[]): RequestHeaderDraft[] {
return names.map((name, index) => ({ id: index + 1, name, value: '', retained: true }));
}
export function requestHeaderUpdates(
drafts: readonly RequestHeaderDraft[],
): readonly RequestHeaderUpdate[] {
return normalizeRequestHeaderUpdates(
drafts.map(({ name, value, retained }) =>
retained && value.length === 0 ? { name } : { name, value },
),
);
}
export function newRequestHeaders(
drafts: readonly RequestHeaderDraft[],
): Readonly<Record<string, string>> {
const updates = normalizeRequestHeaderUpdates(
drafts.map(({ name, value }) => ({ name, value })),
);
return normalizeRequestHeaders(
Object.fromEntries(updates.map(({ name, value }) => [name, value ?? ''])),
);
}
export function parseRequestBodyOverlay(text: string): JsonObject | undefined {
if (!text.trim()) return undefined;
let value: unknown;
try {
value = JSON.parse(text);
} catch {
throw new Error('Extra request body must be valid JSON');
}
return normalizeOptionalRequestBodyOverlay(value);
}
export function formatRequestBodyOverlay(value: JsonObject | undefined): string {
return value === undefined ? '' : JSON.stringify(value, null, 2);
}
export function RequestCustomizationEditor(props: {
headers: readonly RequestHeaderDraft[];
onHeadersChange(headers: RequestHeaderDraft[]): void;
bodyText: string;
onBodyTextChange(value: string): void;
disabled?: boolean;
copy: {
headers: string;
headerName: string;
headerValue: string;
retainedValue: string;
addHeader: string;
removeHeader: string;
noHeaders: string;
body: string;
bodyHelp: string;
};
}) {
return (
<VStack gap={4}>
<RequestHeadersEditor {...props} />
<RequestBodyEditor {...props} />
</VStack>
);
}
export function RequestHeadersEditor(props: {
headers: readonly RequestHeaderDraft[];
onHeadersChange(headers: RequestHeaderDraft[]): void;
disabled?: boolean;
hideTitle?: boolean;
copy: {
headers: string;
headerName: string;
headerValue: string;
retainedValue: string;
addHeader: string;
removeHeader: string;
noHeaders: string;
};
}) {
const nextId = props.headers.reduce((highest, header) => Math.max(highest, header.id), 0) + 1;
function update(id: number, patch: Partial<RequestHeaderDraft>) {
props.onHeadersChange(
props.headers.map((header) =>
header.id === id
? {
...header,
...patch,
...(patch.name !== undefined && patch.name !== header.name ? { retained: false } : {}),
}
: header,
),
);
}
return (
<VStack gap={2}>
{!props.hideTitle && <Text weight="semibold">{props.copy.headers}</Text>}
{props.headers.length === 0 && (
<Text type="supporting" color="secondary">
{props.copy.noHeaders}
</Text>
)}
{props.headers.map((header) => (
<div key={header.id} className="requestHeaderRow">
<div className="requestHeaderName">
<TextInput
label={props.copy.headerName}
isLabelHidden
value={header.name}
onChange={(name) => update(header.id, { name })}
placeholder="HTTP-Referer"
isDisabled={props.disabled}
width="100%"
/>
</div>
<div className="requestHeaderValue">
<PasswordInput
label={props.copy.headerValue}
isLabelHidden
value={header.value}
onChange={(value) => update(header.id, { value })}
placeholder={header.retained ? props.copy.retainedValue : props.copy.headerValue}
isDisabled={props.disabled}
/>
</div>
<div className="requestHeaderRemove">
<IconButton
variant="ghost"
size="sm"
label={props.copy.removeHeader}
icon={<Trash2 size={16} aria-hidden="true" />}
isDisabled={props.disabled}
onClick={() =>
props.onHeadersChange(props.headers.filter(({ id }) => id !== header.id))
}
/>
</div>
</div>
))}
<HStack>
<Button
variant="ghost"
size="sm"
label={props.copy.addHeader}
isDisabled={props.disabled || props.headers.length >= 32}
onClick={() =>
props.onHeadersChange([
...props.headers,
{ id: nextId, name: '', value: '', retained: false },
])
}
/>
</HStack>
</VStack>
);
}
export function RequestBodyEditor(props: {
bodyText: string;
onBodyTextChange(value: string): void;
disabled?: boolean;
hideLabel?: boolean;
copy: { body: string; bodyHelp: string };
}) {
return (
<TextArea
label={props.copy.body}
isLabelHidden={props.hideLabel}
value={props.bodyText}
onChange={props.onBodyTextChange}
placeholder={'{\n "provider": {\n "order": ["Anthropic"]\n }\n}'}
description={props.copy.bodyHelp}
isDisabled={props.disabled}
rows={6}
width="100%"
/>
);
}