This commit is contained in:
Gregory Schier
2025-01-26 13:13:45 -08:00
committed by GitHub
parent 82b1ad35ff
commit f678593903
99 changed files with 3492 additions and 1583 deletions

88
package-lock.json generated
View File

@@ -20,7 +20,7 @@
"src-web"
],
"devDependencies": {
"@tauri-apps/cli": "^2.2.4",
"@tauri-apps/cli": "^2.2.5",
"@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1",
"eslint": "^8",
@@ -2709,9 +2709,9 @@
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.2.4.tgz",
"integrity": "sha512-pihbuHEWJa9SEcN7JdEbMa0oq28MTTbk0nNNnRG8/irNQTKcjwM+KzxG2wuYZYbsXQVqwSu7PstdIEAnXqYHkw==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.2.5.tgz",
"integrity": "sha512-PaefTQUCYYqvZWdH8EhXQkyJEjQwtoy/OHGoPcZx7Gk3D3K6AtGSxZ9OlHIz3Bu5LDGgVBk36vKtHW0WYsWnbw==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
@@ -2725,22 +2725,22 @@
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.2.4",
"@tauri-apps/cli-darwin-x64": "2.2.4",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.2.4",
"@tauri-apps/cli-linux-arm64-gnu": "2.2.4",
"@tauri-apps/cli-linux-arm64-musl": "2.2.4",
"@tauri-apps/cli-linux-x64-gnu": "2.2.4",
"@tauri-apps/cli-linux-x64-musl": "2.2.4",
"@tauri-apps/cli-win32-arm64-msvc": "2.2.4",
"@tauri-apps/cli-win32-ia32-msvc": "2.2.4",
"@tauri-apps/cli-win32-x64-msvc": "2.2.4"
"@tauri-apps/cli-darwin-arm64": "2.2.5",
"@tauri-apps/cli-darwin-x64": "2.2.5",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.2.5",
"@tauri-apps/cli-linux-arm64-gnu": "2.2.5",
"@tauri-apps/cli-linux-arm64-musl": "2.2.5",
"@tauri-apps/cli-linux-x64-gnu": "2.2.5",
"@tauri-apps/cli-linux-x64-musl": "2.2.5",
"@tauri-apps/cli-win32-arm64-msvc": "2.2.5",
"@tauri-apps/cli-win32-ia32-msvc": "2.2.5",
"@tauri-apps/cli-win32-x64-msvc": "2.2.5"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.2.4.tgz",
"integrity": "sha512-+sMLkQBFebn/UENyaXpyQqRkdFQie8RdEvYVz0AGthm2p0lMVlWiBmc4ImBJmfo8569zVeDX8B+5OWt4/AuZzA==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.2.5.tgz",
"integrity": "sha512-qdPmypQE7qj62UJy3Wl/ccCJZwsv5gyBByOrAaG7u5c/PB3QSxhNPegice2k4EHeIuApaVJOoe/CEYVgm/og2Q==",
"cpu": [
"arm64"
],
@@ -2755,9 +2755,9 @@
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.2.4.tgz",
"integrity": "sha512-6fJvXVtQJh7H8q9sll2XC2wO5bpn7bzeh+MQxpcLq6F8SE02sFuNDLN+AqX0DQnuYV0V6jdzM2+bTYOlc1FBsw==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.2.5.tgz",
"integrity": "sha512-8JVlCAb2c3n0EcGW7n/1kU4Rq831SsoLDD/0hNp85Um8HGIH2Mg/qos/MLOc8Qv2mOaoKcRKf4hd0I1y0Rl9Cg==",
"cpu": [
"x64"
],
@@ -2772,9 +2772,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.2.4.tgz",
"integrity": "sha512-QU6Ac6tx79iqkxsDUQesCBNq8RrVSkP9HhVzS2reKthK3xbdTCwNUXoRlfhudKMVrIxV4K7uTwUV99eAnwbm5Q==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.2.5.tgz",
"integrity": "sha512-mzxQCqZg7ljRVgekPpXQ5TOehCNgnXh/DNWU6kFjALaBvaw4fGzc369/hV94wOt29htNFyxf8ty2DaQaYljEHw==",
"cpu": [
"arm"
],
@@ -2789,9 +2789,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.2.4.tgz",
"integrity": "sha512-uZhp312s6VgJJDgUg+HuHZnhjGg93OT+q/aZMoccdZVQ6dvwH8kJzIkKt9zL1U126AXXoesb1EyYmsAruxaUKA==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.2.5.tgz",
"integrity": "sha512-M9nkzx5jsSJSNpp7aSza0qv0/N13SUNzH8ysYSZ7IaCN8coGeMg2KgQ5qC6tqUVij2rbg8A/X1n0pPo/gtLx0A==",
"cpu": [
"arm64"
],
@@ -2806,9 +2806,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.4.tgz",
"integrity": "sha512-k6JCXd9E+XU0J48nVcFr3QO//bzKg/gp8ZKagBfI2wBpHOk14CnHNBQKNs11nMQUwko4bnPeqj4llcdkbmwIbw==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.5.tgz",
"integrity": "sha512-tFhZu950HNRLR1RM5Q9Xj5gAlA6AhyyiZgeoXGFAWto+s2jpWmmA3Qq2GUxnVDr7Xui8PF4UY5kANDIOschuwg==",
"cpu": [
"arm64"
],
@@ -2823,9 +2823,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.2.4.tgz",
"integrity": "sha512-bUBPU46OF1pNfM6SsGbUlkCBh/pTzvFlEdUpDISsS40v9NVt+kqCy3tHzLGB412E3lSlA6FnshB6HxkdRRdTtg==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.2.5.tgz",
"integrity": "sha512-eaGhTQLr3EKeksGsp2tK/Ndi7/oyo3P53Pye6kg0zqXiqu8LQjg1CgvDm1l+5oit04S60zR4AqlDFpoeEtDGgw==",
"cpu": [
"x64"
],
@@ -2840,9 +2840,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.4.tgz",
"integrity": "sha512-vOrpsQDiMtP8q/ZeXfXqgNi3G4Yv5LVX2vI5XkB2yvVuVF1Dvky/hcCJfi9tZQD+IpeiYxjuj7+SxHp82eQ/kA==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.5.tgz",
"integrity": "sha512-NLAO/SymDxeGuOWWQZCpwoED1K1jaHUvW+u9ip+rTetnxFPLvf3zXthx4QVKfCZLdj2WLQz4cLjHyQdMDXAM+w==",
"cpu": [
"x64"
],
@@ -2857,9 +2857,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.2.4.tgz",
"integrity": "sha512-iEP/Cq0ts4Ln4Zh2NSC01lkYEAhr+LotbG4U2z+gxHfCdMrtYYtYdG05C2mpeIxShzL7uEIQb/lhVRBMd7robg==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.2.5.tgz",
"integrity": "sha512-yG5KFbqrHfGjkAQAaaCD4i7cJklBjmMxZ2C92DEnqCOujSsEuLxrwwoKxQ4+hqEHOmF3lyX0vfqhgZcS03H38w==",
"cpu": [
"arm64"
],
@@ -2874,9 +2874,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.2.4.tgz",
"integrity": "sha512-YBbqF0wyHUT00zAGZTTbEbz/C5JDGPnT1Nodor96+tzEU6qAPRYfe5eFe/rpRARbalkpw1UkcVP0Ay8gnksAiA==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.2.5.tgz",
"integrity": "sha512-G5lq+2EdxOc8ttg3uhME5t9U3hMGTxwaKz0X4DplTG2Iv4lcNWqw/AESIJVHa5a+EB+ZCC8I+yOfIykp/Cd5mQ==",
"cpu": [
"ia32"
],
@@ -2891,9 +2891,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.2.4.tgz",
"integrity": "sha512-MMago/SfWZbUFrwFmPCXmmbb42h7u8Y5jvLvnK2mOpOfCAsei2tLO4hx+Inoai0l2DByuYO4Ef1xDyP6shCsZQ==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.2.5.tgz",
"integrity": "sha512-vw4fPVOo0rIQIlqw6xUvK2nwiRFBHNgayDE2Z/SomJlQJAJ1q4VgpHOPl12ouuicmTjK1gWKm7RTouQe3Nig0Q==",
"cpu": [
"x64"
],

View File

@@ -34,7 +34,7 @@
"tauri-before-dev": "npm run --workspaces --if-present dev"
},
"devDependencies": {
"@tauri-apps/cli": "^2.2.4",
"@tauri-apps/cli": "^2.2.5",
"@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1",
"eslint": "^8",

View File

@@ -1,290 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./models.js";
import type { Folder } from "./models.js";
import type { GrpcRequest } from "./models.js";
import type { HttpRequest } from "./models.js";
import type { HttpResponse } from "./models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { Workspace } from "./models.js";
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, };
export type CallHttpAuthenticationRequest = { config: { [key in string]?: JsonValue }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpAuthenticationResponse = {
/**
* HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added.
*/
setHeaders: Array<HttpHeader>, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { key: string, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: string }, };
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
export type CallTemplateFunctionResponse = { value: string | null, };
export type Color = "custom" | "default" | "primary" | "secondary" | "info" | "success" | "notice" | "warning" | "danger";
export type CopyTextRequest = { text: string, };
export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown";
export type EmptyPayload = {};
export type ErrorResponse = { error: string, };
export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
export type ExportHttpRequestResponse = { content: string, };
export type FileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest;
export type FormInputBase = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputCheckbox = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputEditor = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Don't show the editor gutter (line numbers, folds, etc.)
*/
hideGutter?: boolean,
/**
* Language for syntax highlighting
*/
language?: EditorLanguage, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputFile = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputHttpRequest = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputSelect = {
/**
* The options that will be available in the select input
*/
options: Array<FormInputSelectOption>, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputSelectOption = { name: string, value: string, };
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Placeholder for the text input
*/
password?: boolean, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type GetHttpAuthenticationResponse = { name: string, label: string, shortLabel: string, config: Array<FormInput>, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string, };
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpHeader = { name: string, value: string, };
export type HttpRequestAction = { key: string, label: string, icon?: Icon, };
export type Icon = "copy" | "info" | "check_circle" | "alert_triangle" | "_unknown";
export type ImportRequest = { content: string, };
export type ImportResources = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, };
export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "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": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "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_request" } & EmptyPayload | { "type": "get_http_authentication_response" } & GetHttpAuthenticationResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "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": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string,
/**
* Text to add to the cancel button
*/
cancelText?: string,
/**
* Require the user to enter a non-empty value
*/
required?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
export type RenderPurpose = "send" | "preview";
export type SendHttpRequestRequest = { httpRequest: HttpRequest, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, };
export type TemplateFunction = { name: string, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<FormInput>, };
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };

View File

@@ -0,0 +1,405 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { HttpResponse } from "./gen_models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { Workspace } from "./gen_models.js";
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, };
export type CallHttpAuthenticationActionArgs = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type CallHttpAuthenticationActionRequest = { index: number, pluginRefId: string, args: CallHttpAuthenticationActionArgs, };
export type CallHttpAuthenticationRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpAuthenticationResponse = {
/**
* HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added.
*/
setHeaders: Array<HttpHeader>, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: string }, };
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
export type CallTemplateFunctionResponse = { value: string | null, };
export type CloseWindowRequest = { label: string, };
export type Color = "primary" | "secondary" | "info" | "success" | "notice" | "warning" | "danger";
export type CompletionOptionType = "constant" | "variable";
export type Content = { "type": "text", content: string, } | { "type": "markdown", content: string, };
export type CopyTextRequest = { text: string, };
export type DeleteKeyValueRequest = { key: string, };
export type DeleteKeyValueResponse = { deleted: boolean, };
export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown";
export type EmptyPayload = {};
export type ErrorResponse = { error: string, };
export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
export type ExportHttpRequestResponse = { content: string, };
export type FileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown;
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
export type FormInputBanner = { inputs?: Array<FormInput>, hidden?: boolean, color?: Color, };
export type FormInputBase = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputCheckbox = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputEditor = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Don't show the editor gutter (line numbers, folds, etc.)
*/
hideGutter?: boolean,
/**
* Language for syntax highlighting
*/
language?: EditorLanguage, readOnly?: boolean, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputFile = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputHttpRequest = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputMarkdown = { content: string, hidden?: boolean, };
export type FormInputSelect = {
/**
* The options that will be available in the select input
*/
options: Array<FormInputSelectOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputSelectOption = { label: string, value: string, };
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Placeholder for the text input
*/
password?: boolean,
/**
* Whether to allow newlines in the input, like a <textarea/>
*/
multiLine?: boolean, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type GenericCompletionOption = { label: string, detail?: string, info?: string, type?: CompletionOptionType, boost?: number, };
export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
export type GetHttpAuthenticationSummaryResponse = { name: string, label: string, shortLabel: string, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string, };
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
export type GetKeyValueRequest = { key: string, };
export type GetKeyValueResponse = { value?: string, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpAuthenticationAction = { label: string, icon?: Icon, };
export type HttpHeader = { name: string, value: string, };
export type HttpRequestAction = { label: string, icon?: Icon, };
export type Icon = "alert_triangle" | "check" | "check_circle" | "chevron_down" | "copy" | "info" | "pin" | "search" | "trash" | "_unknown";
export type ImportRequest = { content: string, };
export type ImportResources = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, };
export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "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": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "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": "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": "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": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;
export type OpenWindowRequest = { url: string,
/**
* Label for the window. If not provided, a random one will be generated.
*/
label: string, title?: string, size?: WindowSize, };
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string,
/**
* Text to add to the cancel button
*/
cancelText?: string,
/**
* Require the user to enter a non-empty value
*/
required?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
export type RenderPurpose = "send" | "preview";
export type SendHttpRequestRequest = { httpRequest: Partial<HttpRequest>, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
export type SetKeyValueRequest = { key: string, value: string, };
export type SetKeyValueResponse = {};
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, };
export type TemplateFunction = { name: string, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<FormInput>, };
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };
export type WindowNavigateEvent = { url: string, };
export type WindowSize = { width: number, height: number, };

View File

@@ -1 +1,2 @@
export type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
export type MaybePromise<T> = Promise<T> | T;

View File

@@ -1,5 +1,5 @@
export type * from './plugins';
export type * from './themes';
export * from './bindings/models';
export * from './bindings/events';
export * from './bindings/gen_models';
export * from './bindings/gen_events';

View File

@@ -1,13 +1,29 @@
import {
CallHttpAuthenticationActionArgs,
CallHttpAuthenticationRequest,
CallHttpAuthenticationResponse,
GetHttpAuthenticationResponse,
} from '../bindings/events';
FormInput,
GetHttpAuthenticationConfigRequest,
GetHttpAuthenticationSummaryResponse,
HttpAuthenticationAction,
} from '../bindings/gen_events';
import { MaybePromise } from '../helpers';
import { Context } from './Context';
export type AuthenticationPlugin = Omit<GetHttpAuthenticationResponse, 'pluginName'> & {
type DynamicFormInput = FormInput & {
dynamic(
ctx: Context,
args: GetHttpAuthenticationConfigRequest,
): MaybePromise<Partial<FormInput> | undefined | null>;
};
export type AuthenticationPlugin = GetHttpAuthenticationSummaryResponse & {
args: (FormInput | DynamicFormInput)[];
onApply(
ctx: Context,
args: CallHttpAuthenticationRequest,
): Promise<CallHttpAuthenticationResponse> | CallHttpAuthenticationResponse;
): MaybePromise<CallHttpAuthenticationResponse>;
actions?: (HttpAuthenticationAction & {
onSelect(ctx: Context, args: CallHttpAuthenticationActionArgs): Promise<void> | void;
})[];
};

View File

