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

@@ -361,7 +361,11 @@ export type GetKeyValueRequest = { key: string, };
export type GetKeyValueResponse = { value?: string, }; export type GetKeyValueResponse = { value?: string, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, }; export type GetTemplateFunctionConfigRequest = { contextId: string, name: string, values: { [key in string]?: JsonPrimitive }, };
export type GetTemplateFunctionConfigResponse = { function: TemplateFunction, pluginRefId: string, };
export type GetTemplateFunctionSummaryResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type GetThemesRequest = Record<string, never>; export type GetThemesRequest = Record<string, never>;
@@ -385,7 +389,7 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, }; export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse; export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null; export type JsonPrimitive = string | number | boolean | null;

View File

@@ -1,12 +1,21 @@
import { import {
CallTemplateFunctionArgs, CallTemplateFunctionArgs,
FormInput,
GetHttpAuthenticationConfigRequest,
TemplateFunction, TemplateFunction,
} from "../bindings/gen_events"; TemplateFunctionArg,
import { Context } from "./Context"; } from '../bindings/gen_events';
import { MaybePromise } from '../helpers';
import { Context } from './Context';
export type DynamicTemplateFunctionArg = FormInput & {
dynamic(
ctx: Context,
args: GetHttpAuthenticationConfigRequest,
): MaybePromise<Partial<FormInput> | undefined | null>;
};
export type TemplateFunctionPlugin = TemplateFunction & { export type TemplateFunctionPlugin = TemplateFunction & {
onRender( args: (TemplateFunctionArg | DynamicTemplateFunctionArg)[];
ctx: Context, onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null>;
args: CallTemplateFunctionArgs,
): Promise<string | null>;
}; };

View File

@@ -186,20 +186,55 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'get_template_functions_request' && payload.type === 'get_template_function_summary_request' &&
Array.isArray(this.#mod?.templateFunctions) Array.isArray(this.#mod?.templateFunctions)
) { ) {
const reply: TemplateFunction[] = this.#mod.templateFunctions.map((templateFunction) => { const functions: TemplateFunction[] = this.#mod.templateFunctions.map(
return { (templateFunction) => {
...migrateTemplateFunctionSelectOptions(templateFunction), return {
// Add everything except render ...migrateTemplateFunctionSelectOptions(templateFunction),
onRender: undefined, // Add everything except render
}; onRender: undefined,
}); };
},
);
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_template_functions_response', type: 'get_template_function_summary_response',
pluginRefId: this.#workerData.pluginRefId, pluginRefId: this.#workerData.pluginRefId,
functions: reply, functions,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_template_function_config_request' &&
Array.isArray(this.#mod?.templateFunctions)
) {
let templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name);
if (templateFunction == null) {
this.#sendEmpty(windowContext, replyId);
return;
}
templateFunction = migrateTemplateFunctionSelectOptions(templateFunction);
// @ts-ignore
delete templateFunction.onRender;
const resolvedArgs: TemplateFunctionArg[] = [];
for (const arg of templateFunction.args) {
if (arg && 'dynamic' in arg) {
const dynamicAttrs = await arg.dynamic(ctx, payload);
const { dynamic, ...other } = arg;
resolvedArgs.push({ ...other, ...dynamicAttrs } as TemplateFunctionArg);
} else if (arg) {
resolvedArgs.push(arg);
}
templateFunction.args = resolvedArgs;
}
const replyPayload: InternalEventPayload = {
type: 'get_template_function_config_response',
pluginRefId: this.#workerData.pluginRefId,
function: templateFunction,
}; };
this.#sendPayload(windowContext, replyPayload, replyId); this.#sendPayload(windowContext, replyPayload, replyId);
return; return;

View File

