mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-23 01:08:28 +02:00
JSONPath filter plugins working
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import jp from 'jsonpath';
|
import jp from 'jsonpath';
|
||||||
|
|
||||||
export function pluginHookResponseFilter({ text, filter }) {
|
export function pluginHookResponseFilter(filter, text) {
|
||||||
let parsed;
|
let parsed;
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(text);
|
parsed = JSON.parse(text);
|
||||||
@@ -8,6 +8,5 @@ export function pluginHookResponseFilter({ text, filter }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const filtered = jp.query(parsed, filter);
|
const filtered = jp.query(parsed, filter);
|
||||||
|
return { filtered: JSON.stringify(filtered, null, 2) };
|
||||||
return { filtered };
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ extern crate objc;
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env::current_dir;
|
use std::env::current_dir;
|
||||||
use std::fs::{create_dir_all, File};
|
use std::fs::{create_dir_all, File, read_to_string};
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
|
||||||
use fern::colors::ColoredLevelConfig;
|
use fern::colors::ColoredLevelConfig;
|
||||||
@@ -17,13 +17,13 @@ use log::{debug, info, warn};
|
|||||||
use rand::random;
|
use rand::random;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||||
use sqlx::migrate::Migrator;
|
use sqlx::migrate::Migrator;
|
||||||
use sqlx::types::Json;
|
use sqlx::types::Json;
|
||||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use tauri::TitleBarStyle;
|
|
||||||
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
|
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
|
||||||
use tauri::{Manager, WindowEvent};
|
use tauri::{Manager, WindowEvent};
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use tauri::TitleBarStyle;
|
||||||
use tauri_plugin_log::{fern, LogTarget};
|
use tauri_plugin_log::{fern, LogTarget};
|
||||||
use tauri_plugin_window_state::{StateFlags, WindowExt};
|
use tauri_plugin_window_state::{StateFlags, WindowExt};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
@@ -32,8 +32,8 @@ use window_shadows::set_shadow;
|
|||||||
use window_ext::TrafficLightWindowExt;
|
use window_ext::TrafficLightWindowExt;
|
||||||
|
|
||||||
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
||||||
use crate::plugin::{ImportResources, ImportResult};
|
|
||||||
use crate::http::send_http_request;
|
use crate::http::send_http_request;
|
||||||
|
use crate::plugin::{ImportResources, ImportResult};
|
||||||
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
||||||
|
|
||||||
mod analytics;
|
mod analytics;
|
||||||
@@ -87,6 +87,34 @@ async fn send_ephemeral_request(
|
|||||||
send_http_request(request, &response, &environment_id2, &app_handle, pool).await
|
send_http_request(request, &response, &environment_id2, &app_handle, pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn filter_response(
|
||||||
|
window: Window<Wry>,
|
||||||
|
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||||
|
response_id: &str,
|
||||||
|
filter: &str,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let pool = &*db_instance.lock().await;
|
||||||
|
let response = models::get_response(response_id, pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get response");
|
||||||
|
|
||||||
|
if let None = response.body_path {
|
||||||
|
return Err("Response body not found".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = read_to_string(response.body_path.unwrap()).unwrap();
|
||||||
|
let filter_result = plugin::run_plugin_filter(
|
||||||
|
&window.app_handle(),
|
||||||
|
"filter-jsonpath",
|
||||||
|
filter,
|
||||||
|
&body,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to run filter");
|
||||||
|
Ok(filter_result.filtered)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn import_data(
|
async fn import_data(
|
||||||
window: Window<Wry>,
|
window: Window<Wry>,
|
||||||
@@ -752,6 +780,7 @@ fn main() {
|
|||||||
delete_workspace,
|
delete_workspace,
|
||||||
duplicate_request,
|
duplicate_request,
|
||||||
export_data,
|
export_data,
|
||||||
|
filter_response,
|
||||||
get_key_value,
|
get_key_value,
|
||||||
get_environment,
|
get_environment,
|
||||||
get_folder,
|
get_folder,
|
||||||
|
|||||||
@@ -8,13 +8,18 @@ use boa_engine::{
|
|||||||
Context, JsArgs, JsNativeError, JsValue, Module, NativeFunction, Source,
|
Context, JsArgs, JsNativeError, JsValue, Module, NativeFunction, Source,
|
||||||
};
|
};
|
||||||
use boa_runtime::Console;
|
use boa_runtime::Console;
|
||||||
use log::debug;
|
use log::{debug, error};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
|
||||||
use crate::models::{Environment, Folder, HttpRequest, Workspace};
|
use crate::models::{Environment, Folder, HttpRequest, Workspace};
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct FilterResult {
|
||||||
|
pub filtered: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||||
pub struct ImportResult {
|
pub struct ImportResult {
|
||||||
pub resources: ImportResources,
|
pub resources: ImportResources,
|
||||||
@@ -28,6 +33,32 @@ pub struct ImportResources {
|
|||||||
pub requests: Vec<HttpRequest>,
|
pub requests: Vec<HttpRequest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn run_plugin_filter(
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
plugin_name: &str,
|
||||||
|
response_body: &str,
|
||||||
|
filter: &str,
|
||||||
|
) -> Option<FilterResult> {
|
||||||
|
let result_json = run_plugin(
|
||||||
|
app_handle,
|
||||||
|
plugin_name,
|
||||||
|
"pluginHookResponseFilter",
|
||||||
|
&[
|
||||||
|
js_string!(response_body).into(),
|
||||||
|
js_string!(filter).into(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if result_json.is_null() {
|
||||||
|
error!("Plugin {} failed to run", plugin_name);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resources: FilterResult =
|
||||||
|
serde_json::from_value(result_json).expect("failed to parse filter plugin result json");
|
||||||
|
Some(resources)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run_plugin_import(
|
pub async fn run_plugin_import(
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
plugin_name: &str,
|
plugin_name: &str,
|
||||||
|
|||||||
@@ -215,6 +215,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-editor .cm-panels {
|
||||||
|
@apply bg-transparent border-0 text-gray-800 z-50;
|
||||||
|
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
@apply rounded-sm outline-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@apply appearance-none bg-none bg-gray-800 text-gray-100 focus:bg-gray-900 cursor-default;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
@apply bg-gray-50 border border-highlight focus:border-focus outline-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the "All" button */
|
||||||
|
button[name='select'] {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Add default icon. Needs low priority so it can be overwritten */
|
/* Add default icon. Needs low priority so it can be overwritten */
|
||||||
.cm-completionIcon::after {
|
.cm-completionIcon::after {
|
||||||
content: '𝑥';
|
content: '𝑥';
|
||||||
|
|||||||
@@ -219,28 +219,35 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
|||||||
return (
|
return (
|
||||||
<div className="group relative h-full w-full">
|
<div className="group relative h-full w-full">
|
||||||
{cmContainer}
|
{cmContainer}
|
||||||
{format && (
|
{(format || actions) && (
|
||||||
<HStack space={0.5} alignItems="center" className="absolute bottom-2 right-0 ">
|
<HStack
|
||||||
|
space={1}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="end"
|
||||||
|
className="absolute bottom-2 left-0 right-0"
|
||||||
|
>
|
||||||
|
{format && (
|
||||||
|
<IconButton
|
||||||
|
showConfirm
|
||||||
|
size="sm"
|
||||||
|
title="Reformat contents"
|
||||||
|
icon="magicWand"
|
||||||
|
className="transition-all opacity-0 group-hover:opacity-100"
|
||||||
|
onClick={() => {
|
||||||
|
if (cm.current === null) return;
|
||||||
|
const { doc } = cm.current.view.state;
|
||||||
|
const formatted = format(doc.toString());
|
||||||
|
// Update editor and blur because the cursor will reset anyway
|
||||||
|
cm.current.view.dispatch({
|
||||||
|
changes: { from: 0, to: doc.length, insert: formatted },
|
||||||
|
});
|
||||||
|
cm.current.view.contentDOM.blur();
|
||||||
|
// Fire change event
|
||||||
|
onChange?.(formatted);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{actions}
|
{actions}
|
||||||
<IconButton
|
|
||||||
showConfirm
|
|
||||||
size="sm"
|
|
||||||
title="Reformat contents"
|
|
||||||
icon="magicWand"
|
|
||||||
className="transition-opacity opacity-0 group-hover:opacity-70"
|
|
||||||
onClick={() => {
|
|
||||||
if (cm.current === null) return;
|
|
||||||
const { doc } = cm.current.view.state;
|
|
||||||
const formatted = format(doc.toString());
|
|
||||||
// Update editor and blur because the cursor will reset anyway
|
|
||||||
cm.current.view.dispatch({
|
|
||||||
changes: { from: 0, to: doc.length, insert: formatted },
|
|
||||||
});
|
|
||||||
cm.current.view.contentDOM.blur();
|
|
||||||
// Fire change event
|
|
||||||
onChange?.(formatted);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useDebouncedSetState } from '../../hooks/useDebouncedSetState';
|
||||||
|
import { useFilterResponse } from '../../hooks/useFilterResponse';
|
||||||
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
|
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
|
||||||
import { useResponseContentType } from '../../hooks/useResponseContentType';
|
import { useResponseContentType } from '../../hooks/useResponseContentType';
|
||||||
|
import { useToggle } from '../../hooks/useToggle';
|
||||||
import { tryFormatJson } from '../../lib/formatters';
|
import { tryFormatJson } from '../../lib/formatters';
|
||||||
import type { HttpResponse } from '../../lib/models';
|
import type { HttpResponse } from '../../lib/models';
|
||||||
import { Editor } from '../core/Editor';
|
import { Editor } from '../core/Editor';
|
||||||
|
import { IconButton } from '../core/IconButton';
|
||||||
|
import { Input } from '../core/Input';
|
||||||
|
import { HStack } from '../core/Stacks';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
response: HttpResponse;
|
response: HttpResponse;
|
||||||
@@ -10,17 +17,48 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TextViewer({ response, pretty }: Props) {
|
export function TextViewer({ response, pretty }: Props) {
|
||||||
|
const [isSearching, toggleIsSearching] = useToggle();
|
||||||
|
const [filterText, setFilterText] = useDebouncedSetState<string>('', 500);
|
||||||
|
|
||||||
const contentType = useResponseContentType(response);
|
const contentType = useResponseContentType(response);
|
||||||
const rawBody = useResponseBodyText(response) ?? '';
|
const rawBody = useResponseBodyText(response) ?? '';
|
||||||
const body = pretty && contentType?.includes('json') ? tryFormatJson(rawBody) : rawBody;
|
const formattedBody = pretty && contentType?.includes('json') ? tryFormatJson(rawBody) : rawBody;
|
||||||
|
const filteredResponse = useFilterResponse({ filter: filterText, responseId: response.id });
|
||||||
|
|
||||||
|
const body = filteredResponse ?? formattedBody;
|
||||||
|
|
||||||
|
const actions = contentType?.startsWith('application/json') && (
|
||||||
|
<HStack className="w-full" justifyContent="end" space={1}>
|
||||||
|
{isSearching && (
|
||||||
|
<Input
|
||||||
|
hideLabel
|
||||||
|
autoFocus
|
||||||
|
containerClassName="bg-gray-50"
|
||||||
|
size="sm"
|
||||||
|
placeholder="Filter response"
|
||||||
|
label="Filter with JSONPath"
|
||||||
|
name="filter"
|
||||||
|
defaultValue={filterText}
|
||||||
|
onChange={setFilterText}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
icon={isSearching ? 'x' : 'magnifyingGlass'}
|
||||||
|
title="Filter response"
|
||||||
|
onClick={toggleIsSearching}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Editor
|
<Editor
|
||||||
readOnly
|
readOnly
|
||||||
forceUpdateKey={body}
|
|
||||||
className="bg-gray-50 dark:!bg-gray-100"
|
className="bg-gray-50 dark:!bg-gray-100"
|
||||||
|
forceUpdateKey={body}
|
||||||
defaultValue={body}
|
defaultValue={body}
|
||||||
contentType={contentType}
|
contentType={contentType}
|
||||||
|
actions={actions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
23
src-web/hooks/useFilterResponse.ts
Normal file
23
src-web/hooks/useFilterResponse.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
|
||||||
|
export function useFilterResponse({
|
||||||
|
responseId,
|
||||||
|
filter,
|
||||||
|
}: {
|
||||||
|
responseId: string | null;
|
||||||
|
filter: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
useQuery<string | null>({
|
||||||
|
queryKey: [responseId, filter],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (filter === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await invoke('filter_response', { responseId, filter })) as string | null;
|
||||||
|
},
|
||||||
|
}).data ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src-web/hooks/useToggle.ts
Normal file
7
src-web/hooks/useToggle.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
export function useToggle(initialValue = false) {
|
||||||
|
const [value, setValue] = useState<boolean>(initialValue);
|
||||||
|
const toggle = useCallback(() => setValue((v) => !v), []);
|
||||||
|
return [value, toggle] as const;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user