@@ -3,6 +3,7 @@ import type {
FindHttpResponsesResponse,
GetHttpRequestByIdRequest,
GetHttpRequestByIdResponse,
OpenWindowRequest,
PromptTextRequest,
PromptTextResponse,
RenderHttpRequestRequest,
@@ -12,7 +13,7 @@ import type {
ShowToastRequest,
TemplateRenderRequest,
TemplateRenderResponse,
} from "../bindings/events.ts";
} from '../bindings/gen_events.ts';
export interface Context {
clipboard: {
@@ -22,27 +23,27 @@ export interface Context {
show(args: ShowToastRequest): Promise<void>;
};
prompt: {
text(args: PromptTextRequest): Promise<PromptTextResponse["value"]>;
text(args: PromptTextRequest): Promise<PromptTextResponse['value']>;
};
store: {
set<T>(key: string, value: T): Promise<void>;
get<T>(key: string): Promise<T | undefined>;
delete(key: string): Promise<boolean>;
};
window: {
openUrl(
args: OpenWindowRequest & { onNavigate?: (args: { url: string }) => void },
): Promise<{ close: () => void }>;
};
httpRequest: {
send(
args: SendHttpRequestRequest,
): Promise<SendHttpRequestResponse["httpResponse"]>;
getById(
args: GetHttpRequestByIdRequest,
): Promise<GetHttpRequestByIdResponse["httpRequest"]>;
render(
args: RenderHttpRequestRequest,
): Promise<RenderHttpRequestResponse["httpRequest"]>;
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;
render(args: RenderHttpRequestRequest): Promise<RenderHttpRequestResponse['httpRequest']>;
};
httpResponse: {
find(
args: FindHttpResponsesRequest,
): Promise<FindHttpResponsesResponse["httpResponses"]>;
find(args: FindHttpResponsesRequest): Promise<FindHttpResponsesResponse['httpResponses']>;
};
templates: {
render(
args: TemplateRenderRequest,
): Promise<TemplateRenderResponse["data"]>;
render(args: TemplateRenderRequest): Promise<TemplateRenderResponse['data']>;
};
}

View File

@@ -1,4 +1,4 @@
import type { CallHttpRequestActionArgs, HttpRequestAction } from '../bindings/events';
import type { CallHttpRequestActionArgs, HttpRequestAction } from '../bindings/gen_events';
import type { Context } from './Context';
export type HttpRequestActionPlugin = HttpRequestAction & {

View File

@@ -1,29 +1,14 @@
import {
Environment,
Folder,
GrpcRequest,
HttpRequest,
Workspace,
} from "../bindings/models";
import type { AtLeast } from "../helpers";
import type { Context } from "./Context";
import { Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '../bindings/gen_models';
import type { AtLeast } from '../helpers';
import type { Context } from './Context';
type ImportPluginResponse = null | {
resources: {
workspaces: AtLeast<Workspace, "name" | "id" | "model">[];
environments: AtLeast<
Environment,
"name" | "id" | "model" | "workspaceId"
>[];
folders: AtLeast<Folder, "name" | "id" | "model" | "workspaceId">[];
httpRequests: AtLeast<
HttpRequest,
"name" | "id" | "model" | "workspaceId"
>[];
grpcRequests: AtLeast<
GrpcRequest,
"name" | "id" | "model" | "workspaceId"
>[];
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
grpcRequests: AtLeast<GrpcRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
};
};

View File

@@ -1,7 +1,7 @@
import {
CallTemplateFunctionArgs,
TemplateFunction,
} from "../bindings/events";
} from "../bindings/gen_events";
import { Context } from "./Context";
export type TemplateFunctionPlugin = TemplateFunction & {

View File

@@ -1,11 +1,18 @@
// OAuth 2.0 spec -> https://datatracker.ietf.org/doc/html/rfc6749
import type {
BootRequest,
Context,
DeleteKeyValueResponse,
FindHttpResponsesResponse,
FormInput,
GetHttpRequestByIdResponse,
GetKeyValueResponse,
HttpAuthenticationAction,
HttpRequestAction,
InternalEvent,
InternalEventPayload,
JsonPrimitive,
PluginDefinition,
PromptTextResponse,
RenderHttpRequestResponse,
@@ -19,8 +26,16 @@ import type { Stats } from 'node:fs';
import { readFileSync, statSync, watch } from 'node:fs';
import path from 'node:path';
import * as util from 'node:util';
import { parentPort as nullableParentPort, workerData } from 'node:worker_threads';
import Promise from '../../../../../Library/Caches/deno/npm/registry.npmjs.org/any-promise/1.3.0';
import { interceptStdout } from './interceptStdout';
import { parentPort, workerData } from 'node:worker_threads';
import { migrateHttpRequestActionKey, migrateTemplateFunctionSelectOptions } from './migrations';
if (nullableParentPort == null) {
throw new Error('Worker does not have access to parentPort');
}
const parentPort = nullableParentPort;
export interface PluginWorkerData {
bootRequest: BootRequest;
@@ -73,7 +88,7 @@ function initialize(workerData: PluginWorkerData) {
if (event.payload.type !== 'empty_response') {
console.log('Sending event to app', event.id, event.payload.type);
}
parentPort!.postMessage(event);
parentPort.postMessage(event);
}
function sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
@@ -84,14 +99,15 @@ function initialize(workerData: PluginWorkerData) {
const eventToSend = buildEventToSend(windowContext, payload, null);
// 2. Spawn listener in background
const promise = new Promise<InternalEventPayload>((resolve) => {
const promise = new Promise<T>((resolve) => {
const cb = (event: InternalEvent) => {
if (event.replyId === eventToSend.id) {
parentPort!.off('message', cb); // Unlisten, now that we're done
resolve(event.payload); // Not type-safe but oh well
parentPort.off('message', cb); // Unlisten, now that we're done
const { type: _, ...payload } = event.payload;
resolve(payload as T);
}
};
parentPort!.on('message', cb);
parentPort.on('message', cb);
});
// 3. Send the event after we start listening (to prevent race)
@@ -101,10 +117,29 @@ function initialize(workerData: PluginWorkerData) {
return promise as unknown as Promise<T>;
}
function sendAndListenForEvents(
windowContext: WindowContext,
payload: InternalEventPayload,
onEvent: (event: InternalEventPayload) => void,
): void {
// 1. Build event to send
const eventToSend = buildEventToSend(windowContext, payload, null);
// 2. Listen for replies in the background
parentPort.on('message', (event: InternalEvent) => {
if (event.replyId === eventToSend.id) {
onEvent(event.payload);
}
});
// 3. Send the event after we start listening (to prevent race)
sendEvent(eventToSend);
}
// Reload plugin if the JS or package.json changes
const windowContextNone: WindowContext = { type: 'none' };
const fileChangeCallback = async () => {
await importModule();
importModule();
return sendPayload(windowContextNone, { type: 'reload_response' }, null);
};
@@ -130,6 +165,27 @@ function initialize(workerData: PluginWorkerData) {
});
},
},
window: {
async openUrl({ onNavigate, ...args }) {
args.label = args.label || `${Math.random()}`;
const payload: InternalEventPayload = { type: 'open_window_request', ...args };
const onEvent = (event: InternalEventPayload) => {
if (event.type === 'window_navigate_event') {
onNavigate?.(event);
}
};
sendAndListenForEvents(event.windowContext, payload, onEvent);
return {
close: () => {
const closePayload: InternalEventPayload = {
type: 'close_window_request',
label: args.label,
};
sendPayload(event.windowContext, closePayload, null);
},
};
},
},
prompt: {
async text(args) {
const reply: PromptTextResponse = await sendAndWaitForReply(event.windowContext, {
@@ -201,6 +257,30 @@ function initialize(workerData: PluginWorkerData) {
return result.data;
},
},
store: {
async get<T>(key: string) {
const payload = { type: 'get_key_value_request', key } as const;
const result = await sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
return result.value ? (JSON.parse(result.value) as T) : undefined;
},
async set<T>(key: string, value: T) {
const valueStr = JSON.stringify(value);
const payload: InternalEventPayload = {
type: 'set_key_value_request',
key,
value: valueStr,
};
await sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
},
async delete(key: string) {
const payload = { type: 'delete_key_value_request', key } as const;
const result = await sendAndWaitForReply<DeleteKeyValueResponse>(
event.windowContext,
payload,
);
return result.deleted;
},
},
});
let plug: PluginDefinition | null = null;
@@ -210,10 +290,11 @@ function initialize(workerData: PluginWorkerData) {
delete require.cache[id];
plug = require(id).plugin;
}
importModule();
// Message comes into the plugin to be processed
parentPort!.on('message', async (event: InternalEvent) => {
parentPort.on('message', async (event: InternalEvent) => {
const ctx = newCtx(event);
const { windowContext, payload, id: replyId } = event;
try {
@@ -272,7 +353,7 @@ function initialize(workerData: PluginWorkerData) {
Array.isArray(plug?.httpRequestActions)
) {
const reply: HttpRequestAction[] = plug.httpRequestActions.map((a) => ({
...a,
...migrateHttpRequestActionKey(a),
// Add everything except onSelect
onSelect: undefined,
}));
@@ -289,11 +370,13 @@ function initialize(workerData: PluginWorkerData) {
payload.type === 'get_template_functions_request' &&
Array.isArray(plug?.templateFunctions)
) {
const reply: TemplateFunction[] = plug.templateFunctions.map((a) => ({
...a,
// Add everything except render
onRender: undefined,
}));
const reply: TemplateFunction[] = plug.templateFunctions.map((templateFunction) => {
return {
...migrateTemplateFunctionSelectOptions(templateFunction),
// Add everything except render
onRender: undefined,
};
});
const replyPayload: InternalEventPayload = {
type: 'get_template_functions_response',
pluginRefId,
@@ -303,11 +386,42 @@ function initialize(workerData: PluginWorkerData) {
return;
}
if (payload.type === 'get_http_authentication_request' && plug?.authentication) {
const { onApply: _, ...auth } = plug.authentication;
if (payload.type === 'get_http_authentication_summary_request' && plug?.authentication) {
const { name, shortLabel, label } = plug.authentication;
const replyPayload: InternalEventPayload = {
...auth,
type: 'get_http_authentication_response',
type: 'get_http_authentication_summary_response',
name,
label,
shortLabel,
};
sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'get_http_authentication_config_request' && plug?.authentication) {
const { args, actions } = plug.authentication;
const resolvedArgs: FormInput[] = [];
for (let i = 0; i < args.length; i++) {
let v = args[i];
if ('dynamic' in v) {
const dynamicAttrs = await v.dynamic(ctx, payload);
const { dynamic, ...other } = v;
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
} else {
resolvedArgs.push(v);
}
}
const resolvedActions: HttpAuthenticationAction[] = [];
for (const { onSelect, ...action } of actions ?? []) {
resolvedActions.push(action);
}
const replyPayload: InternalEventPayload = {
type: 'get_http_authentication_config_response',
args: resolvedArgs,
actions: resolvedActions,
pluginRefId,
};
sendPayload(windowContext, replyPayload, replyId);
@@ -317,12 +431,13 @@ function initialize(workerData: PluginWorkerData) {
if (payload.type === 'call_http_authentication_request' && plug?.authentication) {
const auth = plug.authentication;
if (typeof auth?.onApply === 'function') {
applyFormInputDefaults(auth.args, payload.values);
const result = await auth.onApply(ctx, payload);
sendPayload(
windowContext,
{
...result,
type: 'call_http_authentication_response',
setHeaders: result.setHeaders,
},
replyId,
);
@@ -330,11 +445,25 @@ function initialize(workerData: PluginWorkerData) {
}
}
if (
payload.type === 'call_http_authentication_action_request' &&
plug?.authentication != null
) {
const action = plug.authentication.actions?.find((a) => a.name === payload.name);
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
sendEmpty(windowContext, replyId);
return;
}
}
if (
payload.type === 'call_http_request_action_request' &&
Array.isArray(plug?.httpRequestActions)
) {
const action = plug.httpRequestActions.find((a) => a.key === payload.key);
const action = plug.httpRequestActions.find(
(a) => migrateHttpRequestActionKey(a).name === payload.name,
);
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
sendEmpty(windowContext, replyId);
@@ -348,6 +477,7 @@ function initialize(workerData: PluginWorkerData) {
) {
const action = plug.templateFunctions.find((a) => a.name === payload.name);
if (typeof action?.onRender === 'function') {
applyFormInputDefaults(action.args, payload.args.values);
const result = await action.onRender(ctx, payload.args);
sendPayload(
windowContext,
@@ -362,7 +492,7 @@ function initialize(workerData: PluginWorkerData) {
}
if (payload.type === 'reload_request') {
await importModule();
importModule();
}
} catch (err) {
console.log('Plugin call threw exception', payload.type, err);
@@ -374,7 +504,7 @@ function initialize(workerData: PluginWorkerData) {
},
replyId,
);
// TODO: Return errors to server
return;
}
// No matches, so send back an empty response so the caller doesn't block forever
@@ -425,3 +555,17 @@ function watchFile(filepath: string, cb: (filepath: string) => void) {
watchedFiles[filepath] = stat;
});
}
/** Recursively apply form input defaults to a set of values */
function applyFormInputDefaults(
inputs: FormInput[],
values: { [p: string]: JsonPrimitive | undefined },
) {
for (const input of inputs) {
if ('inputs' in input) {
applyFormInputDefaults(input.inputs ?? [], values);
} else if ('defaultValue' in input && values[input.name] === undefined) {
values[input.name] = input.defaultValue;
}
}
}

View File

@@ -0,0 +1,25 @@
import {HttpRequestAction, TemplateFunction} from '@yaakapp/api';
export function migrateTemplateFunctionSelectOptions(f: TemplateFunction): TemplateFunction {
const migratedArgs = f.args.map((a) => {
if (a.type === 'select') {
a.options = a.options.map((o) => ({
...o,
label: o.label || (o as any).name,
}));
}
return a;
});
return {
...f,
args: migratedArgs,
};
}
export function migrateHttpRequestActionKey(a: HttpRequestAction): HttpRequestAction {
return {
...a,
name: a.name || (a as any).key,
}
}

61
src-tauri/Cargo.lock generated
View File

@@ -712,7 +712,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"thiserror 2.0.7",
"thiserror 2.0.11",
]
[[package]]
@@ -5212,8 +5212,6 @@ dependencies = [
"serde_json",
"sha2",
"sqlx-core",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.87",
"tempfile",
@@ -5252,7 +5250,6 @@ dependencies = [
"percent-encoding",
"rand 0.8.5",
"rsa",
"serde",
"sha1",
"sha2",
"smallvec",
@@ -5556,9 +5553,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.2.0"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e2e3349fbb2be7af9fad1b43d61ac83ba55ab48d47fbe1b2732f0c8211610a9"
checksum = "78f6efc261c7905839b4914889a5b25df07f0ff89c63fb4afd6ff8c96af15e4d"
dependencies = [
"anyhow",
"bytes",
@@ -5594,7 +5591,7 @@ dependencies = [
"tauri-runtime",
"tauri-runtime-wry",
"tauri-utils",
"thiserror 2.0.7",
"thiserror 2.0.11",
"tokio",
"tray-icon",
"url",
@@ -5647,7 +5644,7 @@ dependencies = [
"sha2",
"syn 2.0.87",
"tauri-utils",
"thiserror 2.0.7",
"thiserror 2.0.11",
"time",
"url",
"uuid",
@@ -5697,7 +5694,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.7",
"thiserror 2.0.11",
]
[[package]]
@@ -5714,7 +5711,7 @@ dependencies = [
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.7",
"thiserror 2.0.11",
"url",
]
@@ -5735,7 +5732,7 @@ dependencies = [
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.7",
"thiserror 2.0.11",
"toml 0.8.19",
"url",
"uuid",
@@ -5759,7 +5756,7 @@ dependencies = [
"swift-rs",
"tauri",
"tauri-plugin",
"thiserror 2.0.7",
"thiserror 2.0.11",
"time",
]
@@ -5779,7 +5776,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.7",
"thiserror 2.0.11",
"url",
"windows",
"zbus 5.3.0",
@@ -5800,7 +5797,7 @@ dependencies = [
"sys-locale",
"tauri",
"tauri-plugin",
"thiserror 2.0.7",
"thiserror 2.0.11",
]
[[package]]
@@ -5820,7 +5817,7 @@ dependencies = [
"shared_child",
"tauri",
"tauri-plugin",
"thiserror 2.0.7",
"thiserror 2.0.11",
"tokio",
]
@@ -5833,7 +5830,7 @@ dependencies = [
"serde",
"serde_json",
"tauri",
"thiserror 2.0.7",
"thiserror 2.0.11",
"tracing",
"windows-sys 0.59.0",
"zbus 5.3.0",
@@ -5861,7 +5858,7 @@ dependencies = [
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.7",
"thiserror 2.0.11",
"time",
"tokio",
"url",
@@ -5881,7 +5878,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.7",
"thiserror 2.0.11",
]
[[package]]
@@ -5898,7 +5895,7 @@ dependencies = [
"serde",
"serde_json",
"tauri-utils",
"thiserror 2.0.7",
"thiserror 2.0.11",
"url",
"windows",
]
@@ -5958,7 +5955,7 @@ dependencies = [
"serde_json",
"serde_with",
"swift-rs",
"thiserror 2.0.7",
"thiserror 2.0.11",
"toml 0.8.19",
"url",
"urlpattern",
@@ -6026,11 +6023,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.7"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
"thiserror-impl 2.0.7",
"thiserror-impl 2.0.11",
]
[[package]]
@@ -6046,9 +6043,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.7"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36"
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
@@ -6458,7 +6455,7 @@ dependencies = [
"log",
"rand 0.8.5",
"sha1",
"thiserror 2.0.7",
"thiserror 2.0.11",
"utf-8",
]
@@ -7406,7 +7403,7 @@ dependencies = [
"sha2",
"soup3",
"tao-macros",
"thiserror 2.0.7",
"thiserror 2.0.11",
"url",
"webkit2gtk",
"webkit2gtk-sys",
@@ -7497,6 +7494,7 @@ dependencies = [
"hex_color",
"http",
"log",
"md5",
"mime_guess",
"objc",
"openssl-sys",
@@ -7571,7 +7569,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.7",
"thiserror 2.0.11",
"ts-rs",
"yaak-models",
]
@@ -7592,7 +7590,7 @@ dependencies = [
"serde_json",
"sqlx",
"tauri",
"thiserror 1.0.63",
"thiserror 2.0.11",
"ts-rs",
]
@@ -7603,6 +7601,7 @@ dependencies = [
"dunce",
"futures-util",
"log",
"md5",
"path-slash",
"rand 0.8.5",
"regex",
@@ -7610,7 +7609,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin-shell",
"thiserror 2.0.7",
"thiserror 2.0.11",
"tokio",
"tokio-tungstenite",
"ts-rs",
@@ -7639,7 +7638,7 @@ dependencies = [
"sha1",
"tauri",
"tauri-plugin",
"thiserror 2.0.7",
"thiserror 2.0.11",
"tokio",
"ts-rs",
"yaak-models",

View File

@@ -44,6 +44,7 @@ eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client
hex_color = "3.0.0"
http = { version = "1.2.0", default-features = false }
log = "0.4.21"
md5 = "0.7.0"
rand = "0.8.5"
regex = "1.10.2"
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider"] }
@@ -85,7 +86,7 @@ yaak-plugins = { path = "yaak-plugins" }
serde = "1.0.215"
serde_json = "1.0.132"
tauri-plugin-shell = "2.2.0"
tauri = "2.2.0"
tauri = "2.2.3"
thiserror = "2.0.3"
ts-rs = "10.0.0"
reqwest = "0.12.12"

View File

@@ -30,10 +30,8 @@
}
]
},
"opener:allow-open-url",
"opener:allow-open-path",
"opener:allow-default-urls",
"opener:allow-reveal-item-in-dir",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
"core:webview:allow-set-webview-zoom",
"core:window:allow-close",
"core:window:allow-internal-toggle-maximize",
@@ -47,8 +45,11 @@
"core:window:allow-start-dragging",
"core:window:allow-theme",
"core:window:allow-unmaximize",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
"opener:allow-default-urls",
"opener:allow-open-path",
"opener:allow-open-url",
"opener:allow-reveal-item-in-dir",
"shell:allow-open",
"yaak-license:default",
"yaak-sync:default"
]

View File

@@ -1 +1 @@
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-dir","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"opener:allow-open-url","opener:allow-open-path","opener:allow-default-urls","opener:allow-reveal-item-in-dir","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-is-maximized","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","yaak-license:default","yaak-sync:default"]}}
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-dir","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-is-maximized","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-unmaximize","opener:allow-default-urls","opener:allow-open-path","opener:allow-open-url","opener:allow-reveal-item-in-dir","shell:allow-open","yaak-license:default","yaak-sync:default"]}}

View File

@@ -0,0 +1,11 @@
CREATE TABLE plugin_key_values
(
model TEXT DEFAULT 'plugin_key_value' NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
plugin_name TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (plugin_name, key)
);

View File

@@ -70,7 +70,7 @@ pub async fn send_http_request<R: Runtime>(
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
url_string = format!("http://{}", url_string);
}
debug!("Sending request to {url_string}");
debug!("Sending request to {} {url_string}", request.method);
let mut client_builder = reqwest::Client::builder()
.redirect(match workspace.setting_follow_redirects {
@@ -262,7 +262,7 @@ pub async fn send_http_request<R: Runtime>(
None => {}
Some(a) => {
for p in a {
let enabled = get_bool(p, "enabled");
let enabled = get_bool(p, "enabled", true);
let name = get_str(p, "name");
if !enabled || name.is_empty() {
continue;
@@ -296,7 +296,7 @@ pub async fn send_http_request<R: Runtime>(
None => {}
Some(fd) => {
for p in fd {
let enabled = get_bool(p, "enabled");
let enabled = get_bool(p, "enabled", true);
let name = get_str(p, "name").to_string();
if !enabled || name.is_empty() {
@@ -376,13 +376,11 @@ pub async fn send_http_request<R: Runtime>(
if let Some(auth_name) = request.authentication_type.to_owned() {
let req = CallHttpAuthenticationRequest {
config: serde_json::to_value(&request.authentication)
.unwrap()
.as_object()
.unwrap()
.to_owned(),
method: sendable_req.method().to_string(),
context_id: format!("{:x}", md5::compute(request.id)),
values: serde_json::from_value(serde_json::to_value(&request.authentication).unwrap())
.unwrap(),
url: sendable_req.url().to_string(),
method: sendable_req.method().to_string(),
headers: sendable_req
.headers()
.iter()
@@ -604,10 +602,10 @@ fn ensure_proto(url_str: &str) -> String {
format!("http://{url_str}")
}
fn get_bool(v: &Value, key: &str) -> bool {
fn get_bool(v: &Value, key: &str, fallback: bool) -> bool {
match v.get(key) {
None => false,
Some(v) => v.as_bool().unwrap_or_default(),
None => fallback,
Some(v) => v.as_bool().unwrap_or(fallback),
}
}

View File

@@ -6,33 +6,25 @@ use crate::encoding::read_response_body;
use crate::grpc::metadata_to_map;
use crate::http_request::send_http_request;
use crate::notifications::YaakNotifier;
use crate::render::{render_grpc_request, render_http_request, render_json_value, render_template};
use crate::render::{render_grpc_request, render_template};
use crate::template_callback::PluginTemplateCallback;
use crate::updates::{UpdateMode, YaakUpdater};
use crate::window_menu::app_menu;
use chrono::Utc;
use eventsource_client::{EventParser, SSE};
use log::{debug, error, info, warn};
use log::{debug, error, warn};
use rand::random;
use regex::Regex;
use serde::Serialize;
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
use std::fs::{create_dir_all, File};
use std::path::PathBuf;
use std::process::exit;
use std::str::FromStr;
use std::time::Duration;
use std::{fs, panic};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::{AppHandle, Emitter, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{AppHandle, Emitter, RunEvent, State, WebviewWindow};
use tauri::{Listener, Runtime};
use tauri::{Manager, WindowEvent};
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_log::fern::colors::ColoredLevelConfig;
use tauri_plugin_log::{Builder, Target, TargetKind};
use tauri_plugin_opener::OpenerExt;
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
use tokio::fs::read_to_string;
use tokio::sync::Mutex;
@@ -51,28 +43,24 @@ use yaak_models::queries::{
delete_all_http_responses_for_workspace, delete_cookie_jar, delete_environment, delete_folder,
delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response,
delete_plugin, delete_workspace, duplicate_folder, duplicate_grpc_request,
duplicate_http_request, ensure_base_environment, generate_id, generate_model_id,
get_base_environment, get_cookie_jar, get_environment, get_folder, get_grpc_connection,
get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
get_or_create_settings, get_or_create_workspace_meta, get_plugin, get_workspace,
get_workspace_export_resources, list_cookie_jars, list_environments, list_folders,
list_grpc_connections_for_workspace, list_grpc_events, list_grpc_requests, list_http_requests,
list_http_responses_for_request, list_http_responses_for_workspace, list_key_values_raw,
list_plugins, list_workspaces, set_key_value_raw, update_response_if_id, update_settings,
upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
duplicate_http_request, ensure_base_environment, generate_model_id, get_base_environment,
get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request,
get_http_request, get_http_response, get_key_value_raw, get_or_create_settings,
get_or_create_workspace_meta, get_plugin, get_workspace, get_workspace_export_resources,
list_cookie_jars, list_environments, list_folders, list_grpc_connections_for_workspace,
list_grpc_events, list_grpc_requests, list_http_requests, list_http_responses_for_workspace,
list_key_values_raw, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id,
update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
upsert_workspace_meta, BatchUpsertResult, UpdateSource,
};
use yaak_plugins::events::{
BootResponse, CallHttpAuthenticationRequest, CallHttpRequestActionRequest, Color,
FilterResponse, FindHttpResponsesResponse, GetHttpAuthenticationResponse,
GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse,
HttpHeader, Icon, InternalEvent, InternalEventPayload, PromptTextResponse,
RenderHttpRequestResponse, RenderPurpose, SendHttpRequestResponse, ShowToastRequest,
TemplateRenderResponse, WindowContext,
BootResponse, CallHttpAuthenticationRequest, CallHttpRequestActionRequest, FilterResponse,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, HttpHeader, InternalEvent,
InternalEventPayload, JsonPrimitive, RenderPurpose, WindowContext,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle;
use yaak_sse::sse::ServerSentEvent;
use yaak_templates::format::format_json;
use yaak_templates::{Parser, Tokens};
@@ -82,11 +70,13 @@ mod encoding;
mod grpc;
mod http_request;
mod notifications;
mod plugin_events;
mod render;
#[cfg(target_os = "macos")]
mod tauri_plugin_mac_window;
mod template_callback;
mod updates;
mod window;
mod window_menu;
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
@@ -242,7 +232,8 @@ async fn cmd_grpc_go<R: Runtime>(
if let Some(auth_name) = request.authentication_type.clone() {
let auth = request.authentication.clone();
let plugin_req = CallHttpAuthenticationRequest {
config: serde_json::to_value(&auth).unwrap().as_object().unwrap().to_owned(),
context_id: format!("{:x}", md5::compute(request_id.to_string())),
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
method: "POST".to_string(),
url: request.url.clone(),
headers: metadata
@@ -969,15 +960,31 @@ async fn cmd_template_functions<R: Runtime>(
}
#[tauri::command]
async fn cmd_get_http_authentication<R: Runtime>(
async fn cmd_get_http_authentication_summaries<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<GetHttpAuthenticationResponse>, String> {
let results =
plugin_manager.get_http_authentication(&window).await.map_err(|e| e.to_string())?;
) -> Result<Vec<GetHttpAuthenticationSummaryResponse>, String> {
let results = plugin_manager
.get_http_authentication_summaries(&window)
.await
.map_err(|e| e.to_string())?;
Ok(results.into_iter().map(|(_, a)| a).collect())
}
#[tauri::command]
async fn cmd_get_http_authentication_config<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
auth_name: &str,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
) -> Result<GetHttpAuthenticationConfigResponse, String> {
plugin_manager
.get_http_authentication_config(&window, auth_name, values, request_id)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_call_http_request_action<R: Runtime>(
window: WebviewWindow<R>,
@@ -987,6 +994,21 @@ async fn cmd_call_http_request_action<R: Runtime>(
plugin_manager.call_http_request_action(&window, req).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_call_http_authentication_action<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
auth_name: &str,
action_index: i32,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
) -> Result<(), String> {
plugin_manager
.call_http_authentication_action(&window, auth_name, action_index, values, request_id)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_curl_to_request<R: Runtime>(
window: WebviewWindow<R>,
@@ -1175,7 +1197,7 @@ async fn cmd_install_plugin<R: Runtime>(
window: WebviewWindow<R>,
) -> Result<Plugin, String> {
plugin_manager
.add_plugin_by_dir(WindowContext::from_window(&window), &directory, true)
.add_plugin_by_dir(&WindowContext::from_window(&window), &directory, true)
.await
.map_err(|e| e.to_string())?;
@@ -1205,7 +1227,7 @@ async fn cmd_uninstall_plugin<R: Runtime>(
.map_err(|e| e.to_string())?;
plugin_manager
.uninstall(WindowContext::from_window(&window), plugin.directory.as_str())
.uninstall(&WindowContext::from_window(&window), plugin.directory.as_str())
.await
.map_err(|e| e.to_string())?;
@@ -1458,7 +1480,7 @@ async fn cmd_reload_plugins<R: Runtime>(
plugin_manager: State<'_, PluginManager>,
) -> Result<(), String> {
plugin_manager
.initialize_all_plugins(window.app_handle(), WindowContext::from_window(&window))
.initialize_all_plugins(window.app_handle(), &WindowContext::from_window(&window))
.await
.map_err(|e| e.to_string())?;
Ok(())
@@ -1656,15 +1678,17 @@ async fn cmd_new_child_window(
current_pos.y + current_size.height / 2.0 - inner_size.1 / 2.0,
);
let config = CreateWindowConfig {
let config = window::CreateWindowConfig {
label: label.as_str(),
title,
url,
inner_size,
position,
inner_size: Some(inner_size),
position: Some(position),
navigation_tx: None,
hide_titlebar: true,
};
let child_window = create_window(&app_handle, config);
let child_window = window::create_window(&app_handle, config);
// NOTE: These listeners will remain active even when the windows close. Unfortunately,
// there's no way to unlisten to events for now, so we just have to be defensive.
@@ -1830,6 +1854,7 @@ pub fn run() {
Ok(())
})
.invoke_handler(tauri::generate_handler![
cmd_call_http_authentication_action,
cmd_call_http_request_action,
cmd_check_for_updates,
cmd_create_cookie_jar,
@@ -1859,7 +1884,8 @@ pub fn run() {
cmd_get_environment,
cmd_get_folder,
cmd_get_grpc_request,
cmd_get_http_authentication,
cmd_get_http_authentication_summaries,
cmd_get_http_authentication_config,
cmd_get_http_request,
cmd_get_key_value,
cmd_get_settings,
@@ -1998,113 +2024,21 @@ fn create_main_window(handle: &AppHandle, url: &str) -> WebviewWindow {
}
.expect("Failed to generate label for new window");
let config = CreateWindowConfig {
let config = window::CreateWindowConfig {
url,
label: label.as_str(),
title: "Yaak",
inner_size: (DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT),
position: (
inner_size: Some((DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)),
position: Some((
// Offset by random amount so it's easier to differentiate
100.0 + random::<f64>() * 20.0,
100.0 + random::<f64>() * 20.0,
),
)),
navigation_tx: None,
hide_titlebar: true,
};
create_window(handle, config)
}
struct CreateWindowConfig<'s> {
url: &'s str,
label: &'s str,
title: &'s str,
inner_size: (f64, f64),
position: (f64, f64),
}
fn create_window(handle: &AppHandle, config: CreateWindowConfig) -> WebviewWindow {
#[allow(unused_variables)]
let menu = app_menu(handle).unwrap();
// This causes the window to not be clickable (in AppImage), so disable on Linux
#[cfg(not(target_os = "linux"))]
handle.set_menu(menu).expect("Failed to set app menu");
info!("Create new window label={}", config.label);
let mut win_builder =
tauri::WebviewWindowBuilder::new(handle, config.label, WebviewUrl::App(config.url.into()))
.title(config.title)
.resizable(true)
.visible(false) // To prevent theme flashing, the frontend code calls show() immediately after configuring the theme
.fullscreen(false)
.disable_drag_drop_handler() // Required for frontend Dnd on windows
.inner_size(config.inner_size.0, config.inner_size.1)
.position(config.position.0, config.position.1)
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
// Add macOS-only things
#[cfg(target_os = "macos")]
{
win_builder = win_builder.hidden_title(true).title_bar_style(TitleBarStyle::Overlay);
}
// Add non-MacOS things
#[cfg(not(target_os = "macos"))]
{
// Doesn't seem to work from Rust, here, so we do it in main.tsx
win_builder = win_builder.decorations(false);
}
if let Some(w) = handle.webview_windows().get(config.label) {
info!("Webview with label {} already exists. Focusing existing", config.label);
w.set_focus().unwrap();
return w.to_owned();
}
let win = win_builder.build().unwrap();
let webview_window = win.clone();
win.on_menu_event(move |w, event| {
if !w.is_focused().unwrap() {
return;
}
let event_id = event.id().0.as_str();
match event_id {
"quit" => exit(0),
"close" => w.close().unwrap(),
"zoom_reset" => w.emit("zoom_reset", true).unwrap(),
"zoom_in" => w.emit("zoom_in", true).unwrap(),
"zoom_out" => w.emit("zoom_out", true).unwrap(),
"settings" => w.emit("settings", true).unwrap(),
"open_feedback" => {
if let Err(e) =
w.app_handle().opener().open_url("https://yaak.app/feedback", None::<&str>)
{
warn!("Failed to open feedback {e:?}")
}
}
// Commands for development
"dev.reset_size" => webview_window
.set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT))
.unwrap(),
"dev.refresh" => webview_window.eval("location.reload()").unwrap(),
"dev.generate_theme_css" => {
w.emit("generate_theme_css", true).unwrap();
}
"dev.toggle_devtools" => {
if webview_window.is_devtools_open() {
webview_window.close_devtools();
} else {
webview_window.open_devtools();
}
}
_ => {}
}
});
win
window::create_window(handle, config)
}
async fn get_update_mode(h: &AppHandle) -> UpdateMode {
@@ -2140,36 +2074,24 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
// We might have recursive back-and-forth calls between app and plugin, so we don't
// want to block here
tauri::async_runtime::spawn(async move {
handle_plugin_event(&app_handle, &event, &plugin).await;
crate::plugin_events::handle_plugin_event(&app_handle, &event, &plugin).await;
});
}
plugin_manager.unsubscribe(rx_id.as_str()).await;
});
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct FrontendCall<T: Serialize + Clone> {
args: T,
reply_id: String,
}
async fn call_frontend<T: Serialize + Clone, R: Runtime>(
async fn call_frontend<R: Runtime>(
window: WebviewWindow<R>,
event_name: &str,
args: T,
) -> PromptTextResponse {
let reply_id = format!("{event_name}_reply_{}", generate_id());
let payload = FrontendCall {
args,
reply_id: reply_id.clone(),
};
window.emit_to(window.label(), event_name, payload).unwrap();
let (tx, mut rx) = tokio::sync::watch::channel(PromptTextResponse::default());
event: &InternalEvent,
) -> Option<InternalEventPayload> {
window.emit_to(window.label(), "plugin_event", event.clone()).unwrap();
let (tx, mut rx) = tokio::sync::watch::channel(None);
let reply_id = event.id.clone();
let event_id = window.clone().listen(reply_id, move |ev| {
let resp: PromptTextResponse = serde_json::from_str(ev.payload()).unwrap();
if let Err(e) = tx.send(resp) {
let resp: InternalEvent = serde_json::from_str(ev.payload()).unwrap();
if let Err(e) = tx.send(Some(resp.payload)) {
warn!("Failed to prompt for text {e:?}");
}
});
@@ -2181,180 +2103,7 @@ async fn call_frontend<T: Serialize + Clone, R: Runtime>(
window.unlisten(event_id);
let v = rx.borrow();
v.clone()
}
async fn handle_plugin_event<R: Runtime>(
app_handle: &AppHandle<R>,
event: &InternalEvent,
plugin_handle: &PluginHandle,
) {
// info!("Got event to app {}", event.id);
let window_context = event.window_context.to_owned();
let response_event: Option<InternalEventPayload> = match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => {
app_handle
.clipboard()
.write_text(req.text.as_str())
.expect("Failed to write text to clipboard");
None
}
InternalEventPayload::ShowToastRequest(req) => {
match window_context {
WindowContext::Label { label } => app_handle
.emit_to(label, "show_toast", req)
.expect("Failed to emit show_toast to window"),
_ => app_handle.emit("show_toast", req).expect("Failed to emit show_toast"),
};
None
}
InternalEventPayload::PromptTextRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
let resp = call_frontend(window, "show_prompt", req).await;
Some(InternalEventPayload::PromptTextResponse(resp))
}
InternalEventPayload::FindHttpResponsesRequest(req) => {
let http_responses = list_http_responses_for_request(
app_handle,
req.request_id.as_str(),
req.limit.map(|l| l as i64),
)
.await
.unwrap_or_default();
Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
}))
}
InternalEventPayload::GetHttpRequestByIdRequest(req) => {
let http_request = get_http_request(app_handle, req.id.as_str()).await.unwrap();
Some(InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse {
http_request,
}))
}
InternalEventPayload::RenderHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render http request");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let http_request = render_http_request(
&req.http_request,
&base_environment,
environment.as_ref(),
&cb,
)
.await;
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request,
}))
}
InternalEventPayload::TemplateRenderRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let data =
render_json_value(req.data, &base_environment, environment.as_ref(), &cb).await;
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
}
InternalEventPayload::ErrorResponse(resp) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let toast_event = plugin_handle.build_event_to_send(
WindowContext::from_window(&window),
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: resp.error,
color: Some(Color::Danger),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::ReloadResponse(_) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let plugins = list_plugins(app_handle).await.unwrap();
for plugin in plugins {
if plugin.directory != plugin_handle.dir {
continue;
}
let new_plugin = Plugin {
updated_at: Utc::now().naive_utc(), // TODO: Add reloaded_at field to use instead
..plugin
};
upsert_plugin(&window, new_plugin, &UpdateSource::Plugin).await.unwrap();
}
let toast_event = plugin_handle.build_event_to_send(
WindowContext::from_window(&window),
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!("Reloaded plugin {}", plugin_handle.dir),
icon: Some(Icon::Info),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::SendHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for sending HTTP request");
let cookie_jar = cookie_jar_from_window(&window).await;
let environment = environment_from_window(&window).await;
let resp = create_default_http_response(
&window,
req.http_request.id.as_str(),
&UpdateSource::Plugin,
)
.await
.unwrap();
let result = send_http_request(
&window,
&req.http_request,
&resp,
environment,
cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel
)
.await;
let http_response = match result {
Ok(r) => r,
Err(_e) => return,
};
Some(InternalEventPayload::SendHttpRequestResponse(SendHttpRequestResponse {
http_response,
}))
}
_ => None,
};
if let Some(e) = response_event {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
if let Err(e) = plugin_manager.reply(&event, &e).await {
warn!("Failed to reply to plugin manager: {:?}", e)
}
}
v.to_owned()
}
fn get_window_from_window_context<R: Runtime>(

View File

@@ -0,0 +1,265 @@
use crate::http_request::send_http_request;
use crate::render::{render_http_request, render_json_value};
use crate::template_callback::PluginTemplateCallback;
use crate::window::{create_window, CreateWindowConfig};
use crate::{
call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_window_context,
workspace_from_window,
};
use chrono::Utc;
use log::warn;
use tauri::{AppHandle, Emitter, Manager, Runtime, State};
use tauri_plugin_clipboard_manager::ClipboardExt;
use yaak_models::models::{HttpResponse, Plugin};
use yaak_models::queries::{
create_default_http_response, delete_plugin_key_value, get_base_environment, get_http_request,
get_plugin_key_value, list_http_responses_for_request, list_plugins, set_plugin_key_value,
upsert_plugin, UpdateSource,
};
use yaak_plugins::events::{
Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse,
GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, InternalEventPayload,
RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest,
TemplateRenderResponse, WindowContext, WindowNavigateEvent,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle;
pub(crate) async fn handle_plugin_event<R: Runtime>(
app_handle: &AppHandle<R>,
event: &InternalEvent,
plugin_handle: &PluginHandle,
) {
// info!("Got event to app {}", event.id);
let window_context = event.window_context.to_owned();
let response_event: Option<InternalEventPayload> = match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => {
app_handle
.clipboard()
.write_text(req.text.as_str())
.expect("Failed to write text to clipboard");
Some(InternalEventPayload::CopyTextResponse(EmptyPayload {}))
}
InternalEventPayload::ShowToastRequest(req) => {
match window_context {
WindowContext::Label { label } => app_handle
.emit_to(label, "show_toast", req)
.expect("Failed to emit show_toast to window"),
_ => app_handle.emit("show_toast", req).expect("Failed to emit show_toast"),
};
Some(InternalEventPayload::ShowToastResponse(EmptyPayload {}))
}
InternalEventPayload::PromptTextRequest(_) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
call_frontend(window, event).await
}
InternalEventPayload::FindHttpResponsesRequest(req) => {
let http_responses = list_http_responses_for_request(
app_handle,
req.request_id.as_str(),
req.limit.map(|l| l as i64),
)
.await
.unwrap_or_default();
Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
}))
}
InternalEventPayload::GetHttpRequestByIdRequest(req) => {
let http_request = get_http_request(app_handle, req.id.as_str()).await.unwrap();
Some(InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse {
http_request,
}))
}
InternalEventPayload::RenderHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render http request");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let http_request = render_http_request(
&req.http_request,
&base_environment,
environment.as_ref(),
&cb,
)
.await;
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request,
}))
}
InternalEventPayload::TemplateRenderRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let data =
render_json_value(req.data, &base_environment, environment.as_ref(), &cb).await;
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
}
InternalEventPayload::ErrorResponse(resp) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let toast_event = plugin_handle.build_event_to_send(
&WindowContext::from_window(&window),
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!(
"Plugin error from {}: {}",
plugin_handle.name().await,
resp.error
),
color: Some(Color::Danger),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::ReloadResponse(_) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let plugins = list_plugins(app_handle).await.unwrap();
for plugin in plugins {
if plugin.directory != plugin_handle.dir {
continue;
}
let new_plugin = Plugin {
updated_at: Utc::now().naive_utc(), // TODO: Add reloaded_at field to use instead
..plugin
};
upsert_plugin(&window, new_plugin, &UpdateSource::Plugin).await.unwrap();
}
let toast_event = plugin_handle.build_event_to_send(
&WindowContext::from_window(&window),
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!("Reloaded plugin {}", plugin_handle.dir),
icon: Some(Icon::Info),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::SendHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for sending HTTP request");
let mut http_request = req.http_request;
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let cookie_jar = cookie_jar_from_window(&window).await;
let environment = environment_from_window(&window).await;
if http_request.workspace_id.is_empty() {
http_request.workspace_id = workspace.id;
}
let resp = if http_request.id.is_empty() {
HttpResponse::new()
} else {
create_default_http_response(
&window,
http_request.id.as_str(),
&UpdateSource::Plugin,
)
.await
.unwrap()
};
let result = send_http_request(
&window,
&http_request,
&resp,
environment,
cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel
)
.await;
let http_response = match result {
Ok(r) => r,
Err(_e) => return,
};
Some(InternalEventPayload::SendHttpRequestResponse(SendHttpRequestResponse {
http_response,
}))
}
InternalEventPayload::OpenWindowRequest(req) => {
let label = req.label;
let (tx, mut rx) = tokio::sync::mpsc::channel(128);
let win_config = CreateWindowConfig {
url: &req.url,
label: &label.clone(),
title: &req.title.unwrap_or_default(),
navigation_tx: Some(tx),
inner_size: req.size.map(|s| (s.width, s.height)),
position: None,
hide_titlebar: false,
};
create_window(app_handle, win_config);
let event_id = event.id.clone();
let plugin_handle = plugin_handle.clone();
tauri::async_runtime::spawn(async move {
while let Some(url) = rx.recv().await {
let label = label.clone();
let url = url.to_string();
let event_to_send = plugin_handle.build_event_to_send(
&WindowContext::Label { label },
&InternalEventPayload::WindowNavigateEvent(WindowNavigateEvent { url }),
Some(event_id.clone()),
);
plugin_handle.send(&event_to_send).await.unwrap();
}
});
None
}
InternalEventPayload::CloseWindowRequest(req) => {
if let Some(window) = app_handle.webview_windows().get(&req.label) {
window.close().expect("Failed to close window");
}
None
}
InternalEventPayload::SetKeyValueRequest(req) => {
let name = plugin_handle.name().await;
set_plugin_key_value(app_handle, &name, &req.key, &req.value).await;
Some(InternalEventPayload::SetKeyValueResponse(SetKeyValueResponse {}))
}
InternalEventPayload::GetKeyValueRequest(req) => {
let name = plugin_handle.name().await;
let value = get_plugin_key_value(app_handle, &name, &req.key).await.map(|v| v.value);
Some(InternalEventPayload::GetKeyValueResponse(GetKeyValueResponse { value }))
}
InternalEventPayload::DeleteKeyValueRequest(req) => {
let name = plugin_handle.name().await;
let deleted = delete_plugin_key_value(app_handle, &name, &req.key).await;
Some(InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted }))
}
_ => None,
};
if let Some(e) = response_event {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
if let Err(e) = plugin_manager.reply(&event, &e).await {
warn!("Failed to reply to plugin manager: {:?}", e)
}
}
}

View File

@@ -28,14 +28,13 @@ impl PluginTemplateCallback {
impl TemplateCallback for PluginTemplateCallback {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String, String> {
let window_context = self.window_context.to_owned();
// The beta named the function `Response` but was changed in stable.
// Keep this here for a while because there's no easy way to migrate
let fn_name = if fn_name == "Response" { "response" } else { fn_name };
let function = self
.plugin_manager
.get_template_functions_with_context(window_context.to_owned())
.get_template_functions_with_context(&self.window_context)
.await
.map_err(|e| e.to_string())?
.iter()
@@ -54,6 +53,9 @@ impl TemplateCallback for PluginTemplateCallback {
FormInput::Checkbox(a) => a.base,
FormInput::File(a) => a.base,
FormInput::HttpRequest(a) => a.base,
FormInput::Accordion(_) => continue,
FormInput::Banner(_) => continue,
FormInput::Markdown(_) => continue,
};
if let None = args_with_defaults.get(base.name.as_str()) {
args_with_defaults.insert(base.name, base.default_value.unwrap_or_default());
@@ -63,7 +65,7 @@ impl TemplateCallback for PluginTemplateCallback {
let resp = self
.plugin_manager
.call_template_function(
window_context,
&self.window_context,
fn_name,
args_with_defaults,
self.render_purpose.to_owned(),

129
src-tauri/src/window.rs Normal file
View File

@@ -0,0 +1,129 @@
use crate::window_menu::app_menu;
use crate::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH};
use log::{info, warn};
use std::process::exit;
use tauri::{
AppHandle, Emitter, LogicalSize, Manager, Runtime, TitleBarStyle, WebviewUrl, WebviewWindow,
};
use tauri_plugin_opener::OpenerExt;
use tokio::sync::mpsc;
#[derive(Default, Debug)]
pub(crate) struct CreateWindowConfig<'s> {
pub url: &'s str,
pub label: &'s str,
pub title: &'s str,
pub inner_size: Option<(f64, f64)>,
pub position: Option<(f64, f64)>,
pub navigation_tx: Option<mpsc::Sender<String>>,
pub hide_titlebar: bool,
}
pub(crate) fn create_window<R: Runtime>(
handle: &AppHandle<R>,
config: CreateWindowConfig,
) -> WebviewWindow<R> {
#[allow(unused_variables)]
let menu = app_menu(handle).unwrap();
// This causes the window to not be clickable (in AppImage), so disable on Linux
#[cfg(not(target_os = "linux"))]
handle.set_menu(menu).expect("Failed to set app menu");
info!("Create new window label={}", config.label);
let mut win_builder =
tauri::WebviewWindowBuilder::new(handle, config.label, WebviewUrl::App(config.url.into()))
.title(config.title)
.resizable(true)
.visible(false) // To prevent theme flashing, the frontend code calls show() immediately after configuring the theme
.fullscreen(false)
.disable_drag_drop_handler() // Required for frontend Dnd on windows
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
if let Some((w, h)) = config.inner_size {
win_builder = win_builder.inner_size(w, h);
} else {
win_builder = win_builder.inner_size(600.0, 600.0);
}
if let Some((x, y)) = config.position {
win_builder = win_builder.position(x, y);
} else {
win_builder = win_builder.center();
}
if let Some(tx) = config.navigation_tx {
win_builder = win_builder.on_navigation(move |url| {
let url = url.to_string();
let tx = tx.clone();
tauri::async_runtime::block_on(async move {
tx.send(url).await.unwrap();
});
true
});
}
if config.hide_titlebar {
#[cfg(target_os = "macos")]
{
win_builder = win_builder.hidden_title(true).title_bar_style(TitleBarStyle::Overlay);
}
#[cfg(not(target_os = "macos"))]
{
// Doesn't seem to work from Rust, here, so we do it in main.tsx
win_builder = win_builder.decorations(false);
}
}
if let Some(w) = handle.webview_windows().get(config.label) {
info!("Webview with label {} already exists. Focusing existing", config.label);
w.set_focus().unwrap();
return w.to_owned();
}
let win = win_builder.build().unwrap();
let webview_window = win.clone();
win.on_menu_event(move |w, event| {
if !w.is_focused().unwrap() {
return;
}
let event_id = event.id().0.as_str();
match event_id {
"quit" => exit(0),
"close" => w.close().unwrap(),
"zoom_reset" => w.emit("zoom_reset", true).unwrap(),
"zoom_in" => w.emit("zoom_in", true).unwrap(),
"zoom_out" => w.emit("zoom_out", true).unwrap(),
"settings" => w.emit("settings", true).unwrap(),
"open_feedback" => {
if let Err(e) =
w.app_handle().opener().open_url("https://yaak.app/feedback", None::<&str>)
{
warn!("Failed to open feedback {e:?}")
}
}
// Commands for development
"dev.reset_size" => webview_window
.set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT))
.unwrap(),
"dev.refresh" => webview_window.eval("location.reload()").unwrap(),
"dev.generate_theme_css" => {
w.emit("generate_theme_css", true).unwrap();
}
"dev.toggle_devtools" => {
if webview_window.is_devtools_open() {
webview_window.close_devtools();
} else {
webview_window.open_devtools();
}
}
_ => {}
}
});
win
}

View File

@@ -3,9 +3,9 @@ use tauri::menu::{
WINDOW_SUBMENU_ID,
};
pub use tauri::AppHandle;
use tauri::Wry;
use tauri::Runtime;
pub fn app_menu(app_handle: &AppHandle) -> tauri::Result<Menu<Wry>> {
pub fn app_menu<R: Runtime>(app_handle: &AppHandle<R>) -> tauri::Result<Menu<R>> {
let pkg_info = app_handle.package_info();
let config = app_handle.config();
let about_metadata = AboutMetadata {
@@ -37,7 +37,7 @@ pub fn app_menu(app_handle: &AppHandle) -> tauri::Result<Menu<Wry>> {
true,
&[
#[cfg(not(target_os = "macos"))]
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?,
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata.clone()))?,
#[cfg(target_os = "macos")]
&MenuItemBuilder::with_id("open_feedback".to_string(), "Give Feedback")
.build(app_handle)?,

View File

@@ -28,7 +28,7 @@ var plugin = {
name: "basic",
label: "Basic Auth",
shortLabel: "Basic",
config: [{
args: [{
type: "text",
name: "username",
label: "Username",
@@ -40,8 +40,8 @@ var plugin = {
optional: true,
password: true
}],
async onApply(_ctx, args) {
const { username, password } = args.config;
async onApply(_ctx, { values }) {
const { username, password } = values;
const value = "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
return { setHeaders: [{ name: "Authorization", value }] };
}

View File

@@ -28,15 +28,15 @@ var plugin = {
name: "bearer",
label: "Bearer Token",
shortLabel: "Bearer",
config: [{
args: [{
type: "text",
name: "token",
label: "Token",
optional: true,
password: true
}],
async onApply(_ctx, args) {
const { token } = args.config;
async onApply(_ctx, { values }) {
const { token } = values;
const value = `Bearer ${token}`.trim();
return { setHeaders: [{ name: "Authorization", value }] };
}

View File

@@ -3814,21 +3814,22 @@ var plugin = {
name: "jwt",
label: "JWT Bearer",
shortLabel: "JWT",
config: [
args: [
{
type: "select",
name: "algorithm",
label: "Algorithm",
hideLabel: true,
defaultValue: defaultAlgorithm,
options: algorithms.map((value) => ({ name: value === "none" ? "None" : value, value }))
options: algorithms.map((value) => ({ label: value === "none" ? "None" : value, value }))
},
{
type: "editor",
type: "text",
name: "secret",
label: "Secret or Private Key",
password: true,
optional: true,
hideGutter: true
multiLine: true
},
{
type: "checkbox",
@@ -3844,8 +3845,8 @@ var plugin = {
placeholder: "{ }"
}
],
async onApply(_ctx, args) {
const { algorithm, secret: _secret, secretBase64, payload } = args.config;
async onApply(_ctx, { values }) {
const { algorithm, secret: _secret, secretBase64, payload } = values;
const secret = secretBase64 ? Buffer.from(`${_secret}`, "base64") : `${_secret}`;
const token = import_jsonwebtoken.default.sign(`${payload}`, secret, { algorithm });
const value = `Bearer ${token}`;

View File

@@ -0,0 +1,673 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
plugin: () => plugin
});
module.exports = __toCommonJS(src_exports);
// src/grants/authorizationCode.ts
var import_node_crypto = require("node:crypto");
// src/getAccessToken.ts
var import_node_fs = require("node:fs");
async function getAccessToken(ctx, {
accessTokenUrl,
scope,
params,
grantType,
credentialsInBody,
clientId,
clientSecret
}) {
console.log("Getting access token", accessTokenUrl);
const httpRequest = {
method: "POST",
url: accessTokenUrl,
bodyType: "application/x-www-form-urlencoded",
body: {
form: [
{ name: "grant_type", value: grantType },
...params
]
},
headers: [
{ name: "User-Agent", value: "yaak" },
{ name: "Accept", value: "application/x-www-form-urlencoded, application/json" },
{ name: "Content-Type", value: "application/x-www-form-urlencoded" }
]
};
if (scope) httpRequest.body.form.push({ name: "scope", value: scope });
if (credentialsInBody) {
httpRequest.body.form.push({ name: "client_id", value: clientId });
httpRequest.body.form.push({ name: "client_secret", value: clientSecret });
} else {
const value = "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
httpRequest.headers.push({ name: "Authorization", value });
}
const resp = await ctx.httpRequest.send({ httpRequest });
if (resp.status < 200 || resp.status >= 300) {
throw new Error("Failed to fetch access token with status=" + resp.status);
}
const body = (0, import_node_fs.readFileSync)(resp.bodyPath ?? "", "utf8");
let response;
try {
response = JSON.parse(body);
} catch {
response = Object.fromEntries(new URLSearchParams(body));
}
if (response.error) {
throw new Error("Failed to fetch access token with " + response.error);
}
return response;
}
// src/getOrRefreshAccessToken.ts
var import_node_fs2 = require("node:fs");
// src/store.ts
async function storeToken(ctx, contextId, response) {
if (!response.access_token) {
throw new Error(`Token not found in response`);
}
const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1e3 : null;
const token = {
response,
expiresAt
};
await ctx.store.set(tokenStoreKey(contextId), token);
return token;
}
async function getToken(ctx, contextId) {
return ctx.store.get(tokenStoreKey(contextId));
}
async function deleteToken(ctx, contextId) {
return ctx.store.delete(tokenStoreKey(contextId));
}
function tokenStoreKey(context_id) {
return ["token", context_id].join("::");
}
// src/getOrRefreshAccessToken.ts
async function getOrRefreshAccessToken(ctx, contextId, {
scope,
accessTokenUrl,
credentialsInBody,
clientId,
clientSecret,
forceRefresh
}) {
const token = await getToken(ctx, contextId);
if (token == null) {
return null;
}
const now = Date.now() / 1e3;
const isExpired = token.expiresAt && now > token.expiresAt;
if (!isExpired && !forceRefresh) {
return token;
}
if (!token.response.refresh_token) {
return null;
}
const httpRequest = {
method: "POST",
url: accessTokenUrl,
bodyType: "application/x-www-form-urlencoded",
body: {
form: [
{ name: "grant_type", value: "refresh_token" },
{ name: "refresh_token", value: token.response.refresh_token }
]
},
headers: [
{ name: "User-Agent", value: "yaak" },
{ name: "Accept", value: "application/x-www-form-urlencoded, application/json" },
{ name: "Content-Type", value: "application/x-www-form-urlencoded" }
]
};
if (scope) httpRequest.body.form.push({ name: "scope", value: scope });
if (credentialsInBody) {
httpRequest.body.form.push({ name: "client_id", value: clientId });
httpRequest.body.form.push({ name: "client_secret", value: clientSecret });
} else {
const value = "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
httpRequest.headers.push({ name: "Authorization", value });
}
const resp = await ctx.httpRequest.send({ httpRequest });
if (resp.status === 401) {
console.log("Unauthorized refresh_token request");
await deleteToken(ctx, contextId);
return null;
}
if (resp.status < 200 || resp.status >= 300) {
throw new Error("Failed to fetch access token with status=" + resp.status);
}
const body = (0, import_node_fs2.readFileSync)(resp.bodyPath ?? "", "utf8");
let response;
try {
response = JSON.parse(body);
} catch {
response = Object.fromEntries(new URLSearchParams(body));
}
if (response.error) {
throw new Error(`Failed to fetch access token with ${response.error} -> ${response.error_description}`);
}
const newResponse = {
...response,
// Assign a new one or keep the old one,
refresh_token: response.refresh_token ?? token.response.refresh_token
};
return storeToken(ctx, contextId, newResponse);
}
// src/grants/authorizationCode.ts
var PKCE_SHA256 = "S256";
var PKCE_PLAIN = "plain";
var DEFAULT_PKCE_METHOD = PKCE_SHA256;
async function getAuthorizationCode(ctx, contextId, {
authorizationUrl: authorizationUrlRaw,
accessTokenUrl,
clientId,
clientSecret,
redirectUri,
scope,
state,
credentialsInBody,
pkce
}) {
const token = await getOrRefreshAccessToken(ctx, contextId, {
accessTokenUrl,
scope,
clientId,
clientSecret,
credentialsInBody
});
if (token != null) {
return token;
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ""}`);
authorizationUrl.searchParams.set("response_type", "code");
authorizationUrl.searchParams.set("client_id", clientId);
if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri);
if (scope) authorizationUrl.searchParams.set("scope", scope);
if (state) authorizationUrl.searchParams.set("state", state);
if (pkce) {
const verifier = pkce.codeVerifier || createPkceCodeVerifier();
const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD;
authorizationUrl.searchParams.set("code_challenge", createPkceCodeChallenge(verifier, challengeMethod));
authorizationUrl.searchParams.set("code_challenge_method", challengeMethod);
}
return new Promise(async (resolve, reject) => {
const authorizationUrlStr = authorizationUrl.toString();
console.log("Authorizing", authorizationUrlStr);
let { close } = await ctx.window.openUrl({
url: authorizationUrlStr,
label: "oauth-authorization-url",
async onNavigate({ url: urlStr }) {
const url = new URL(urlStr);
if (url.searchParams.has("error")) {
return reject(new Error(`Failed to authorize: ${url.searchParams.get("error")}`));
}
const code = url.searchParams.get("code");
if (!code) {
return;
}
close();
const response = await getAccessToken(ctx, {
grantType: "authorization_code",
accessTokenUrl,
clientId,
clientSecret,
scope,
credentialsInBody,
params: [
{ name: "code", value: code },
...redirectUri ? [{ name: "redirect_uri", value: redirectUri }] : []
]
});
try {
resolve(await storeToken(ctx, contextId, response));
} catch (err) {
reject(err);
}
}
});
});
}
function createPkceCodeVerifier() {
return encodeForPkce((0, import_node_crypto.randomBytes)(32));
}
function createPkceCodeChallenge(verifier, method) {
if (method === "plain") {
return verifier;
}
const hash = encodeForPkce((0, import_node_crypto.createHash)("sha256").update(verifier).digest());
return hash.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
function encodeForPkce(bytes) {
return bytes.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
// src/grants/clientCredentials.ts
async function getClientCredentials(ctx, contextId, {
accessTokenUrl,
clientId,
clientSecret,
scope,
credentialsInBody
}) {
const token = await getToken(ctx, contextId);
if (token) {
}
const response = await getAccessToken(ctx, {
grantType: "client_credentials",
accessTokenUrl,
clientId,
clientSecret,
scope,
credentialsInBody,
params: []
});
return storeToken(ctx, contextId, response);
}
// src/grants/implicit.ts
function getImplicit(ctx, contextId, {
authorizationUrl: authorizationUrlRaw,
responseType,
clientId,
redirectUri,
scope,
state
}) {
return new Promise(async (resolve, reject) => {
const token = await getToken(ctx, contextId);
if (token) {
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ""}`);
authorizationUrl.searchParams.set("response_type", "code");
authorizationUrl.searchParams.set("client_id", clientId);
if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri);
if (scope) authorizationUrl.searchParams.set("scope", scope);
if (state) authorizationUrl.searchParams.set("state", state);
if (responseType.includes("id_token")) {
authorizationUrl.searchParams.set("nonce", String(Math.floor(Math.random() * 9999999999999) + 1));
}
const authorizationUrlStr = authorizationUrl.toString();
let { close } = await ctx.window.openUrl({
url: authorizationUrlStr,
label: "oauth-authorization-url",
async onNavigate({ url: urlStr }) {
const url = new URL(urlStr);
if (url.searchParams.has("error")) {
return reject(Error(`Failed to authorize: ${url.searchParams.get("error")}`));
}
close();
const hash = url.hash.slice(1);
const params = new URLSearchParams(hash);
const idToken = params.get("id_token");
if (idToken) {
params.set("access_token", idToken);
params.delete("id_token");
}
const response = Object.fromEntries(params);
try {
resolve(await storeToken(ctx, contextId, response));
} catch (err) {
reject(err);
}
}
});
});
}
// src/grants/password.ts
async function getPassword(ctx, contextId, {
accessTokenUrl,
clientId,
clientSecret,
username,
password,
credentialsInBody,
scope
}) {
const token = await getOrRefreshAccessToken(ctx, contextId, {
accessTokenUrl,
scope,
clientId,
clientSecret,
credentialsInBody
});
if (token != null) {
return token;
}
const response = await getAccessToken(ctx, {
accessTokenUrl,
clientId,
clientSecret,
scope,
grantType: "password",
credentialsInBody,
params: [
{ name: "username", value: username },
{ name: "password", value: password }
]
});
return storeToken(ctx, contextId, response);
}
// src/index.ts
var grantTypes = [
{ label: "Authorization Code", value: "authorization_code" },
{ label: "Implicit", value: "implicit" },
{ label: "Resource Owner Password Credential", value: "password" },
{ label: "Client Credentials", value: "client_credentials" }
];
var defaultGrantType = grantTypes[0].value;
function hiddenIfNot(grantTypes2, ...other) {
return (_ctx, { values }) => {
const hasGrantType = grantTypes2.find((t) => t === String(values.grantType ?? defaultGrantType));
const hasOtherBools = other.every((t) => t(values));
const show = hasGrantType && hasOtherBools;
return { hidden: !show };
};
}
var authorizationUrls = [
"https://github.com/login/oauth/authorize",
"https://account.box.com/api/oauth2/authorize",
"https://accounts.google.com/o/oauth2/v2/auth",
"https://api.imgur.com/oauth2/authorize",
"https://bitly.com/oauth/authorize",
"https://gitlab.example.com/oauth/authorize",
"https://medium.com/m/oauth/authorize",
"https://public-api.wordpress.com/oauth2/authorize",
"https://slack.com/oauth/authorize",
"https://todoist.com/oauth/authorize",
"https://www.dropbox.com/oauth2/authorize",
"https://www.linkedin.com/oauth/v2/authorization",
"https://MY_SHOP.myshopify.com/admin/oauth/access_token"
];
var accessTokenUrls = [
"https://github.com/login/oauth/access_token",
"https://api-ssl.bitly.com/oauth/access_token",
"https://api.box.com/oauth2/token",
"https://api.dropboxapi.com/oauth2/token",
"https://api.imgur.com/oauth2/token",
"https://api.medium.com/v1/tokens",
"https://gitlab.example.com/oauth/token",
"https://public-api.wordpress.com/oauth2/token",
"https://slack.com/api/oauth.access",
"https://todoist.com/oauth/access_token",
"https://www.googleapis.com/oauth2/v4/token",
"https://www.linkedin.com/oauth/v2/accessToken",
"https://MY_SHOP.myshopify.com/admin/oauth/authorize"
];
var plugin = {
authentication: {
name: "oauth2",
label: "OAuth 2.0",
shortLabel: "OAuth 2",
actions: [
{
label: "Copy Current Token",
name: "copyCurrentToken",
icon: "copy",
async onSelect(ctx, { contextId }) {
const token = await getToken(ctx, contextId);
if (token == null) {
await ctx.toast.show({ message: "No token to copy", color: "warning" });
} else {
await ctx.clipboard.copyText(token.response.access_token);
await ctx.toast.show({ message: "Token copied to clipboard", icon: "copy", color: "success" });
}
}
},
{
label: "Delete Token",
name: "clearToken",
icon: "trash",
async onSelect(ctx, { contextId }) {
if (await deleteToken(ctx, contextId)) {
await ctx.toast.show({ message: "Token deleted", color: "success" });
} else {
await ctx.toast.show({ message: "No token to delete", color: "warning" });
}
}
}
],
args: [
{
type: "select",
name: "grantType",
label: "Grant Type",
hideLabel: true,
defaultValue: defaultGrantType,
options: grantTypes
},
// Always-present fields
{ type: "text", name: "clientId", label: "Client ID" },
{
type: "text",
name: "clientSecret",
label: "Client Secret",
password: true,
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"])
},
{
type: "text",
name: "authorizationUrl",
label: "Authorization URL",
dynamic: hiddenIfNot(["authorization_code", "implicit"]),
placeholder: authorizationUrls[0],
completionOptions: authorizationUrls.map((url) => ({ label: url, value: url }))
},
{
type: "text",
name: "accessTokenUrl",
label: "Access Token URL",
placeholder: accessTokenUrls[0],
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"]),
completionOptions: accessTokenUrls.map((url) => ({ label: url, value: url }))
},
{
type: "text",
name: "redirectUri",
label: "Redirect URI",
optional: true,
dynamic: hiddenIfNot(["authorization_code", "implicit"])
},
{
type: "text",
name: "state",
label: "State",
optional: true,
dynamic: hiddenIfNot(["authorization_code", "implicit"])
},
{
type: "checkbox",
name: "usePkce",
label: "Use PKCE",
dynamic: hiddenIfNot(["authorization_code"])
},
{
type: "select",
name: "pkceChallengeMethod",
label: "Code Challenge Method",
options: [{ label: "SHA-256", value: PKCE_SHA256 }, { label: "Plain", value: PKCE_PLAIN }],
defaultValue: DEFAULT_PKCE_METHOD,
dynamic: hiddenIfNot(["authorization_code"], ({ usePkce }) => !!usePkce)
},
{
type: "text",
name: "pkceCodeVerifier",
label: "Code Verifier",
placeholder: "Automatically generated if not provided",
optional: true,
dynamic: hiddenIfNot(["authorization_code"], ({ usePkce }) => !!usePkce)
},
{
type: "text",
name: "username",
label: "Username",
optional: true,
dynamic: hiddenIfNot(["password"])
},
{
type: "text",
name: "password",
label: "Password",
password: true,
optional: true,
dynamic: hiddenIfNot(["password"])
},
{
type: "select",
name: "responseType",
label: "Response Type",
defaultValue: "token",
options: [
{ label: "Access Token", value: "token" },
{ label: "ID Token", value: "id_token" },
{ label: "ID and Access Token", value: "id_token token" }
],
dynamic: hiddenIfNot(["implicit"])
},
{
type: "accordion",
label: "Advanced",
inputs: [
{ type: "text", name: "scope", label: "Scope", optional: true },
{ type: "text", name: "headerPrefix", label: "Header Prefix", optional: true, defaultValue: "Bearer" },
{
type: "select",
name: "credentials",
label: "Send Credentials",
defaultValue: "body",
options: [
{ label: "In Request Body", value: "body" },
{ label: "As Basic Authentication", value: "basic" }
]
}
]
},
{
type: "accordion",
label: "Access Token Response",
async dynamic(ctx, { contextId }) {
const token = await getToken(ctx, contextId);
if (token == null) {
return { hidden: true };
}
return {
label: "Access Token Response",
inputs: [
{
type: "editor",
defaultValue: JSON.stringify(token.response, null, 2),
hideLabel: true,
readOnly: true,
language: "json"
}
]
};
}
}
],
async onApply(ctx, { values, contextId }) {
const headerPrefix = optionalString(values, "headerPrefix") ?? "";
const grantType = requiredString(values, "grantType");
const credentialsInBody = values.credentials === "body";
console.log("Performing OAuth", values);
let token;
if (grantType === "authorization_code") {
const authorizationUrl = requiredString(values, "authorizationUrl");
const accessTokenUrl = requiredString(values, "accessTokenUrl");
token = await getAuthorizationCode(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
clientId: requiredString(values, "clientId"),
clientSecret: requiredString(values, "clientSecret"),
redirectUri: optionalString(values, "redirectUri"),
scope: optionalString(values, "scope"),
state: optionalString(values, "state"),
credentialsInBody,
pkce: values.usePkce ? {
challengeMethod: requiredString(values, "pkceChallengeMethod"),
codeVerifier: optionalString(values, "pkceCodeVerifier")
} : null
});
} else if (grantType === "implicit") {
const authorizationUrl = requiredString(values, "authorizationUrl");
token = await getImplicit(ctx, contextId, {
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
clientId: requiredString(values, "clientId"),
redirectUri: optionalString(values, "redirectUri"),
responseType: requiredString(values, "responseType"),
scope: optionalString(values, "scope"),
state: optionalString(values, "state")
});
} else if (grantType === "client_credentials") {
const accessTokenUrl = requiredString(values, "accessTokenUrl");
token = await getClientCredentials(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
clientId: requiredString(values, "clientId"),
clientSecret: requiredString(values, "clientSecret"),
scope: optionalString(values, "scope"),
credentialsInBody
});
} else if (grantType === "password") {
const accessTokenUrl = requiredString(values, "accessTokenUrl");
token = await getPassword(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
clientId: requiredString(values, "clientId"),
clientSecret: requiredString(values, "clientSecret"),
username: requiredString(values, "username"),
password: requiredString(values, "password"),
scope: optionalString(values, "scope"),
credentialsInBody
});
} else {
throw new Error("Invalid grant type " + grantType);
}
const headerValue = `${headerPrefix} ${token.response.access_token}`.trim();
return {
setHeaders: [{
name: "Authorization",
value: headerValue
}]
};
}
}
};
function optionalString(values, name) {
const arg = values[name];
if (arg == null || arg == "") return null;
return `${arg}`;
}
function requiredString(values, name) {
const arg = optionalString(values, name);
if (!arg) throw new Error(`Missing required argument ${name}`);
return arg;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

@@ -0,0 +1,9 @@
{
"name": "@yaakapp/auth-oauth2",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
}
}

View File

@@ -27,14 +27,14 @@ module.exports = __toCommonJS(src_exports);
var NEWLINE = "\\\n ";
var plugin = {
httpRequestActions: [{
key: "export-curl",
name: "export-curl",
label: "Copy as Curl",
icon: "copy",
async onSelect(ctx, args) {
const rendered_request = await ctx.httpRequest.render({ httpRequest: args.httpRequest, purpose: "preview" });
const data = await convertToCurl(rendered_request);
ctx.clipboard.copyText(data);
ctx.toast.show({ message: "Curl copied to clipboard", icon: "copy" });
await ctx.clipboard.copyText(data);
await ctx.toast.show({ message: "Curl copied to clipboard", icon: "copy", color: "success" });
}
}]
};

View File

@@ -8824,8 +8824,8 @@ var behaviorArg = {
label: "Sending Behavior",
defaultValue: "smart",
options: [
{ name: "When no responses", value: "smart" },
{ name: "Always", value: "always" }
{ label: "When no responses", value: "smart" },
{ label: "Always", value: "always" }
]
};
var requestArg = {

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 CheckActivationRequestPayload = { activationId: string, };

View File

@@ -15,7 +15,7 @@ const TRIAL_SECONDS: u64 = 3600 * 24 * 14;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct CheckActivationRequestPayload {
pub activation_id: String,
}

View File

@@ -15,7 +15,7 @@ sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] }
sea-query-rusqlite = { version = "0.7.0", features = ["with-chrono"] }
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.122"
sqlx = { version = "0.8.0", features = ["sqlite", "runtime-tokio-rustls"] }
sqlx = { version = "0.8.0", default-features = false, features = ["migrate", "sqlite", "runtime-tokio-rustls"] }
tauri = { workspace = true }
thiserror = "1.0.63"
thiserror = "2.0.11"
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }

View File

@@ -1 +1 @@
export * from './bindings/models';
export * from './bindings/gen_models';

View File

@@ -10,7 +10,7 @@ use ts_rs::TS;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", tag = "type")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub enum ProxySetting {
Enabled {
http: String,
@@ -22,7 +22,7 @@ pub enum ProxySetting {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct ProxySettingAuth {
pub user: String,
pub password: String,
@@ -30,7 +30,7 @@ pub struct ProxySettingAuth {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub enum EditorKeymap {
Default,
Vim,
@@ -72,7 +72,7 @@ impl Default for EditorKeymap {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct Settings {
#[ts(type = "\"settings\"")]
pub model: String,
@@ -149,7 +149,7 @@ impl<'s> TryFrom<&Row<'s>> for Settings {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct Workspace {
#[ts(type = "\"workspace\"")]
pub model: String,
@@ -215,7 +215,7 @@ impl Workspace {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct WorkspaceMeta {
#[ts(type = "\"workspace_meta\"")]
pub model: String,
@@ -255,7 +255,7 @@ impl<'s> TryFrom<&Row<'s>> for WorkspaceMeta {
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
enum CookieDomain {
HostOnly(String),
Suffix(String),
@@ -264,14 +264,14 @@ enum CookieDomain {
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
enum CookieExpires {
AtUtc(String),
SessionEnd,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct Cookie {
raw_cookie: String,
domain: CookieDomain,
@@ -281,7 +281,7 @@ pub struct Cookie {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct CookieJar {
#[ts(type = "\"cookie_jar\"")]
pub model: String,
@@ -327,7 +327,7 @@ impl<'s> TryFrom<&Row<'s>> for CookieJar {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct Environment {
#[ts(type = "\"environment\"")]
pub model: String,
@@ -376,7 +376,7 @@ impl<'s> TryFrom<&Row<'s>> for Environment {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct EnvironmentVariable {
#[serde(default = "default_true")]
#[ts(optional, as = "Option<bool>")]
@@ -389,7 +389,7 @@ pub struct EnvironmentVariable {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct Folder {
#[ts(type = "\"folder\"")]
pub model: String,
@@ -440,7 +440,7 @@ impl<'s> TryFrom<&Row<'s>> for Folder {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct HttpRequestHeader {
#[serde(default = "default_true")]
#[ts(optional, as = "Option<bool>")]
@@ -453,7 +453,7 @@ pub struct HttpRequestHeader {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct HttpUrlParameter {
#[serde(default = "default_true")]
#[ts(optional, as = "Option<bool>")]
@@ -466,7 +466,7 @@ pub struct HttpUrlParameter {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct HttpRequest {
#[ts(type = "\"http_request\"")]
pub model: String,
@@ -548,7 +548,7 @@ impl<'s> TryFrom<&Row<'s>> for HttpRequest {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct HttpResponseHeader {
pub name: String,
pub value: String,
@@ -556,7 +556,7 @@ pub struct HttpResponseHeader {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub enum HttpResponseState {
Initialized,
Connected,
@@ -571,7 +571,7 @@ impl Default for HttpResponseState {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct HttpResponse {
#[ts(type = "\"http_response\"")]
pub model: String,
@@ -660,7 +660,7 @@ impl HttpResponse {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct GrpcMetadataEntry {
#[serde(default = "default_true")]
#[ts(optional, as = "Option<bool>")]
@@ -673,7 +673,7 @@ pub struct GrpcMetadataEntry {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct GrpcRequest {
#[ts(type = "\"grpc_request\"")]
pub model: String,
@@ -748,7 +748,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcRequest {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub enum GrpcConnectionState {
Initialized,
Connected,
@@ -763,7 +763,7 @@ impl Default for GrpcConnectionState {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct GrpcConnection {
#[ts(type = "\"grpc_connection\"")]
pub model: String,
@@ -831,7 +831,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcConnection {
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub enum GrpcEventType {
Info,
Error,
@@ -849,7 +849,7 @@ impl Default for GrpcEventType {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct GrpcEvent {
#[ts(type = "\"grpc_event\"")]
pub model: String,
@@ -911,7 +911,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcEvent {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct Plugin {
#[ts(type = "\"plugin\"")]
pub model: String,
@@ -959,7 +959,7 @@ impl<'s> TryFrom<&Row<'s>> for Plugin {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct SyncState {
#[ts(type = "\"sync_state\"")]
pub model: String,
@@ -977,7 +977,7 @@ pub struct SyncState {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct SyncHistory {
#[ts(type = "\"sync_history\"")]
pub model: String,
@@ -1029,7 +1029,7 @@ impl<'s> TryFrom<&Row<'s>> for SyncState {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct KeyValue {
#[ts(type = "\"key_value\"")]
pub model: String,
@@ -1069,6 +1069,48 @@ impl<'s> TryFrom<&Row<'s>> for KeyValue {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
pub struct PluginKeyValue {
#[ts(type = "\"plugin_key_value\"")]
pub model: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub plugin_name: String,
pub key: String,
pub value: String,
}
#[derive(Iden)]
pub enum PluginKeyValueIden {
#[iden = "plugin_key_values"]
Table,
Model,
CreatedAt,
UpdatedAt,
PluginName,
Key,
Value,
}
impl<'s> TryFrom<&Row<'s>> for PluginKeyValue {
type Error = rusqlite::Error;
fn try_from(r: &Row<'s>) -> Result<Self, Self::Error> {
Ok(PluginKeyValue {
model: r.get("model")?,
created_at: r.get("created_at")?,
updated_at: r.get("updated_at")?,
plugin_name: r.get("plugin_name")?,
key: r.get("key")?,
value: r.get("value")?,
})
}
}
fn default_true() -> bool {
true
}
@@ -1114,7 +1156,7 @@ impl ModelType {
#[derive(Debug, Clone, Serialize, TS)]
#[serde(rename_all = "camelCase", untagged)]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub enum AnyModel {
CookieJar(CookieJar),
Environment(Environment),

View File

@@ -1,13 +1,6 @@
use crate::error::Error::ModelNotFound;
use crate::error::Result;
use crate::models::{
AnyModel, CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden,
GrpcConnection, GrpcConnectionIden, GrpcConnectionState, GrpcEvent, GrpcEventIden, GrpcRequest,
GrpcRequestIden, HttpRequest, HttpRequestIden, HttpResponse, HttpResponseHeader,
HttpResponseIden, HttpResponseState, KeyValue, KeyValueIden, ModelType, Plugin, PluginIden,
Settings, SettingsIden, SyncState, SyncStateIden, Workspace, WorkspaceIden, WorkspaceMeta,
WorkspaceMetaIden,
};
use crate::models::{AnyModel, CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection, GrpcConnectionIden, GrpcConnectionState, GrpcEvent, GrpcEventIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestIden, HttpResponse, HttpResponseHeader, HttpResponseIden, HttpResponseState, KeyValue, KeyValueIden, ModelType, Plugin, PluginIden, PluginKeyValue, PluginKeyValueIden, Settings, SettingsIden, SyncState, SyncStateIden, Workspace, WorkspaceIden, WorkspaceMeta, WorkspaceMetaIden};
use crate::plugin::SqliteConnection;
use chrono::{NaiveDateTime, Utc};
use log::{debug, error, info, warn};
@@ -165,6 +158,90 @@ pub async fn get_key_value_raw<R: Runtime>(
db.query_row(sql.as_str(), &*params.as_params(), |row| row.try_into()).ok()
}
pub async fn get_plugin_key_value<R: Runtime>(
mgr: &impl Manager<R>,
plugin_name: &str,
key: &str,
) -> Option<PluginKeyValue> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
.from(PluginKeyValueIden::Table)
.column(Asterisk)
.cond_where(
Cond::all()
.add(Expr::col(PluginKeyValueIden::PluginName).eq(plugin_name))
.add(Expr::col(PluginKeyValueIden::Key).eq(key)),
)
.build_rusqlite(SqliteQueryBuilder);
db.query_row(sql.as_str(), &*params.as_params(), |row| row.try_into()).ok()
}
pub async fn set_plugin_key_value<R: Runtime>(
mgr: &impl Manager<R>,
plugin_name: &str,
key: &str,
value: &str,
) -> (PluginKeyValue, bool) {
let existing = get_plugin_key_value(mgr, plugin_name, key).await;
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::insert()
.into_table(PluginKeyValueIden::Table)
.columns([
PluginKeyValueIden::CreatedAt,
PluginKeyValueIden::UpdatedAt,
PluginKeyValueIden::PluginName,
PluginKeyValueIden::Key,
PluginKeyValueIden::Value,
])
.values_panic([
CurrentTimestamp.into(),
CurrentTimestamp.into(),
plugin_name.into(),
key.into(),
value.into(),
])
.on_conflict(
OnConflict::new()
.update_columns([PluginKeyValueIden::UpdatedAt, PluginKeyValueIden::Value])
.to_owned(),
)
.returning_all()
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str()).expect("Failed to prepare PluginKeyValue upsert");
let m: PluginKeyValue = stmt
.query_row(&*params.as_params(), |row| row.try_into())
.expect("Failed to upsert KeyValue");
(m, existing.is_none())
}
pub async fn delete_plugin_key_value<R: Runtime>(
mgr: &impl Manager<R>,
plugin_name: &str,
key: &str,
) -> bool {
if let None = get_plugin_key_value(mgr, plugin_name, key).await {
return false;
}
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::delete()
.from_table(PluginKeyValueIden::Table)
.cond_where(
Cond::all()
.add(Expr::col(PluginKeyValueIden::PluginName).eq(plugin_name))
.add(Expr::col(PluginKeyValueIden::Key).eq(key)),
)
.build_rusqlite(SqliteQueryBuilder);
db.execute(sql.as_str(), &*params.as_params()).expect("Failed to delete PluginKeyValue");
true
}
pub async fn list_workspaces<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<Workspace>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -1999,7 +2076,7 @@ pub fn generate_id() -> String {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub struct ModelPayload {
pub model: AnyModel,
pub window_label: String,
@@ -2008,7 +2085,7 @@ pub struct ModelPayload {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub enum UpdateSource {
Sync,
Window,

View File

@@ -6,17 +6,18 @@ publish = false
[dependencies]
dunce = "1.0.4"
futures-util = "0.3.30"
log = "0.4.21"
md5 = "0.7.0"
path-slash = "0.2.1"
rand = "0.8.5"
regex = "1.10.6"
serde = { version = "1.0.198", features = ["derive"] }
serde_json = "1.0.113"
tauri = { workspace = true }
tauri-plugin-shell = { workspace = true }
tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread", "process"] }
ts-rs = { workspace = true, features = ["import-esm"] }
thiserror = "2.0.7"
yaak-models = { workspace = true }
regex = "1.10.6"
path-slash = "0.2.1"
tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread", "process"] }
tokio-tungstenite = "0.26.1"
futures-util = "0.3.30"
ts-rs = { workspace = true, features = ["import-esm"] }
yaak-models = { workspace = true }

View File

@@ -1,290 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./models.js";
import type { Folder } from "./models.js";
import type { GrpcRequest } from "./models.js";
import type { HttpRequest } from "./models.js";
import type { HttpResponse } from "./models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { Workspace } from "./models.js";
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, };
export type CallHttpAuthenticationRequest = { config: { [key in string]?: JsonValue }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpAuthenticationResponse = {
/**
* HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added.
*/
setHeaders: Array<HttpHeader>, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { key: string, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: string }, };
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
export type CallTemplateFunctionResponse = { value: string | null, };
export type Color = "custom" | "default" | "primary" | "secondary" | "info" | "success" | "notice" | "warning" | "danger";
export type CopyTextRequest = { text: string, };
export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown";
export type EmptyPayload = {};
export type ErrorResponse = { error: string, };
export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
export type ExportHttpRequestResponse = { content: string, };
export type FileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest;
export type FormInputBase = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputCheckbox = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputEditor = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Don't show the editor gutter (line numbers, folds, etc.)
*/
hideGutter?: boolean,
/**
* Language for syntax highlighting
*/
language?: EditorLanguage, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputFile = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputHttpRequest = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputSelect = {
/**
* The options that will be available in the select input
*/
options: Array<FormInputSelectOption>, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputSelectOption = { name: string, value: string, };
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Placeholder for the text input
*/
password?: boolean, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type GetHttpAuthenticationResponse = { name: string, label: string, shortLabel: string, config: Array<FormInput>, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string, };
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpHeader = { name: string, value: string, };
export type HttpRequestAction = { key: string, label: string, icon?: Icon, };
export type Icon = "copy" | "info" | "check_circle" | "alert_triangle" | "_unknown";
export type ImportRequest = { content: string, };
export type ImportResources = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, };
export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "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": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "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_request" } & EmptyPayload | { "type": "get_http_authentication_response" } & GetHttpAuthenticationResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "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": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string,
/**
* Text to add to the cancel button
*/
cancelText?: string,
/**
* Require the user to enter a non-empty value
*/
required?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
export type RenderPurpose = "send" | "preview";
export type SendHttpRequestRequest = { httpRequest: HttpRequest, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, };
export type TemplateFunction = { name: string, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<FormInput>, };
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };

View File

@@ -0,0 +1,405 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { HttpResponse } from "./gen_models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { Workspace } from "./gen_models.js";
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, };
export type CallHttpAuthenticationActionArgs = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type CallHttpAuthenticationActionRequest = { index: number, pluginRefId: string, args: CallHttpAuthenticationActionArgs, };
export type CallHttpAuthenticationRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpAuthenticationResponse = {
/**
* HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added.
*/
setHeaders: Array<HttpHeader>, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: string }, };
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
export type CallTemplateFunctionResponse = { value: string | null, };
export type CloseWindowRequest = { label: string, };
export type Color = "primary" | "secondary" | "info" | "success" | "notice" | "warning" | "danger";
export type CompletionOptionType = "constant" | "variable";
export type Content = { "type": "text", content: string, } | { "type": "markdown", content: string, };
export type CopyTextRequest = { text: string, };
export type DeleteKeyValueRequest = { key: string, };
export type DeleteKeyValueResponse = { deleted: boolean, };
export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown";
export type EmptyPayload = {};
export type ErrorResponse = { error: string, };
export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
export type ExportHttpRequestResponse = { content: string, };
export type FileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown;
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
export type FormInputBanner = { inputs?: Array<FormInput>, hidden?: boolean, color?: Color, };
export type FormInputBase = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputCheckbox = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputEditor = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Don't show the editor gutter (line numbers, folds, etc.)
*/
hideGutter?: boolean,
/**
* Language for syntax highlighting
*/
language?: EditorLanguage, readOnly?: boolean, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputFile = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputHttpRequest = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputMarkdown = { content: string, hidden?: boolean, };
export type FormInputSelect = {
/**
* The options that will be available in the select input
*/
options: Array<FormInputSelectOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputSelectOption = { label: string, value: string, };
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Placeholder for the text input
*/
password?: boolean,
/**
* Whether to allow newlines in the input, like a <textarea/>
*/
multiLine?: boolean, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type GenericCompletionOption = { label: string, detail?: string, info?: string, type?: CompletionOptionType, boost?: number, };
export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
export type GetHttpAuthenticationSummaryResponse = { name: string, label: string, shortLabel: string, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string, };
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
export type GetKeyValueRequest = { key: string, };
export type GetKeyValueResponse = { value?: string, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpAuthenticationAction = { label: string, icon?: Icon, };
export type HttpHeader = { name: string, value: string, };
export type HttpRequestAction = { label: string, icon?: Icon, };
export type Icon = "alert_triangle" | "check" | "check_circle" | "chevron_down" | "copy" | "info" | "pin" | "search" | "trash" | "_unknown";
export type ImportRequest = { content: string, };
export type ImportResources = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, };
export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "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": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "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": "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": "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": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;
export type OpenWindowRequest = { url: string,
/**
* Label for the window. If not provided, a random one will be generated.
*/
label: string, title?: string, size?: WindowSize, };
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string,
/**
* Text to add to the cancel button
*/
cancelText?: string,
/**
* Require the user to enter a non-empty value
*/
required?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
export type RenderPurpose = "send" | "preview";
export type SendHttpRequestRequest = { httpRequest: Partial<HttpRequest>, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
export type SetKeyValueRequest = { key: string, value: string, };
export type SetKeyValueResponse = {};
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, };
export type TemplateFunction = { name: string, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<FormInput>, };
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };
export type WindowNavigateEvent = { url: string, };
export type WindowSize = { width: number, height: number, };

View File

@@ -1,2 +1,2 @@
export * from './bindings/models';
export * from './bindings/events';
export * from './bindings/gen_models';
export * from './bindings/gen_events';

View File

@@ -20,6 +20,9 @@ pub enum Error {
#[error("JSON error: {0}")]
JsonErr(#[from] serde_json::Error),
#[error("Timeout elapsed: {0}")]
TimeoutElapsed(#[from] tokio::time::error::Elapsed),
#[error("Plugin not found: {0}")]
PluginNotFoundErr(String),

View File

@@ -1,5 +1,4 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use tauri::{Runtime, WebviewWindow};
use ts_rs::TS;
@@ -8,7 +7,7 @@ use yaak_models::models::{Environment, Folder, GrpcRequest, HttpRequest, HttpRes
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct InternalEvent {
pub id: String,
pub plugin_ref_id: String,
@@ -29,12 +28,12 @@ pub(crate) struct InternalEventRawPayload {
pub plugin_name: String,
pub reply_id: Option<String>,
pub window_context: WindowContext,
pub payload: Value,
pub payload: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub enum WindowContext {
None,
Label { label: String },
@@ -50,7 +49,7 @@ impl WindowContext {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub enum InternalEventPayload {
BootRequest(BootRequest),
BootResponse(BootResponse),
@@ -84,20 +83,38 @@ pub enum InternalEventPayload {
CallTemplateFunctionRequest(CallTemplateFunctionRequest),
CallTemplateFunctionResponse(CallTemplateFunctionResponse),
GetHttpAuthenticationRequest(EmptyPayload),
GetHttpAuthenticationResponse(GetHttpAuthenticationResponse),
// Http Authentication
GetHttpAuthenticationSummaryRequest(EmptyPayload),
GetHttpAuthenticationSummaryResponse(GetHttpAuthenticationSummaryResponse),
GetHttpAuthenticationConfigRequest(GetHttpAuthenticationConfigRequest),
GetHttpAuthenticationConfigResponse(GetHttpAuthenticationConfigResponse),
CallHttpAuthenticationRequest(CallHttpAuthenticationRequest),
CallHttpAuthenticationResponse(CallHttpAuthenticationResponse),
CallHttpAuthenticationActionRequest(CallHttpAuthenticationActionRequest),
CallHttpAuthenticationActionResponse(EmptyPayload),
CopyTextRequest(CopyTextRequest),
CopyTextResponse(EmptyPayload),
RenderHttpRequestRequest(RenderHttpRequestRequest),
RenderHttpRequestResponse(RenderHttpRequestResponse),
GetKeyValueRequest(GetKeyValueRequest),
GetKeyValueResponse(GetKeyValueResponse),
SetKeyValueRequest(SetKeyValueRequest),
SetKeyValueResponse(SetKeyValueResponse),
DeleteKeyValueRequest(DeleteKeyValueRequest),
DeleteKeyValueResponse(DeleteKeyValueResponse),
OpenWindowRequest(OpenWindowRequest),
WindowNavigateEvent(WindowNavigateEvent),
CloseWindowRequest(CloseWindowRequest),
TemplateRenderRequest(TemplateRenderRequest),
TemplateRenderResponse(TemplateRenderResponse),
ShowToastRequest(ShowToastRequest),
ShowToastResponse(EmptyPayload),
PromptTextRequest(PromptTextRequest),
PromptTextResponse(PromptTextResponse),
@@ -117,19 +134,19 @@ pub enum InternalEventPayload {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)]
#[ts(export, type = "{}", export_to = "events.ts")]
#[ts(export, type = "{}", export_to = "gen_events.ts")]
pub struct EmptyPayload {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct ErrorResponse {
pub error: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct BootRequest {
pub dir: String,
pub watch: bool,
@@ -137,7 +154,7 @@ pub struct BootRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct BootResponse {
pub name: String,
pub version: String,
@@ -145,21 +162,21 @@ pub struct BootResponse {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct ImportRequest {
pub content: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct ImportResponse {
pub resources: ImportResources,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FilterRequest {
pub content: String,
pub filter: String,
@@ -167,49 +184,50 @@ pub struct FilterRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FilterResponse {
pub content: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct ExportHttpRequestRequest {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct ExportHttpRequestResponse {
pub content: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct SendHttpRequestRequest {
#[ts(type = "Partial<HttpRequest>")]
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct SendHttpRequestResponse {
pub http_response: HttpResponse,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CopyTextRequest {
pub text: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct RenderHttpRequestRequest {
pub http_request: HttpRequest,
pub purpose: RenderPurpose,
@@ -217,14 +235,14 @@ pub struct RenderHttpRequestRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct RenderHttpRequestResponse {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct TemplateRenderRequest {
pub data: serde_json::Value,
pub purpose: RenderPurpose,
@@ -232,25 +250,62 @@ pub struct TemplateRenderRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct TemplateRenderResponse {
pub data: serde_json::Value,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct OpenWindowRequest {
pub url: String,
/// Label for the window. If not provided, a random one will be generated.
pub label: String,
#[ts(optional)]
pub title: Option<String>,
#[ts(optional)]
pub size: Option<WindowSize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct WindowSize {
pub width: f64,
pub height: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CloseWindowRequest {
pub label: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct WindowNavigateEvent {
pub url: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct ShowToastRequest {
pub message: String,
#[ts(optional)]
pub color: Option<Color>,
#[ts(optional)]
pub icon: Option<Icon>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct PromptTextRequest {
// A unique ID to identify the prompt (eg. "enter-password")
pub id: String,
@@ -277,17 +332,15 @@ pub struct PromptTextRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct PromptTextResponse {
pub value: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub enum Color {
Custom,
Default,
Primary,
Secondary,
Info,
@@ -299,18 +352,23 @@ pub enum Color {
impl Default for Color {
fn default() -> Self {
Color::Default
Color::Secondary
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub enum Icon {
AlertTriangle,
Check,
CheckCircle,
ChevronDown,
Copy,
Info,
CheckCircle,
AlertTriangle,
Pin,
Search,
Trash,
#[serde(untagged)]
#[ts(type = "\"_unknown\"")]
@@ -319,17 +377,45 @@ pub enum Icon {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct GetHttpAuthenticationResponse {
#[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpAuthenticationSummaryResponse {
pub name: String,
pub label: String,
pub short_label: String,
pub config: Vec<FormInput>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct HttpAuthenticationAction {
pub label: String,
#[ts(optional)]
pub icon: Option<Icon>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpAuthenticationConfigRequest {
pub context_id: 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 GetHttpAuthenticationConfigResponse {
pub args: Vec<FormInput>,
pub plugin_ref_id: String,
#[ts(optional)]
pub actions: Option<Vec<HttpAuthenticationAction>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct HttpHeader {
pub name: String,
pub value: String,
@@ -337,9 +423,10 @@ pub struct HttpHeader {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallHttpAuthenticationRequest {
pub config: serde_json::Map<String, serde_json::Value>,
pub context_id: String,
pub values: HashMap<String, JsonPrimitive>,
pub method: String,
pub url: String,
pub headers: Vec<HttpHeader>,
@@ -347,7 +434,34 @@ pub struct CallHttpAuthenticationRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallHttpAuthenticationActionRequest {
pub index: i32,
pub plugin_ref_id: String,
pub args: CallHttpAuthenticationActionArgs,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallHttpAuthenticationActionArgs {
pub context_id: String,
pub values: HashMap<String, JsonPrimitive>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(untagged)]
#[ts(export, export_to = "gen_events.ts")]
pub enum JsonPrimitive {
String(String),
Number(f64),
Boolean(bool),
Null,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallHttpAuthenticationResponse {
/// HTTP headers to add to the request. Existing headers will be replaced, while
/// new headers will be added.
@@ -356,7 +470,7 @@ pub struct CallHttpAuthenticationResponse {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetTemplateFunctionsResponse {
pub functions: Vec<TemplateFunction>,
pub plugin_ref_id: String,
@@ -364,9 +478,10 @@ pub struct GetTemplateFunctionsResponse {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct TemplateFunction {
pub name: String,
#[ts(optional)]
pub description: Option<String>,
@@ -379,7 +494,7 @@ pub struct TemplateFunction {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub enum FormInput {
Text(FormInputText),
Editor(FormInputEditor),
@@ -387,14 +502,23 @@ pub enum FormInput {
Checkbox(FormInputCheckbox),
File(FormInputFile),
HttpRequest(FormInputHttpRequest),
Accordion(FormInputAccordion),
Banner(FormInputBanner),
Markdown(FormInputMarkdown),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputBase {
/// The name of the input. The value will be stored at this object attribute in the resulting data
pub name: String,
/// Whether this input is visible for the given configuration. Use this to
/// make branching forms.
#[ts(optional)]
pub hidden: Option<bool>,
/// Whether the user must fill in the argument
#[ts(optional)]
pub optional: Option<bool>,
@@ -410,11 +534,14 @@ pub struct FormInputBase {
/// The default value
#[ts(optional)]
pub default_value: Option<String>,
#[ts(optional)]
pub disabled: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputText {
#[serde(flatten)]
pub base: FormInputBase,
@@ -426,11 +553,18 @@ pub struct FormInputText {
/// Placeholder for the text input
#[ts(optional)]
pub password: Option<bool>,
/// Whether to allow newlines in the input, like a <textarea/>
#[ts(optional)]
pub multi_line: Option<bool>,
#[ts(optional)]
pub completion_options: Option<Vec<GenericCompletionOption>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub enum EditorLanguage {
Text,
Javascript,
@@ -449,7 +583,7 @@ impl Default for EditorLanguage {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputEditor {
#[serde(flatten)]
pub base: FormInputBase,
@@ -465,11 +599,45 @@ pub struct FormInputEditor {
/// Language for syntax highlighting
#[ts(optional)]
pub language: Option<EditorLanguage>,
#[ts(optional)]
pub read_only: Option<bool>,
#[ts(optional)]
pub completion_options: Option<Vec<GenericCompletionOption>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GenericCompletionOption {
label: String,
#[ts(optional)]
detail: Option<String>,
#[ts(optional)]
info: Option<String>,
#[ts(optional)]
#[serde(rename = "type")]
pub type_: Option<CompletionOptionType>,
#[ts(optional)]
pub boost: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "gen_events.ts")]
pub enum CompletionOptionType {
Constant,
Variable,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputHttpRequest {
#[serde(flatten)]
pub base: FormInputBase,
@@ -477,7 +645,7 @@ pub struct FormInputHttpRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputFile {
#[serde(flatten)]
pub base: FormInputBase,
@@ -504,7 +672,7 @@ pub struct FormInputFile {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FileFilter {
pub name: String,
/// File extensions to require
@@ -513,7 +681,7 @@ pub struct FileFilter {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputSelect {
#[serde(flatten)]
pub base: FormInputBase,
@@ -524,7 +692,7 @@ pub struct FormInputSelect {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputCheckbox {
#[serde(flatten)]
pub base: FormInputBase,
@@ -532,15 +700,68 @@ pub struct FormInputCheckbox {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputSelectOption {
pub name: String,
pub label: String,
pub value: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputAccordion {
pub label: String,
#[ts(optional)]
pub inputs: Option<Vec<FormInput>>,
#[ts(optional)]
pub hidden: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputBanner {
#[ts(optional)]
pub inputs: Option<Vec<FormInput>>,
#[ts(optional)]
pub hidden: Option<bool>,
#[ts(optional)]
pub color: Option<Color>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputMarkdown {
pub content: String,
#[ts(optional)]
pub hidden: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "gen_events.ts")]
pub enum Content {
Text { content: String },
Markdown { content: String },
}
impl Default for Content {
fn default() -> Self {
Self::Text {
content: String::default(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallTemplateFunctionRequest {
pub name: String,
pub args: CallTemplateFunctionArgs,
@@ -548,14 +769,14 @@ pub struct CallTemplateFunctionRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallTemplateFunctionResponse {
pub value: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallTemplateFunctionArgs {
pub purpose: RenderPurpose,
pub values: HashMap<String, String>,
@@ -563,7 +784,7 @@ pub struct CallTemplateFunctionArgs {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub enum RenderPurpose {
Send,
Preview,
@@ -577,12 +798,12 @@ impl Default for RenderPurpose {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpRequestActionsRequest {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpRequestActionsResponse {
pub actions: Vec<HttpRequestAction>,
pub plugin_ref_id: String,
@@ -590,9 +811,8 @@ pub struct GetHttpRequestActionsResponse {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct HttpRequestAction {
pub key: String,
pub label: String,
#[ts(optional)]
pub icon: Option<Icon>,
@@ -600,37 +820,37 @@ pub struct HttpRequestAction {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallHttpRequestActionRequest {
pub key: String,
pub index: i32,
pub plugin_ref_id: String,
pub args: CallHttpRequestActionArgs,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallHttpRequestActionArgs {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpRequestByIdRequest {
pub id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpRequestByIdResponse {
pub http_request: Option<HttpRequest>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FindHttpResponsesRequest {
pub request_id: String,
#[ts(optional)]
@@ -639,14 +859,14 @@ pub struct FindHttpResponsesRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FindHttpResponsesResponse {
pub http_responses: Vec<HttpResponse>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
#[ts(export, export_to = "gen_events.ts")]
pub struct ImportResources {
pub workspaces: Vec<Workspace>,
pub environments: Vec<Environment>,
@@ -654,3 +874,45 @@ pub struct ImportResources {
pub http_requests: Vec<HttpRequest>,
pub grpc_requests: Vec<GrpcRequest>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetKeyValueRequest {
pub key: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetKeyValueResponse {
#[ts(optional)]
pub value: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct SetKeyValueRequest {
pub key: String,
pub value: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)]
#[ts(export, type = "{}", export_to = "gen_events.ts")]
pub struct SetKeyValueResponse {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct DeleteKeyValueRequest {
pub key: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)]
#[ts(export, export_to = "gen_events.ts")]
pub struct DeleteKeyValueResponse {
pub deleted: bool,
}

View File

@@ -3,12 +3,13 @@ use crate::error::Error::{
};
use crate::error::Result;
use crate::events::{
BootRequest, CallHttpAuthenticationRequest, CallHttpAuthenticationResponse,
CallHttpRequestActionRequest, CallTemplateFunctionArgs, CallTemplateFunctionRequest,
CallTemplateFunctionResponse, EmptyPayload, FilterRequest, FilterResponse, FormInput,
GetHttpAuthenticationResponse, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse,
ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, RenderPurpose,
WindowContext,
BootRequest, CallHttpAuthenticationActionArgs, CallHttpAuthenticationActionRequest,
CallHttpAuthenticationRequest, CallHttpAuthenticationResponse, CallHttpRequestActionRequest,
CallTemplateFunctionArgs, CallTemplateFunctionRequest, CallTemplateFunctionResponse,
EmptyPayload, FilterRequest, FilterResponse, GetHttpAuthenticationConfigRequest,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, ImportRequest, ImportResponse,
InternalEvent, InternalEventPayload, JsonPrimitive, RenderPurpose, WindowContext,
};
use crate::nodejs::start_nodejs_plugin_runtime;
use crate::plugin_handle::PluginHandle;
@@ -24,6 +25,7 @@ use tauri::{AppHandle, Manager, Runtime, WebviewWindow};
use tokio::fs::read_dir;
use tokio::net::TcpListener;
use tokio::sync::{mpsc, Mutex};
use tokio::time::timeout;
use yaak_models::queries::{generate_id, list_plugins};
#[derive(Clone)]
@@ -86,7 +88,7 @@ impl PluginManager {
let addr = listener.local_addr().expect("Failed to get local address");
// 1. Reload all plugins when the Node.js runtime connects
{
let init_plugins_task = {
let plugin_manager = plugin_manager.clone();
let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
@@ -94,7 +96,7 @@ impl PluginManager {
Ok(_) => {
info!("Plugin runtime client connected!");
plugin_manager
.initialize_all_plugins(&app_handle, WindowContext::None)
.initialize_all_plugins(&app_handle, &WindowContext::None)
.await
.expect("Failed to reload plugins");
}
@@ -102,7 +104,7 @@ impl PluginManager {
warn!("Failed to receive from client connection rx {e:?}");
}
}
});
})
};
// 1. Spawn server in the background
@@ -114,8 +116,13 @@ impl PluginManager {
// 2. Start Node.js runtime and initialize plugins
tauri::async_runtime::block_on(async move {
start_nodejs_plugin_runtime(&app_handle, addr, &kill_server_rx).await.unwrap();
info!("Waiting for plugins to initialize");
init_plugins_task.await.unwrap();
});
// 3. Block waiting for plugins to initialize
tauri::async_runtime::block_on(async move {});
plugin_manager
}
@@ -160,14 +167,14 @@ impl PluginManager {
[bundled_plugin_dirs, installed_plugin_dirs].concat()
}
pub async fn uninstall(&self, window_context: WindowContext, dir: &str) -> Result<()> {
pub async fn uninstall(&self, window_context: &WindowContext, dir: &str) -> Result<()> {
let plugin = self.get_plugin_by_dir(dir).await.ok_or(PluginNotFoundErr(dir.to_string()))?;
self.remove_plugin(window_context, &plugin).await
}
async fn remove_plugin(
&self,
window_context: WindowContext,
window_context: &WindowContext,
plugin: &PluginHandle,
) -> Result<()> {
// Terminate the plugin
@@ -185,7 +192,7 @@ impl PluginManager {
pub async fn add_plugin_by_dir(
&self,
window_context: WindowContext,
window_context: &WindowContext,
dir: &str,
watch: bool,
) -> Result<()> {
@@ -197,20 +204,22 @@ impl PluginManager {
};
let plugin_handle = PluginHandle::new(dir, tx.clone());
// Add the new plugin
self.plugins.lock().await.push(plugin_handle.clone());
// Boot the plugin
let event = self
.send_to_plugin_and_wait(
let event = timeout(
Duration::from_secs(1),
self.send_to_plugin_and_wait(
window_context,
&plugin_handle,
&InternalEventPayload::BootRequest(BootRequest {
dir: dir.to_string(),
watch,
}),
)
.await?;
),
)
.await??;
// Add the new plugin
self.plugins.lock().await.push(plugin_handle.clone());
let resp = match event.payload {
InternalEventPayload::BootResponse(resp) => resp,
@@ -226,20 +235,21 @@ impl PluginManager {
pub async fn initialize_all_plugins<R: Runtime>(
&self,
app_handle: &AppHandle<R>,
window_context: WindowContext,
window_context: &WindowContext,
) -> Result<()> {
let dirs = self.list_plugin_dirs(app_handle).await;
for d in dirs.clone() {
let candidates = self.list_plugin_dirs(app_handle).await;
for candidate in candidates.clone() {
// First remove the plugin if it exists
if let Some(plugin) = self.get_plugin_by_dir(d.dir.as_str()).await {
if let Err(e) = self.remove_plugin(window_context.to_owned(), &plugin).await {
warn!("Failed to remove plugin {} {e:?}", d.dir);
if let Some(plugin) = self.get_plugin_by_dir(candidate.dir.as_str()).await {
if let Err(e) = self.remove_plugin(window_context, &plugin).await {
warn!("Failed to remove plugin {} {e:?}", candidate.dir);
}
}
if let Err(e) =
self.add_plugin_by_dir(window_context.to_owned(), d.dir.as_str(), d.watch).await
if let Err(e) = self
.add_plugin_by_dir(window_context, candidate.dir.as_str(), candidate.watch)
.await
{
warn!("Failed to add plugin {} {e:?}", d.dir);
warn!("Failed to add plugin {} {e:?}", candidate.dir);
}
}
@@ -280,13 +290,13 @@ impl PluginManager {
source_event: &InternalEvent,
payload: &InternalEventPayload,
) -> Result<()> {
let window_label = source_event.to_owned().window_context;
let window_context = source_event.to_owned().window_context;
let reply_id = Some(source_event.to_owned().id);
let plugin = self
.get_plugin_by_ref_id(source_event.plugin_ref_id.as_str())
.await
.ok_or(PluginNotFoundErr(source_event.plugin_ref_id.to_string()))?;
let event = plugin.build_event_to_send_raw(window_label, &payload, reply_id);
let event = plugin.build_event_to_send_raw(&window_context, &payload, reply_id);
plugin.send(&event).await
}
@@ -310,7 +320,7 @@ impl PluginManager {
async fn send_to_plugin_and_wait(
&self,
window_context: WindowContext,
window_context: &WindowContext,
plugin: &PluginHandle,
payload: &InternalEventPayload,
) -> Result<InternalEvent> {
@@ -321,7 +331,7 @@ impl PluginManager {
async fn send_and_wait(
&self,
window_context: WindowContext,
window_context: &WindowContext,
payload: &InternalEventPayload,
) -> Result<Vec<InternalEvent>> {
let plugins = { self.plugins.lock().await.clone() };
@@ -330,7 +340,7 @@ impl PluginManager {
async fn send_to_plugins_and_wait(
&self,
window_context: WindowContext,
window_context: &WindowContext,
payload: &InternalEventPayload,
plugins: Vec<PluginHandle>,
) -> Result<Vec<InternalEvent>> {
@@ -340,7 +350,7 @@ impl PluginManager {
// 1. Build the events with IDs and everything
let events_to_send = plugins
.iter()
.map(|p| p.build_event_to_send(window_context.to_owned(), payload, None))
.map(|p| p.build_event_to_send(window_context, payload, None))
.collect::<Vec<InternalEvent>>();
// 2. Spawn thread to subscribe to incoming events and check reply ids
@@ -358,9 +368,9 @@ impl PluginManager {
if matched_sent_event {
found_events.push(event.clone());
};
let found_them_all = found_events.len() == events_to_send.len();
if found_them_all{
if found_them_all {
break;
}
}
@@ -393,7 +403,7 @@ impl PluginManager {
) -> Result<Vec<GetHttpRequestActionsResponse>> {
let reply_events = self
.send_and_wait(
WindowContext::from_window(window),
&WindowContext::from_window(window),
&InternalEventPayload::GetHttpRequestActionsRequest(EmptyPayload {}),
)
.await?;
@@ -412,12 +422,12 @@ impl PluginManager {
&self,
window: &WebviewWindow<R>,
) -> Result<Vec<GetTemplateFunctionsResponse>> {
self.get_template_functions_with_context(WindowContext::from_window(window)).await
self.get_template_functions_with_context(&WindowContext::from_window(window)).await
}
pub async fn get_template_functions_with_context(
&self,
window_context: WindowContext,
window_context: &WindowContext,
) -> Result<Vec<GetTemplateFunctionsResponse>> {
let reply_events = self
.send_and_wait(window_context, &InternalEventPayload::GetTemplateFunctionsRequest)
@@ -442,7 +452,7 @@ impl PluginManager {
let plugin =
self.get_plugin_by_ref_id(ref_id.as_str()).await.ok_or(PluginNotFoundErr(ref_id))?;
let event = plugin.build_event_to_send(
WindowContext::from_window(window),
&WindowContext::from_window(window),
&InternalEventPayload::CallHttpRequestActionRequest(req),
None,
);
@@ -450,21 +460,22 @@ impl PluginManager {
Ok(())
}
pub async fn get_http_authentication<R: Runtime>(
pub async fn get_http_authentication_summaries<R: Runtime>(
&self,
window: &WebviewWindow<R>,
) -> Result<Vec<(PluginHandle, GetHttpAuthenticationResponse)>> {
) -> Result<Vec<(PluginHandle, GetHttpAuthenticationSummaryResponse)>> {
let window_context = WindowContext::from_window(window);
let reply_events = self
.send_and_wait(
window_context,
&InternalEventPayload::GetHttpAuthenticationRequest(EmptyPayload {}),
&window_context,
&InternalEventPayload::GetHttpAuthenticationSummaryRequest(EmptyPayload {}),
)
.await?;
let mut results = Vec::new();
for event in reply_events {
if let InternalEventPayload::GetHttpAuthenticationResponse(resp) = event.payload {
if let InternalEventPayload::GetHttpAuthenticationSummaryResponse(resp) = event.payload
{
let plugin = self
.get_plugin_by_ref_id(&event.plugin_ref_id)
.await
@@ -476,43 +487,84 @@ impl PluginManager {
Ok(results)
}
pub async fn get_http_authentication_config<R: Runtime>(
&self,
window: &WebviewWindow<R>,
auth_name: &str,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
) -> Result<GetHttpAuthenticationConfigResponse> {
let results = self.get_http_authentication_summaries(window).await?;
let plugin = results
.iter()
.find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None })
.ok_or(PluginNotFoundErr(auth_name.into()))?;
let context_id = format!("{:x}", md5::compute(request_id.to_string()));
let event = self
.send_to_plugin_and_wait(
&WindowContext::from_window(window),
&plugin,
&InternalEventPayload::GetHttpAuthenticationConfigRequest(
GetHttpAuthenticationConfigRequest { values, context_id },
),
)
.await?;
match event.payload {
InternalEventPayload::GetHttpAuthenticationConfigResponse(resp) => Ok(resp),
InternalEventPayload::EmptyResponse(_) => {
Err(PluginErr("Auth plugin returned empty".to_string()))
}
e => Err(PluginErr(format!("Auth plugin returned invalid event {:?}", e))),
}
}
pub async fn call_http_authentication_action<R: Runtime>(
&self,
window: &WebviewWindow<R>,
auth_name: &str,
action_index: i32,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
) -> Result<()> {
let results = self.get_http_authentication_summaries(window).await?;
let plugin = results
.iter()
.find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None })
.ok_or(PluginNotFoundErr(auth_name.into()))?;
let context_id = format!("{:x}", md5::compute(request_id.to_string()));
self
.send_to_plugin_and_wait(
&WindowContext::from_window(window),
&plugin,
&InternalEventPayload::CallHttpAuthenticationActionRequest(
CallHttpAuthenticationActionRequest {
index: action_index,
plugin_ref_id: plugin.clone().ref_id,
args: CallHttpAuthenticationActionArgs { context_id, values },
},
),
)
.await?;
Ok(())
}
pub async fn call_http_authentication<R: Runtime>(
&self,
window: &WebviewWindow<R>,
auth_name: &str,
req: CallHttpAuthenticationRequest,
) -> Result<CallHttpAuthenticationResponse> {
let handlers = self.get_http_authentication(window).await?;
let (plugin, authentication) = handlers
let handlers = self.get_http_authentication_summaries(window).await?;
let (plugin, _) = handlers
.iter()
.find(|(_, a)| a.name == auth_name)
.ok_or(AuthPluginNotFound(auth_name.to_string()))?;
// Clone for mutability
let mut req = req.clone();
// Fill in default values
for arg in authentication.config.clone() {
let base = match arg {
FormInput::Text(a) => a.base,
FormInput::Editor(a) => a.base,
FormInput::Select(a) => a.base,
FormInput::Checkbox(a) => a.base,
FormInput::File(a) => a.base,
FormInput::HttpRequest(a) => a.base,
};
if let None = req.config.get(base.name.as_str()) {
let default = match base.default_value {
None => serde_json::Value::Null,
Some(s) => serde_json::Value::String(s),
};
req.config.insert(base.name, default);
}
}
let event = self
.send_to_plugin_and_wait(
WindowContext::from_window(window),
&WindowContext::from_window(window),
&plugin,
&InternalEventPayload::CallHttpAuthenticationRequest(req),
)
@@ -528,7 +580,7 @@ impl PluginManager {
pub async fn call_template_function(
&self,
window_context: WindowContext,
window_context: &WindowContext,
fn_name: &str,
args: HashMap<String, String>,
purpose: RenderPurpose,
@@ -562,7 +614,7 @@ impl PluginManager {
) -> Result<(ImportResponse, String)> {
let reply_events = self
.send_and_wait(
WindowContext::from_window(window),
&WindowContext::from_window(window),
&InternalEventPayload::ImportRequest(ImportRequest {
content: content.to_string(),
}),
@@ -607,7 +659,7 @@ impl PluginManager {
let event = self
.send_to_plugin_and_wait(
WindowContext::from_window(window),
&WindowContext::from_window(window),
&plugin,
&InternalEventPayload::FilterRequest(FilterRequest {
filter: filter.to_string(),

View File

@@ -26,6 +26,10 @@ impl PluginHandle {
}
}
pub async fn name(&self) -> String {
self.boot_resp.lock().await.name.clone()
}
pub async fn info(&self) -> BootResponse {
let resp = &*self.boot_resp.lock().await;
resp.clone()
@@ -33,7 +37,7 @@ impl PluginHandle {
pub fn build_event_to_send(
&self,
window_context: WindowContext,
window_context: &WindowContext,
payload: &InternalEventPayload,
reply_id: Option<String>,
) -> InternalEvent {
@@ -42,7 +46,7 @@ impl PluginHandle {
pub(crate) fn build_event_to_send_raw(
&self,
window_context: WindowContext,
window_context: &WindowContext,
payload: &InternalEventPayload,
reply_id: Option<String>,
) -> InternalEvent {
@@ -53,18 +57,18 @@ impl PluginHandle {
plugin_name: dir.file_name().unwrap().to_str().unwrap().to_string(),
reply_id,
payload: payload.clone(),
window_context,
window_context: window_context.clone(),
}
}
pub async fn terminate(&self, window_context: WindowContext) -> Result<()> {
pub async fn terminate(&self, window_context: &WindowContext) -> Result<()> {
info!("Terminating plugin {}", self.dir);
let event =
self.build_event_to_send(window_context, &InternalEventPayload::TerminateRequest, None);
self.send(&event).await
}
pub(crate) async fn send(&self, event: &InternalEvent) -> Result<()> {
pub async fn send(&self, event: &InternalEvent) -> Result<()> {
self.to_plugin_tx.lock().await.send(event.to_owned()).await?;
Ok(())
}

View File

@@ -85,8 +85,9 @@ impl PluginRuntimeServerWebsocket {
};
// Parse everything but the payload so we can catch errors on that, specifically
let payload = serde_json::from_value::<InternalEventPayload>(event.payload)
let payload = serde_json::from_value::<InternalEventPayload>(event.payload.clone())
.unwrap_or_else(|e| {
warn!("Plugin error from {}: {:?} {}", event.plugin_name, e, event.payload);
InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Plugin error from {}: {e:?}", event.plugin_name),
})

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SyncModel } from "./models.js";
import type { SyncState } from "./models.js";
import type { SyncModel } from "./gen_models.js";
import type { SyncState } from "./gen_models.js";
export type FsCandidate = { "type": "FsCandidate", model: SyncModel, relPath: string, checksum: string, };

View File

@@ -1,7 +1,7 @@
import { Channel, invoke } from '@tauri-apps/api/core';
import { emit } from '@tauri-apps/api/event';
import { SyncOp } from './bindings/sync';
import { WatchEvent, WatchResult } from './bindings/watch';
import { SyncOp } from './bindings/gen_sync';
import { WatchEvent, WatchResult } from './bindings/gen_watch';
export async function calculateSync(workspaceId: string, syncDir: string) {
return invoke<SyncOp[]>('plugin:yaak-sync|calculate', {

View File

@@ -51,7 +51,7 @@ pub async fn apply<R: Runtime>(
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "watch.ts")]
#[ts(export, export_to = "gen_watch.ts")]
pub(crate) struct WatchResult {
unlisten_event: String,
}

View File

@@ -10,7 +10,7 @@ use yaak_models::models::{AnyModel, Environment, Folder, GrpcRequest, HttpReques
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "models.ts")]
#[ts(export, export_to = "gen_models.ts")]
pub enum SyncModel {
Workspace(Workspace),
Environment(Environment),

View File

@@ -21,7 +21,7 @@ use yaak_models::queries::{
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", tag = "type")]
#[ts(export, export_to = "sync.ts")]
#[ts(export, export_to = "gen_sync.ts")]
pub(crate) enum SyncOp {
FsCreate {
model: SyncModel,
@@ -98,7 +98,7 @@ impl DbCandidate {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", tag = "type")]
#[ts(export, export_to = "sync.ts")]
#[ts(export, export_to = "gen_sync.ts")]
pub(crate) struct FsCandidate {
pub(crate) model: SyncModel,
pub(crate) rel_path: PathBuf,

View File

@@ -10,7 +10,7 @@ use ts_rs::TS;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "watch.ts")]
#[ts(export, export_to = "gen_watch.ts")]
pub(crate) struct WatchEvent {
paths: Vec<PathBuf>,
kind: String,

View File

@@ -153,9 +153,10 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
label: 'Send Request',
onSelect: () => sendRequest(activeRequest.id),
});
for (const a of httpRequestActions) {
for (let i = 0; i < httpRequestActions.length; i++) {
const a = httpRequestActions[i]!;
commands.push({
key: a.key,
key: `http_request_action.${i}`,
label: a.label,
onSelect: () => a.call(activeRequest),
});

View File

@@ -73,7 +73,6 @@ export const CookieDropdown = memo(function CookieDropdown() {
...(((cookieJars ?? []).length > 1 // Never delete the last one
? [
{
key: 'delete',
label: 'Delete',
leftSlot: <Icon icon="trash" />,
color: 'danger',

View File

@@ -7,6 +7,7 @@ import type {
FormInputHttpRequest,
FormInputSelect,
FormInputText,
JsonPrimitive,
} from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { useCallback } from 'react';
@@ -15,51 +16,93 @@ import { useFolders } from '../hooks/useFolders';
import { useHttpRequests } from '../hooks/useHttpRequests';
import { capitalize } from '../lib/capitalize';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { Banner } from './core/Banner';
import { Checkbox } from './core/Checkbox';
import { Editor } from './core/Editor/Editor';
import { Input } from './core/Input';
import { Label } from './core/Label';
import { Select } from './core/Select';
import { VStack } from './core/Stacks';
import { Markdown } from './Markdown';
import { SelectFile } from './SelectFile';
// eslint-disable-next-line react-refresh/only-export-components
export const DYNAMIC_FORM_NULL_ARG = '__NULL__';
const INPUT_SIZE = 'sm';
export function DynamicForm<T extends Record<string, string | boolean>>({
config,
data,
onChange,
useTemplating,
autocompleteVariables,
stateKey,
}: {
config: FormInput[];
interface Props<T> {
inputs: FormInput[] | undefined | null;
onChange: (value: T) => void;
data: T;
useTemplating?: boolean;
autocompleteVariables?: boolean;
stateKey: string;
}) {
disabled?: boolean;
}
export function DynamicForm<T extends Record<string, JsonPrimitive>>({
inputs,
data,
onChange,
useTemplating,
autocompleteVariables,
stateKey,
disabled,
}: Props<T>) {
const setDataAttr = useCallback(
(name: string, value: string | boolean | null) => {
(name: string, value: JsonPrimitive) => {
onChange({ ...data, [name]: value == DYNAMIC_FORM_NULL_ARG ? undefined : value });
},
[data, onChange],
);
return (
<FormInputs
disabled={disabled}
inputs={inputs}
setDataAttr={setDataAttr}
stateKey={stateKey}
useTemplating={useTemplating}
autocompleteVariables={autocompleteVariables}
data={data}
/>
);
}
function FormInputs<T extends Record<string, JsonPrimitive>>({
inputs,
autocompleteVariables,
stateKey,
useTemplating,
setDataAttr,
data,
disabled,
}: Pick<Props<T>, 'inputs' | 'useTemplating' | 'autocompleteVariables' | 'stateKey' | 'data'> & {
setDataAttr: (name: string, value: JsonPrimitive) => void;
disabled?: boolean;
}) {
return (
<VStack space={3} className="h-full overflow-auto">
{config.map((a, i) => {
switch (a.type) {
{inputs?.map((input, i) => {
if ('hidden' in input && input.hidden) {
return null;
}
if ('disabled' in input && disabled != null) {
input.disabled = disabled;
}
switch (input.type) {
case 'select':
return (
<SelectArg
key={i + stateKey}
arg={a}
onChange={(v) => setDataAttr(a.name, v)}
arg={input}
onChange={(v) => setDataAttr(input.name, v)}
value={
data[a.name] ? String(data[a.name]) : (a.defaultValue ?? DYNAMIC_FORM_NULL_ARG)
data[input.name]
? String(data[input.name])
: (input.defaultValue ?? DYNAMIC_FORM_NULL_ARG)
}
/>
);
@@ -68,11 +111,13 @@ export function DynamicForm<T extends Record<string, string | boolean>>({
<TextArg
key={i}
stateKey={stateKey}
arg={a}
arg={input}
useTemplating={useTemplating || false}
autocompleteVariables={autocompleteVariables || false}
onChange={(v) => setDataAttr(a.name, v)}
value={data[a.name] ? String(data[a.name]) : (a.defaultValue ?? '')}
onChange={(v) => setDataAttr(input.name, v)}
value={
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '')
}
/>
);
case 'editor':
@@ -80,40 +125,79 @@ export function DynamicForm<T extends Record<string, string | boolean>>({
<EditorArg
key={i}
stateKey={stateKey}
arg={a}
arg={input}
useTemplating={useTemplating || false}
autocompleteVariables={autocompleteVariables || false}
onChange={(v) => setDataAttr(a.name, v)}
value={data[a.name] ? String(data[a.name]) : (a.defaultValue ?? '')}
onChange={(v) => setDataAttr(input.name, v)}
value={
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '')
}
/>
);
case 'checkbox':
return (
<CheckboxArg
key={i + stateKey}
arg={a}
onChange={(v) => setDataAttr(a.name, v)}
value={data[a.name] !== undefined ? data[a.name] === true : false}
arg={input}
onChange={(v) => setDataAttr(input.name, v)}
value={data[input.name] != null ? data[input.name] === true : false}
/>
);
case 'http_request':
return (
<HttpRequestArg
key={i + stateKey}
arg={a}
onChange={(v) => setDataAttr(a.name, v)}
value={data[a.name] ? String(data[a.name]) : DYNAMIC_FORM_NULL_ARG}
arg={input}
onChange={(v) => setDataAttr(input.name, v)}
value={data[input.name] != null ? String(data[input.name]) : DYNAMIC_FORM_NULL_ARG}
/>
);
case 'file':
return (
<FileArg
key={i + stateKey}
arg={a}
onChange={(v) => setDataAttr(a.name, v)}
filePath={data[a.name] ? String(data[a.name]) : DYNAMIC_FORM_NULL_ARG}
arg={input}
onChange={(v) => setDataAttr(input.name, v)}
filePath={
data[input.name] != null ? String(data[input.name]) : DYNAMIC_FORM_NULL_ARG
}
/>
);
case 'accordion':
return (
<Banner key={i} className={classNames('!p-0', disabled && 'opacity-disabled')}>
<details>
<summary className="px-3 py-1.5 text-text-subtle">{input.label}</summary>
<div className="mb-3 px-3">
<FormInputs
data={data}
disabled={disabled}
inputs={input.inputs}
setDataAttr={setDataAttr}
stateKey={stateKey}
/>
</div>
</details>
</Banner>
);
case 'banner':
return (
<Banner
key={i}
color={input.color}
className={classNames(disabled && 'opacity-disabled')}
>
<FormInputs
data={data}
disabled={disabled}
inputs={input.inputs}
setDataAttr={setDataAttr}
stateKey={stateKey}
/>
</Banner>
);
case 'markdown':
return <Markdown>{input.content}</Markdown>;
}
})}
</VStack>
@@ -145,13 +229,17 @@ function TextArg({
return (
<Input
name={arg.name}
multiLine={arg.multiLine}
onChange={handleChange}
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
required={!arg.optional}
disabled={arg.disabled}
type={arg.password ? 'password' : 'text'}
label={arg.label ?? arg.name}
size={INPUT_SIZE}
hideLabel={arg.label == null}
placeholder={arg.placeholder ?? arg.defaultValue ?? ''}
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
useTemplating={useTemplating}
autocompleteVariables={autocompleteVariables}
stateKey={stateKey}
@@ -184,23 +272,29 @@ function EditorArg({
const id = `input-${arg.name}`;
// Read-only editor force refresh for every defaultValue change
// Should this be built into the <Editor/> component?
const forceUpdateKey = arg.readOnly ? arg.defaultValue + stateKey : stateKey;
return (
<div className=" w-full grid grid-cols-1 grid-rows-[auto_minmax(0,1fr)]">
<Label
htmlFor={id}
optional={arg.optional}
required={!arg.optional}
visuallyHidden={arg.hideLabel}
otherTags={arg.language ? [capitalize(arg.language)] : undefined}
tags={arg.language ? [capitalize(arg.language)] : undefined}
>
{arg.label}
</Label>
<Editor
id={id}
className={classNames(
'border border-border rounded-md overflow-hidden px-2 py-1.5',
'border border-border rounded-md overflow-hidden px-2 py-1',
'focus-within:border-border-focus',
'max-h-[15rem]', // So it doesn't take up too much space
)}
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
disabled={arg.disabled}
language={arg.language}
onChange={handleChange}
heightMode="auto"
@@ -209,7 +303,7 @@ function EditorArg({
useTemplating={useTemplating}
autocompleteVariables={autocompleteVariables}
stateKey={stateKey}
forceUpdateKey={stateKey}
forceUpdateKey={forceUpdateKey}
hideGutter
/>
</div>
@@ -232,12 +326,9 @@ function SelectArg({
onChange={onChange}
hideLabel={arg.hideLabel}
value={value}
options={[
...arg.options.map((a) => ({
label: a.name,
value: a.value,
})),
]}
size={INPUT_SIZE}
disabled={arg.disabled}
options={arg.options}
/>
);
}
@@ -253,6 +344,7 @@ function FileArg({
}) {
return (
<SelectFile
disabled={arg.disabled}
onChange={({ filePath }) => onChange(filePath)}
filePath={filePath === '__NULL__' ? null : filePath}
directory={!!arg.directory}
@@ -278,6 +370,7 @@ function HttpRequestArg({
name={arg.name}
onChange={onChange}
value={value}
disabled={arg.disabled}
options={[
...httpRequests.map((r) => {
return {
@@ -323,6 +416,7 @@ function CheckboxArg({
<Checkbox
onChange={onChange}
checked={value}
disabled={arg.disabled}
title={arg.label ?? arg.name}
hideLabel={arg.label == null}
/>

View File

@@ -1,4 +1,5 @@
import type { Environment } from '@yaakapp-internal/models';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
@@ -11,10 +12,7 @@ import { showPrompt } from '../lib/prompt';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { ContextMenu } from './core/Dropdown';
import type {
GenericCompletionConfig,
GenericCompletionOption,
} from './core/Editor/genericCompletion';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { Heading } from './core/Heading';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
@@ -255,7 +253,6 @@ function SidebarButton({
onClose={() => setShowContextMenu(null)}
items={[
{
key: 'rename',
label: 'Rename',
leftSlot: <Icon icon="pencil" size="sm" />,
onSelect: async () => {
@@ -277,7 +274,6 @@ function SidebarButton({
},
},
{
key: 'delete-environment',
color: 'danger',
label: 'Delete',
leftSlot: <Icon icon="trash" size="sm" />,

View File

@@ -1,5 +1,5 @@
import { emit } from '@tauri-apps/api/event';
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugins';
import type { InternalEvent } from '@yaakapp-internal/plugins';
import type { ShowToastRequest } from '@yaakapp/api';
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
@@ -12,6 +12,7 @@ import { useSyncModelStores } from '../hooks/useSyncModelStores';
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions';
import { generateId } from '../lib/generateId';
import { showPrompt } from '../lib/prompt';
import { showToast } from '../lib/toast';
@@ -36,15 +37,24 @@ export function GlobalHooks() {
showToast({ ...event.payload });
});
// Listen for prompts
useListenToTauriEvent<{ replyId: string; args: PromptTextRequest }>(
'show_prompt',
async (event) => {
const value = await showPrompt(event.payload.args);
const result: PromptTextResponse = { value };
await emit(event.payload.replyId, result);
},
);
// Listen for plugin events
useListenToTauriEvent<InternalEvent>('plugin_event', async ({ payload: event }) => {
if (event.payload.type === 'prompt_text_request') {
const value = await showPrompt(event.payload);
const result: InternalEvent = {
id: generateId(),
replyId: event.id,
pluginName: event.pluginName,
pluginRefId: event.pluginRefId,
windowContext: event.windowContext,
payload: {
type: 'prompt_text_response',
value,
},
};
await emit(event.id, result);
}
});
return null;
}

View File

@@ -69,13 +69,11 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
<Dropdown
items={[
{
key: 'refresh',
label: 'Refetch',
leftSlot: <Icon icon="refresh" />,
onSelect: refetch,
},
{
key: 'clear',
label: 'Clear',
onSelect: clear,
hidden: !schema,
@@ -84,7 +82,6 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
},
{ type: 'separator', label: 'Setting' },
{
key: 'auto_fetch',
label: 'Automatic Introspection',
onSelect: () => {
setAutoIntrospectDisabled({

View File

@@ -6,7 +6,7 @@ import type { CSSProperties } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
import { useContainerSize } from '../hooks/useContainerQuery';
import type { ReflectResponseService } from '../hooks/useGrpc';
import { useHttpAuthentication } from '../hooks/useHttpAuthentication';
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { fallbackRequestName } from '../lib/fallbackRequestName';
@@ -69,7 +69,7 @@ export function GrpcConnectionSetupPane({
onSend,
}: Props) {
const updateRequest = useUpdateAnyGrpcRequest();
const authentication = useHttpAuthentication();
const authentication = useHttpAuthenticationSummaries();
const [activeTabs, setActiveTabs] = useAtom(tabsAtom);
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
@@ -237,7 +237,6 @@ export function GrpcConnectionSetupPane({
{
label: 'Refresh',
type: 'default',
key: 'custom',
leftSlot: <Icon className="text-text-subtlest" size="sm" icon="refresh" />,
},
]}

View File

@@ -1,4 +1,5 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import { charsets } from '../lib/data/charsets';
import { connections } from '../lib/data/connections';
import { encodings } from '../lib/data/encodings';
@@ -44,7 +45,7 @@ const headerOptionsMap: Record<string, string[]> = {
const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => {
const name = headerName.toLowerCase().trim();
const options: GenericCompletionConfig['options'] =
const options: GenericCompletionOption[] =
headerOptionsMap[name]?.map((o) => ({
label: o,
type: 'constant',

View File

@@ -1,8 +1,14 @@
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import React, { useCallback } from 'react';
import { useHttpAuthentication } from '../hooks/useHttpAuthentication';
import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { Checkbox } from './core/Checkbox';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
import { DynamicForm } from './DynamicForm';
import { EmptyStateText } from './EmptyStateText';
@@ -13,11 +19,15 @@ interface Props {
export function HttpAuthenticationEditor({ request }: Props) {
const updateHttpRequest = useUpdateAnyHttpRequest();
const updateGrpcRequest = useUpdateAnyGrpcRequest();
const auths = useHttpAuthentication();
const auth = auths.find((a) => a.name === request.authenticationType);
const auth = useHttpAuthenticationConfig(
request.authenticationType,
request.authentication,
request.id,
);
const handleChange = useCallback(
(authentication: Record<string, boolean>) => {
console.log('UPDATE', authentication);
if (request.model === 'http_request') {
updateHttpRequest.mutate({
id: request.id,
@@ -33,18 +43,42 @@ export function HttpAuthenticationEditor({ request }: Props) {
[request.id, request.model, updateGrpcRequest, updateHttpRequest],
);
if (auth == null) {
if (auth.data == null) {
return <EmptyStateText>No Authentication {request.authenticationType}</EmptyStateText>;
}
return (
<DynamicForm
autocompleteVariables
useTemplating
stateKey={`auth.${request.id}.${request.authenticationType}`}
config={auth.config}
data={request.authentication}
onChange={handleChange}
/>
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
<HStack space={2} className="mb-1" alignItems="center">
<Checkbox
className="w-full"
checked={!request.authentication.disabled}
onChange={(disabled) => handleChange({ ...request.authentication, disabled: !disabled })}
title="Enabled"
/>
{auth.data.actions && (
<Dropdown
items={auth.data.actions.map(
(a): DropdownItem => ({
label: a.label,
leftSlot: a.icon ? <Icon icon={a.icon} /> : null,
onSelect: () => a.call(request),
}),
)}
>
<IconButton title="Authentication Actions" icon="settings" size="xs" />
</Dropdown>
)}
</HStack>
<DynamicForm
disabled={request.authentication.disabled}
autocompleteVariables
useTemplating
stateKey={`auth.${request.id}.${request.authenticationType}`}
inputs={auth.data.args}
data={request.authentication}
onChange={handleChange}
/>
</div>
);
}

View File

@@ -0,0 +1,27 @@
import remarkGfm from 'remark-gfm';
import ReactMarkdown, { type Components } from 'react-markdown';
import { Prose } from './Prose';
export function Markdown({ children, className }: { children: string; className?: string }) {
return (
<Prose className={className}>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{children}
</ReactMarkdown>
</Prose>
);
}
const markdownComponents: Partial<Components> = {
// Ensure links open in external browser by adding target="_blank"
a: ({ href, children, ...rest }) => {
if (href && !href.match(/https?:\/\//)) {
href = `http://${href}`;
}
return (
<a target="_blank" rel="noreferrer noopener" href={href} {...rest}>
{children}
</a>
);
},
};

View File

@@ -1,12 +1,9 @@
import classNames from 'classnames';
import { useRef, useState } from 'react';
import type { Components } from 'react-markdown';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import type { EditorProps } from './core/Editor/Editor';
import { Editor } from './core/Editor/Editor';
import { Prose } from './Prose';
import { SegmentedControl } from './core/SegmentedControl';
import { Markdown } from './Markdown';
type ViewMode = 'edit' | 'preview';
@@ -47,11 +44,9 @@ export function MarkdownEditor({
defaultValue.length === 0 ? (
<p className="text-text-subtlest">No description</p>
) : (
<Prose className="max-w-xl overflow-y-auto max-h-full [&_*]:cursor-auto [&_*]:select-auto">
<Markdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{defaultValue}
</Markdown>
</Prose>
<Markdown className="max-w-xl overflow-y-auto max-h-full [&_*]:cursor-auto [&_*]:select-auto">
{defaultValue}
</Markdown>
);
const contents = viewMode === 'preview' ? preview : editor;
@@ -88,17 +83,3 @@ export function MarkdownEditor({
</div>
);
}
const markdownComponents: Partial<Components> = {
// Ensure links open in external browser by adding target="_blank"
a: ({ href, children, ...rest }) => {
if (href && !href.match(/https?:\/\//)) {
href = `http://${href}`;
}
return (
<a target="_blank" rel="noreferrer noopener" href={href} {...rest}>
{children}
</a>
);
},
};

View File

@@ -5,6 +5,10 @@
@apply mt-0;
}
& > :last-child {
@apply mb-0;
}
img,
video,
p,
@@ -107,6 +111,7 @@
ul code {
@apply text-xs bg-surface-active text-info font-normal whitespace-nowrap;
@apply px-1.5 py-0.5 rounded not-italic;
@apply select-text;
}
pre {

View File

@@ -27,13 +27,11 @@ export function RecentConnectionsDropdown({
<Dropdown
items={[
{
key: 'clear-single',
label: 'Clear Connection',
onSelect: deleteConnection.mutate,
disabled: connections.length === 0,
},
{
key: 'clear-all',
label: `Clear ${pluralizeCount('Connection', connections.length)}`,
onSelect: deleteAllConnections.mutate,
hidden: connections.length <= 1,
@@ -41,7 +39,6 @@ export function RecentConnectionsDropdown({
},
{ type: 'separator', label: 'History' },
...connections.slice(0, 20).map((c) => ({
key: c.id,
label: (
<HStack space={2}>
{formatDistanceToNowStrict(c.createdAt + 'Z')} ago &bull;{' '}

View File

@@ -58,7 +58,6 @@ export function RecentRequestsDropdown({ className }: Props) {
if (request === undefined) continue;
recentRequestItems.push({
key: request.id,
label: fallbackRequestName(request),
// leftSlot: <CountBadge className="!ml-0 px-0 w-5" count={recentRequestItems.length} />,
leftSlot: <HttpMethodTag className="text-right" shortNames request={request} />,

View File

@@ -32,7 +32,6 @@ export const RecentResponsesDropdown = function ResponsePane({
<Dropdown
items={[
{
key: 'save',
label: 'Save to File',
onSelect: saveResponse.mutate,
leftSlot: <Icon icon="save" />,
@@ -40,7 +39,6 @@ export const RecentResponsesDropdown = function ResponsePane({
disabled: activeResponse.state !== 'closed' && activeResponse.status >= 100,
},
{
key: 'copy',
label: 'Copy Body',
onSelect: copyResponse.mutate,
leftSlot: <Icon icon="copy" />,
@@ -48,13 +46,11 @@ export const RecentResponsesDropdown = function ResponsePane({
disabled: activeResponse.state !== 'closed' && activeResponse.status >= 100,
},
{
key: 'clear-single',
label: 'Delete',
leftSlot: <Icon icon="trash" />,
onSelect: deleteResponse.mutate,
},
{
key: 'unpin',
label: 'Unpin Response',
onSelect: () => onPinnedResponseId(activeResponse.id),
leftSlot: <Icon icon="unpin" />,
@@ -63,7 +59,6 @@ export const RecentResponsesDropdown = function ResponsePane({
},
{ type: 'separator', label: 'History' },
{
key: 'clear-all',
label: `Delete ${responses.length} ${pluralize('Response', responses.length)}`,
onSelect: deleteAllResponses.mutate,
hidden: responses.length === 0,
@@ -71,7 +66,6 @@ export const RecentResponsesDropdown = function ResponsePane({
},
{ type: 'separator' },
...responses.slice(0, 20).map((r: HttpResponse) => ({
key: r.id,
label: (
<HStack space={2}>
<StatusTag className="text-sm" response={r} />

View File

@@ -1,4 +1,5 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import type {GenericCompletionOption} from "@yaakapp-internal/plugins";
import classNames from 'classnames';
import { atom, useAtom, useAtomValue } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
@@ -8,7 +9,7 @@ import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
import { useHttpAuthentication } from '../hooks/useHttpAuthentication';
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
import { httpRequestsAtom } from '../hooks/useHttpRequests';
import { useImportCurl } from '../hooks/useImportCurl';
import { useImportQuerystring } from '../hooks/useImportQuerystring';
@@ -36,10 +37,7 @@ import { showToast } from '../lib/toast';
import { BinaryFileEditor } from './BinaryFileEditor';
import { CountBadge } from './core/CountBadge';
import { Editor } from './core/Editor/Editor';
import type {
GenericCompletionConfig,
GenericCompletionOption,
} from './core/Editor/genericCompletion';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { InlineCode } from './core/InlineCode';
import type { Pair } from './core/PairEditor';
import { PlainInput } from './core/PlainInput';
@@ -93,7 +91,7 @@ export const RequestPane = memo(function RequestPane({
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
const [{ urlKey }] = useRequestEditor();
const contentType = useContentTypeFromHeaders(activeRequest.headers);
const authentication = useHttpAuthentication();
const authentication = useHttpAuthenticationSummaries();
const handleContentTypeChange = useCallback(
async (contentType: string | null) => {

View File

@@ -28,14 +28,12 @@ export function SettingsDropdown() {
ref={dropdownRef}
items={[
{
key: 'settings',
label: 'Settings',
hotKeyAction: 'settings.show',
leftSlot: <Icon icon="settings" />,
onSelect: openSettings.mutate,
},
{
key: 'hotkeys',
label: 'Keyboard shortcuts',
hotKeyAction: 'hotkeys.showHelp',
leftSlot: <Icon icon="keyboard" />,
@@ -49,33 +47,28 @@ export function SettingsDropdown() {
},
},
{
key: 'import-data',
label: 'Import Data',
leftSlot: <Icon icon="folder_input" />,
onSelect: () => importData.mutate(),
},
{
key: 'export-data',
label: 'Export Data',
leftSlot: <Icon icon="folder_output" />,
onSelect: () => exportData.mutate(),
},
{ type: 'separator', label: `Yaak v${appInfo.version}` },
{
key: 'update-check',
label: 'Check for Updates',
leftSlot: <Icon icon="update" />,
onSelect: () => checkForUpdates.mutate(),
},
{
key: 'feedback',
label: 'Feedback',
leftSlot: <Icon icon="chat" />,
rightSlot: <Icon icon="external_link" />,
onSelect: () => openUrl('https://yaak.app/roadmap'),
},
{
key: 'changelog',
label: 'Changelog',
leftSlot: <Icon icon="cake" />,
rightSlot: <Icon icon="external_link" />,

View File

@@ -46,13 +46,11 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
if (child.model === 'folder') {
return [
{
key: 'send-all',
label: 'Send All',
leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.id)),
},
{
key: 'folder-settings',
label: 'Settings',
leftSlot: <Icon icon="settings" />,
onSelect: () =>
@@ -64,13 +62,11 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
}),
},
{
key: 'duplicateFolder',
label: 'Duplicate',
leftSlot: <Icon icon="copy" />,
onSelect: () => duplicateFolder.mutate(),
},
{
key: 'delete-folder',
label: 'Delete',
color: 'danger',
leftSlot: <Icon icon="trash" />,
@@ -84,7 +80,6 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
child.model === 'http_request'
? [
{
key: 'send-request',
label: 'Send',
hotKeyAction: 'http_request.send',
hotKeyLabelOnly: true, // Already bound in URL bar
@@ -92,7 +87,6 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
onSelect: () => sendRequest.mutate(child.id),
},
...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'} />,
@@ -107,13 +101,11 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
return [
...requestItems,
{
key: 'rename-request',
label: 'Rename',
leftSlot: <Icon icon="pencil" />,
onSelect: renameRequest.mutate,
},
{
key: 'duplicate-request',
label: 'Duplicate',
hotKeyAction: 'http_request.duplicate',
hotKeyLabelOnly: true, // Would trigger for every request (bad)
@@ -124,14 +116,12 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
: duplicateGrpcRequest.mutate(),
},
{
key: 'move-workspace',
label: 'Move',
leftSlot: <Icon icon="arrow_right_circle" />,
hidden: workspaces.length <= 1,
onSelect: moveToWorkspace.mutate,
},
{
key: 'delete-request',
color: 'danger',
label: 'Delete',
hotKeyAction: 'http_request.delete',

View File

@@ -25,6 +25,10 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
? initialTokens.tokens[0]?.val.args
: [];
for (const arg of templateFunction.args) {
if (!('name' in arg)) {
// Skip visual-only args
continue;
}
const initialArg = initialArgs.find((a) => a.name === arg.name);
const initialArgValue =
initialArg?.value.type === 'str'
@@ -79,7 +83,7 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
<VStack className="pb-3" space={4}>
<h1 className="font-mono !text-base">{templateFunction.name}()</h1>
<DynamicForm
config={templateFunction.args}
inputs={templateFunction.args}
data={argValues}
onChange={setArgValues}
stateKey={`template_function.${templateFunction.name}`}

View File

@@ -46,7 +46,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const extraItems: DropdownItem[] = [
{
key: 'workspace-settings',
label: 'Workspace Settings',
leftSlot: <Icon icon="settings" />,
hotKeyAction: 'workspace_settings.show',
@@ -62,7 +61,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
},
},
{
key: 'reveal-workspace-sync-dir',
label: revealInFinderText,
hidden: workspaceMeta == null || workspaceMeta.settingSyncDir == null,
leftSlot: <Icon icon="folder_open" />,
@@ -72,7 +70,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
},
},
{
key: 'delete-responses',
label: 'Clear Send History',
color: 'warning',
leftSlot: <Icon icon="history" />,
@@ -80,13 +77,11 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
},
{ type: 'separator' },
{
key: 'create-workspace',
label: 'New Workspace',
leftSlot: <Icon icon="plus" />,
onSelect: createWorkspace,
},
{
key: 'open-workspace',
label: 'Open Workspace',
leftSlot: <Icon icon="folder" />,
onSelect: openWorkspaceFromSyncDir.mutate,

View File

@@ -7,7 +7,7 @@ interface Props {
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger' | 'info';
}
export function Banner({ children, className, color = 'secondary' }: Props) {
export function Banner({ children, className, color }: Props) {
return (
<div>
<div
@@ -16,7 +16,7 @@ export function Banner({ children, className, color = 'secondary' }: Props) {
`x-theme-banner--${color}`,
'whitespace-pre-wrap',
'border border-dashed border-border bg-surface',
'px-3 py-2 rounded select-auto cursor-text',
'px-3 py-2 rounded select-auto',
'overflow-x-auto text-text',
)}
>

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import { type ReactNode } from 'react';
import { trackEvent } from '../../lib/analytics';
import { Icon } from './Icon';
import { HStack } from './Stacks';
@@ -26,17 +26,15 @@ export function Checkbox({
event,
}: CheckboxProps) {
return (
<HStack
as="label"
space={2}
className={classNames(className, 'text-text mr-auto', disabled && 'opacity-disabled')}
>
<HStack as="label" space={2} className={classNames(className, 'text-text mr-auto')}>
<div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex')}>
<input
aria-hidden
className={classNames(
'appearance-none w-4 h-4 flex-shrink-0 border border-border',
'rounded hocus:border-border-focus hocus:bg-focus/[5%] outline-none ring-0',
'rounded outline-none ring-0',
!disabled && 'hocus:border-border-focus hocus:bg-focus/[5%] ',
disabled && 'border-dotted',
)}
type="checkbox"
disabled={disabled}
@@ -54,7 +52,7 @@ export function Checkbox({
/>
</div>
</div>
{!hideLabel && title}
<span className={classNames(disabled && 'opacity-disabled')}>{!hideLabel && title}</span>
</HStack>
);
}

View File

@@ -44,7 +44,6 @@ export type DropdownItemSeparator = {
};
export type DropdownItemDefault = {
key: string;
type?: 'default';
label: ReactNode;
keepOpen?: boolean;
@@ -465,11 +464,12 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
return (
<>
{items.map(
(item) =>
(item, i) =>
item.type !== 'separator' &&
!item.hotKeyLabelOnly && (
!item.hotKeyLabelOnly &&
item.hotKeyAction && (
<MenuItemHotKey
key={item.key}
key={`${item.hotKeyAction}::${i}`}
onSelect={handleSelect}
item={item}
action={item.hotKeyAction}
@@ -542,7 +542,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
focused={i === selectedIndex}
onFocus={handleFocus}
onSelect={handleSelect}
key={item.key}
key={`item_${i}`}
item={item}
/>
);

View File

@@ -5,6 +5,7 @@
@apply w-full block text-base;
/* Regular cursor */
.cm-cursor {
@apply border-text !important;
/* Widen the cursor a bit */
@@ -12,6 +13,7 @@
}
/* Vim-mode cursor */
.cm-fat-cursor {
@apply outline-0 bg-text !important;
@apply text-surface !important;
@@ -181,7 +183,7 @@
@apply hidden !important;
}
&.cm-singleline .cm-line {
&.cm-singleline * {
@apply cursor-default;
}
}

View File

@@ -1,4 +1,4 @@
import { defaultKeymap, historyField } from '@codemirror/commands';
import { defaultKeymap, historyField, indentWithTab } from '@codemirror/commands';
import { foldState, forceParsing } from '@codemirror/language';
import type { EditorStateConfig, Extension } from '@codemirror/state';
import { Compartment, EditorState } from '@codemirror/state';
@@ -34,14 +34,22 @@ import { TemplateVariableDialog } from '../../TemplateVariableDialog';
import { IconButton } from '../IconButton';
import { HStack } from '../Stacks';
import './Editor.css';
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
import {
baseExtensions,
getLanguageExtension,
multiLineExtensions,
readonlyExtensions,
} from './extensions';
import type { GenericCompletionConfig } from './genericCompletion';
import { singleLineExtensions } from './singleLine';
// VSCode's Tab actions mess with the single-line editor tab actions, so remove it.
const vsCodeWithoutTab = vscodeKeymap.filter((k) => k.key !== 'Tab');
const keymapExtensions: Record<EditorKeymap, Extension> = {
vim: vim(),
emacs: emacs(),
vscode: keymap.of(vscodeKeymap),
vscode: keymap.of(vsCodeWithoutTab),
default: [],
};
@@ -68,6 +76,7 @@ export interface EditorProps {
onKeyDown?: (e: KeyboardEvent) => void;
singleLine?: boolean;
wrapLines?: boolean;
disableTabIndent?: boolean;
format?: (v: string) => Promise<string>;
autocomplete?: GenericCompletionConfig;
autocompleteVariables?: boolean;
@@ -85,9 +94,9 @@ const emptyExtension: Extension = [];
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
{
readOnly,
type = 'text',
type,
heightMode,
language = 'text',
language,
autoFocus,
autoSelect,
placeholder,
@@ -101,6 +110,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
onBlur,
onKeyDown,
className,
disabled,
singleLine,
format,
autocomplete,
@@ -108,6 +118,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
autocompleteVariables,
actions,
wrapLines,
disableTabIndent,
hideGutter,
stateKey,
}: EditorProps,
@@ -122,6 +133,20 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
wrapLines = settings.editorSoftWrap;
}
if (disabled) {
readOnly = true;
}
if (
singleLine ||
language == null ||
language === 'text' ||
language === 'url' ||
language === 'pairs'
) {
disableTabIndent = true;
}
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
useImperativeHandle(ref, () => cm.current?.view, []);
@@ -166,7 +191,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
useEffect(
function configurePlaceholder() {
if (cm.current === null) return;
const ext = placeholderExt(placeholderElFromText(placeholder ?? '', type));
const ext = placeholderExt(placeholderElFromText(placeholder, type));
const effects = placeholderCompartment.current.reconfigure(ext);
cm.current?.view.dispatch({ effects });
},
@@ -209,6 +234,23 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
[wrapLines],
);
// Update tab indent
const tabIndentCompartment = useRef(new Compartment());
useEffect(
function configureTabIndent() {
if (cm.current === null) return;
const current = tabIndentCompartment.current.get(cm.current.view.state) ?? emptyExtension;
// PERF: This is expensive with hundreds of editors on screen, so only do it when necessary
if (disableTabIndent && current !== emptyExtension) return; // Nothing to do
if (!disableTabIndent && current === emptyExtension) return; // Nothing to do
const ext = !disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension;
const effects = tabIndentCompartment.current.reconfigure(ext);
cm.current?.view.dispatch({ effects });
},
[disableTabIndent],
);
const onClickFunction = useCallback(
async (fn: TemplateFunction, tagValue: string, startPos: number) => {
const initialTokens = await parseTemplate(tagValue);
@@ -342,9 +384,12 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
const extensions = [
languageCompartment.of(langExt),
placeholderCompartment.current.of(
placeholderExt(placeholderElFromText(placeholder ?? '', type)),
placeholderExt(placeholderElFromText(placeholder, type)),
),
wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : emptyExtension),
tabIndentCompartment.current.of(
!disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension,
),
keymapCompartment.current.of(
keymapExtensions[settings.editorKeymap] ?? keymapExtensions['default'],
),
@@ -475,6 +520,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
className={classNames(
className,
'cm-wrapper text-base',
disabled && 'opacity-disabled',
type === 'password' && 'cm-obscure-text',
heightMode === 'auto' ? 'cm-auto-height' : 'cm-full-height',
singleLine ? 'cm-singleline' : 'cm-multiline',
@@ -557,10 +603,8 @@ function getExtensions({
tooltips({ parent }),
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
...(singleLine ? [singleLineExtensions()] : []),
...(!singleLine ? [multiLineExtensions({ hideGutter })] : []),
...(readOnly
? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })]
: []),
...(!singleLine ? multiLineExtensions({ hideGutter }) : []),
...(readOnly ? readonlyExtensions : []),
// ------------------------ //
// Things that must be last //
@@ -580,13 +624,15 @@ function getExtensions({
];
}
const placeholderElFromText = (text: string, type: EditorProps['type']) => {
const placeholderElFromText = (text: string | undefined, type: EditorProps['type']) => {
const el = document.createElement('div');
if (type === 'password') {
// Will be obscured (dots) so just needs to be something to take up space
el.innerHTML = 'something-cool';
el.setAttribute('aria-hidden', 'true');
} else {
// Default to <SPACE> because codemirror needs it for sizing. I'm not sure why, but probably something
// to do with how Yaak "hacks" it with CSS for single line input.
el.innerHTML = text ? text.replaceAll('\n', '<br/>') : ' ';
}
return el;
@@ -596,8 +642,8 @@ function saveCachedEditorState(stateKey: string | null, state: EditorState | nul
if (!stateKey || state == null) return;
const stateObj = state.toJSON(stateFields);
// Save state in sessionStorage by removing doc and saving the hash of it instead
// This will be checked on restore and put back in if it matches
// Save state in sessionStorage by removing doc and saving the hash of it instead.
// This will be checked on restore and put back in if it matches.
stateObj.docHash = md5(stateObj.doc);
delete stateObj.doc;

View File

@@ -4,7 +4,7 @@ import {
closeBracketsKeymap,
completionKeymap,
} from '@codemirror/autocomplete';
import { history, historyKeymap, indentWithTab } from '@codemirror/commands';
import { history, historyKeymap } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
@@ -142,6 +142,11 @@ export const baseExtensions = [
keymap.of([...historyKeymap, ...completionKeymap]),
];
export const readonlyExtensions = [
EditorState.readOnly.of(true),
EditorView.contentAttributes.of({ tabindex: '-1' }),
];
export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [
hideGutter
? []
@@ -208,5 +213,5 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
rectangularSelection(),
crosshairCursor(),
highlightActiveLineGutter(),
keymap.of([indentWithTab, ...closeBracketsKeymap, ...searchKeymap, ...foldKeymap, ...lintKeymap]),
keymap.of([...closeBracketsKeymap, ...searchKeymap, ...foldKeymap, ...lintKeymap]),
];

View File

@@ -1,16 +1,5 @@
import type { CompletionContext } from '@codemirror/autocomplete';
export interface GenericCompletionOption {
label: string;
type: 'constant' | 'variable';
detail?: string;
info?: string;
/** When given, should be a number from -99 to 99 that adjusts
* how this completion is ranked compared to other completions
* that match the input as well as this one. A negative number
* moves it down the list, a positive number moves it up. */
boost?: number;
}
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
export interface GenericCompletionConfig {
minMatch?: number;

View File

@@ -16,6 +16,7 @@ export type InputProps = Pick<
| 'useTemplating'
| 'autocomplete'
| 'forceUpdateKey'
| 'disabled'
| 'autoFocus'
| 'autoSelect'
| 'autocompleteVariables'
@@ -75,6 +76,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
readOnly,
stateKey,
multiLine,
disabled,
...props
}: InputProps,
ref,
@@ -82,18 +84,26 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
const [focused, setFocused] = useState(false);
const [hasChanged, setHasChanged] = useStateWithDeps<boolean>(false, [stateKey, forceUpdateKey]);
const editorRef = useRef<EditorView | null>(null);
useImperativeHandle<EditorView | null, EditorView | null>(ref, () => editorRef.current);
const handleFocus = useCallback(() => {
if (readOnly) return;
setFocused(true);
// Select all text on focus
editorRef.current?.dispatch({
selection: { anchor: 0, head: editorRef.current.state.doc.length },
});
onFocus?.();
}, [onFocus, readOnly]);
const handleBlur = useCallback(() => {
setFocused(false);
editorRef.current?.dispatch({ selection: { anchor: 0 } });
// Move selection to the end on blur
editorRef.current?.dispatch({
selection: { anchor: editorRef.current.state.doc.length },
});
onBlur?.();
}, [onBlur]);
@@ -114,13 +124,14 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
(value: string) => {
setCurrentValue(value);
onChange?.(value);
setHasChanged(true);
},
[onChange],
[onChange, setHasChanged],
);
const wrapperRef = useRef<HTMLDivElement>(null);
// Submit nearest form on Enter key press
// Submit the nearest form on Enter key press
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key !== 'Enter') return;
@@ -145,7 +156,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
>
<Label
htmlFor={id.current}
optional={!required}
required={required}
visuallyHidden={hideLabel}
className={classNames(labelClassName)}
>
@@ -158,8 +169,9 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
'x-theme-input',
'relative w-full rounded-md text',
'border',
focused ? 'border-border-focus' : 'border-border',
!isValid && '!border-danger',
focused && !disabled ? 'border-border-focus' : 'border-border',
disabled && 'border-dotted',
!isValid && hasChanged && '!border-danger',
size === 'md' && 'min-h-md',
size === 'sm' && 'min-h-sm',
size === 'xs' && 'min-h-xs',
@@ -190,7 +202,12 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
onChange={handleChange}
onPaste={onPaste}
onPasteOverwrite={onPasteOverwrite}
className={classNames(editorClassName, multiLine && 'py-1.5')}
disabled={disabled}
className={classNames(
editorClassName,
multiLine && size === 'md' && 'py-1.5',
multiLine && size === 'sm' && 'py-1',
)}
onFocus={handleFocus}
onBlur={handleBlur}
readOnly={readOnly}
@@ -201,7 +218,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
<IconButton
title={obscured ? `Show ${label}` : `Obscure ${label}`}
size="xs"
className="mr-0.5 group/obscure !h-auto my-0.5"
className={classNames("mr-0.5 group/obscure !h-auto my-0.5", disabled && 'opacity-disabled')}
iconClassName="text-text-subtle group-hover/obscure:text"
iconSize="sm"
icon={obscured ? 'eye' : 'eye_closed'}

View File

@@ -4,30 +4,32 @@ import type { HTMLAttributes } from 'react';
export function Label({
htmlFor,
className,
optional,
children,
visuallyHidden,
otherTags = [],
tags = [],
required,
...props
}: HTMLAttributes<HTMLLabelElement> & {
htmlFor: string;
optional?: boolean;
otherTags?: string[];
required?: boolean;
tags?: string[];
visuallyHidden?: boolean;
}) {
const tags = optional ? ['optional', ...otherTags] : otherTags;
return (
<label
htmlFor={htmlFor}
className={classNames(
className,
visuallyHidden && 'sr-only',
'flex-shrink-0',
'flex-shrink-0 text-sm',
'text-text-subtle whitespace-nowrap flex items-center gap-1',
)}
htmlFor={htmlFor}
{...props}
>
{children}
<span>
{children}
{required === true && <span className="text-text-subtlest">*</span>}
</span>
{tags.map((tag, i) => (
<span key={i} className="text-xs text-text-subtlest">
({tag})

View File

@@ -168,7 +168,8 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
);
const handleChange = useCallback(
(pair: PairWithId) => setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))),
(pair: PairWithId) =>
setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))),
[setPairsAndSave],
);
@@ -344,7 +345,6 @@ function PairEditorRow({
const deleteItems = useMemo(
(): DropdownItem[] => [
{
key: 'delete',
label: 'Delete',
onSelect: handleDelete,
color: 'danger',
@@ -570,7 +570,6 @@ function FileActionsDropdown({
const extraItems = useMemo<DropdownItem[]>(
() => [
{
key: 'mime',
label: 'Set Content-Type',
leftSlot: <Icon icon="pencil" />,
hidden: !pair.isFile,
@@ -589,7 +588,6 @@ function FileActionsDropdown({
},
},
{
key: 'clear-file',
label: 'Unset File',
leftSlot: <Icon icon="x" />,
hidden: pair.isFile,
@@ -598,7 +596,6 @@ function FileActionsDropdown({
},
},
{
key: 'delete',
label: 'Delete',
onSelect: onDelete,
variant: 'danger',

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import type { HTMLAttributes, FocusEvent } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import type { FocusEvent, HTMLAttributes } from 'react';
import { useCallback, useRef, useState } from 'react';
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
import { IconButton } from './IconButton';
import type { InputProps } from './Input';
@@ -41,8 +41,8 @@ export function PlainInput({
onFocusRaw,
}: PlainInputProps) {
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
const [focused, setFocused] = useState(false);
const [hasChanged, setHasChanged] = useState<boolean>(false);
const inputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -71,19 +71,19 @@ export function PlainInput({
'px-2 text-xs font-mono cursor-text',
);
const isValid = useMemo(() => {
if (required && !validateRequire(currentValue)) return false;
if (typeof validate === 'boolean') return validate;
if (typeof validate === 'function' && !validate(currentValue)) return false;
return true;
}, [required, currentValue, validate]);
const handleChange = useCallback(
(value: string) => {
setCurrentValue(value);
onChange?.(value);
setHasChanged(true);
const isValid = (value: string) => {
if (required && !validateRequire(value)) return false;
if (typeof validate === 'boolean') return validate;
if (typeof validate === 'function' && !validate(value)) return false;
return true;
};
inputRef.current?.setCustomValidity(isValid(value) ? '' : 'Invalid value');
},
[onChange],
[onChange, required, validate],
);
const wrapperRef = useRef<HTMLDivElement>(null);
@@ -98,12 +98,7 @@ export function PlainInput({
labelPosition === 'top' && 'flex-row gap-0.5',
)}
>
<Label
htmlFor={id}
className={labelClassName}
visuallyHidden={hideLabel}
optional={!required}
>
<Label htmlFor={id} className={labelClassName} visuallyHidden={hideLabel} required={required}>
{label}
</Label>
<HStack
@@ -114,7 +109,7 @@ export function PlainInput({
'relative w-full rounded-md text',
'border',
focused ? 'border-border-focus' : 'border-border-subtle',
!isValid && '!border-danger',
hasChanged && 'has-[:invalid]:border-danger', // For built-in HTML validation
size === 'md' && 'min-h-md',
size === 'sm' && 'min-h-sm',
size === 'xs' && 'min-h-xs',

View File

@@ -2,7 +2,7 @@ import type { PromptTextRequest } from '@yaakapp-internal/plugins';
import type { FormEvent, ReactNode } from 'react';
import { useCallback, useState } from 'react';
import { Button } from './Button';
import { Input } from './Input';
import { PlainInput } from './PlainInput';
import { HStack } from './Stacks';
export type PromptProps = Omit<PromptTextRequest, 'id' | 'title' | 'description'> & {
@@ -35,7 +35,7 @@ export function Prompt({
className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-4 mb-4"
onSubmit={handleSubmit}
>
<Input
<PlainInput
hideLabel
autoSelect
required={required}
@@ -43,7 +43,6 @@ export function Prompt({
label={label}
defaultValue={defaultValue}
onChange={setValue}
stateKey={null}
/>
<HStack space={2} justifyContent="end">
<Button onClick={onCancel} variant="border" color="secondary">

View File

@@ -23,12 +23,14 @@ export interface SelectProps<T extends string> {
size?: ButtonProps['size'];
className?: string;
event?: string;
disabled?: boolean;
}
export function Select<T extends string>({
labelPosition = 'top',
name,
labelClassName,
disabled,
hideLabel,
label,
value,
@@ -72,7 +74,8 @@ export function Select<T extends string>({
'w-full rounded-md text text-sm font-mono',
'pl-2',
'border',
focused ? 'border-border-focus' : 'border-border',
focused && !disabled ? 'border-border-focus' : 'border-border',
disabled && 'border-dotted',
isInvalidSelection && 'border-danger',
size === 'xs' && 'h-xs',
size === 'sm' && 'h-sm',
@@ -86,7 +89,8 @@ export function Select<T extends string>({
onChange={(e) => handleChange(e.target.value as T)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
className={classNames('pr-7 w-full outline-none bg-transparent')}
className={classNames('pr-7 w-full outline-none bg-transparent disabled:opacity-disabled')}
disabled={disabled}
>
{isInvalidSelection && <option value={'__NONE__'}>-- Select an Option --</option>}
{options.map((o) => {
@@ -109,6 +113,7 @@ export function Select<T extends string>({
variant="border"
size={size}
leftSlot={leftSlot}
disabled={disabled}
forDropdown
>
{options.find((o) => o.type !== 'separator' && o.value === value)?.label ?? '--'}

View File

@@ -20,9 +20,8 @@ export interface ToastProps {
color?: ShowToastRequest['color'];
}
const ICONS: Record<NonNullable<ToastProps['color']>, IconProps['icon'] | null> = {
const ICONS: Record<NonNullable<ToastProps['color'] | 'custom'>, IconProps['icon'] | null> = {
custom: null,
default: 'info',
danger: 'alert_triangle',
info: 'info',
notice: 'alert_triangle',
@@ -42,9 +41,8 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
{},
[open],
);
color = color ?? 'default';
const toastIcon = icon ?? (color in ICONS && ICONS[color]);
const toastIcon = icon ?? (color && color in ICONS && ICONS[color]);
return (
<motion.div

View File

@@ -26,7 +26,6 @@ export function useCreateDropdownItems({
return [
{
key: 'create-http-request',
label: 'HTTP Request',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () => {
@@ -34,7 +33,6 @@ export function useCreateDropdownItems({
},
},
{
key: 'create-graphql-request',
label: 'GraphQL Query',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () =>
@@ -46,7 +44,6 @@ export function useCreateDropdownItems({
}),
},
{
key: 'create-grpc-request',
label: 'gRPC Call',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () => createGrpcRequest({ folderId }),
@@ -54,11 +51,8 @@ export function useCreateDropdownItems({
...((hideFolder
? []
: [
{ type: 'separator' },
{
type: 'separator',
},
{
key: 'create-folder',
label: 'Folder',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () => createFolder.mutate({ folderId }),

View File

@@ -1,39 +1,42 @@
import { useQuery } from '@tanstack/react-query';
import type { GetHttpAuthenticationResponse } from '@yaakapp-internal/plugins';
import type { GetHttpAuthenticationSummaryResponse } from '@yaakapp-internal/plugins';
import { useAtomValue } from 'jotai';
import { atom, useSetAtom } from 'jotai/index';
import { atom } from 'jotai/index';
import { useState } from 'react';
import { jotaiStore } from '../lib/jotai';
import { invokeCmd } from '../lib/tauri';
import { showErrorToast } from '../lib/toast';
import { usePluginsKey } from './usePlugins';
const httpAuthenticationAtom = atom<GetHttpAuthenticationResponse[]>([]);
const httpAuthenticationSummariesAtom = atom<GetHttpAuthenticationSummaryResponse[]>([]);
const orderedHttpAuthenticationAtom = atom((get) =>
get(httpAuthenticationAtom).sort((a, b) => a.name.localeCompare(b.name)),
get(httpAuthenticationSummariesAtom)?.sort((a, b) => a.name.localeCompare(b.name)),
);
export function useHttpAuthentication() {
export function useHttpAuthenticationSummaries() {
return useAtomValue(orderedHttpAuthenticationAtom);
}
export function useSubscribeHttpAuthentication() {
const [numResults, setNumResults] = useState<number>(0);
const setAtom = useSetAtom(httpAuthenticationAtom);
const pluginsKey = usePluginsKey();
useQuery({
queryKey: ['http_authentication'],
queryKey: ['http_authentication_summaries', pluginsKey],
// Fetch periodically until functions are returned
// NOTE: visibilitychange (refetchOnWindowFocus) does not work on Windows, so we'll rely on this logic
// to refetch things until that's working again
// TODO: Update plugin system to wait for plugins to initialize before sending the first event to them
refetchInterval: numResults > 0 ? Infinity : 1000,
refetchOnMount: true,
placeholderData: (prev) => prev, // Keep previous data on refetch
queryFn: async () => {
try {
const result = await invokeCmd<GetHttpAuthenticationResponse[]>(
'cmd_get_http_authentication',
const result = await invokeCmd<GetHttpAuthenticationSummaryResponse[]>(
'cmd_get_http_authentication_summaries',
);
setNumResults(result.length);
setAtom(result);
jotaiStore.set(httpAuthenticationSummariesAtom, result);
return result;
} catch (err) {
showErrorToast('http-authentication-error', err);

View File

@@ -0,0 +1,59 @@
import { useQuery } from '@tanstack/react-query';
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import type { GetHttpAuthenticationConfigResponse, JsonPrimitive } from '@yaakapp-internal/plugins';
import { md5 } from 'js-md5';
import { useState } from 'react';
import { invokeCmd } from '../lib/tauri';
import { useHttpResponses } from './useHttpResponses';
export function useHttpAuthenticationConfig(
authName: string | null,
values: Record<string, JsonPrimitive>,
requestId: string,
) {
const responses = useHttpResponses();
const [forceRefreshCounter, setForceRefreshCounter] = useState<number>(0);
// 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: ['http_authentication_config', requestId, authName, values, responseKey, forceRefreshCounter],
placeholderData: (prev) => prev, // Keep previous data on refetch
queryFn: async () => {
const config = await invokeCmd<GetHttpAuthenticationConfigResponse>(
'cmd_get_http_authentication_config',
{
authName,
values,
requestId,
},
);
return {
...config,
actions: config.actions?.map((a, i) => ({
...a,
call: async ({ id: requestId }: HttpRequest | GrpcRequest) => {
await invokeCmd('cmd_call_http_authentication_action', {
pluginRefId: config.pluginRefId,
actionIndex: i,
authName,
values,
requestId,
});
// Ensure the config is refreshed after the action is done
setForceRefreshCounter((c) => c + 1);
},
})),
};
},
});
}

View File

@@ -5,11 +5,11 @@ import type {
GetHttpRequestActionsResponse,
HttpRequestAction,
} from '@yaakapp-internal/plugins';
import { useMemo } from 'react';
import { invokeCmd } from '../lib/tauri';
import { usePluginsKey } from './usePlugins';
import { useMemo } from 'react';
export type CallableHttpRequestAction = Pick<HttpRequestAction, 'key' | 'label' | 'icon'> & {
export type CallableHttpRequestAction = Pick<HttpRequestAction, 'label' | 'icon'> & {
call: (httpRequest: HttpRequest) => Promise<void>;
};
@@ -24,13 +24,12 @@ export function useHttpRequestActions() {
);
return responses.flatMap((r) =>
r.actions.map((a) => ({
key: a.key,
r.actions.map((a, i) => ({
label: a.label,
icon: a.icon,
call: async (httpRequest: HttpRequest) => {
const payload: CallHttpRequestActionRequest = {
key: a.key,
index: i,
pluginRefId: r.pluginRefId,
args: { httpRequest },
};

View File

@@ -25,7 +25,6 @@ export function useNotificationToast() {
id: payload.id,
timeout: null,
message: payload.message,
color: 'custom',
onClose: () => markRead(payload.id),
action: ({ hide }) =>
actionLabel && actionUrl ? (

View File

@@ -3,7 +3,7 @@ import type { GetTemplateFunctionsResponse, TemplateFunction } from '@yaakapp-in
import { atom, useAtomValue } from 'jotai';
import { useSetAtom } from 'jotai/index';
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 { usePluginsKey } from './usePlugins';
@@ -17,8 +17,9 @@ export function useTemplateFunctionCompletionOptions(
return (
templateFunctions.map((fn) => {
const NUM_ARGS = 2;
const argsWithName = fn.args.filter((a) => 'name' in a);
const shortArgs =
fn.args
argsWithName
.slice(0, NUM_ARGS)
.map((a) => a.name)
.join(', ') + (fn.args.length > NUM_ARGS ? ', …' : '');
@@ -27,7 +28,7 @@ export function useTemplateFunctionCompletionOptions(
aliases: fn.aliases,
type: 'function',
description: fn.description,
args: fn.args.map((a) => ({ name: a.name })),
args: argsWithName.map((a) => ({ name: a.name })),
value: null,
label: `${fn.name}(${shortArgs})`,
onClick: (rawTag: string, startPos: number) => onClick(fn, rawTag, startPos),

View File

@@ -1,4 +1,4 @@
import type { GenericCompletionOption } from '../../components/core/Editor/genericCompletion';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
export const headerNames: (GenericCompletionOption | string)[] = [
{

View File

@@ -2,17 +2,16 @@ import type { InvokeArgs } from '@tauri-apps/api/core';
import { invoke } from '@tauri-apps/api/core';
type TauriCmd =
| 'cmd_call_http_authentication_action'
| 'cmd_call_http_request_action'
| 'cmd_check_for_updates'
| 'cmd_create_cookie_jar'
| 'cmd_create_environment'
| 'cmd_template_tokens_to_string'
| 'cmd_create_grpc_request'
| 'cmd_create_http_request'
| 'cmd_curl_to_request'
| 'cmd_delete_all_grpc_connections'
| 'cmd_delete_all_http_responses'
| 'cmd_delete_send_history'
| 'cmd_delete_cookie_jar'
| 'cmd_delete_environment'
| 'cmd_delete_folder'
@@ -20,6 +19,7 @@ type TauriCmd =
| 'cmd_delete_grpc_request'
| 'cmd_delete_http_request'
| 'cmd_delete_http_response'
| 'cmd_delete_send_history'
| 'cmd_delete_workspace'
| 'cmd_dismiss_notification'
| 'cmd_duplicate_folder'
@@ -32,11 +32,12 @@ type TauriCmd =
| 'cmd_get_environment'
| 'cmd_get_folder'
| 'cmd_get_grpc_request'
| 'cmd_get_http_authentication'
| 'cmd_get_http_authentication_config'
| 'cmd_get_http_authentication_summaries'
| 'cmd_get_http_request'
| 'cmd_get_sse_events'
| 'cmd_get_key_value'
| 'cmd_get_settings'
| 'cmd_get_sse_events'
| 'cmd_get_workspace'
| 'cmd_get_workspace_meta'
| 'cmd_grpc_go'
@@ -45,7 +46,6 @@ type TauriCmd =
| 'cmd_import_data'
| 'cmd_install_plugin'
| 'cmd_list_cookie_jars'
| 'cmd_list_key_values'
| 'cmd_list_environments'
| 'cmd_list_folders'
| 'cmd_list_grpc_connections'
@@ -53,21 +53,23 @@ type TauriCmd =
| 'cmd_list_grpc_requests'
| 'cmd_list_http_requests'
| 'cmd_list_http_responses'
| 'cmd_list_key_values'
| 'cmd_list_plugins'
| 'cmd_list_workspaces'
| 'cmd_metadata'
| 'cmd_new_main_window'
| 'cmd_new_child_window'
| 'cmd_new_main_window'
| 'cmd_parse_template'
| 'cmd_plugin_info'
| 'cmd_render_template'
| 'cmd_reload_plugins'
| 'cmd_render_template'
| 'cmd_save_response'
| 'cmd_send_ephemeral_request'
| 'cmd_send_http_request'
| 'cmd_set_key_value'
| 'cmd_set_update_mode'
| 'cmd_template_functions'
| 'cmd_template_tokens_to_string'
| 'cmd_track_event'
| 'cmd_uninstall_plugin'
| 'cmd_update_cookie_jar'