Gregory Schier
2025-07-10 13:49:53 -07:00
parent f63da432b9
commit a31f818424
12 changed files with 66 additions and 43 deletions

View File

@@ -65,7 +65,7 @@ extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, }; export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, }; export type FilterResponse = { content: string, error?: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, }; export type FindHttpResponsesRequest = { requestId: string, limit?: number, };

View File

@@ -1,12 +1,11 @@
import { FilterResponse } from '../bindings/gen_events';
import type { Context } from './Context'; import type { Context } from './Context';
type FilterPluginResponse = { filtered: string };
export type FilterPlugin = { export type FilterPlugin = {
name: string; name: string;
description?: string; description?: string;
onFilter( onFilter(
ctx: Context, ctx: Context,
args: { payload: string; filter: string; mimeType: string }, args: { payload: string; filter: string; mimeType: string },
): Promise<FilterPluginResponse> | FilterPluginResponse; ): Promise<FilterResponse> | FilterResponse;
}; };

View File

@@ -7,7 +7,8 @@ import {
GetCookieValueRequest, GetCookieValueRequest,
GetCookieValueResponse, GetCookieValueResponse,
GetHttpRequestByIdResponse, GetHttpRequestByIdResponse,
GetKeyValueResponse, GrpcRequestAction, GetKeyValueResponse,
GrpcRequestAction,
HttpAuthenticationAction, HttpAuthenticationAction,
HttpRequestAction, HttpRequestAction,
InternalEvent, InternalEvent,
@@ -15,8 +16,8 @@ import {
ListCookieNamesResponse, ListCookieNamesResponse,
PluginWindowContext, PluginWindowContext,
PromptTextResponse, PromptTextResponse,
RenderHttpRequestResponse,
RenderGrpcRequestResponse, RenderGrpcRequestResponse,
RenderHttpRequestResponse,
SendHttpRequestResponse, SendHttpRequestResponse,
TemplateFunction, TemplateFunction,
TemplateFunctionArg, TemplateFunctionArg,
@@ -138,11 +139,7 @@ export class PluginInstance {
payload: payload.content, payload: payload.content,
mimeType: payload.type, mimeType: payload.type,
}); });
const replyPayload: InternalEventPayload = { this.#sendPayload(windowContext, { type: 'filter_response', ...reply }, replyId);
type: 'filter_response',
content: reply.filtered,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return; return;
} }

View File

@@ -1,4 +1,4 @@
import { PluginDefinition } from '@yaakapp/api'; import type { PluginDefinition } from '@yaakapp/api';
import { JSONPath } from 'jsonpath-plus'; import { JSONPath } from 'jsonpath-plus';
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
@@ -7,8 +7,12 @@ export const plugin: PluginDefinition = {
description: 'Filter JSONPath', description: 'Filter JSONPath',
onFilter(_ctx, args) { onFilter(_ctx, args) {
const parsed = JSON.parse(args.payload); const parsed = JSON.parse(args.payload);
const filtered = JSONPath({ path: args.filter, json: parsed }); try {
return { filtered: JSON.stringify(filtered, null, 2) }; const filtered = JSONPath({ path: args.filter, json: parsed });
return { content: JSON.stringify(filtered, null, 2) };
} catch (err) {
return { content: '', error: `Invalid filter: ${err}` };
}
}, },
}, },
}; };

View File

@@ -1,5 +1,5 @@
import { DOMParser } from '@xmldom/xmldom'; import { DOMParser } from '@xmldom/xmldom';
import { PluginDefinition } from '@yaakapp/api'; import type { PluginDefinition } from '@yaakapp/api';
import xpath from 'xpath'; import xpath from 'xpath';
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
@@ -8,13 +8,16 @@ export const plugin: PluginDefinition = {
description: 'Filter XPath', description: 'Filter XPath',
onFilter(_ctx, args) { onFilter(_ctx, args) {
const doc = new DOMParser().parseFromString(args.payload, 'text/xml'); const doc = new DOMParser().parseFromString(args.payload, 'text/xml');
const result = xpath.select(args.filter, doc, false); try {
const result = xpath.select(args.filter, doc, false);
if (Array.isArray(result)) { if (Array.isArray(result)) {
return { filtered: result.map(r => String(r)).join('\n') }; return { content: result.map((r) => String(r)).join('\n') };
} else { } else {
// Not sure what cases this happens in (?) // Not sure what cases this happens in (?)
return { filtered: String(result) }; return { content: String(result) };
}
} catch (err) {
return { content: '', error: `Invalid filter: ${err}` };
} }
}, },
}, },

View File