@@ -1,6 +1,8 @@
import { TemplateFunction } from '@yaakapp/api'; import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
export function migrateTemplateFunctionSelectOptions(f: TemplateFunction): TemplateFunction { export function migrateTemplateFunctionSelectOptions(
f: TemplateFunctionPlugin,
): TemplateFunctionPlugin {
const migratedArgs = f.args.map((a) => { const migratedArgs = f.args.map((a) => {
if (a.type === 'select') { if (a.type === 'select') {
a.options = a.options.map((o) => ({ a.options = a.options.map((o) => ({

View File

@@ -3,25 +3,44 @@ import type {
CallTemplateFunctionArgs, CallTemplateFunctionArgs,
Context, Context,
FormInput, FormInput,
GetHttpAuthenticationConfigRequest,
HttpResponse, HttpResponse,
PluginDefinition, PluginDefinition,
RenderPurpose, RenderPurpose,
} from '@yaakapp/api'; } from '@yaakapp/api';
import type { DynamicTemplateFunctionArg } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
import { JSONPath } from 'jsonpath-plus'; import { JSONPath } from 'jsonpath-plus';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import xpath from 'xpath'; import xpath from 'xpath';
const BEHAVIOR_TTL = 'ttl';
const BEHAVIOR_ALWAYS = 'always';
const BEHAVIOR_SMART = 'smart';
const behaviorArg: FormInput = { const behaviorArg: FormInput = {
type: 'select', type: 'select',
name: 'behavior', name: 'behavior',
label: 'Sending Behavior', label: 'Sending Behavior',
defaultValue: 'smart', defaultValue: 'smart',
options: [ options: [
{ label: 'When no responses', value: 'smart' }, { label: 'When no responses', value: BEHAVIOR_SMART },
{ label: 'Always', value: 'always' }, { label: 'Always', value: BEHAVIOR_ALWAYS },
{ label: 'When expired', value: BEHAVIOR_TTL },
], ],
}; };
const ttlArg: DynamicTemplateFunctionArg = {
type: 'text',
name: 'ttl',
label: 'Expiration Time (seconds)',
placeholder: '0',
description: 'Resend the request when the latest response is older than this many seconds, or if there are no responses yet.',
dynamic(_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) {
const show = values.behavior === BEHAVIOR_TTL;
return { hidden: !show };
},
};
const requestArg: FormInput = { const requestArg: FormInput = {
type: 'http_request', type: 'http_request',
name: 'request', name: 'request',
@@ -42,6 +61,7 @@ export const plugin: PluginDefinition = {
placeholder: 'Content-Type', placeholder: 'Content-Type',
}, },
behaviorArg, behaviorArg,
ttlArg,
], ],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request || !args.values.header) return null; if (!args.values.request || !args.values.header) return null;
@@ -50,6 +70,7 @@ export const plugin: PluginDefinition = {
requestId: String(args.values.request || ''), requestId: String(args.values.request || ''),
purpose: args.purpose, purpose: args.purpose,
behavior: args.values.behavior ? String(args.values.behavior) : null, behavior: args.values.behavior ? String(args.values.behavior) : null,
ttl: String(args.values.ttl || ''),
}); });
if (response == null) return null; if (response == null) return null;
@@ -72,6 +93,7 @@ export const plugin: PluginDefinition = {
placeholder: '$.books[0].id or /books[0]/id', placeholder: '$.books[0].id or /books[0]/id',
}, },
behaviorArg, behaviorArg,
ttlArg,
], ],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request || !args.values.path) return null; if (!args.values.request || !args.values.path) return null;
@@ -80,6 +102,7 @@ export const plugin: PluginDefinition = {
requestId: String(args.values.request || ''), requestId: String(args.values.request || ''),
purpose: args.purpose, purpose: args.purpose,
behavior: args.values.behavior ? String(args.values.behavior) : null, behavior: args.values.behavior ? String(args.values.behavior) : null,
ttl: String(args.values.ttl || ''),
}); });
if (response == null) return null; if (response == null) return null;
@@ -113,7 +136,7 @@ export const plugin: PluginDefinition = {
name: 'response.body.raw', name: 'response.body.raw',
description: 'Access the entire response body, as text', description: 'Access the entire response body, as text',
aliases: ['response'], aliases: ['response'],
args: [requestArg, behaviorArg], args: [requestArg, behaviorArg, ttlArg],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request) return null; if (!args.values.request) return null;
@@ -121,6 +144,7 @@ export const plugin: PluginDefinition = {
requestId: String(args.values.request || ''), requestId: String(args.values.request || ''),
purpose: args.purpose, purpose: args.purpose,
behavior: args.values.behavior ? String(args.values.behavior) : null, behavior: args.values.behavior ? String(args.values.behavior) : null,
ttl: String(args.values.ttl || ''),
}); });
if (response == null) return null; if (response == null) return null;
@@ -177,9 +201,11 @@ async function getResponse(
requestId, requestId,
behavior, behavior,
purpose, purpose,
ttl,
}: { }: {
requestId: string; requestId: string;
behavior: string | null; behavior: string | null;
ttl: string | null;
purpose: RenderPurpose; purpose: RenderPurpose;
}, },
): Promise<HttpResponse | null> { ): Promise<HttpResponse | null> {
@@ -203,7 +229,11 @@ async function getResponse(
const finalBehavior = behavior === 'always' && purpose === 'preview' ? 'smart' : behavior; const finalBehavior = behavior === 'always' && purpose === 'preview' ? 'smart' : behavior;
// Send if no responses and "smart," or "always" // Send if no responses and "smart," or "always"
if ((finalBehavior === 'smart' && response == null) || finalBehavior === 'always') { if (
(finalBehavior === 'smart' && response == null) ||
finalBehavior === 'always' ||
(finalBehavior === BEHAVIOR_TTL && shouldSendExpired(response, ttl))
) {
// NOTE: Render inside this conditional, or we'll get infinite recursion (render->render->...) // NOTE: Render inside this conditional, or we'll get infinite recursion (render->render->...)
const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest, purpose }); const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest, purpose });
response = await ctx.httpRequest.send({ httpRequest: renderedHttpRequest }); response = await ctx.httpRequest.send({ httpRequest: renderedHttpRequest });
@@ -211,3 +241,12 @@ async function getResponse(
return response; return response;
} }
function shouldSendExpired(response: HttpResponse | null, ttl: string | null): boolean {
if (response == null) return true;
const ttlSeconds = parseInt(ttl || '0');
if (isNaN(ttlSeconds)) throw new Error(`Invalid TTL "${ttl}"`);
const nowMillis = Date.now();
const respMillis = new Date(response.createdAt + 'Z').getTime();
return respMillis + ttlSeconds * 1000 < nowMillis;
}

