mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-11-13 12:13:42 +01:00
The actions row moves out of the code editor into ActionsBar, a plain component a wrapper places over its content. FormatAction reformats an editor through its ref, and BodyViewModeSwitch names its first state for the body's format (Pretty, Table, Parts, Preview, Form) next to Raw. Yaak Proxy's request and response body tabs now share one viewer choice, with the switch in each tab's bar beside the text viewer's filter. The choice is remembered for both panes; bodies over the large size open raw. "Response (Raw)" leaves the proxy's tab menu, while Yaak's response pane keeps it until the remaining sites move. SegmentedControl moves from the client into packages/ui. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
260 lines
7.8 KiB
TypeScript
260 lines
7.8 KiB
TypeScript
import type {
|
|
Folder,
|
|
GrpcRequest,
|
|
HttpRequest,
|
|
WebsocketRequest,
|
|
Workspace,
|
|
} from "@yaakapp-internal/models";
|
|
import { patchModel } from "@yaakapp-internal/models";
|
|
import { HStack, Icon, InlineCode, SegmentedControl } from "@yaakapp-internal/ui";
|
|
import { useCallback } from "react";
|
|
import { openFolderSettings } from "../commands/openFolderSettings";
|
|
import { openWorkspaceSettings } from "../commands/openWorkspaceSettings";
|
|
import { useAuthDropdownOptions } from "../hooks/useAuthTab";
|
|
import { useHttpAuthenticationConfig } from "../hooks/useHttpAuthenticationConfig";
|
|
import { useInheritedAuthentication } from "../hooks/useInheritedAuthentication";
|
|
import { useRenderTemplate } from "../hooks/useRenderTemplate";
|
|
import { resolvedModelName } from "../lib/resolvedModelName";
|
|
import { Button } from "./core/Button";
|
|
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
|
import { IconButton } from "./core/IconButton";
|
|
import { Input, type InputProps } from "./core/Input";
|
|
import { Link } from "./core/Link";
|
|
import { RadioDropdown } from "./core/RadioDropdown";
|
|
import { DynamicForm } from "./DynamicForm";
|
|
import { EmptyStateText } from "./EmptyStateText";
|
|
|
|
interface Props {
|
|
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace;
|
|
}
|
|
|
|
export function HttpAuthenticationEditor({ model }: Props) {
|
|
const inheritedAuth = useInheritedAuthentication(model);
|
|
const authConfig = useHttpAuthenticationConfig(
|
|
model.authenticationType,
|
|
model.authentication,
|
|
model,
|
|
);
|
|
|
|
const handleChange = useCallback(
|
|
async (authentication: Record<string, unknown>) =>
|
|
await patchModel(model, { authentication }),
|
|
[model],
|
|
);
|
|
|
|
if (model.authenticationType === "none") {
|
|
return <EmptyStateText>No authentication</EmptyStateText>;
|
|
}
|
|
|
|
if (model.authenticationType != null && authConfig.data == null) {
|
|
return (
|
|
<EmptyStateText>
|
|
<p>
|
|
Auth plugin not found for{" "}
|
|
<InlineCode>{model.authenticationType}</InlineCode>
|
|
</p>
|
|
</EmptyStateText>
|
|
);
|
|
}
|
|
|
|
if (inheritedAuth == null) {
|
|
if (model.model === "workspace" || model.model === "folder") {
|
|
return (
|
|
<EmptyStateText className="flex-col gap-3">
|
|
<div className="not-italic flex flex-col items-center gap-3 text-center">
|
|
<p className="max-w-md text-sm text-text-subtle">
|
|
Choose an auth method to apply it to all requests in{" "}
|
|
<strong className="font-semibold text-text-subtle">
|
|
{resolvedModelName(model)}
|
|
</strong>
|
|
.
|
|
</p>
|
|
<AuthenticationTypeDropdown model={model} />
|
|
<Link href="https://yaak.app/docs/using-yaak/request-inheritance">
|
|
Documentation
|
|
</Link>
|
|
</div>
|
|
</EmptyStateText>
|
|
);
|
|
}
|
|
return <EmptyStateText>No authentication</EmptyStateText>;
|
|
}
|
|
|
|
if (inheritedAuth.authenticationType === "none") {
|
|
return <EmptyStateText>No authentication</EmptyStateText>;
|
|
}
|
|
|
|
const wasAuthInherited = inheritedAuth?.id !== model.id;
|
|
if (wasAuthInherited) {
|
|
const name = resolvedModelName(inheritedAuth);
|
|
const cta = inheritedAuth.model === "workspace" ? "Workspace" : name;
|
|
return (
|
|
<EmptyStateText>
|
|
<p>
|
|
Inherited from{" "}
|
|
<button
|
|
type="submit"
|
|
className="underline hover:text-text"
|
|
onClick={() => {
|
|
if (inheritedAuth.model === "folder")
|
|
openFolderSettings(inheritedAuth.id, "auth");
|
|
else openWorkspaceSettings("auth");
|
|
}}
|
|
>
|
|
{cta}
|
|
</button>
|
|
</p>
|
|
</EmptyStateText>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)] gap-y-3">
|
|
<div>
|
|
<HStack space={2} alignItems="start">
|
|
<SegmentedControl
|
|
label="Enabled"
|
|
hideLabel
|
|
name="enabled"
|
|
value={
|
|
model.authentication.disabled === false ||
|
|
model.authentication.disabled == null
|
|
? "__TRUE__"
|
|
: model.authentication.disabled === true
|
|
? "__FALSE__"
|
|
: "__DYNAMIC__"
|
|
}
|
|
options={[
|
|
{ label: "Enabled", value: "__TRUE__" },
|
|
{ label: "Disabled", value: "__FALSE__" },
|
|
{ label: "Enabled when...", value: "__DYNAMIC__" },
|
|
]}
|
|
onChange={async (enabled) => {
|
|
let disabled: boolean | string;
|
|
if (enabled === "__TRUE__") {
|
|
disabled = false;
|
|
} else if (enabled === "__FALSE__") {
|
|
disabled = true;
|
|
} else {
|
|
disabled = "";
|
|
}
|
|
await handleChange({ ...model.authentication, disabled });
|
|
}}
|
|
/>
|
|
{authConfig.data?.actions && authConfig.data.actions.length > 0 && (
|
|
<Dropdown
|
|
items={authConfig.data.actions.map(
|
|
(a): DropdownItem => ({
|
|
label: a.label,
|
|
leftSlot: a.icon ? <Icon icon={a.icon} /> : null,
|
|
onSelect: () => a.call(model),
|
|
}),
|
|
)}
|
|
>
|
|
<IconButton
|
|
title="Authentication Actions"
|
|
icon="settings"
|
|
size="xs"
|
|
className="text-secondary!"
|
|
/>
|
|
</Dropdown>
|
|
)}
|
|
</HStack>
|
|
{typeof model.authentication.disabled === "string" && (
|
|
<div className="mt-3">
|
|
<AuthenticationDisabledInput
|
|
className="w-full"
|
|
stateKey={`auth.${model.id}.dynamic`}
|
|
value={model.authentication.disabled}
|
|
onChange={(v) =>
|
|
handleChange({ ...model.authentication, disabled: v })
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<DynamicForm
|
|
disabled={model.authentication.disabled === true}
|
|
autocompleteVariables
|
|
autocompleteFunctions
|
|
stateKey={`auth.${model.id}.${model.authenticationType}`}
|
|
inputs={authConfig.data?.args ?? []}
|
|
data={model.authentication}
|
|
onChange={handleChange}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AuthenticationTypeDropdown({ model }: Props) {
|
|
const options = useAuthDropdownOptions(model);
|
|
|
|
if (options == null) return null;
|
|
|
|
return (
|
|
<RadioDropdown
|
|
items={options.items}
|
|
itemsAfter={options.itemsAfter}
|
|
itemsBefore={options.itemsBefore}
|
|
value={options.value}
|
|
onChange={options.onChange}
|
|
>
|
|
<Button
|
|
color="secondary"
|
|
variant="border"
|
|
size="sm"
|
|
rightSlot={
|
|
<Icon icon="chevron_down" size="sm" className="text-text-subtle" />
|
|
}
|
|
>
|
|
Select Auth
|
|
</Button>
|
|
</RadioDropdown>
|
|
);
|
|
}
|
|
|
|
function AuthenticationDisabledInput({
|
|
value,
|
|
onChange,
|
|
stateKey,
|
|
className,
|
|
}: {
|
|
value: string;
|
|
onChange: InputProps["onChange"];
|
|
stateKey: string;
|
|
className?: string;
|
|
}) {
|
|
const rendered = useRenderTemplate({
|
|
template: value,
|
|
enabled: true,
|
|
purpose: "preview",
|
|
refreshKey: value,
|
|
});
|
|
|
|
return (
|
|
<Input
|
|
size="sm"
|
|
className={className}
|
|
label="Dynamic Disabled"
|
|
hideLabel
|
|
defaultValue={value}
|
|
placeholder="Enabled when this renders a non-empty value"
|
|
rightSlot={
|
|
<div className="px-1 flex items-center">
|
|
<div className="rounded-full bg-surface-highlight text-xs px-1.5 py-0.5 text-text-subtle whitespace-nowrap">
|
|
{rendered.isPending
|
|
? "loading"
|
|
: rendered.data
|
|
? "enabled"
|
|
: "disabled"}
|
|
</div>
|
|
</div>
|
|
}
|
|
autocompleteFunctions
|
|
autocompleteVariables
|
|
onChange={onChange}
|
|
stateKey={stateKey}
|
|
/>
|
|
);
|
|
}
|