@@ -36,12 +36,11 @@ 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, CallHttpAuthenticationActionArgs, CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs,
CallHttpRequestActionArgs, CallHttpRequestActionRequest, FilterResponse, CallHttpRequestActionRequest, FilterResponse, GetGrpcRequestActionsResponse,
GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent,
GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload, JsonPrimitive, InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose,
PluginWindowContext, RenderPurpose,
}; };
use yaak_plugins::manager::PluginManager; use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_meta::PluginMetadata; use yaak_plugins::plugin_meta::PluginMetadata;

View File

@@ -1,6 +1,6 @@
use crate::http_request::send_http_request; use crate::http_request::send_http_request;
use crate::render::{render_grpc_request, render_http_request, render_json_value}; use crate::render::{render_grpc_request, render_http_request, render_json_value};
use crate::window::{create_window, CreateWindowConfig}; use crate::window::{CreateWindowConfig, create_window};
use crate::{ use crate::{
call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_window_context, call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_window_context,
workspace_from_window, workspace_from_window,
@@ -13,7 +13,13 @@ use tauri_plugin_clipboard_manager::ClipboardExt;
use yaak_models::models::{HttpResponse, Plugin}; use yaak_models::models::{HttpResponse, Plugin};
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
use yaak_plugins::events::{Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse, GetCookieValueResponse, GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, InternalEventPayload, ListCookieNamesResponse, PluginWindowContext, RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse, WindowNavigateEvent}; use yaak_plugins::events::{
Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse, GetCookieValueResponse,
GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, InternalEventPayload,
ListCookieNamesResponse, PluginWindowContext, RenderGrpcRequestResponse,
RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest,
TemplateRenderResponse, WindowNavigateEvent,
};
use yaak_plugins::manager::PluginManager; use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle; use yaak_plugins::plugin_handle::PluginHandle;
use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_plugins::template_callback::PluginTemplateCallback;
@@ -80,8 +86,8 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
environment.as_ref(), environment.as_ref(),
&cb, &cb,
) )
.await .await
.expect("Failed to render grpc request"); .expect("Failed to render grpc request");
Some(InternalEventPayload::RenderGrpcRequestResponse(RenderGrpcRequestResponse { Some(InternalEventPayload::RenderGrpcRequestResponse(RenderGrpcRequestResponse {
grpc_request, grpc_request,
})) }))
@@ -104,8 +110,8 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
environment.as_ref(), environment.as_ref(),
&cb, &cb,
) )
.await .await
.expect("Failed to render http request"); .expect("Failed to render http request");
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse { Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request, http_request,
})) }))
@@ -206,7 +212,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
cookie_jar, cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel &mut tokio::sync::watch::channel(false).1, // No-op cancel channel
) )
.await; .await;
let http_response = match result { let http_response = match result {
Ok(r) => r, Ok(r) => r,

View File

@@ -65,7 +65,7 @@ extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, }; export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, }; export type FilterResponse = { content: string, error?: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, }; export type FindHttpResponsesRequest = { requestId: string, limit?: number, };

View File

@@ -216,6 +216,8 @@ pub struct FilterRequest {
#[ts(export, export_to = "gen_events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FilterResponse { pub struct FilterResponse {
pub content: String, pub content: String,
#[ts(optional)]
pub error: Option<String>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]

View File