View File

@@ -38,13 +38,7 @@ use yaak_models::models::{
}; };
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources}; use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
use yaak_plugins::events::{ use yaak_plugins::events::{CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionSummaryResponse, GetTemplateFunctionConfigResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest};
CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs,
CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent,
InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest,
};
use yaak_plugins::manager::PluginManager; use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_meta::PluginMetadata; use yaak_plugins::plugin_meta::PluginMetadata;
use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_plugins::template_callback::PluginTemplateCallback;
@@ -827,11 +821,36 @@ async fn cmd_grpc_request_actions<R: Runtime>(
} }
#[tauri::command] #[tauri::command]
async fn cmd_template_functions<R: Runtime>( async fn cmd_template_function_summaries<R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>, plugin_manager: State<'_, PluginManager>,
) -> YaakResult<Vec<GetTemplateFunctionsResponse>> { ) -> YaakResult<Vec<GetTemplateFunctionSummaryResponse>> {
Ok(plugin_manager.get_template_functions(&window).await?) let results = plugin_manager.get_template_function_summaries(&window).await?;
Ok(results)
}
#[tauri::command]
async fn cmd_template_function_config<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
function_name: &str,
values: HashMap<String, JsonPrimitive>,
model: AnyModel,
environment_id: Option<&str>,
) -> YaakResult<GetTemplateFunctionConfigResponse> {
let (workspace_id, folder_id) = match model.clone() {
AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::Folder(m) => (m.workspace_id, m.folder_id),
AnyModel::Workspace(m) => (m.id, None),
m => {
return Err(GenericError(format!("Unsupported model to call template functions {m:?}")));
}
};
let environment_chain =
window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?;
Ok(plugin_manager.get_template_function_config(&window, function_name, environment_chain, values, model.id()).await?)
} }
#[tauri::command] #[tauri::command]
@@ -849,10 +868,10 @@ async fn cmd_get_http_authentication_config<R: Runtime>(
plugin_manager: State<'_, PluginManager>, plugin_manager: State<'_, PluginManager>,
auth_name: &str, auth_name: &str,
values: HashMap<String, JsonPrimitive>, values: HashMap<String, JsonPrimitive>,
request: AnyModel, model: AnyModel,
environment_id: Option<&str>, environment_id: Option<&str>,
) -> YaakResult<GetHttpAuthenticationConfigResponse> { ) -> YaakResult<GetHttpAuthenticationConfigResponse> {
let (workspace_id, folder_id) = match request.clone() { let (workspace_id, folder_id) = match model.clone() {
AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id), AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id), AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id), AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id),
@@ -867,7 +886,7 @@ async fn cmd_get_http_authentication_config<R: Runtime>(
window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?; window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?;
Ok(plugin_manager Ok(plugin_manager
.get_http_authentication_config(&window, environment_chain, auth_name, values, request.id()) .get_http_authentication_config(&window, environment_chain, auth_name, values, model.id())
.await?) .await?)
} }
@@ -1416,7 +1435,8 @@ pub fn run() {
cmd_send_ephemeral_request, cmd_send_ephemeral_request,
cmd_send_http_request, cmd_send_http_request,
cmd_send_folder, cmd_send_folder,
cmd_template_functions, cmd_template_function_config,
cmd_template_function_summaries,
cmd_template_tokens_to_string, cmd_template_tokens_to_string,
// //
// //

View File

@@ -361,7 +361,11 @@ export type GetKeyValueRequest = { key: string, };
export type GetKeyValueResponse = { value?: string, }; export type GetKeyValueResponse = { value?: string, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, }; export type GetTemplateFunctionConfigRequest = { contextId: string, name: string, values: { [key in string]?: JsonPrimitive }, };
export type GetTemplateFunctionConfigResponse = { function: TemplateFunction, pluginRefId: string, };
export type GetTemplateFunctionSummaryResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type GetThemesRequest = Record<string, never>; export type GetThemesRequest = Record<string, never>;
@@ -385,7 +389,7 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, }; export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse; export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null; export type JsonPrimitive = string | number | boolean | null;

