Dynamic template function args and TTL option for request chaining (#266)

This commit is contained in:
Gregory Schier
2025-10-16 14:39:30 -07:00
committed by GitHub
parent d46479cd22
commit d83aabd2be
15 changed files with 365 additions and 95 deletions

View File

@@ -1,9 +1,17 @@
import type {
Folder,
GrpcRequest,
HttpRequest,
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
import type { TemplateFunction } from '@yaakapp-internal/plugins';
import type { FnArg, Tokens } from '@yaakapp-internal/templates';
import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react';
import { useDebouncedValue } from '../hooks/useDebouncedValue';
import { useRenderTemplate } from '../hooks/useRenderTemplate';
import { useTemplateFunctionConfig } from '../hooks/useTemplateFunctionConfig';
import {
templateTokensToString,
useTemplateTokensToString,
@@ -24,6 +32,7 @@ interface Props {
initialTokens: Tokens;
hide: () => void;
onChange: (insert: string) => void;
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace;
}
export function TemplateFunctionDialog({ initialTokens, templateFunction, ...props }: Props) {
@@ -84,14 +93,15 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
}
function InitializedTemplateFunctionDialog({
templateFunction,
hide,
templateFunction: { name },
initialArgValues,
hide,
onChange,
model,
}: Omit<Props, 'initialTokens'> & {
initialArgValues: Record<string, string | boolean>;
}) {
const enablePreview = templateFunction.name !== 'secure';
const enablePreview = name !== 'secure';
const [showSecretsInPreview, toggleShowSecretsInPreview] = useToggle(false);
const [argValues, setArgValues] = useState<Record<string, string | boolean>>(initialArgValues);
@@ -112,15 +122,16 @@ function InitializedTemplateFunctionDialog({
type: 'tag',
val: {
type: 'fn',
name: templateFunction.name,
name,
args: argTokens,
},
},
],
};
}, [argValues, templateFunction.name]);
}, [argValues, name]);
const tagText = useTemplateTokensToString(tokens);
const templateFunction = useTemplateFunctionConfig(name, argValues, model).data;
const handleDone = () => {
if (tagText.data) {
@@ -134,7 +145,7 @@ function InitializedTemplateFunctionDialog({
const tooLarge = rendered.data ? rendered.data.length > 10000 : false;
const dataContainsSecrets = useMemo(() => {
for (const [name, value] of Object.entries(argValues)) {
const arg = templateFunction.args.find((a) => 'name' in a && a.name === name);
const arg = templateFunction?.args.find((a) => 'name' in a && a.name === name);
const isTextPassword = arg?.type === 'text' && arg.password;
if (isTextPassword && typeof value === 'string' && value && rendered.data?.includes(value)) {
return true;
@@ -145,6 +156,8 @@ function InitializedTemplateFunctionDialog({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rendered.data]);
if (templateFunction == null) return null;
return (
<VStack
as="form"
@@ -155,7 +168,7 @@ function InitializedTemplateFunctionDialog({
handleDone();
}}
>
{templateFunction.name === 'secure' ? (
{name === 'secure' ? (
<PlainInput
required
label="Value"

View File

@@ -28,6 +28,7 @@ import {
useMemo,
useRef,
} from 'react';
import { activeWorkspaceAtom } from '../../../hooks/useActiveWorkspace';
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables';
import { useRandomKey } from '../../../hooks/useRandomKey';
@@ -36,6 +37,7 @@ import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplate
import { showDialog } from '../../../lib/dialog';
import { editEnvironment } from '../../../lib/editEnvironment';
import { tryFormatJson, tryFormatXml } from '../../../lib/formatters';
import { jotaiStore } from '../../../lib/jotai';
import { withEncryptionEnabled } from '../../../lib/setupOrConfigureEncryption';
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
import { TemplateVariableDialog } from '../../TemplateVariableDialog';
@@ -292,18 +294,22 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
size: 'md',
title: <InlineCode>{fn.name}()</InlineCode>,
description: fn.description,
render: ({ hide }) => (
<TemplateFunctionDialog
templateFunction={fn}
hide={hide}
initialTokens={initialTokens}
onChange={(insert) => {
cm.current?.view.dispatch({
changes: [{ from: startPos, to: startPos + tagValue.length, insert }],
});
}}
/>
),
render: ({ hide }) => {
const model = jotaiStore.get(activeWorkspaceAtom)!;
return (
<TemplateFunctionDialog
templateFunction={fn}
model={model}
hide={hide}
initialTokens={initialTokens}
onChange={(insert) => {
cm.current?.view.dispatch({
changes: [{ from: startPos, to: startPos + tagValue.length, insert }],
});
}}
/>
);
},
});
if (fn.name === 'secure') {

View File

@@ -18,7 +18,7 @@ import { activeWorkspaceIdAtom } from './useActiveWorkspace';
export function useHttpAuthenticationConfig(
authName: string | null,
values: Record<string, JsonPrimitive>,
request: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace,
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace,
) {
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const environmentId = useAtomValue(activeEnvironmentIdAtom);
@@ -37,7 +37,7 @@ export function useHttpAuthenticationConfig(
return useQuery({
queryKey: [
'http_authentication_config',
request,
model,
authName,
values,
responseKey,
@@ -53,7 +53,7 @@ export function useHttpAuthenticationConfig(
{
authName,
values,
request,
model,
environmentId,
},
);

View File

@@ -0,0 +1,60 @@
import { useQuery } from '@tanstack/react-query';
import type {
Folder,
GrpcRequest,
HttpRequest,
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
import { httpResponsesAtom } from '@yaakapp-internal/models';
import type { GetTemplateFunctionConfigResponse, JsonPrimitive } from '@yaakapp-internal/plugins';
import { useAtomValue } from 'jotai';
import { md5 } from 'js-md5';
import { invokeCmd } from '../lib/tauri';
import { activeEnvironmentIdAtom } from './useActiveEnvironment';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
export function useTemplateFunctionConfig(
functionName: string | null,
values: Record<string, JsonPrimitive>,
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace,
) {
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const environmentId = useAtomValue(activeEnvironmentIdAtom);
const responses = useAtomValue(httpResponsesAtom);
// Some auth handlers like OAuth 2.0 show the current token after a successful request. To
// handle that, we'll force the auth to re-fetch after each new response closes
const responseKey = md5(
responses
.filter((r) => r.state === 'closed')
.map((r) => r.id)
.join(':'),
);
return useQuery({
queryKey: [
'template_function_config',
model,
functionName,
values,
responseKey,
workspaceId,
environmentId,
],
placeholderData: (prev) => prev, // Keep previous data on refetch
queryFn: async () => {
if (functionName == null) return null;
const config = await invokeCmd<GetTemplateFunctionConfigResponse>(
'cmd_template_function_config',
{
functionName: functionName,
values,
model,
environmentId,
},
);
return config.function;
},
});
}

View File

@@ -1,6 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import type { GetTemplateFunctionsResponse, TemplateFunction } from '@yaakapp-internal/plugins';
import { atom, useAtomValue , useSetAtom } from 'jotai';
import type {
GetTemplateFunctionSummaryResponse,
TemplateFunction,
} from '@yaakapp-internal/plugins';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import { useMemo, useState } from 'react';
import type { TwigCompletionOption } from '../components/core/Editor/twig/completion';
import { invokeCmd } from '../lib/tauri';
@@ -55,7 +58,9 @@ export function useSubscribeTemplateFunctions() {
refetchInterval: numFns > 0 ? Infinity : 1000,
refetchOnMount: true,
queryFn: async () => {
const result = await invokeCmd<GetTemplateFunctionsResponse[]>('cmd_template_functions');
const result = await invokeCmd<GetTemplateFunctionSummaryResponse[]>(
'cmd_template_function_summaries',
);
setNumFns(result.length);
const functions = result.flatMap((r) => r.functions) ?? [];
setAtom(functions);

View File

@@ -40,7 +40,8 @@ type TauriCmd =
| 'cmd_send_folder'
| 'cmd_send_http_request'
| 'cmd_show_workspace_key'
| 'cmd_template_functions'
| 'cmd_template_function_summaries'
| 'cmd_template_function_config'
| 'cmd_template_tokens_to_string';
export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> {