@@ -3,6 +3,7 @@ import classNames from 'classnames';
import type { CSSProperties, ReactNode } from 'react'; import type { CSSProperties, ReactNode } from 'react';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { useLocalStorage } from 'react-use'; import { useLocalStorage } from 'react-use';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse'; import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useResponseViewMode } from '../hooks/useResponseViewMode'; import { useResponseViewMode } from '../hooks/useResponseViewMode';
import { getMimeTypeFromContentType } from '../lib/contentType'; import { getMimeTypeFromContentType } from '../lib/contentType';
@@ -14,7 +15,7 @@ import { HttpResponseDurationTag } from './core/HttpResponseDurationTag';
import { HotKeyList } from './core/HotKeyList'; import { HotKeyList } from './core/HotKeyList';
import { LoadingIcon } from './core/LoadingIcon'; import { LoadingIcon } from './core/LoadingIcon';
import { SizeTag } from './core/SizeTag'; import { SizeTag } from './core/SizeTag';
import { HStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
import { HttpStatusTag } from './core/HttpStatusTag'; import { HttpStatusTag } from './core/HttpStatusTag';
import type { TabItem } from './core/Tabs/Tabs'; import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs';
@@ -31,6 +32,7 @@ import { PdfViewer } from './responseViewers/PdfViewer';
import { SvgViewer } from './responseViewers/SvgViewer'; import { SvgViewer } from './responseViewers/SvgViewer';
import { VideoViewer } from './responseViewers/VideoViewer'; import { VideoViewer } from './responseViewers/VideoViewer';
import { ErrorBoundary } from './ErrorBoundary'; import { ErrorBoundary } from './ErrorBoundary';
import { Button } from './core/Button';
interface Props { interface Props {
style?: CSSProperties; style?: CSSProperties;
@@ -90,6 +92,8 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
[activeRequestId, setActiveTabs], [activeRequestId, setActiveTabs],
); );
const cancel = useCancelHttpResponse(activeResponse?.id ?? null);
return ( return (
<div <div
style={style} style={style}
@@ -160,7 +164,13 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
<ConfirmLargeResponse response={activeResponse}> <ConfirmLargeResponse response={activeResponse}>
{activeResponse.state === 'initialized' ? ( {activeResponse.state === 'initialized' ? (
<EmptyStateText> <EmptyStateText>
<LoadingIcon size="xl" className="text-text-subtlest" /> <VStack space={3}>
<HStack space={3} className="text-lg">
<LoadingIcon size="lg" className="text-text-subtlest" />
Sending Request
</HStack>
<Button variant="border" onClick={() => cancel.mutate()}>Cancel</Button>
</VStack>
</EmptyStateText> </EmptyStateText>
) : activeResponse.state === 'closed' && activeResponse.contentLength === 0 ? ( ) : activeResponse.state === 'closed' && activeResponse.contentLength === 0 ? (
<EmptyStateText>Empty </EmptyStateText> <EmptyStateText>Empty </EmptyStateText>

View File

@@ -27,7 +27,7 @@ const useFilterText = createGlobalState<Record<string, string | null>>({});
export function TextViewer({ language, text, responseId, requestId, pretty, className }: Props) { export function TextViewer({ language, text, responseId, requestId, pretty, className }: Props) {
const [filterTextMap, setFilterTextMap] = useFilterText(); const [filterTextMap, setFilterTextMap] = useFilterText();
const filterText = filterTextMap[requestId] ?? null; const filterText = filterTextMap[requestId] ?? null;
const debouncedFilterText = useDebouncedValue(filterText, 200); const debouncedFilterText = useDebouncedValue(filterText);
const setFilterText = useCallback( const setFilterText = useCallback(
(v: string | null) => { (v: string | null) => {
setFilterTextMap((m) => ({ ...m, [requestId]: v })); setFilterTextMap((m) => ({ ...m, [requestId]: v }));
@@ -58,7 +58,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
<div key="input" className="w-full !opacity-100"> <div key="input" className="w-full !opacity-100">
<Input <Input
key={requestId} key={requestId}
validate={!filteredResponse.error} validate={!(filteredResponse.error || filteredResponse.data?.error)}
hideLabel hideLabel
autoFocus autoFocus
containerClassName="bg-surface" containerClassName="bg-surface"
@@ -79,6 +79,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
<IconButton <IconButton
key="icon" key="icon"
size="sm" size="sm"
isLoading={filteredResponse.isPending}
icon={isSearching ? 'x' : 'filter'} icon={isSearching ? 'x' : 'filter'}
title={isSearching ? 'Close filter' : 'Filter response'} title={isSearching ? 'Close filter' : 'Filter response'}
onClick={toggleSearch} onClick={toggleSearch}
@@ -90,7 +91,9 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
}, [ }, [
canFilter, canFilter,
filterText, filterText,
filteredResponse.data?.error,
filteredResponse.error, filteredResponse.error,
filteredResponse.isPending,
isSearching, isSearching,
language, language,
requestId, requestId,
@@ -109,7 +112,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
if (filteredResponse.error) { if (filteredResponse.error) {
body = ''; body = '';
} else { } else {
body = filteredResponse.data != null ? filteredResponse.data : ''; body = filteredResponse.data?.content != null ? filteredResponse.data.content : '';
} }
} else { } else {
body = formattedBody; body = formattedBody;

View File

@@ -9,7 +9,7 @@ export function useFilterResponse({
responseId: string | null; responseId: string | null;
filter: string; filter: string;
}) { }) {
return useQuery<string | null, string>({ return useQuery({
queryKey: ['filter_response', responseId, filter], queryKey: ['filter_response', responseId, filter],
queryFn: async () => { queryFn: async () => {
if (filter === '') { if (filter === '') {
@@ -21,7 +21,7 @@ export function useFilterResponse({
filter, filter,
})) as FilterResponse; })) as FilterResponse;
return result.content; return result;
}, },
}); });
} }