Request actions (#65)

This commit is contained in:
Gregory Schier
2024-08-14 15:31:52 -07:00
committed by GitHub
parent 12f4c2c668
commit b95fa25898
29 changed files with 392 additions and 116 deletions

View File

@@ -14,3 +14,8 @@ cargo sqlx migrate add ${MIGRATION_NAME}
cargo sqlx migrate run --database-url 'sqlite://db.sqlite?mode=rw'
cargo sqlx prepare --database-url 'sqlite://db.sqlite'
```
## Add App->Plugin API
- Add event in `events.rs`
- Add handler to `index.worker.ts`

8
package-lock.json generated
View File

@@ -27,7 +27,7 @@
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@yaakapp/api": "^0.1.4",
"@yaakapp/api": "^0.1.6",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",
@@ -2989,9 +2989,9 @@
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="
},
"node_modules/@yaakapp/api": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.4.tgz",
"integrity": "sha512-dI5b2WPjTWXkaYBE/ltfxrJDIjIf/ETjMOzrfWDDcgT2GSBNlYmywZRTsk7j5cEbSQbSrDNgXYGJwG0I89aqSg==",
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.6.tgz",
"integrity": "sha512-5lYXKcOVmLzVUrkfU4JOCbz+CBV5Dm/cALoZvfbelvZWOVu3sTrBxS9cbNVQQq2B6WwLInSevk7pMq58GqIj5Q==",
"dependencies": {
"@types/node": "^22.0.0"
}

View File

@@ -42,7 +42,7 @@
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
"@yaakapp/api": "^0.1.4",
"@yaakapp/api": "^0.1.6",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp/api",
"version": "0.1.4",
"version": "0.1.6",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HttpRequest } from "./HttpRequest";
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CallHttpRequestActionArgs } from "./CallHttpRequestActionArgs";
export type CallHttpRequestActionRequest = { key: string, pluginRefId: string, args: CallHttpRequestActionArgs, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CopyTextRequest = { text: string, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HttpRequestAction } from "./HttpRequestAction";
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HttpRequestAction = { key: string, label: string, icon: string | null, };

View File

@@ -1,16 +1,22 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BootRequest } from "./BootRequest";
import type { BootResponse } from "./BootResponse";
import type { CallHttpRequestActionRequest } from "./CallHttpRequestActionRequest";
import type { CopyTextRequest } from "./CopyTextRequest";
import type { EmptyResponse } from "./EmptyResponse";
import type { ExportHttpRequestRequest } from "./ExportHttpRequestRequest";
import type { ExportHttpRequestResponse } from "./ExportHttpRequestResponse";
import type { FilterRequest } from "./FilterRequest";
import type { FilterResponse } from "./FilterResponse";
import type { GetHttpRequestActionsResponse } from "./GetHttpRequestActionsResponse";
import type { GetHttpRequestByIdRequest } from "./GetHttpRequestByIdRequest";
import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse";
import type { ImportRequest } from "./ImportRequest";
import type { ImportResponse } from "./ImportResponse";
import type { RenderHttpRequestRequest } from "./RenderHttpRequestRequest";
import type { RenderHttpRequestResponse } from "./RenderHttpRequestResponse";
import type { SendHttpRequestRequest } from "./SendHttpRequestRequest";
import type { SendHttpRequestResponse } from "./SendHttpRequestResponse";
import type { ShowToastRequest } from "./ShowToastRequest";
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "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": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "empty_response" } & EmptyResponse;
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "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": "get_http_request_actions_request" } | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "empty_response" } & EmptyResponse;

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HttpRequest } from "./HttpRequest";
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HttpRequest } from "./HttpRequest";
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RenderRequest = { template: string, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RenderResponse = { rendered: string, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ToastVariant } from "./ToastVariant";
export type ShowToastRequest = { message: string, variant: ToastVariant, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ToastVariant = "custom" | "copied" | "success" | "info" | "warning" | "error";

View File

@@ -1,8 +1,11 @@
export type * from './plugins';
export type * from './themes';
// TODO: The next ts-rs release includes the ability to put everything in 1 file!
export * from './gen/BootRequest';
export * from './gen/BootResponse';
export * from './gen/CallHttpRequestActionRequest';
export * from './gen/CallHttpRequestActionArgs';
export * from './gen/Cookie';
export * from './gen/CookieDomain';
export * from './gen/CookieExpires';
@@ -15,11 +18,16 @@ export * from './gen/ExportHttpRequestResponse';
export * from './gen/FilterRequest';
export * from './gen/FilterResponse';
export * from './gen/Folder';
export * from './gen/GetHttpRequestActionsResponse';
export * from './gen/GetHttpRequestByIdRequest';
export * from './gen/CopyTextRequest';
export * from './gen/GetHttpRequestByIdResponse';
export * from './gen/GrpcConnection';
export * from './gen/GrpcEvent';
export * from './gen/GrpcMetadataEntry';
export * from './gen/GrpcRequest';
export * from './gen/HttpRequest';
export * from './gen/HttpRequestAction';
export * from './gen/HttpRequestHeader';
export * from './gen/HttpResponse';
export * from './gen/HttpResponseHeader';
@@ -32,9 +40,11 @@ export * from './gen/InternalEventPayload';
export * from './gen/KeyValue';
export * from './gen/Model';
export * from './gen/SendHttpRequestRequest';
export * from './gen/ToastVariant';
export * from './gen/ShowToastRequest';
export * from './gen/RenderHttpRequestRequest';
export * from './gen/RenderHttpRequestResponse';
export * from './gen/SendHttpRequestResponse';
export * from './gen/GetHttpRequestByIdRequest';
export * from './gen/GetHttpRequestByIdResponse';
export * from './gen/SendHttpRequestResponse';
export * from './gen/Settings';
export * from './gen/Workspace';

View File

@@ -1,11 +1,21 @@
import { GetHttpRequestByIdRequest } from '../gen/GetHttpRequestByIdRequest';
import { GetHttpRequestByIdResponse } from '../gen/GetHttpRequestByIdResponse';
import { RenderHttpRequestRequest } from '../gen/RenderHttpRequestRequest';
import { RenderHttpRequestResponse } from '../gen/RenderHttpRequestResponse';
import { SendHttpRequestRequest } from '../gen/SendHttpRequestRequest';
import { SendHttpRequestResponse } from '../gen/SendHttpRequestResponse';
import { ShowToastRequest } from '../gen/ShowToastRequest';
export type YaakContext = {
clipboard: {
copyText(text: string): void;
};
toast: {
show(args: ShowToastRequest): void;
};
httpRequest: {
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;
render(args: RenderHttpRequestRequest): Promise<RenderHttpRequestResponse['httpRequest']>;
};
};

View File

@@ -1,8 +1,7 @@
import { HttpRequest } from '../gen/HttpRequest';
import { CallHttpRequestActionArgs } from '../gen/CallHttpRequestActionArgs';
import { HttpRequestAction } from '../gen/HttpRequestAction';
import { YaakContext } from './context';
export type HttpRequestActionPlugin = {
key: string;
label: string;
onSelect(ctx: YaakContext, args: { httpRequest: HttpRequest }): void;
export type HttpRequestActionPlugin = HttpRequestAction & {
onSelect(ctx: YaakContext, args: CallHttpRequestActionArgs): Promise<void> | void;
};

View File

@@ -1,16 +1,16 @@
import { OneOrMany } from '../helpers';
import { FilterPlugin } from './filter';
import { HttpRequestActionPlugin } from './httpRequestAction';
import { ImporterPlugin } from './import';
import { ThemePlugin } from './theme';
export { YaakContext } from './context';
/**
* The global structure of a Yaak plugin
*/
export type YaakPlugin = {
importer?: OneOrMany<ImporterPlugin>;
theme?: OneOrMany<ThemePlugin>;
filter?: OneOrMany<FilterPlugin>;
httpRequestAction?: OneOrMany<HttpRequestActionPlugin>;
importer?: ImporterPlugin;
theme?: ThemePlugin;
filter?: FilterPlugin;
httpRequestActions?: HttpRequestActionPlugin[];
};

View File

@@ -1,11 +1,14 @@
import {
GetHttpRequestByIdResponse,
HttpRequestAction,
ImportResponse,
InternalEvent,
InternalEventPayload,
RenderHttpRequestResponse,
SendHttpRequestResponse,
} from '@yaakapp/api';
import { YaakContext } from '@yaakapp/api/lib/plugins/context';
import { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/httpRequestAction';
import interceptStdout from 'intercept-stdout';
import * as console from 'node:console';
import { readFileSync } from 'node:fs';
@@ -47,6 +50,10 @@ new Promise<void>(async (resolve, reject) => {
return { pluginRefId, id: genId(), replyId, payload };
}
function sendEmpty(replyId: string | null = null): string {
return sendPayload({ type: 'empty_response' }, replyId);
}
function sendPayload(payload: InternalEventPayload, replyId: string | null = null): string {
const event = buildEventToSend(payload, replyId);
sendEvent(event);
@@ -82,6 +89,16 @@ new Promise<void>(async (resolve, reject) => {
}
const ctx: YaakContext = {
clipboard: {
async copyText(text) {
await sendAndWaitForReply({ type: 'copy_text_request', text });
},
},
toast: {
async show(args) {
await sendAndWaitForReply({ type: 'show_toast_request', ...args });
},
},
httpRequest: {
async getById({ id }) {
const payload = { type: 'get_http_request_by_id_request', id } as const;
@@ -93,6 +110,11 @@ new Promise<void>(async (resolve, reject) => {
const { httpResponse } = await sendAndWaitForReply<SendHttpRequestResponse>(payload);
return httpResponse;
},
async render({ httpRequest }) {
const payload = { type: 'render_http_request_request', httpRequest } as const;
const result = await sendAndWaitForReply<RenderHttpRequestResponse>(payload);
return result.httpRequest;
},
},
};
@@ -151,13 +173,45 @@ new Promise<void>(async (resolve, reject) => {
sendPayload(replyPayload, replyId);
return;
}
if (
payload.type === 'get_http_request_actions_request' &&
Array.isArray(mod.plugin?.httpRequestActions)
) {
const reply: HttpRequestAction[] = mod.plugin.httpRequestActions.map(
(a: HttpRequestActionPlugin) => ({
...a,
onSelect: undefined,
// Add everything except onSelect
}),
);
const replyPayload: InternalEventPayload = {
type: 'get_http_request_actions_response',
pluginRefId,
actions: reply,
};
sendPayload(replyPayload, replyId);
return;
}
if (
payload.type === 'call_http_request_action_request' &&
Array.isArray(mod.plugin?.httpRequestActions)
) {
const action = mod.plugin.httpRequestActions.find((a) => a.key === payload.key);
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
sendEmpty(replyId);
return;
}
}
} catch (err) {
console.log('Plugin call threw exception', payload.type, err);
// TODO: Return errors to server
}
// No matches, so send back an empty response so the caller doesn't block forever
sendPayload({ type: 'empty_response' }, replyId);
sendEmpty(replyId);
});
resolve();

View File

@@ -21,6 +21,7 @@ use tauri::TitleBarStyle;
use tauri::{AppHandle, Emitter, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{Listener, Runtime};
use tauri::{Manager, WindowEvent};
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_log::{fern, Target, TargetKind};
use tauri_plugin_shell::ShellExt;
use tokio::sync::{watch, Mutex};
@@ -55,7 +56,8 @@ use yaak_models::queries::{
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace,
};
use yaak_plugin_runtime::events::{
FilterResponse, GetHttpRequestByIdResponse, InternalEvent, InternalEventPayload,
CallHttpRequestActionRequest, FilterResponse, GetHttpRequestActionsResponse,
GetHttpRequestByIdResponse, InternalEvent, InternalEventPayload, RenderHttpRequestResponse,
SendHttpRequestResponse,
};
@@ -870,29 +872,24 @@ async fn cmd_import_data(
}
#[tauri::command]
async fn cmd_request_to_curl(
app: AppHandle,
request_id: &str,
async fn cmd_http_request_actions(
plugin_manager: State<'_, PluginManager>,
environment_id: Option<&str>,
) -> Result<String, String> {
let request = get_http_request(&app, request_id)
) -> Result<Vec<GetHttpRequestActionsResponse>, String> {
plugin_manager
.run_http_request_actions()
.await
.map_err(|e| e.to_string())?;
let environment = match environment_id {
Some(id) => Some(get_environment(&app, id).await.map_err(|e| e.to_string())?),
None => None,
};
let workspace = get_workspace(&app, &request.workspace_id)
.await
.map_err(|e| e.to_string())?;
let rendered = render_request(&request, &workspace, environment.as_ref());
.map_err(|e| e.to_string())
}
let import_response = plugin_manager
.run_export_curl(&rendered)
#[tauri::command]
async fn cmd_call_http_request_action(
req: CallHttpRequestActionRequest,
plugin_manager: State<'_, PluginManager>,
) -> Result<(), String> {
plugin_manager
.call_http_request_action(req)
.await
.map_err(|e| e.to_string())?;
Ok(import_response.content)
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1624,6 +1621,7 @@ pub fn run() {
Ok(())
})
.invoke_handler(tauri::generate_handler![
cmd_call_http_request_action,
cmd_check_for_updates,
cmd_create_cookie_jar,
cmd_create_environment,
@@ -1642,6 +1640,7 @@ pub fn run() {
cmd_delete_http_request,
cmd_delete_http_response,
cmd_delete_workspace,
cmd_dismiss_notification,
cmd_duplicate_grpc_request,
cmd_duplicate_http_request,
cmd_export_data,
@@ -1656,6 +1655,7 @@ pub fn run() {
cmd_get_workspace,
cmd_grpc_go,
cmd_grpc_reflect,
cmd_http_request_actions,
cmd_import_data,
cmd_list_cookie_jars,
cmd_list_environments,
@@ -1669,8 +1669,6 @@ pub fn run() {
cmd_metadata,
cmd_new_nested_window,
cmd_new_window,
cmd_request_to_curl,
cmd_dismiss_notification,
cmd_save_response,
cmd_send_ephemeral_request,
cmd_send_http_request,
@@ -1915,9 +1913,49 @@ async fn handle_plugin_event<R: Runtime>(
let event = match event.clone().payload {
InternalEventPayload::GetHttpRequestByIdRequest(req) => {
let http_request = get_http_request(app_handle, req.id.as_str()).await.ok();
InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse {
http_request,
})
Some(InternalEventPayload::GetHttpRequestByIdResponse(
GetHttpRequestByIdResponse { http_request },
))
}
InternalEventPayload::CopyTextRequest(req) => {
app_handle
.clipboard()
.write_text(req.text.as_str())
.expect("Failed to write text to clipboard");
None
}
InternalEventPayload::ShowToastRequest(req) => {
app_handle
.emit("show_toast", req)
.expect("Failed to emit show_toast");
None
}
InternalEventPayload::RenderHttpRequestRequest(req) => {
let webview_windows = app_handle.get_focused_window()?.webview_windows();
let w = match webview_windows.iter().next() {
None => return None,
Some((_, w)) => w,
};
let workspace = get_workspace(app_handle, req.http_request.workspace_id.as_str())
.await
.expect("Failed to get workspace for request");
let url = w.url().unwrap();
let mut query_pairs = url.query_pairs();
let environment_id = query_pairs
.find(|(k, _v)| k == "environment_id")
.map(|(_k, v)| v.to_string());
let environment = match environment_id {
None => None,
Some(id) => get_environment(w, id.as_str()).await.ok(),
};
let rendered_http_request =
render_request(&req.http_request, &workspace, environment.as_ref());
Some(InternalEventPayload::RenderHttpRequestResponse(
RenderHttpRequestResponse {
http_request: rendered_http_request,
},
))
}
InternalEventPayload::SendHttpRequestRequest(req) => {
let webview_windows = app_handle.get_focused_window()?.webview_windows();
@@ -1964,10 +2002,12 @@ async fn handle_plugin_event<R: Runtime>(
Err(_e) => return None,
};
InternalEventPayload::SendHttpRequestResponse(SendHttpRequestResponse { http_response })
Some(InternalEventPayload::SendHttpRequestResponse(
SendHttpRequestResponse { http_response },
))
}
_ => return None,
_ => None,
};
Some(event)
event
}

View File

@@ -32,13 +32,24 @@ pub enum InternalEventPayload {
ExportHttpRequestRequest(ExportHttpRequestRequest),
ExportHttpRequestResponse(ExportHttpRequestResponse),
SendHttpRequestRequest(SendHttpRequestRequest),
SendHttpRequestResponse(SendHttpRequestResponse),
GetHttpRequestActionsRequest,
GetHttpRequestActionsResponse(GetHttpRequestActionsResponse),
CallHttpRequestActionRequest(CallHttpRequestActionRequest),
CopyTextRequest(CopyTextRequest),
RenderHttpRequestRequest(RenderHttpRequestRequest),
RenderHttpRequestResponse(RenderHttpRequestResponse),
ShowToastRequest(ShowToastRequest),
GetHttpRequestByIdRequest(GetHttpRequestByIdRequest),
GetHttpRequestByIdResponse(GetHttpRequestByIdResponse),
/// Returned when a plugin doesn't get run, just so the server
/// has something to listen for
EmptyResponse(EmptyResponse),
@@ -122,6 +133,86 @@ pub struct SendHttpRequestResponse {
pub http_response: HttpResponse,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct CopyTextRequest {
pub text: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct RenderHttpRequestRequest {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct RenderHttpRequestResponse {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct ShowToastRequest {
pub message: String,
pub variant: ToastVariant,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub enum ToastVariant {
Custom,
Copied,
Success,
Info,
Warning,
Error,
}
impl Default for ToastVariant {
fn default() -> Self {
ToastVariant::Info
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct GetHttpRequestActionsResponse {
pub actions: Vec<HttpRequestAction>,
pub plugin_ref_id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct HttpRequestAction {
pub key: String,
pub label: String,
pub icon: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct CallHttpRequestActionRequest {
pub key: String,
pub plugin_ref_id: String,
pub args: CallHttpRequestActionArgs,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct CallHttpRequestActionArgs {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]

View File

@@ -1,8 +1,5 @@
use crate::error::Result;
use crate::events::{
ExportHttpRequestRequest, ExportHttpRequestResponse, FilterRequest, FilterResponse
, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload,
};
use crate::events::{CallHttpRequestActionRequest, FilterRequest, FilterResponse, GetHttpRequestActionsResponse, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload};
use crate::error::Error::PluginErr;
use crate::nodejs::start_nodejs_plugin_runtime;
@@ -12,7 +9,6 @@ use std::time::Duration;
use tauri::{AppHandle, Runtime};
use tokio::sync::mpsc;
use tokio::sync::watch::Sender;
use yaak_models::models::HttpRequest;
pub struct PluginManager {
kill_tx: Sender<bool>,
@@ -61,6 +57,29 @@ impl PluginManager {
.send(&payload, source_event.plugin_ref_id.as_str(), reply_id)
.await
}
pub async fn run_http_request_actions(&self) -> Result<Vec<GetHttpRequestActionsResponse>> {
let reply_events = self
.server
.send_and_wait(&InternalEventPayload::GetHttpRequestActionsRequest)
.await?;
let mut all_actions = Vec::new();
for event in reply_events {
if let InternalEventPayload::GetHttpRequestActionsResponse(resp) = event.payload {
all_actions.push(resp.clone());
}
}
Ok(all_actions)
}
pub async fn call_http_request_action(&self, req: CallHttpRequestActionRequest) -> Result<()> {
let plugin = self.server.plugin_by_ref_id(req.plugin_ref_id.as_str()).await?;
let event = plugin.build_event_to_send(&InternalEventPayload::CallHttpRequestActionRequest(req), None);
plugin.send(&event).await?;
Ok(())
}
pub async fn run_import(&self, content: &str) -> Result<(ImportResponse, String)> {
let reply_events = self
@@ -72,43 +91,17 @@ impl PluginManager {
// TODO: Don't just return the first valid response
for event in reply_events {
match event.payload {
InternalEventPayload::ImportResponse(resp) => {
let ref_id = event.plugin_ref_id.as_str();
let plugin = self.server.plugin_by_ref_id(ref_id).await?;
let plugin_name = plugin.name().await;
return Ok((resp, plugin_name));
}
_ => {}
if let InternalEventPayload::ImportResponse(resp) = event.payload {
let ref_id = event.plugin_ref_id.as_str();
let plugin = self.server.plugin_by_ref_id(ref_id).await?;
let plugin_name = plugin.name().await;
return Ok((resp, plugin_name));
}
}
Err(PluginErr("No import responses found".to_string()))
}
pub async fn run_export_curl(
&self,
request: &HttpRequest,
) -> Result<ExportHttpRequestResponse> {
let event = self
.server
.send_to_plugin_and_wait(
"exporter-curl",
&InternalEventPayload::ExportHttpRequestRequest(ExportHttpRequestRequest {
http_request: request.to_owned(),
}),
)
.await?;
match event.payload {
InternalEventPayload::ExportHttpRequestResponse(resp) => Ok(resp),
InternalEventPayload::EmptyResponse(_) => {
Err(PluginErr("Export returned empty".to_string()))
}
e => Err(PluginErr(format!("Export returned invalid event {:?}", e))),
}
}
pub async fn run_filter(
&self,
filter: &str,

View File

@@ -10,7 +10,6 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCopyAsCurl } from '../hooks/useCopyAsCurl';
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
@@ -18,6 +17,7 @@ import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
import { useFolders } from '../hooks/useFolders';
import { useHotKey } from '../hooks/useHotKey';
import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
import { useKeyValue } from '../hooks/useKeyValue';
import { useLatestGrpcConnection } from '../hooks/useLatestGrpcConnection';
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
@@ -34,6 +34,7 @@ import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { isResponseLoading } from '../lib/models';
import { getHttpRequest } from '../lib/store';
import type { DropdownItem } from './core/Dropdown';
import { ContextMenu } from './core/Dropdown';
import { HttpMethodTag } from './core/HttpMethodTag';
@@ -653,7 +654,7 @@ function SidebarItem({
const renameRequest = useRenameRequest(itemId);
const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true });
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
const copyAsCurl = useCopyAsCurl(itemId);
const httpRequestActions = useHttpRequestActions();
const sendRequest = useSendAnyHttpRequest();
const moveToWorkspace = useMoveToWorkspace(itemId);
const sendManyRequests = useSendManyRequests();
@@ -782,12 +783,16 @@ function SidebarItem({
leftSlot: <Icon icon="sendHorizontal" />,
onSelect: () => sendRequest.mutate(itemId),
},
{
key: 'copyCurl',
label: 'Copy as Curl',
leftSlot: <Icon icon="copy" />,
onSelect: copyAsCurl.mutate,
},
...httpRequestActions.map((a) => ({
key: a.key,
label: a.label,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
onSelect: async () => {
const request = await getHttpRequest(itemId);
if (request != null) await a.call(request);
},
})),
{ type: 'separator' },
]
: [];
@@ -829,12 +834,12 @@ function SidebarItem({
}
}, [
child.children,
copyAsCurl.mutate,
createDropdownItems,
deleteFolder,
deleteRequest,
duplicateGrpcRequest,
duplicateHttpRequest,
httpRequestActions,
itemId,
itemModel,
itemName,

View File

@@ -1,5 +1,7 @@
import type { ReactNode } from 'react';
import React, { createContext, useContext, useMemo, useRef, useState } from 'react';
import type { ShowToastRequest } from '../../plugin-runtime-types/src';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import type { ToastProps } from './core/Toast';
import { Toast } from './core/Toast';
import { generateId } from '../lib/generateId';
@@ -61,6 +63,10 @@ export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
[],
);
useListenToTauriEvent<ShowToastRequest>('show_toast', (event) => {
actions.show({ ...event.payload });
});
const state: State = { toasts, actions };
return <ToastContext.Provider value={state}>{children}</ToastContext.Provider>;
};

View File

@@ -1,20 +0,0 @@
import { useMutation } from '@tanstack/react-query';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useCopy } from './useCopy';
export function useCopyAsCurl(requestId: string) {
const copy = useCopy();
const [environment] = useActiveEnvironment();
return useMutation({
mutationKey: ['copy_as_curl', requestId],
mutationFn: async () => {
const cmd: string = await invokeCmd('cmd_request_to_curl', {
requestId,
environmentId: environment?.id,
});
copy(cmd);
return cmd;
},
});
}

View File

@@ -0,0 +1,37 @@
import { useQuery } from '@tanstack/react-query';
import type {
CallHttpRequestActionRequest,
GetHttpRequestActionsResponse,
HttpRequest,
} from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
export function useHttpRequestActions() {
const httpRequestActions = useQuery({
queryKey: ['http_request_actions'],
queryFn: async () => {
const responses = (await invokeCmd(
'cmd_http_request_actions',
)) as GetHttpRequestActionsResponse[];
return responses;
},
});
return (
httpRequestActions.data?.flatMap((r) =>
r.actions.map((a) => ({
key: a.key,
label: a.label,
icon: a.icon,
call: async (httpRequest: HttpRequest) => {
const payload: CallHttpRequestActionRequest = {
key: a.key,
pluginRefId: r.pluginRefId,
args: { httpRequest },
};
await invokeCmd('cmd_call_http_request_action', { req: payload });
},
})),
) ?? []
);
}

View File

@@ -2,6 +2,7 @@ import type { InvokeArgs } from '@tauri-apps/api/core';
import { invoke } from '@tauri-apps/api/core';
type TauriCmd =
| 'cmd_call_http_request_action'
| 'cmd_check_for_updates'
| 'cmd_create_cookie_jar'
| 'cmd_create_environment'
@@ -47,7 +48,6 @@ type TauriCmd =
| 'cmd_metadata'
| 'cmd_new_nested_window'
| 'cmd_new_window'
| 'cmd_request_to_curl'
| 'cmd_dismiss_notification'
| 'cmd_save_response'
| 'cmd_send_ephemeral_request'
@@ -62,6 +62,7 @@ type TauriCmd =
| 'cmd_update_http_request'
| 'cmd_update_settings'
| 'cmd_update_workspace'
| 'cmd_http_request_actions'
| 'cmd_write_file_dev';
export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> {