mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-09-08 19:02:00 +02:00
It has never had an onSelect. Introduced inert in #193 and carried through the codebase split, so clicking it only ever closed the dropdown. Reload Schema in the message editor's schema dropdown does the job it looked like it was for.
317 lines
10 KiB
TypeScript
317 lines
10 KiB
TypeScript
import {
|
|
type GrpcRequest,
|
|
type HttpRequestHeader,
|
|
patchModel,
|
|
patchModelDebounced,
|
|
} from "@yaakapp-internal/models";
|
|
import { HStack, Icon, useContainerSize, VStack } from "@yaakapp-internal/ui";
|
|
import classNames from "classnames";
|
|
import type { CSSProperties } from "react";
|
|
import { useCallback, useMemo, useRef } from "react";
|
|
import { useAuthTab } from "../hooks/useAuthTab";
|
|
import type { ReflectResponseService } from "../hooks/useGrpc";
|
|
import { useHeadersTab } from "../hooks/useHeadersTab";
|
|
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
|
|
import { useRequestUpdateKey } from "../hooks/useRequestUpdateKey";
|
|
import { resolvedModelName } from "../lib/resolvedModelName";
|
|
import { Button } from "./core/Button";
|
|
import { CountBadge } from "./core/CountBadge";
|
|
import { IconButton } from "./core/IconButton";
|
|
import { PlainInput } from "./core/PlainInput";
|
|
import { RadioDropdown } from "./core/RadioDropdown";
|
|
import type { TabItem } from "./core/Tabs/Tabs";
|
|
import { TabContent, Tabs } from "./core/Tabs/Tabs";
|
|
import { GrpcEditor } from "./GrpcEditor";
|
|
import { HeadersEditor } from "./HeadersEditor";
|
|
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
|
import { MarkdownEditor } from "./MarkdownEditor";
|
|
import { countOverriddenSettings, ModelSettingsEditor } from "./ModelSettingsEditor";
|
|
import { UrlBar } from "./UrlBar";
|
|
|
|
interface Props {
|
|
style?: CSSProperties;
|
|
className?: string;
|
|
activeRequest: GrpcRequest;
|
|
protoFiles: string[];
|
|
reflectionError?: string;
|
|
reflectionLoading?: boolean;
|
|
methodType:
|
|
| "unary"
|
|
| "client_streaming"
|
|
| "server_streaming"
|
|
| "streaming"
|
|
| "no-schema"
|
|
| "no-method";
|
|
isStreaming: boolean;
|
|
onCommit: () => void;
|
|
onCancel: () => void;
|
|
onSend: (v: { message: string }) => void;
|
|
onGo: () => void;
|
|
services: ReflectResponseService[] | null;
|
|
}
|
|
|
|
const TAB_MESSAGE = "message";
|
|
const TAB_METADATA = "metadata";
|
|
const TAB_AUTH = "auth";
|
|
const TAB_SETTINGS = "settings";
|
|
const TAB_DESCRIPTION = "description";
|
|
|
|
export function GrpcRequestPane({
|
|
style,
|
|
services,
|
|
methodType,
|
|
activeRequest,
|
|
protoFiles,
|
|
reflectionError,
|
|
reflectionLoading,
|
|
isStreaming,
|
|
onGo,
|
|
onCommit,
|
|
onCancel,
|
|
onSend,
|
|
}: Props) {
|
|
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
|
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, "Metadata");
|
|
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
|
const numSettingsOverrides = countOverriddenSettings(activeRequest);
|
|
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
|
|
|
|
const urlContainerEl = useRef<HTMLDivElement>(null);
|
|
const { width: paneWidth } = useContainerSize(urlContainerEl);
|
|
|
|
const handleChangeUrl = useCallback(
|
|
(url: string) => patchModelDebounced(activeRequest, { url }),
|
|
[activeRequest],
|
|
);
|
|
|
|
const handleChangeMessage = useCallback(
|
|
// Not debounced: handleSend reads message from the store, so a pending
|
|
// debounced patch would send stale text
|
|
(message: string) => patchModel(activeRequest, { message }),
|
|
[activeRequest],
|
|
);
|
|
|
|
const select = useMemo(() => {
|
|
const options =
|
|
services?.flatMap((s) =>
|
|
s.methods.map((m) => ({
|
|
label: `${s.name.split(".").pop() ?? s.name}/${m.name}`,
|
|
value: `${s.name}/${m.name}`,
|
|
})),
|
|
) ?? [];
|
|
const value = `${activeRequest?.service ?? ""}/${activeRequest?.method ?? ""}`;
|
|
return { value, options };
|
|
}, [activeRequest?.method, activeRequest?.service, services]);
|
|
|
|
const handleChangeService = useCallback(
|
|
async (v: string) => {
|
|
const [serviceName, methodName] = v.split("/", 2);
|
|
if (serviceName == null || methodName == null) throw new Error("Should never happen");
|
|
await patchModel(activeRequest, {
|
|
service: serviceName,
|
|
method: methodName,
|
|
});
|
|
},
|
|
[activeRequest],
|
|
);
|
|
|
|
const handleConnect = useCallback(async () => {
|
|
if (activeRequest == null) return;
|
|
|
|
if (activeRequest.service == null || activeRequest.method == null) {
|
|
alert({
|
|
id: "grpc-invalid-service-method",
|
|
title: "Error",
|
|
body: "Service or method not selected",
|
|
});
|
|
}
|
|
onGo();
|
|
}, [activeRequest, onGo]);
|
|
|
|
const handleSend = useCallback(async () => {
|
|
if (activeRequest == null) return;
|
|
onSend({ message: activeRequest.message });
|
|
}, [activeRequest, onSend]);
|
|
|
|
const tabs: TabItem[] = useMemo(
|
|
() => [
|
|
{ value: TAB_MESSAGE, label: "Message" },
|
|
...metadataTab,
|
|
...authTab,
|
|
{
|
|
value: TAB_SETTINGS,
|
|
label: "Settings",
|
|
rightSlot: <CountBadge count={numSettingsOverrides} />,
|
|
},
|
|
{
|
|
value: TAB_DESCRIPTION,
|
|
label: "Info",
|
|
rightSlot: activeRequest.description && <CountBadge count={true} />,
|
|
},
|
|
],
|
|
[activeRequest.description, authTab, metadataTab, numSettingsOverrides],
|
|
);
|
|
|
|
const handleMetadataChange = useCallback(
|
|
(metadata: HttpRequestHeader[]) => patchModelDebounced(activeRequest, { metadata }),
|
|
[activeRequest],
|
|
);
|
|
|
|
const handleDescriptionChange = useCallback(
|
|
(description: string) => patchModelDebounced(activeRequest, { description }),
|
|
[activeRequest],
|
|
);
|
|
|
|
return (
|
|
<VStack style={style}>
|
|
<div
|
|
ref={urlContainerEl}
|
|
className={classNames(
|
|
"grid grid-cols-[minmax(0,1fr)_auto] gap-1.5",
|
|
paneWidth === 0 && "opacity-0",
|
|
paneWidth > 0 && paneWidth < 400 && "grid-cols-1!",
|
|
)}
|
|
>
|
|
<UrlBar
|
|
key={forceUpdateKey}
|
|
url={activeRequest.url ?? ""}
|
|
submitIcon={null}
|
|
forceUpdateKey={forceUpdateKey}
|
|
placeholder="localhost:50051"
|
|
onSend={handleConnect}
|
|
onUrlChange={handleChangeUrl}
|
|
onCancel={onCancel}
|
|
isLoading={isStreaming}
|
|
stateKey={`grpc_url.${activeRequest.id}`}
|
|
/>
|
|
<HStack space={1.5}>
|
|
<RadioDropdown
|
|
value={select.value}
|
|
onChange={handleChangeService}
|
|
items={select.options.map((o) => ({
|
|
label: o.label,
|
|
value: o.value,
|
|
type: "default",
|
|
shortLabel: o.label,
|
|
}))}
|
|
>
|
|
<Button
|
|
size="sm"
|
|
variant="border"
|
|
rightSlot={<Icon size="sm" icon="chevron_down" />}
|
|
disabled={isStreaming || services == null}
|
|
className={classNames(
|
|
"font-mono text-editor min-w-20 ring-0!",
|
|
paneWidth < 400 && "flex-1",
|
|
)}
|
|
>
|
|
{select.options.find((o) => o.value === select.value)?.label ?? "No Schema"}
|
|
</Button>
|
|
</RadioDropdown>
|
|
{methodType === "client_streaming" || methodType === "streaming" ? (
|
|
<>
|
|
{isStreaming && (
|
|
<>
|
|
<IconButton
|
|
variant="border"
|
|
size="sm"
|
|
title="Cancel"
|
|
onClick={onCancel}
|
|
icon="x"
|
|
/>
|
|
<IconButton
|
|
variant="border"
|
|
size="sm"
|
|
title="Commit"
|
|
onClick={onCommit}
|
|
icon="check"
|
|
/>
|
|
</>
|
|
)}
|
|
<IconButton
|
|
size="sm"
|
|
variant="border"
|
|
title={isStreaming ? "Connect" : "Send"}
|
|
hotkeyAction="request.send"
|
|
onClick={isStreaming ? handleSend : handleConnect}
|
|
icon={isStreaming ? "send_horizontal" : "arrow_up_down"}
|
|
/>
|
|
</>
|
|
) : (
|
|
<IconButton
|
|
size="sm"
|
|
variant="border"
|
|
title={methodType === "unary" ? "Send" : "Connect"}
|
|
hotkeyAction="request.send"
|
|
onClick={isStreaming ? onCancel : handleConnect}
|
|
disabled={methodType === "no-schema" || methodType === "no-method"}
|
|
icon={
|
|
isStreaming
|
|
? "x"
|
|
: methodType.includes("streaming")
|
|
? "arrow_up_down"
|
|
: "send_horizontal"
|
|
}
|
|
/>
|
|
)}
|
|
</HStack>
|
|
</div>
|
|
<Tabs
|
|
label="Request"
|
|
tabs={tabs}
|
|
tabListClassName="mt-1 mb-1.5!"
|
|
storageKey="grpc_request_tabs"
|
|
activeTabKey={activeRequest.id}
|
|
>
|
|
<TabContent value="message">
|
|
<GrpcEditor
|
|
onChange={handleChangeMessage}
|
|
forceUpdateKey={forceUpdateKey}
|
|
services={services}
|
|
reflectionError={reflectionError}
|
|
reflectionLoading={reflectionLoading}
|
|
request={activeRequest}
|
|
protoFiles={protoFiles}
|
|
/>
|
|
</TabContent>
|
|
<TabContent value={TAB_AUTH}>
|
|
<HttpAuthenticationEditor model={activeRequest} />
|
|
</TabContent>
|
|
<TabContent value={TAB_METADATA}>
|
|
<HeadersEditor
|
|
inheritedHeaders={inheritedHeaders}
|
|
forceUpdateKey={forceUpdateKey}
|
|
headers={activeRequest.metadata}
|
|
stateKey={`headers.${activeRequest.id}`}
|
|
onChange={handleMetadataChange}
|
|
/>
|
|
</TabContent>
|
|
<TabContent value={TAB_SETTINGS}>
|
|
<ModelSettingsEditor model={activeRequest} />
|
|
</TabContent>
|
|
<TabContent value={TAB_DESCRIPTION}>
|
|
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
|
|
<PlainInput
|
|
label="Request Name"
|
|
hideLabel
|
|
forceUpdateKey={forceUpdateKey}
|
|
defaultValue={activeRequest.name}
|
|
className="font-sans text-xl! px-0!"
|
|
containerClassName="border-0"
|
|
placeholder={resolvedModelName(activeRequest)}
|
|
onChange={(name) => patchModelDebounced(activeRequest, { name })}
|
|
/>
|
|
<MarkdownEditor
|
|
name="request-description"
|
|
placeholder="Request description"
|
|
defaultValue={activeRequest.description}
|
|
stateKey={`description.${activeRequest.id}`}
|
|
onChange={handleDescriptionChange}
|
|
/>
|
|
</div>
|
|
</TabContent>
|
|
</Tabs>
|
|
</VStack>
|
|
);
|
|
}
|