View File

@@ -99,8 +99,10 @@ pub enum InternalEventPayload {
CallGrpcRequestActionRequest(CallGrpcRequestActionRequest), CallGrpcRequestActionRequest(CallGrpcRequestActionRequest),
// Template Functions // Template Functions
GetTemplateFunctionsRequest, GetTemplateFunctionSummaryRequest(EmptyPayload),
GetTemplateFunctionsResponse(GetTemplateFunctionsResponse), GetTemplateFunctionSummaryResponse(GetTemplateFunctionSummaryResponse),
GetTemplateFunctionConfigRequest(GetTemplateFunctionConfigRequest),
GetTemplateFunctionConfigResponse(GetTemplateFunctionConfigResponse),
CallTemplateFunctionRequest(CallTemplateFunctionRequest), CallTemplateFunctionRequest(CallTemplateFunctionRequest),
CallTemplateFunctionResponse(CallTemplateFunctionResponse), CallTemplateFunctionResponse(CallTemplateFunctionResponse),
@@ -673,11 +675,28 @@ pub struct CallHttpAuthenticationResponse {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct GetTemplateFunctionsResponse { pub struct GetTemplateFunctionSummaryResponse {
pub functions: Vec<TemplateFunction>, pub functions: Vec<TemplateFunction>,
pub plugin_ref_id: String, pub plugin_ref_id: String,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetTemplateFunctionConfigRequest {
pub context_id: String,
pub name: String,
pub values: HashMap<String, JsonPrimitive>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetTemplateFunctionConfigResponse {
pub function: TemplateFunction,
pub plugin_ref_id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")] #[ts(export, export_to = "gen_events.ts")]

View File

@@ -10,7 +10,8 @@ use crate::events::{
FilterRequest, FilterResponse, GetGrpcRequestActionsResponse, FilterRequest, FilterResponse, GetGrpcRequestActionsResponse,
GetHttpAuthenticationConfigRequest, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationConfigRequest, GetHttpAuthenticationConfigResponse,
GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse,
GetTemplateFunctionsResponse, GetThemesRequest, GetThemesResponse, ImportRequest, GetTemplateFunctionConfigRequest, GetTemplateFunctionConfigResponse,
GetTemplateFunctionSummaryResponse, GetThemesRequest, GetThemesResponse, ImportRequest,
ImportResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext, ImportResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext,
RenderPurpose, RenderPurpose,
}; };
@@ -489,35 +490,59 @@ impl PluginManager {
Ok(all_actions) Ok(all_actions)
} }
pub async fn get_template_functions<R: Runtime>( pub async fn get_template_function_config<R: Runtime>(
&self, &self,
window: &WebviewWindow<R>, window: &WebviewWindow<R>,
) -> Result<Vec<GetTemplateFunctionsResponse>> { fn_name: &str,
self.get_template_functions_with_context(&PluginWindowContext::new(&window)).await environment_chain: Vec<Environment>,
} values: HashMap<String, JsonPrimitive>,
model_id: &str,
) -> Result<GetTemplateFunctionConfigResponse> {
let results = self.get_template_function_summaries(window).await?;
let r = results
.iter()
.find(|r| r.functions.iter().any(|f| f.name == fn_name))
.ok_or_else(|| PluginNotFoundErr(fn_name.into()))?;
let plugin = self
.get_plugin_by_ref_id(&r.plugin_ref_id)
.await
.ok_or_else(|| PluginNotFoundErr(r.plugin_ref_id.clone()))?;
pub async fn get_template_functions_with_context( let window_context = &PluginWindowContext::new(&window);
&self, let vars = &make_vars_hashmap(environment_chain);
window_context: &PluginWindowContext, let cb = PluginTemplateCallback::new(
) -> Result<Vec<GetTemplateFunctionsResponse>> { window.app_handle(),
let reply_events = self &window_context,
.send_and_wait(window_context, &InternalEventPayload::GetTemplateFunctionsRequest) RenderPurpose::Preview,
);
// We don't want to fail for this op because the UI will not be able to list any auth types then
let render_opt = RenderOptions {
error_behavior: RenderErrorBehavior::ReturnEmpty,
};
let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?;
let context_id = format!("{:x}", md5::compute(model_id.to_string()));
let event = self
.send_to_plugin_and_wait(
&PluginWindowContext::new(window),
&plugin,
&InternalEventPayload::GetTemplateFunctionConfigRequest(
GetTemplateFunctionConfigRequest {
values: serde_json::from_value(rendered_values)?,
name: fn_name.to_string(),
context_id,
},
),
)
.await?; .await?;
match event.payload {
let mut result = Vec::new(); InternalEventPayload::GetTemplateFunctionConfigResponse(resp) => Ok(resp),
for event in reply_events { InternalEventPayload::EmptyResponse(_) => {
if let InternalEventPayload::GetTemplateFunctionsResponse(resp) = event.payload { Err(PluginErr("Template function plugin returned empty".to_string()))
result.push(resp.clone());
} }
InternalEventPayload::ErrorResponse(e) => Err(PluginErr(e.error)),
e => Err(PluginErr(format!("Template function plugin returned invalid event {:?}", e))),
} }
// Add Rust-based functions
result.push(GetTemplateFunctionsResponse {
plugin_ref_id: "__NATIVE__".to_string(), // Meh
functions: vec![template_function_secure(), template_function_keyring()],
});
Ok(result)
} }
pub async fn call_http_request_action<R: Runtime>( pub async fn call_http_request_action<R: Runtime>(
@@ -587,7 +612,7 @@ impl PluginManager {
environment_chain: Vec<Environment>, environment_chain: Vec<Environment>,
auth_name: &str, auth_name: &str,
values: HashMap<String, JsonPrimitive>, values: HashMap<String, JsonPrimitive>,
request_id: &str, model_id: &str,
) -> Result<GetHttpAuthenticationConfigResponse> { ) -> Result<GetHttpAuthenticationConfigResponse> {
let results = self.get_http_authentication_summaries(window).await?; let results = self.get_http_authentication_summaries(window).await?;
let plugin = results let plugin = results
@@ -606,7 +631,7 @@ impl PluginManager {
error_behavior: RenderErrorBehavior::ReturnEmpty, error_behavior: RenderErrorBehavior::ReturnEmpty,
}; };
let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?; let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?;
let context_id = format!("{:x}", md5::compute(request_id.to_string())); let context_id = format!("{:x}", md5::compute(model_id.to_string()));
let event = self let event = self
.send_to_plugin_and_wait( .send_to_plugin_and_wait(
&PluginWindowContext::new(window), &PluginWindowContext::new(window),
@@ -720,6 +745,34 @@ impl PluginManager {
} }
} }
pub async fn get_template_function_summaries<R: Runtime>(
&self,
window: &WebviewWindow<R>,
) -> Result<Vec<GetTemplateFunctionSummaryResponse>> {
let window_context = PluginWindowContext::new(window);
let reply_events = self
.send_and_wait(
&window_context,
&InternalEventPayload::GetTemplateFunctionSummaryRequest(EmptyPayload {}),
)
.await?;
let mut results = Vec::new();
for event in reply_events {
if let InternalEventPayload::GetTemplateFunctionSummaryResponse(resp) = event.payload {
results.push(resp.clone());
}
}
// Add Rust-based functions
results.push(GetTemplateFunctionSummaryResponse {
plugin_ref_id: "__NATIVE__".to_string(), // Meh
functions: vec![template_function_secure(), template_function_keyring()],
});
Ok(results)
}
pub async fn call_template_function( pub async fn call_template_function(
&self, &self,
window_context: &PluginWindowContext, window_context: &PluginWindowContext,

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

View File

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

View File

@@ -18,7 +18,7 @@ import { activeWorkspaceIdAtom } from './useActiveWorkspace';
export function useHttpAuthenticationConfig( export function useHttpAuthenticationConfig(
authName: string | null, authName: string | null,
values: Record<string, JsonPrimitive>, values: Record<string, JsonPrimitive>,
request: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace, model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace,
) { ) {
const workspaceId = useAtomValue(activeWorkspaceIdAtom); const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const environmentId = useAtomValue(activeEnvironmentIdAtom); const environmentId = useAtomValue(activeEnvironmentIdAtom);
@@ -37,7 +37,7 @@ export function useHttpAuthenticationConfig(
return useQuery({ return useQuery({
queryKey: [ queryKey: [
'http_authentication_config', 'http_authentication_config',
request, model,
authName, authName,
values, values,
responseKey, responseKey,
@@ -53,7 +53,7 @@ export function useHttpAuthenticationConfig(
{ {
authName, authName,
values, values,
request, model,
environmentId, 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 { useQuery } from '@tanstack/react-query';
import type { GetTemplateFunctionsResponse, TemplateFunction } from '@yaakapp-internal/plugins'; import type {
import { atom, useAtomValue , useSetAtom } from 'jotai'; GetTemplateFunctionSummaryResponse,
TemplateFunction,
} from '@yaakapp-internal/plugins';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import type { TwigCompletionOption } from '../components/core/Editor/twig/completion'; import type { TwigCompletionOption } from '../components/core/Editor/twig/completion';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
@@ -55,7 +58,9 @@ export function useSubscribeTemplateFunctions() {
refetchInterval: numFns > 0 ? Infinity : 1000, refetchInterval: numFns > 0 ? Infinity : 1000,
refetchOnMount: true, refetchOnMount: true,
queryFn: async () => { queryFn: async () => {
const result = await invokeCmd<GetTemplateFunctionsResponse[]>('cmd_template_functions'); const result = await invokeCmd<GetTemplateFunctionSummaryResponse[]>(
'cmd_template_function_summaries',
);
setNumFns(result.length); setNumFns(result.length);
const functions = result.flatMap((r) => r.functions) ?? []; const functions = result.flatMap((r) => r.functions) ?? [];
setAtom(functions); setAtom(functions);

View File

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