Auth plugins (#155)

This commit is contained in:
Gregory Schier
2025-01-17 05:53:03 -08:00
committed by GitHub
parent e21df98a30
commit bd322162c8
56 changed files with 5468 additions and 1474 deletions

2
package-lock.json generated
View File

@@ -15808,7 +15808,7 @@
},
"packages/plugin-runtime-types": {
"name": "@yaakapp/api",
"version": "0.2.17",
"version": "0.2.25",
"dependencies": {
"@types/node": "^22.5.4"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp/api",
"version": "0.2.17",
"version": "0.2.26",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [
@@ -11,8 +11,9 @@
"build": "run-s build:copy-types build:tsc",
"build:tsc": "tsc",
"build:copy-types": "run-p build:copy-types:*",
"build:copy-types:root": "cpy --flat ../../src-tauri/yaak-plugin-runtime/bindings/*.ts ./src/bindings",
"build:copy-types:next": "cpy --flat ../../src-tauri/yaak-plugin-runtime/bindings/serde_json/*.ts ./src/bindings/serde_json",
"build:copy-types:root": "cpy --flat ../../src-tauri/yaak-plugins/bindings/*.ts ./src/bindings",
"build:copy-types:next": "cpy --flat ../../src-tauri/yaak-plugins/bindings/serde_json/*.ts ./src/bindings/serde_json",
"publish": "npm publish",
"prepublishOnly": "npm run build"
},
"dependencies": {

View File

@@ -1,338 +1,223 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type {
Environment,
Folder,
GrpcRequest,
HttpRequest,
HttpResponse,
Workspace,
} from './models';
import type { JsonValue } from './serde_json/JsonValue';
import type { Environment } from "./models";
import type { Folder } from "./models";
import type { GrpcRequest } from "./models";
import type { HttpRequest } from "./models";
import type { HttpResponse } from "./models";
import type { JsonValue } from "./serde_json/JsonValue";
import type { Workspace } from "./models";
export type BootRequest = { dir: string; watch: boolean };
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string; version: string; capabilities: Array<string> };
export type BootResponse = { name: string, version: string, capabilities: Array<string>, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest };
export type CallHttpAuthenticationRequest = { config: { [key in string]?: JsonValue }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpRequestActionRequest = {
key: string;
pluginRefId: string;
args: CallHttpRequestActionArgs;
};
export type CallHttpAuthenticationResponse = { url: string, headers: Array<HttpHeader>, };
export type CallTemplateFunctionArgs = {
purpose: RenderPurpose;
values: { [key in string]?: string };
};
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallTemplateFunctionRequest = { name: string; args: CallTemplateFunctionArgs };
export type CallHttpRequestActionRequest = { key: string, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallTemplateFunctionResponse = { value: string | null };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: string }, };
export type Color =
| 'custom'
| 'default'
| 'primary'
| 'secondary'
| 'info'
| 'success'
| 'notice'
| 'warning'
| 'danger';
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
export type CopyTextRequest = { text: string };
export type CallTemplateFunctionResponse = { value: string | null, };
export type ExportHttpRequestRequest = { httpRequest: HttpRequest };
export type Color = "custom" | "default" | "primary" | "secondary" | "info" | "success" | "notice" | "warning" | "danger";
export type ExportHttpRequestResponse = { content: string };
export type CopyTextRequest = { text: string, };
export type FilterRequest = { content: string; filter: string };
export type EmptyPayload = {};
export type FilterResponse = { content: string };
export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
export type FindHttpResponsesRequest = { requestId: string; limit?: number };
export type ExportHttpRequestResponse = { content: string, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse> };
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": "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,
/**
* 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,
/**
* 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,
/**
* 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,
/**
* 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,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputSelectOption = { name: string, value: string, };
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type GetHttpAuthenticationResponse = { name: string, pluginName: string, config: Array<FormInput>, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = {
actions: Array<HttpRequestAction>;
pluginRefId: string;
};
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string };
export type GetHttpRequestByIdRequest = { id: string, };
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null };
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
export type GetTemplateFunctionsResponse = {
functions: Array<TemplateFunction>;
pluginRefId: string;
};
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpRequestAction = { key: string; label: string; icon?: Icon };
export type HttpHeader = { name: string, value: string, };
export type Icon = 'copy' | 'info' | 'check_circle' | 'alert_triangle' | '_unknown';
export type HttpRequestAction = { key: string, label: string, icon?: Icon, };
export type ImportRequest = { content: string };
export type Icon = "copy" | "info" | "check_circle" | "alert_triangle" | "_unknown";
export type ImportResources = {
workspaces: Array<Workspace>;
environments: Array<Environment>;
folders: Array<Folder>;
httpRequests: Array<HttpRequest>;
grpcRequests: Array<GrpcRequest>;
};
export type ImportRequest = { content: string, };
export type ImportResponse = { resources: ImportResources };
export type ImportResources = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, };
export type InternalEvent = {
id: string;
pluginRefId: string;
replyId: string | null;
payload: InternalEventPayload;
windowContext: WindowContext;
};
export type ImportResponse = { resources: ImportResources, };
export type InternalEventPayload =
| ({ type: 'boot_request' } & BootRequest)
| ({ type: 'boot_response' } & BootResponse)
| { type: 'reload_request' }
| { type: 'reload_response' }
| { 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' } & GetHttpRequestActionsRequest)
| ({ 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: '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' };
export type InternalEvent = { id: string, pluginRefId: string, replyId: string | null, payload: InternalEventPayload, windowContext: WindowContext, };
export type OpenFileFilter = {
name: string;
/**
* File extensions to require
*/
extensions: Array<string>;
};
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;
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
*/
require?: boolean;
};
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
*/
require?: boolean, };
export type PromptTextResponse = { value: string | null };
export type PromptTextResponse = { value: string | null, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest; purpose: RenderPurpose };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
export type RenderPurpose = 'send' | 'preview';
export type RenderPurpose = "send" | "preview";
export type SendHttpRequestRequest = { httpRequest: HttpRequest };
export type SendHttpRequestRequest = { httpRequest: HttpRequest, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
export type ShowToastRequest = { message: string; color?: Color; icon?: Icon };
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<TemplateFunctionArg>;
};
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 TemplateFunctionArg =
| ({ type: 'text' } & TemplateFunctionTextArg)
| ({
type: 'select';
} & TemplateFunctionSelectArg)
| ({ type: 'checkbox' } & TemplateFunctionCheckboxArg)
| ({
type: 'http_request';
} & TemplateFunctionHttpRequestArg)
| ({ type: 'file' } & TemplateFunctionFileArg);
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateFunctionBaseArg = {
/**
* The name of the argument. Should be `camelCase` format
*/
name: string;
/**
* Whether the user must fill in the argument
*/
optional?: boolean;
/**
* The label of the input
*/
label?: string;
/**
* The default value
*/
defaultValue?: string;
};
export type TemplateRenderResponse = { data: JsonValue, };
export type TemplateFunctionCheckboxArg = {
/**
* The name of the argument. Should be `camelCase` format
*/
name: string;
/**
* Whether the user must fill in the argument
*/
optional?: boolean;
/**
* The label of the input
*/
label?: string;
/**
* The default value
*/
defaultValue?: string;
};
export type TemplateFunctionFileArg = {
/**
* The title of the file selection window
*/
title: string;
/**
* Allow selecting multiple files
*/
multiple?: boolean;
directory?: boolean;
defaultPath?: string;
filters?: Array<OpenFileFilter>;
/**
* The name of the argument. Should be `camelCase` format
*/
name: string;
/**
* Whether the user must fill in the argument
*/
optional?: boolean;
/**
* The label of the input
*/
label?: string;
/**
* The default value
*/
defaultValue?: string;
};
export type TemplateFunctionHttpRequestArg = {
/**
* The name of the argument. Should be `camelCase` format
*/
name: string;
/**
* Whether the user must fill in the argument
*/
optional?: boolean;
/**
* The label of the input
*/
label?: string;
/**
* The default value
*/
defaultValue?: string;
};
export type TemplateFunctionSelectArg = {
/**
* The options that will be available in the select input
*/
options: Array<TemplateFunctionSelectOption>;
/**
* The name of the argument. Should be `camelCase` format
*/
name: string;
/**
* Whether the user must fill in the argument
*/
optional?: boolean;
/**
* The label of the input
*/
label?: string;
/**
* The default value
*/
defaultValue?: string;
};
export type TemplateFunctionSelectOption = { label: string; value: string };
export type TemplateFunctionTextArg = {
/**
* Placeholder for the text input
*/
placeholder?: string;
/**
* The name of the argument. Should be `camelCase` format
*/
name: string;
/**
* Whether the user must fill in the argument
*/
optional?: boolean;
/**
* The label of the input
*/
label?: string;
/**
* The default value
*/
defaultValue?: string;
};
export type TemplateRenderRequest = { data: JsonValue; purpose: RenderPurpose };
export type TemplateRenderResponse = { data: JsonValue };
export type WindowContext = { type: 'none' } | { type: 'label'; label: string };
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };

View File

@@ -2,17 +2,17 @@
export type Environment = { model: "environment", id: string, workspaceId: string, environmentId: string | null, createdAt: string, updatedAt: string, name: string, variables: Array<EnvironmentVariable>, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id: string, };
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, };
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, };
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, };
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, };
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
@@ -20,6 +20,6 @@ export type HttpResponseHeader = { name: string, value: string, };
export type HttpResponseState = "initialized" | "connected" | "closed";
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, };
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };

View File

@@ -0,0 +1,9 @@
import {CallHttpAuthenticationRequest, CallHttpAuthenticationResponse, GetHttpAuthenticationResponse} from '..';
import type { Context } from './Context';
export type AuthenticationPlugin = Omit<GetHttpAuthenticationResponse, 'pluginName'> & {
onApply(
ctx: Context,
args: CallHttpAuthenticationRequest,
): Promise<CallHttpAuthenticationResponse> | CallHttpAuthenticationResponse;
};

View File

@@ -1,3 +1,4 @@
import { AuthenticationPlugin } from './AuthenticationPlugin';
import type { FilterPlugin } from './FilterPlugin';
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
import type { ImporterPlugin } from './ImporterPlugin';
@@ -13,6 +14,7 @@ export type PluginDefinition = {
importer?: ImporterPlugin;
theme?: ThemePlugin;
filter?: FilterPlugin;
authentication?: AuthenticationPlugin;
httpRequestActions?: HttpRequestActionPlugin[];
templateFunctions?: TemplateFunctionPlugin[];
};

View File

@@ -18,7 +18,7 @@ import type { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/HttpReque
import type { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
import interceptStdout from 'intercept-stdout';
import * as console from 'node:console';
import type { Stats} from 'node:fs';
import type { Stats } from 'node:fs';
import { readFileSync, statSync, watch } from 'node:fs';
import path from 'node:path';
import * as util from 'node:util';
@@ -303,6 +303,33 @@ async function initialize() {
return;
}
if (payload.type === 'get_http_authentication_request' && mod.plugin?.authentication) {
const replyPayload: InternalEventPayload = {
type: 'get_http_authentication_response',
name: mod.plugin.authentication.name,
pluginName: pkg.name,
config: mod.plugin.authentication.config,
};
sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'call_http_authentication_request' && mod.plugin?.authentication) {
const auth = mod.plugin.authentication;
if (typeof auth?.onApply === 'function') {
const result = await auth.onApply(ctx, payload);
sendPayload(
windowContext,
{
...result,
type: 'call_http_authentication_response',
},
replyId,
);
return;
}
}
if (
payload.type === 'call_http_request_action_request' &&
Array.isArray(mod.plugin?.httpRequestActions)

494
src-tauri/Cargo.lock generated
View File

@@ -392,34 +392,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "axum"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
dependencies = [
"async-trait",
"axum-core 0.3.4",
"bitflags 1.3.2",
"bytes",
"futures-util",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.30",
"itoa 1.0.11",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"sync_wrapper 0.1.2",
"tower 0.4.13",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum"
version = "0.7.5"
@@ -427,11 +399,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
dependencies = [
"async-trait",
"axum-core 0.4.3",
"axum-core",
"bytes",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"http",
"http-body",
"http-body-util",
"itoa 1.0.11",
"matchit",
@@ -447,23 +419,6 @@ dependencies = [
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http 0.2.12",
"http-body 0.4.6",
"mime",
"rustversion",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.4.3"
@@ -473,8 +428,8 @@ dependencies = [
"async-trait",
"bytes",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
@@ -1528,17 +1483,11 @@ dependencies = [
[[package]]
name = "eventsource-client"
version = "0.13.0"
source = "git+https://github.com/yaakapp/rust-eventsource-client#e9e1e52421f11f0409179389b997aa49275a8461"
version = "0.14.0"
source = "git+https://github.com/yaakapp/rust-eventsource-client#60e0e3ac5038149c4778dc4979b09b152214f9a8"
dependencies = [
"futures",
"hyper 0.14.30",
"hyper-rustls 0.24.2",
"hyper-timeout 0.4.1",
"log",
"pin-project",
"rand 0.8.5",
"tokio",
]
[[package]]
@@ -1709,21 +1658,6 @@ dependencies = [
"new_debug_unreachable",
]
[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
@@ -1810,7 +1744,6 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@@ -2136,25 +2069,6 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "h2"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap 2.3.0",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "h2"
version = "0.4.5"
@@ -2166,7 +2080,7 @@ dependencies = [
"fnv",
"futures-core",
"futures-sink",
"http 1.1.0",
"http",
"indexmap 2.3.0",
"slab",
"tokio",
@@ -2284,37 +2198,15 @@ dependencies = [
[[package]]
name = "http"
version = "0.2.12"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
dependencies = [
"bytes",
"fnv",
"itoa 1.0.11",
]
[[package]]
name = "http"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
"itoa 1.0.11",
]
[[package]]
name = "http-body"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http 0.2.12",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.1"
@@ -2322,7 +2214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http 1.1.0",
"http",
]
[[package]]
@@ -2333,8 +2225,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"http",
"http-body",
"pin-project-lite",
]
@@ -2358,40 +2250,16 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "0.14.30"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa 1.0.11",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2 0.4.5",
"http 1.1.0",
"http-body 1.0.1",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa 1.0.11",
@@ -2403,57 +2271,30 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.24.2"
version = "0.27.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.30",
"log",
"rustls 0.21.12",
"rustls-native-certs 0.6.3",
"tokio",
"tokio-rustls 0.24.1",
]
[[package]]
name = "hyper-rustls"
version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [
"futures-util",
"http 1.1.0",
"hyper 1.4.1",
"http",
"hyper",
"hyper-util",
"rustls 0.23.21",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"tokio",
"tokio-rustls 0.26.0",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]]
name = "hyper-timeout"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
dependencies = [
"hyper 0.14.30",
"pin-project-lite",
"tokio",
"tokio-io-timeout",
]
[[package]]
name = "hyper-timeout"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793"
dependencies = [
"hyper 1.4.1",
"hyper",
"hyper-util",
"pin-project-lite",
"tokio",
@@ -2468,7 +2309,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper 1.4.1",
"hyper",
"hyper-util",
"native-tls",
"tokio",
@@ -2485,9 +2326,9 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"hyper 1.4.1",
"http",
"http-body",
"hyper",
"pin-project-lite",
"socket2",
"tokio",
@@ -2672,15 +2513,6 @@ dependencies = [
"nom 4.2.3",
]
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.13.0"
@@ -4114,22 +3946,12 @@ dependencies = [
[[package]]
name = "prost"
version = "0.12.6"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29"
checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec"
dependencies = [
"bytes",
"prost-derive 0.12.6",
]
[[package]]
name = "prost"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f"
dependencies = [
"bytes",
"prost-derive 0.13.3",
"prost-derive",
]
[[package]]
@@ -4140,14 +3962,14 @@ checksum = "5bb182580f71dd070f88d01ce3de9f4da5021db7115d2e1c3605a754153b77c1"
dependencies = [
"bytes",
"heck 0.5.0",
"itertools 0.13.0",
"itertools",
"log",
"multimap",
"once_cell",
"petgraph",
"prettyplease",
"prost 0.13.3",
"prost-types 0.13.3",
"prost",
"prost-types",
"regex",
"syn 2.0.87",
"tempfile",
@@ -4155,25 +3977,12 @@ dependencies = [
[[package]]
name = "prost-derive"
version = "0.12.6"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
dependencies = [
"anyhow",
"itertools 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "prost-derive"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5"
dependencies = [
"anyhow",
"itertools 0.13.0",
"itertools",
"proc-macro2",
"quote",
"syn 2.0.87",
@@ -4181,24 +3990,24 @@ dependencies = [
[[package]]
name = "prost-reflect"
version = "0.12.0"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057237efdb71cf4b3f9396302a3d6599a92fa94063ba537b66130980ea9909f3"
checksum = "bc9647f03b808b79abca8408b1609be9887ba90453c940d00332a60eeb6f5748"
dependencies = [
"base64 0.21.7",
"base64 0.22.1",
"once_cell",
"prost 0.12.6",
"prost",
"prost-reflect-derive",
"prost-types 0.12.6",
"prost-types",
"serde",
"serde-value",
]
[[package]]
name = "prost-reflect-derive"
version = "0.12.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "172da1212c02be2c94901440cb27183cd92bff00ebacca5c323bf7520b8f9c04"
checksum = "f4fce6b22f15cc8d8d400a2b98ad29202b33bd56c7d9ddd815bc803a807ecb65"
dependencies = [
"proc-macro2",
"quote",
@@ -4207,20 +4016,11 @@ dependencies = [
[[package]]
name = "prost-types"
version = "0.12.6"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0"
checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc"
dependencies = [
"prost 0.12.6",
]
[[package]]
name = "prost-types"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670"
dependencies = [
"prost 0.13.3",
"prost",
]
[[package]]
@@ -4288,7 +4088,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls 0.23.21",
"rustls",
"socket2",
"thiserror 1.0.63",
"tokio",
@@ -4305,7 +4105,7 @@ dependencies = [
"rand 0.8.5",
"ring",
"rustc-hash",
"rustls 0.23.21",
"rustls",
"slab",
"thiserror 1.0.63",
"tinyvec",
@@ -4536,12 +4336,12 @@ dependencies = [
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.4.5",
"http 1.1.0",
"http-body 1.0.1",
"h2",
"http",
"http-body",
"http-body-util",
"hyper 1.4.1",
"hyper-rustls 0.27.3",
"hyper",
"hyper-rustls",
"hyper-tls",
"hyper-util",
"ipnet",
@@ -4554,8 +4354,8 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.21",
"rustls-pemfile 2.1.3",
"rustls",
"rustls-pemfile",
"rustls-pki-types",
"serde",
"serde_json",
@@ -4564,7 +4364,7 @@ dependencies = [
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.26.0",
"tokio-rustls",
"tokio-util",
"tower 0.5.2",
"tower-service",
@@ -4741,18 +4541,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring",
"rustls-webpki 0.101.7",
"sct",
]
[[package]]
name = "rustls"
version = "0.23.21"
@@ -4762,23 +4550,11 @@ dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.102.8",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe",
"rustls-pemfile 1.0.4",
"schannel",
"security-framework 2.11.1",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.0"
@@ -4786,21 +4562,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a"
dependencies = [
"openssl-probe",
"rustls-pemfile 2.1.3",
"rustls-pemfile",
"rustls-pki-types",
"schannel",
"security-framework 2.11.1",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64 0.21.7",
]
[[package]]
name = "rustls-pemfile"
version = "2.1.3"
@@ -4828,10 +4595,10 @@ dependencies = [
"jni",
"log",
"once_cell",
"rustls 0.23.21",
"rustls-native-certs 0.8.0",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki 0.102.8",
"rustls-webpki",
"security-framework 3.2.0",
"security-framework-sys",
"webpki-root-certs",
@@ -4844,16 +4611,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.102.8"
@@ -4943,16 +4700,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "sea-query"
version = "0.32.1"
@@ -5445,8 +5192,8 @@ dependencies = [
"once_cell",
"paste",
"percent-encoding",
"rustls 0.23.21",
"rustls-pemfile 2.1.3",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
"sha2",
@@ -5854,7 +5601,7 @@ dependencies = [
"glob",
"gtk",
"heck 0.5.0",
"http 1.1.0",
"http",
"http-range",
"jni",
"libc",
@@ -6133,7 +5880,7 @@ dependencies = [
"dirs",
"flate2",
"futures-util",
"http 1.1.0",
"http",
"infer",
"minisign-verify",
"percent-encoding",
@@ -6176,7 +5923,7 @@ checksum = "2274ef891ccc0a8d318deffa9d70053f947664d12d58b9c0d1ae5e89237e01f7"
dependencies = [
"dpi",
"gtk",
"http 1.1.0",
"http",
"jni",
"raw-window-handle",
"serde",
@@ -6194,7 +5941,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3707b40711d3b9f6519150869e358ffbde7c57567fb9b5a8b51150606939b2a0"
dependencies = [
"gtk",
"http 1.1.0",
"http",
"jni",
"log",
"objc2",
@@ -6225,7 +5972,7 @@ dependencies = [
"dunce",
"glob",
"html5ever",
"http 1.1.0",
"http",
"infer",
"json-patch",
"kuchikiki",
@@ -6416,16 +6163,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-io-timeout"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
dependencies = [
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-macros"
version = "2.4.0"
@@ -6447,23 +6184,13 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls 0.21.12",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.21",
"rustls",
"rustls-pki-types",
"tokio",
]
@@ -6564,52 +6291,25 @@ dependencies = [
[[package]]
name = "tonic"
version = "0.10.2"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e"
checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
dependencies = [
"async-stream",
"async-trait",
"axum 0.6.20",
"base64 0.21.7",
"bytes",
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.30",
"hyper-timeout 0.4.1",
"percent-encoding",
"pin-project",
"prost 0.12.6",
"tokio",
"tokio-stream",
"tower 0.4.13",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tonic"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38659f4a91aba8598d27821589f5db7dddd94601e7a01b1e485a50e5484c7401"
dependencies = [
"async-stream",
"async-trait",
"axum 0.7.5",
"axum",
"base64 0.22.1",
"bytes",
"h2 0.4.5",
"http 1.1.0",
"http-body 1.0.1",
"h2",
"http",
"http-body",
"http-body-util",
"hyper 1.4.1",
"hyper-timeout 0.5.1",
"hyper",
"hyper-timeout",
"hyper-util",
"percent-encoding",
"pin-project",
"prost 0.13.3",
"prost",
"socket2",
"tokio",
"tokio-stream",
@@ -6634,15 +6334,15 @@ dependencies = [
[[package]]
name = "tonic-reflection"
version = "0.10.2"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fa37c513df1339d197f4ba21d28c918b9ef1ac1768265f11ecb6b7f1cba1b76"
checksum = "878d81f52e7fcfd80026b7fdb6a9b578b3c3653ba987f87f0dce4b64043cba27"
dependencies = [
"prost 0.12.6",
"prost-types 0.12.6",
"prost",
"prost-types",
"tokio",
"tokio-stream",
"tonic 0.10.2",
"tonic",
]
[[package]]
@@ -7703,7 +7403,7 @@ dependencies = [
"gdkx11",
"gtk",
"html5ever",
"http 1.1.0",
"http",
"javascriptcore-rs",
"jni",
"kuchikiki",
@@ -7803,13 +7503,12 @@ dependencies = [
name = "yaak-app"
version = "0.0.0"
dependencies = [
"base64 0.22.1",
"chrono",
"cocoa 0.26.0",
"datetime",
"eventsource-client",
"hex_color",
"http 1.1.0",
"http",
"log",
"mime_guess",
"objc",
@@ -7818,7 +7517,7 @@ dependencies = [
"regex",
"reqwest",
"reqwest_cookie_store",
"rustls 0.23.21",
"rustls",
"rustls-platform-verifier",
"serde",
"serde_json",
@@ -7855,20 +7554,21 @@ dependencies = [
"anyhow",
"async-recursion",
"dunce",
"hyper 0.14.30",
"hyper-rustls 0.24.2",
"hyper",
"hyper-rustls",
"hyper-util",
"log",
"md5",
"prost 0.12.6",
"prost",
"prost-reflect",
"prost-types 0.12.6",
"prost-types",
"serde",
"serde_json",
"tauri",
"tauri-plugin-shell",
"tokio",
"tokio-stream",
"tonic 0.10.2",
"tonic",
"tonic-reflection",
"uuid",
]
@@ -7916,7 +7616,7 @@ dependencies = [
"dunce",
"log",
"path-slash",
"prost 0.13.3",
"prost",
"rand 0.8.5",
"regex",
"serde",
@@ -7925,7 +7625,7 @@ dependencies = [
"tauri-plugin-shell",
"thiserror 2.0.7",
"tokio",
"tonic 0.12.1",
"tonic",
"tonic-build",
"ts-rs",
"yaak-models",

View File

@@ -38,12 +38,11 @@ cocoa = "0.26.0"
openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
[dependencies]
base64 = "0.22.0"
chrono = { version = "0.4.31", features = ["serde"] }
datetime = "0.5.2"
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.13.0" }
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
hex_color = "3.0.0"
http = "1"
http = { version = "1.2.0", default-features = false }
log = "0.4.21"
rand = "0.8.5"
regex = "1.10.2"

View File

@@ -1,10 +1,8 @@
use crate::render::render_http_request;
use crate::response_err;
use crate::template_callback::PluginTemplateCallback;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue};
use http::{HeaderMap, HeaderName, HeaderValue, Uri};
use log::{debug, error, warn};
use mime_guess::Mime;
use reqwest::redirect::Policy;
@@ -32,7 +30,8 @@ use yaak_models::queries::{
get_base_environment, get_http_response, get_or_create_settings, get_workspace,
update_response_if_id, upsert_cookie_jar, UpdateSource,
};
use yaak_plugins::events::{RenderPurpose, WindowContext};
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose, WindowContext};
use yaak_plugins::manager::PluginManager;
pub async fn send_http_request<R: Runtime>(
window: &WebviewWindow<R>,
@@ -42,6 +41,7 @@ pub async fn send_http_request<R: Runtime>(
cookie_jar: Option<CookieJar>,
cancelled_rx: &mut Receiver<bool>,
) -> Result<HttpResponse, String> {
let plugin_manager = window.state::<PluginManager>();
let workspace =
get_workspace(window, &request.workspace_id).await.expect("Failed to get Workspace");
let base_environment = get_base_environment(window, &request.workspace_id)
@@ -160,7 +160,7 @@ pub async fn send_http_request<R: Runtime>(
query_params.push((p.name, p.value));
}
let uri = match http::Uri::from_str(url_string.as_str()) {
let uri = match Uri::from_str(url_string.as_str()) {
Ok(u) => u,
Err(e) => {
return Ok(response_err(
@@ -234,29 +234,6 @@ pub async fn send_http_request<R: Runtime>(
headers.insert(header_name, header_value);
}
if let Some(b) = &rendered_request.authentication_type {
let empty_value = &serde_json::to_value("").unwrap();
let a = rendered_request.authentication;
if b == "basic" {
let username = a.get("username").unwrap_or(empty_value).as_str().unwrap_or_default();
let password = a.get("password").unwrap_or(empty_value).as_str().unwrap_or_default();
let auth = format!("{username}:{password}");
let encoded = BASE64_STANDARD.encode(auth);
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
);
} else if b == "bearer" {
let token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or_default();
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
);
}
}
let request_body = rendered_request.body;
if let Some(body_type) = &rendered_request.body_type {
if body_type == "graphql" {
@@ -383,7 +360,7 @@ pub async fn send_http_request<R: Runtime>(
// Add headers last, because previous steps may modify them
request_builder = request_builder.headers(headers);
let sendable_req = match request_builder.build() {
let mut sendable_req = match request_builder.build() {
Ok(r) => r,
Err(e) => {
warn!("Failed to build request builder {e:?}");
@@ -391,6 +368,56 @@ pub async fn send_http_request<R: Runtime>(
}
};
// Apply authentication
// Map legacy auth name values from before they were plugins
let auth_plugin_name = match request.authentication_type.clone() {
Some(s) if s == "basic" => Some("@yaakapp/auth-basic".to_string()),
Some(s) if s == "bearer" => Some("@yaakapp/auth-bearer".to_string()),
_ => request.authentication_type.to_owned(),
};
if let Some(plugin_name) = auth_plugin_name {
let req = CallHttpAuthenticationRequest {
config: serde_json::to_value(&request.authentication)
.unwrap()
.as_object()
.unwrap()
.to_owned(),
method: sendable_req.method().to_string(),
url: sendable_req.url().to_string(),
headers: sendable_req
.headers()
.iter()
.map(|(name, value)| HttpHeader {
name: name.to_string(),
value: value.to_str().unwrap_or_default().to_string(),
})
.collect(),
};
let plugin_result =
match plugin_manager.call_http_authentication(window, &plugin_name, req).await {
Ok(r) => r,
Err(e) => {
return Ok(response_err(&*response.lock().await, e.to_string(), window).await);
}
};
{
let url = sendable_req.url_mut();
*url = Url::parse(&plugin_result.url).unwrap();
}
{
let headers = sendable_req.headers_mut();
for header in plugin_result.headers {
headers.insert(
HeaderName::from_str(&header.name).unwrap(),
HeaderValue::from_str(&header.value).unwrap(),
);
}
};
}
let (resp_tx, resp_rx) = oneshot::channel::<Result<Response, reqwest::Error>>();
let (done_tx, done_rx) = oneshot::channel::<HttpResponse>();

View File

@@ -9,8 +9,6 @@ use crate::render::{render_grpc_request, render_http_request, render_json_value,
use crate::template_callback::PluginTemplateCallback;
use crate::updates::{UpdateMode, YaakUpdater};
use crate::window_menu::app_menu;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use chrono::Utc;
use eventsource_client::{EventParser, SSE};
use log::{debug, error, info, warn};
@@ -65,11 +63,11 @@ use yaak_models::queries::{
upsert_workspace_meta, BatchUpsertResult, UpdateSource,
};
use yaak_plugins::events::{
BootResponse, CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse, Icon,
InternalEvent, InternalEventPayload, PromptTextResponse, RenderHttpRequestResponse,
RenderPurpose, SendHttpRequestResponse, ShowToastRequest, TemplateRenderResponse,
WindowContext,
BootResponse, CallHttpAuthenticationRequest, CallHttpRequestActionRequest, FilterResponse,
FindHttpResponsesResponse, GetHttpAuthenticationResponse, GetHttpRequestActionsResponse,
GetHttpRequestByIdResponse, GetTemplateFunctionsResponse, HttpHeader, Icon, InternalEvent,
InternalEventPayload, PromptTextResponse, RenderHttpRequestResponse, RenderPurpose,
SendHttpRequestResponse, ShowToastRequest, TemplateRenderResponse, WindowContext,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle;
@@ -154,7 +152,7 @@ async fn cmd_render_template<R: Runtime>(
RenderPurpose::Preview,
),
)
.await;
.await;
Ok(rendered)
}
@@ -198,6 +196,7 @@ async fn cmd_grpc_go<R: Runtime>(
environment_id: Option<&str>,
proto_files: Vec<String>,
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
grpc_handle: State<'_, Mutex<GrpcHandle>>,
) -> Result<String, String> {
let environment = match environment_id {
@@ -210,7 +209,7 @@ async fn cmd_grpc_go<R: Runtime>(
.ok_or("Failed to find GRPC request")?;
let base_environment =
get_base_environment(&window, &req.workspace_id).await.map_err(|e| e.to_string())?;
let req = render_grpc_request(
let mut req = render_grpc_request(
&req,
&base_environment,
environment.as_ref(),
@@ -220,7 +219,7 @@ async fn cmd_grpc_go<R: Runtime>(
RenderPurpose::Send,
),
)
.await;
.await;
let mut metadata = BTreeMap::new();
// Add the rest of metadata
@@ -236,21 +235,37 @@ async fn cmd_grpc_go<R: Runtime>(
metadata.insert(h.name, h.value);
}
if let Some(b) = &req.authentication_type {
let req = req.clone();
let empty_value = &serde_json::to_value("").unwrap();
let a = req.authentication;
// Map legacy auth name values from before they were plugins
let auth_plugin_name = match req.authentication_type.clone() {
Some(s) if s == "basic" => Some("@yaakapp/auth-basic".to_string()),
Some(s) if s == "bearer" => Some("@yaakapp/auth-bearer".to_string()),
_ => req.authentication_type.to_owned(),
};
if let Some(plugin_name) = auth_plugin_name {
let plugin_req = CallHttpAuthenticationRequest {
config: serde_json::to_value(&req.authentication)
.unwrap()
.as_object()
.unwrap()
.to_owned(),
method: "POST".to_string(),
url: req.url.clone(),
headers: metadata
.iter()
.map(|(name, value)| HttpHeader {
name: name.to_string(),
value: value.to_string(),
})
.collect(),
};
let plugin_result = plugin_manager
.call_http_authentication(&window, &plugin_name, plugin_req)
.await
.map_err(|e| e.to_string())?;
if b == "basic" {
let username = a.get("username").unwrap_or(empty_value).as_str().unwrap_or("");
let password = a.get("password").unwrap_or(empty_value).as_str().unwrap_or("");
let auth = format!("{username}:{password}");
let encoded = BASE64_STANDARD.encode(auth);
metadata.insert("Authorization".to_string(), format!("Basic {}", encoded));
} else if b == "bearer" {
let token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
metadata.insert("Authorization".to_string(), format!("Bearer {token}"));
req.url = plugin_result.url;
for header in plugin_result.headers {
metadata.insert(header.name, header.value);
}
}
@@ -269,8 +284,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())?
};
let conn_id = conn.id.clone();
@@ -322,8 +337,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())?;
.await
.map_err(|e| e.to_string())?;
return Ok(conn_id);
}
};
@@ -378,7 +393,7 @@ async fn cmd_grpc_go<R: Runtime>(
RenderPurpose::Send,
),
)
.await
.await
})
})
};
@@ -396,8 +411,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
});
return;
}
@@ -413,8 +428,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
});
}
Ok(IncomingMsg::Commit) => {
@@ -446,7 +461,7 @@ async fn cmd_grpc_go<R: Runtime>(
RenderPurpose::Send,
),
)
.await;
.await;
upsert_grpc_event(
&window,
@@ -458,8 +473,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
async move {
let (maybe_stream, maybe_msg) =
@@ -497,8 +512,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
}
match maybe_msg {
@@ -512,14 +527,14 @@ async fn cmd_grpc_go<R: Runtime>(
} else {
"Received response with metadata"
}
.to_string(),
.to_string(),
event_type: GrpcEventType::Info,
..base_event.clone()
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
upsert_grpc_event(
&window,
&GrpcEvent {
@@ -529,8 +544,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
upsert_grpc_event(
&window,
&GrpcEvent {
@@ -541,8 +556,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
}
Some(Err(e)) => {
upsert_grpc_event(
@@ -566,8 +581,8 @@ async fn cmd_grpc_go<R: Runtime>(
}),
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
}
None => {
// Server streaming doesn't return the initial message
@@ -585,14 +600,14 @@ async fn cmd_grpc_go<R: Runtime>(
} else {
"Received response with metadata"
}
.to_string(),
.to_string(),
event_type: GrpcEventType::Info,
..base_event.clone()
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
stream.into_inner()
}
Some(Err(e)) => {
@@ -618,8 +633,8 @@ async fn cmd_grpc_go<R: Runtime>(
}),
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
return;
}
None => return,
@@ -638,8 +653,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
}
Ok(None) => {
let trailers =
@@ -655,8 +670,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
break;
}
Err(status) => {
@@ -671,8 +686,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
}
}
}
@@ -930,8 +945,8 @@ async fn cmd_import_data<R: Runtime>(
grpc_requests,
&UpdateSource::Import,
)
.await
.map_err(|e| e.to_string())?;
.await
.map_err(|e| e.to_string())?;
analytics::track_event(
&window,
@@ -939,7 +954,7 @@ async fn cmd_import_data<R: Runtime>(
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
)
.await;
.await;
Ok(upserted)
}
@@ -960,6 +975,14 @@ async fn cmd_template_functions<R: Runtime>(
plugin_manager.get_template_functions(&window).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_get_http_authentication<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<GetHttpAuthenticationResponse>, String> {
plugin_manager.get_http_authentication(&window).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_call_http_request_action<R: Runtime>(
window: WebviewWindow<R>,
@@ -985,7 +1008,7 @@ async fn cmd_curl_to_request<R: Runtime>(
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
)
.await;
.await;
import_result.resources.http_requests.get(0).ok_or("No curl command found".to_string()).map(
|r| {
@@ -1170,8 +1193,8 @@ async fn cmd_install_plugin<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())?;
.await
.map_err(|e| e.to_string())?;
Ok(plugin)
}
@@ -1222,8 +1245,8 @@ async fn cmd_create_cookie_jar(
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1245,8 +1268,8 @@ async fn cmd_create_environment(
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1268,8 +1291,8 @@ async fn cmd_create_grpc_request(
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1508,8 +1531,8 @@ async fn cmd_list_cookie_jars(
},
&UpdateSource::Window,
)
.await
.expect("Failed to create CookieJar");
.await
.expect("Failed to create CookieJar");
Ok(vec![cookie_jar])
} else {
Ok(cookie_jars)
@@ -1598,8 +1621,8 @@ async fn cmd_list_workspaces(window: WebviewWindow) -> Result<Vec<Workspace>, St
},
&UpdateSource::Window,
)
.await
.expect("Failed to create Workspace");
.await
.expect("Failed to create Workspace");
Ok(vec![workspace])
} else {
Ok(workspaces)
@@ -1853,6 +1876,7 @@ pub fn run() {
cmd_get_environment,
cmd_get_folder,
cmd_get_grpc_request,
cmd_get_http_authentication,
cmd_get_http_request,
cmd_get_key_value,
cmd_get_settings,
@@ -1989,7 +2013,7 @@ fn create_main_window(handle: &AppHandle, url: &str) -> WebviewWindow {
Some(_) => counter += 1,
}
}
.expect("Failed to generate label for new window");
.expect("Failed to generate label for new window");
let config = CreateWindowConfig {
url,
@@ -2224,8 +2248,8 @@ async fn handle_plugin_event<R: Runtime>(
req.request_id.as_str(),
req.limit.map(|l| l as i64),
)
.await
.unwrap_or_default();
.await
.unwrap_or_default();
Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
}))
@@ -2254,7 +2278,7 @@ async fn handle_plugin_event<R: Runtime>(
environment.as_ref(),
&cb,
)
.await;
.await;
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request,
}))
@@ -2275,7 +2299,7 @@ async fn handle_plugin_event<R: Runtime>(
render_json_value(req.data, &base_environment, environment.as_ref(), &cb).await;
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
}
InternalEventPayload::ReloadResponse => {
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();
@@ -2313,8 +2337,8 @@ async fn handle_plugin_event<R: Runtime>(
req.http_request.id.as_str(),
&UpdateSource::Plugin,
)
.await
.unwrap();
.await
.unwrap();
let result = send_http_request(
&window,
@@ -2324,7 +2348,7 @@ async fn handle_plugin_event<R: Runtime>(
cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel
)
.await;
.await;
let http_response = match result {
Ok(r) => r,

View File

@@ -312,7 +312,7 @@ mod placeholder_tests {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo/bar"),
@@ -326,7 +326,7 @@ mod placeholder_tests {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
@@ -340,7 +340,7 @@ mod placeholder_tests {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo?:foo"),
@@ -354,7 +354,7 @@ mod placeholder_tests {
enabled: true,
name: "".to_string(),
value: "".to_string(),
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:missing"),
@@ -368,7 +368,7 @@ mod placeholder_tests {
enabled: false,
name: ":foo".to_string(),
value: "xxx".to_string(),
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
@@ -382,7 +382,7 @@ mod placeholder_tests {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foooo"),
@@ -396,7 +396,7 @@ mod placeholder_tests {
name: ":foo".into(),
value: "Hello World".into(),
enabled: true,
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
@@ -413,13 +413,13 @@ mod placeholder_tests {
name: "b".to_string(),
value: "bbb".to_string(),
enabled: true,
id: "p1".into(),
id: None,
},
HttpUrlParameter {
name: ":a".to_string(),
value: "aaa".to_string(),
enabled: true,
id: "p2".into(),
id: None,
},
],
..Default::default()

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use tauri::{AppHandle, Manager, Runtime};
use yaak_plugins::events::{RenderPurpose, TemplateFunctionArg, WindowContext};
use yaak_plugins::events::{FormInput, RenderPurpose, WindowContext};
use yaak_plugins::manager::PluginManager;
use yaak_templates::TemplateCallback;
@@ -48,11 +48,11 @@ impl TemplateCallback for PluginTemplateCallback {
// Fill in default values for all args
for a_def in function.args {
let base = match a_def {
TemplateFunctionArg::Text(a) => a.base,
TemplateFunctionArg::Select(a) => a.base,
TemplateFunctionArg::Checkbox(a) => a.base,
TemplateFunctionArg::File(a) => a.base,
TemplateFunctionArg::HttpRequest(a) => a.base,
FormInput::Text(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 = args_with_defaults.get(base.name.as_str()) {
args_with_defaults.insert(base.name, base.default_value.unwrap_or_default());

View File

@@ -0,0 +1,55 @@
"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);
var plugin = {
authentication: {
name: "Basic",
config: [{
type: "text",
name: "username",
label: "Username",
optional: true
}, {
type: "text",
name: "password",
label: "Password",
optional: true
}],
async onApply(_ctx, args) {
const { username, password } = args.config;
return {
url: args.url,
headers: [{
name: "Authorization",
value: "Basic " + Buffer.from(`${username}:${password}`).toString("base64")
}]
};
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

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

View File

@@ -0,0 +1,50 @@
"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);
var plugin = {
authentication: {
name: "Bearer",
config: [{
type: "text",
name: "token",
label: "Token",
optional: true
}],
async onApply(_ctx, args) {
const { token } = args.config;
return {
url: args.url,
headers: [{
name: "Authorization",
value: `Bearer ${token}`.trim()
}]
};
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
{
"name": "@yaakapp/auth-jwt",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
},
"dependencies": {
"jsonwebtoken": "^9.0.2"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.7"
}
}

View File

@@ -5,19 +5,20 @@ edition = "2021"
publish = false
[dependencies]
tonic = "0.10.2"
prost = "0.12"
tonic = "0.12.3"
prost = "0.13.4"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "fs"] }
tonic-reflection = "0.10.2"
tonic-reflection = "0.12.3"
tokio-stream = "0.1.14"
prost-types = "0.12.3"
prost-types = "0.13.4"
serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.113"
prost-reflect = { version = "0.12.0", features = ["serde", "derive"] }
prost-reflect = { version = "0.14.4", features = ["serde", "derive"] }
log = "0.4.20"
anyhow = "1.0.79"
hyper = { version = "0.14" }
hyper-rustls = { version = "0.24.0", features = ["http2"] }
hyper = "1.5.2"
hyper-util = { version = "0.1.10", features = ["client-legacy", "client"] }
hyper-rustls = { version = "0.27.5", default-features = false, features = ["http2", "rustls-platform-verifier"] }
uuid = { version = "1.7.0", features = ["v4"] }
tauri = { workspace = true }
tauri-plugin-shell = { workspace = true }

View File

@@ -0,0 +1,172 @@
use crate::transport::get_transport;
use async_recursion::async_recursion;
use hyper_rustls::HttpsConnector;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::Client;
use log::debug;
use tokio_stream::StreamExt;
use tonic::body::BoxBody;
use tonic::transport::Uri;
use tonic::Request;
use tonic_reflection::pb::v1::server_reflection_request::MessageRequest;
use tonic_reflection::pb::v1::server_reflection_response::MessageResponse;
use tonic_reflection::pb::v1::{
ErrorResponse, ExtensionNumberResponse, ListServiceResponse, ServerReflectionRequest,
ServiceResponse,
};
use tonic_reflection::pb::v1::{ExtensionRequest, FileDescriptorResponse};
use tonic_reflection::pb::{v1, v1alpha};
pub struct AutoReflectionClient<T = Client<HttpsConnector<HttpConnector>, BoxBody>> {
use_v1alpha: bool,
client_v1: v1::server_reflection_client::ServerReflectionClient<T>,
client_v1alpha: v1alpha::server_reflection_client::ServerReflectionClient<T>,
}
impl AutoReflectionClient {
pub fn new(uri: &Uri) -> Self {
let client_v1 = v1::server_reflection_client::ServerReflectionClient::with_origin(
get_transport(),
uri.clone(),
);
let client_v1alpha = v1alpha::server_reflection_client::ServerReflectionClient::with_origin(
get_transport(),
uri.clone(),
);
AutoReflectionClient {
use_v1alpha: false,
client_v1,
client_v1alpha,
}
}
#[async_recursion]
pub async fn send_reflection_request(
&mut self,
message: MessageRequest,
) -> Result<MessageResponse, String> {
let reflection_request = ServerReflectionRequest {
host: "".into(), // Doesn't matter
message_request: Some(message.clone()),
};
if self.use_v1alpha {
let request = Request::new(tokio_stream::once(to_v1alpha_request(reflection_request)));
self.client_v1alpha
.server_reflection_info(request)
.await
.map_err(|e| match e.code() {
tonic::Code::Unavailable => "Failed to connect to endpoint".to_string(),
tonic::Code::Unauthenticated => "Authentication failed".to_string(),
tonic::Code::DeadlineExceeded => "Deadline exceeded".to_string(),
_ => e.to_string(),
})?
.into_inner()
.next()
.await
.expect("steamed response")
.map_err(|e| e.to_string())?
.message_response
.ok_or("No reflection response".to_string())
.map(|resp| to_v1_msg_response(resp))
} else {
let request = Request::new(tokio_stream::once(reflection_request));
let resp = self.client_v1.server_reflection_info(request).await;
match resp {
Ok(r) => Ok(r),
Err(e) => match e.code().clone() {
tonic::Code::Unimplemented => {
// If v1 fails, change to v1alpha and try again
debug!("gRPC schema reflection falling back to v1alpha");
self.use_v1alpha = true;
return self.send_reflection_request(message).await;
}
_ => Err(e),
},
}
.map_err(|e| match e.code() {
tonic::Code::Unavailable => "Failed to connect to endpoint".to_string(),
tonic::Code::Unauthenticated => "Authentication failed".to_string(),
tonic::Code::DeadlineExceeded => "Deadline exceeded".to_string(),
_ => e.to_string(),
})?
.into_inner()
.next()
.await
.expect("steamed response")
.map_err(|e| e.to_string())?
.message_response
.ok_or("No reflection response".to_string())
}
}
}
fn to_v1_msg_response(
response: v1alpha::server_reflection_response::MessageResponse,
) -> MessageResponse {
match response {
v1alpha::server_reflection_response::MessageResponse::FileDescriptorResponse(v) => {
MessageResponse::FileDescriptorResponse(FileDescriptorResponse {
file_descriptor_proto: v.file_descriptor_proto,
})
}
v1alpha::server_reflection_response::MessageResponse::AllExtensionNumbersResponse(v) => {
MessageResponse::AllExtensionNumbersResponse(ExtensionNumberResponse {
extension_number: v.extension_number,
base_type_name: v.base_type_name,
})
}
v1alpha::server_reflection_response::MessageResponse::ListServicesResponse(v) => {
MessageResponse::ListServicesResponse(ListServiceResponse {
service: v
.service
.iter()
.map(|s| ServiceResponse {
name: s.name.clone(),
})
.collect(),
})
}
v1alpha::server_reflection_response::MessageResponse::ErrorResponse(v) => {
MessageResponse::ErrorResponse(ErrorResponse {
error_code: v.error_code,
error_message: v.error_message,
})
}
}
}
fn to_v1alpha_request(request: ServerReflectionRequest) -> v1alpha::ServerReflectionRequest {
v1alpha::ServerReflectionRequest {
host: request.host,
message_request: request.message_request.map(|m| to_v1alpha_msg_request(m)),
}
}
fn to_v1alpha_msg_request(
message: MessageRequest,
) -> v1alpha::server_reflection_request::MessageRequest {
match message {
MessageRequest::FileByFilename(v) => {
v1alpha::server_reflection_request::MessageRequest::FileByFilename(v)
}
MessageRequest::FileContainingSymbol(v) => {
v1alpha::server_reflection_request::MessageRequest::FileContainingSymbol(v)
}
MessageRequest::FileContainingExtension(ExtensionRequest {
extension_number,
containing_type,
}) => v1alpha::server_reflection_request::MessageRequest::FileContainingExtension(
v1alpha::ExtensionRequest {
extension_number,
containing_type,
},
),
MessageRequest::AllExtensionNumbersOfType(v) => {
v1alpha::server_reflection_request::MessageRequest::AllExtensionNumbersOfType(v)
}
MessageRequest::ListServices(v) => {
v1alpha::server_reflection_request::MessageRequest::ListServices(v)
}
}
}

View File

@@ -5,7 +5,9 @@ use serde_json::Deserializer;
mod codec;
mod json_schema;
pub mod manager;
mod proto;
mod reflection;
mod transport;
mod client;
pub use tonic::metadata::*;
pub use tonic::Code;

View File

@@ -2,9 +2,9 @@ use std::collections::BTreeMap;
use std::path::PathBuf;
use std::str::FromStr;
use hyper::client::HttpConnector;
use hyper::Client;
use hyper_rustls::HttpsConnector;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::Client;
pub use prost_reflect::DynamicMessage;
use prost_reflect::{DescriptorPool, MethodDescriptor, ServiceDescriptor};
use serde_json::Deserializer;
@@ -16,10 +16,11 @@ use tonic::transport::Uri;
use tonic::{IntoRequest, IntoStreamingRequest, Request, Response, Status, Streaming};
use crate::codec::DynamicCodec;
use crate::proto::{
fill_pool_from_files, fill_pool_from_reflection, get_transport, method_desc_to_path,
use crate::reflection::{
fill_pool_from_files, fill_pool_from_reflection, method_desc_to_path,
};
use crate::{json_schema, MethodDefinition, ServiceDefinition};
use crate::transport::get_transport;
#[derive(Clone)]
pub struct GrpcConnection {

View File

@@ -3,11 +3,9 @@ use std::ops::Deref;
use std::path::PathBuf;
use std::str::FromStr;
use crate::client::AutoReflectionClient;
use anyhow::anyhow;
use async_recursion::async_recursion;
use hyper::client::HttpConnector;
use hyper::Client;
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
use log::{debug, warn};
use prost::Message;
use prost_reflect::{DescriptorPool, MethodDescriptor};
@@ -16,15 +14,10 @@ use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager};
use tauri_plugin_shell::ShellExt;
use tokio::fs;
use tokio_stream::StreamExt;
use tonic::body::BoxBody;
use tonic::codegen::http::uri::PathAndQuery;
use tonic::transport::Uri;
use tonic::Request;
use tonic_reflection::pb::server_reflection_client::ServerReflectionClient;
use tonic_reflection::pb::server_reflection_request::MessageRequest;
use tonic_reflection::pb::server_reflection_response::MessageResponse;
use tonic_reflection::pb::ServerReflectionRequest;
use tonic_reflection::pb::v1::server_reflection_request::MessageRequest;
use tonic_reflection::pb::v1::server_reflection_response::MessageResponse;
pub async fn fill_pool_from_files(
app_handle: &AppHandle,
@@ -98,7 +91,7 @@ pub async fn fill_pool_from_files(
pub async fn fill_pool_from_reflection(uri: &Uri) -> Result<DescriptorPool, String> {
let mut pool = DescriptorPool::new();
let mut client = ServerReflectionClient::with_origin(get_transport(), uri.clone());
let mut client = AutoReflectionClient::new(uri);
for service in list_services(&mut client).await? {
if service == "grpc.reflection.v1alpha.ServerReflection" {
@@ -114,21 +107,8 @@ pub async fn fill_pool_from_reflection(uri: &Uri) -> Result<DescriptorPool, Stri
Ok(pool)
}
pub fn get_transport() -> Client<HttpsConnector<HttpConnector>, BoxBody> {
let connector = HttpsConnectorBuilder::new().with_native_roots();
let connector = connector.https_or_http().enable_http2().wrap_connector({
let mut http_connector = HttpConnector::new();
http_connector.enforce_http(false);
http_connector
});
Client::builder().pool_max_idle_per_host(0).http2_only(true).build(connector)
}
async fn list_services(
reflect_client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
) -> Result<Vec<String>, String> {
let response =
send_reflection_request(reflect_client, MessageRequest::ListServices("".into())).await?;
async fn list_services(client: &mut AutoReflectionClient) -> Result<Vec<String>, String> {
let response = client.send_reflection_request(MessageRequest::ListServices("".into())).await?;
let list_services_response = match response {
MessageResponse::ListServicesResponse(resp) => resp,
@@ -141,13 +121,11 @@ async fn list_services(
async fn file_descriptor_set_from_service_name(
service_name: &str,
pool: &mut DescriptorPool,
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
client: &mut AutoReflectionClient,
) {
let response = match send_reflection_request(
client,
MessageRequest::FileContainingSymbol(service_name.into()),
)
.await
let response = match client
.send_reflection_request(MessageRequest::FileContainingSymbol(service_name.into()))
.await
{
Ok(resp) => resp,
Err(e) => {
@@ -169,7 +147,7 @@ async fn file_descriptor_set_from_service_name(
async fn add_file_descriptors_to_pool(
fds: Vec<Vec<u8>>,
pool: &mut DescriptorPool,
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
client: &mut AutoReflectionClient,
) {
let mut topo_sort = topology::SimpleTopoSort::new();
let mut fd_mapping = std::collections::HashMap::with_capacity(fds.len());
@@ -198,15 +176,15 @@ async fn add_file_descriptors_to_pool(
async fn file_descriptor_set_by_filename(
filename: &str,
pool: &mut DescriptorPool,
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
client: &mut AutoReflectionClient,
) {
// We already fetched this file
if let Some(_) = pool.get_file_by_name(filename) {
return;
}
let response =
send_reflection_request(client, MessageRequest::FileByFilename(filename.into())).await;
let msg = MessageRequest::FileByFilename(filename.into());
let response = client.send_reflection_request(msg).await;
let file_descriptor_response = match response {
Ok(MessageResponse::FileDescriptorResponse(resp)) => resp,
Ok(_) => {
@@ -222,35 +200,6 @@ async fn file_descriptor_set_by_filename(
.await;
}
async fn send_reflection_request(
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
message: MessageRequest,
) -> Result<MessageResponse, String> {
let reflection_request = ServerReflectionRequest {
host: "".into(), // Doesn't matter
message_request: Some(message),
};
let request = Request::new(tokio_stream::once(reflection_request));
client
.server_reflection_info(request)
.await
.map_err(|e| match e.code() {
tonic::Code::Unavailable => "Failed to connect to endpoint".to_string(),
tonic::Code::Unauthenticated => "Authentication failed".to_string(),
tonic::Code::DeadlineExceeded => "Deadline exceeded".to_string(),
_ => e.to_string(),
})?
.into_inner()
.next()
.await
.expect("steamed response")
.map_err(|e| e.to_string())?
.message_response
.ok_or("No reflection response".to_string())
}
pub fn method_desc_to_path(md: &MethodDescriptor) -> PathAndQuery {
let full_name = md.full_name();
let (namespace, method_name) = full_name
@@ -292,8 +241,8 @@ mod topology {
where
T: Eq + std::hash::Hash + Clone,
{
type IntoIter = SimpleTopoSortIter<T>;
type Item = <SimpleTopoSortIter<T> as Iterator>::Item;
type IntoIter = SimpleTopoSortIter<T>;
fn into_iter(self) -> Self::IntoIter {
SimpleTopoSortIter::new(self)

View File

@@ -0,0 +1,19 @@
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
use hyper_util::client::legacy::Client;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::rt::TokioExecutor;
use tonic::body::BoxBody;
pub(crate) fn get_transport() -> Client<HttpsConnector<HttpConnector>, BoxBody> {
let connector = HttpsConnectorBuilder::new().with_platform_verifier();
let connector = connector.https_or_http().enable_http2().wrap_connector({
let mut http_connector = HttpConnector::new();
http_connector.enforce_http(false);
http_connector
});
Client::builder(TokioExecutor::new())
.pool_max_idle_per_host(0)
.http2_only(true)
.build(connector)
}

View File

@@ -26,13 +26,13 @@ export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, up
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id: string, };
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, };
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id: string, };
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
@@ -40,7 +40,7 @@ export type HttpResponseHeader = { name: string, value: string, };
export type HttpResponseState = "initialized" | "connected" | "closed";
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id: string, };
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
export type KeyValue = { model: "key_value", createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };

View File

@@ -446,7 +446,8 @@ pub struct HttpRequestHeader {
pub enabled: bool,
pub name: String,
pub value: String,
pub id: String,
#[ts(optional, as = "Option<String>")]
pub id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
@@ -458,7 +459,8 @@ pub struct HttpUrlParameter {
pub enabled: bool,
pub name: String,
pub value: String,
pub id: String,
#[ts(optional, as = "Option<String>")]
pub id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
@@ -664,7 +666,8 @@ pub struct GrpcMetadataEntry {
pub enabled: bool,
pub name: String,
pub value: String,
pub id: String,
#[ts(optional, as = "Option<String>")]
pub id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]

View File

@@ -13,9 +13,9 @@ serde = { version = "1.0.198", features = ["derive"] }
serde_json = "1.0.113"
tauri = { workspace = true }
tauri-plugin-shell = { workspace = true }
tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "process"] }
tonic = "0.12.1"
ts-rs = "10.0.0"
tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread", "process"] }
tonic = { version = "0.12.3"}
ts-rs = { workspace = true }
thiserror = "2.0.7"
yaak-models = { workspace = true }
regex = "1.10.6"

View File

@@ -11,6 +11,10 @@ export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, capabilities: Array<string>, };
export type CallHttpAuthenticationRequest = { config: { [key in string]?: JsonValue }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpAuthenticationResponse = { url: string, headers: Array<HttpHeader>, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { key: string, pluginRefId: string, args: CallHttpRequestActionArgs, };
@@ -25,10 +29,18 @@ export type Color = "custom" | "default" | "primary" | "secondary" | "info" | "s
export type CopyTextRequest = { text: string, };
export type EmptyPayload = {};
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, };
@@ -37,6 +49,112 @@ export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FormInput = { "type": "text" } & FormInputText | { "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,
/**
* 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,
/**
* 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,
/**
* 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,
/**
* 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,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputSelectOption = { name: string, value: string, };
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type GetHttpAuthenticationResponse = { name: string, pluginName: string, config: Array<FormInput>, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
@@ -47,6 +165,8 @@ 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";
@@ -59,13 +179,7 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, replyId: string | null, payload: InternalEventPayload, windowContext: WindowContext, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } | { "type": "reload_response" } | { "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" } & GetHttpRequestActionsRequest | { "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": "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" };
export type OpenFileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
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;
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
@@ -100,135 +214,7 @@ 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<TemplateFunctionArg>, };
export type TemplateFunctionArg = { "type": "text" } & TemplateFunctionTextArg | { "type": "select" } & TemplateFunctionSelectArg | { "type": "checkbox" } & TemplateFunctionCheckboxArg | { "type": "http_request" } & TemplateFunctionHttpRequestArg | { "type": "file" } & TemplateFunctionFileArg;
export type TemplateFunctionBaseArg = {
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionCheckboxArg = {
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionFileArg = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<OpenFileFilter>,
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionHttpRequestArg = {
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionSelectArg = {
/**
* The options that will be available in the select input
*/
options: Array<TemplateFunctionSelectOption>,
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
export type TemplateFunctionSelectOption = { label: string, value: string, };
export type TemplateFunctionTextArg = {
/**
* Placeholder for the text input
*/
placeholder?: string,
/**
* The name of the argument. Should be `camelCase` format
*/
name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* The default value
*/
defaultValue?: string, };
aliases?: Array<string>, args: Array<FormInput>, };
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };

View File

@@ -6,13 +6,13 @@ export type EnvironmentVariable = { enabled?: boolean, name: string, value: stri
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, };
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id: string, };
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, };
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id: string, };
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
@@ -20,6 +20,6 @@ export type HttpResponseHeader = { name: string, value: string, };
export type HttpResponseState = "initialized" | "connected" | "closed";
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id: string, };
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };

View File

@@ -39,8 +39,8 @@ pub enum InternalEventPayload {
BootRequest(BootRequest),
BootResponse(BootResponse),
ReloadRequest,
ReloadResponse,
ReloadRequest(EmptyPayload),
ReloadResponse(EmptyPayload),
TerminateRequest,
TerminateResponse,
@@ -57,15 +57,22 @@ pub enum InternalEventPayload {
SendHttpRequestRequest(SendHttpRequestRequest),
SendHttpRequestResponse(SendHttpRequestResponse),
GetHttpRequestActionsRequest(GetHttpRequestActionsRequest),
// Request Actions
GetHttpRequestActionsRequest(EmptyPayload),
GetHttpRequestActionsResponse(GetHttpRequestActionsResponse),
CallHttpRequestActionRequest(CallHttpRequestActionRequest),
// Template Functions
GetTemplateFunctionsRequest,
GetTemplateFunctionsResponse(GetTemplateFunctionsResponse),
CallTemplateFunctionRequest(CallTemplateFunctionRequest),
CallTemplateFunctionResponse(CallTemplateFunctionResponse),
GetHttpAuthenticationRequest(EmptyPayload),
GetHttpAuthenticationResponse(GetHttpAuthenticationResponse),
CallHttpAuthenticationRequest(CallHttpAuthenticationRequest),
CallHttpAuthenticationResponse(CallHttpAuthenticationResponse),
CopyTextRequest(CopyTextRequest),
RenderHttpRequestRequest(RenderHttpRequestRequest),
@@ -87,9 +94,14 @@ pub enum InternalEventPayload {
/// Returned when a plugin doesn't get run, just so the server
/// has something to listen for
EmptyResponse,
EmptyResponse(EmptyPayload),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)]
#[ts(export, type = "{}", export_to = "events.ts")]
pub struct EmptyPayload {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
@@ -281,6 +293,41 @@ pub enum Icon {
_Unknown(String),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct GetHttpAuthenticationResponse {
pub name: String,
pub plugin_name: String,
pub config: Vec<FormInput>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct CallHttpAuthenticationRequest {
pub config: serde_json::Map<String, serde_json::Value>,
pub method: String,
pub url: String,
pub headers: Vec<HttpHeader>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct HttpHeader {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct CallHttpAuthenticationResponse {
pub url: String,
pub headers: Vec<HttpHeader>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
@@ -301,25 +348,24 @@ pub struct TemplateFunction {
/// tags when changing the `name` property
#[ts(optional)]
pub aliases: Option<Vec<String>>,
pub args: Vec<TemplateFunctionArg>,
pub args: Vec<FormInput>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "events.ts")]
pub enum TemplateFunctionArg {
Text(TemplateFunctionTextArg),
Select(TemplateFunctionSelectArg),
Checkbox(TemplateFunctionCheckboxArg),
HttpRequest(TemplateFunctionHttpRequestArg),
File(TemplateFunctionFileArg),
pub enum FormInput {
Text(FormInputText),
Select(FormInputSelect),
Checkbox(FormInputCheckbox),
File(FormInputFile),
HttpRequest(FormInputHttpRequest),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionBaseArg {
/// The name of the argument. Should be `camelCase` format
pub struct FormInputBase {
pub name: String,
/// Whether the user must fill in the argument
@@ -338,29 +384,29 @@ pub struct TemplateFunctionBaseArg {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionTextArg {
pub struct FormInputText {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
pub base: FormInputBase,
/// Placeholder for the text input
#[ts(optional)]
#[ts(optional = nullable)]
pub placeholder: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionHttpRequestArg {
pub struct FormInputHttpRequest {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
pub base: FormInputBase,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionFileArg {
pub struct FormInputFile {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
pub base: FormInputBase,
/// The title of the file selection window
pub title: String,
@@ -379,13 +425,13 @@ pub struct TemplateFunctionFileArg {
// Specify to only allow selection of certain file extensions
#[ts(optional)]
pub filters: Option<Vec<OpenFileFilter>>,
pub filters: Option<Vec<FileFilter>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct OpenFileFilter {
pub struct FileFilter {
pub name: String,
/// File extensions to require
pub extensions: Vec<String>,
@@ -394,27 +440,27 @@ pub struct OpenFileFilter {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionSelectArg {
pub struct FormInputSelect {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
pub base: FormInputBase,
/// The options that will be available in the select input
pub options: Vec<TemplateFunctionSelectOption>,
pub options: Vec<FormInputSelectOption>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionCheckboxArg {
pub struct FormInputCheckbox {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
pub base: FormInputBase,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")]
pub struct TemplateFunctionSelectOption {
pub label: String,
pub struct FormInputSelectOption {
pub name: String,
pub value: String,
}

View File

@@ -1,9 +1,10 @@
use crate::error::Error::{ClientNotInitializedErr, PluginErr, PluginNotFoundErr, UnknownEventErr};
use crate::error::Result;
use crate::events::{
BootRequest, CallHttpRequestActionRequest, CallTemplateFunctionArgs,
CallTemplateFunctionRequest, CallTemplateFunctionResponse, FilterRequest, FilterResponse,
GetHttpRequestActionsRequest, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse,
BootRequest, CallHttpAuthenticationRequest, CallHttpAuthenticationResponse,
CallHttpRequestActionRequest, CallTemplateFunctionArgs, CallTemplateFunctionRequest,
CallTemplateFunctionResponse, EmptyPayload, FilterRequest, FilterResponse,
GetHttpAuthenticationResponse, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse,
ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, RenderPurpose,
WindowContext,
};
@@ -402,9 +403,7 @@ impl PluginManager {
let reply_events = self
.send_and_wait(
WindowContext::from_window(window),
&InternalEventPayload::GetHttpRequestActionsRequest(
GetHttpRequestActionsRequest {},
),
&InternalEventPayload::GetHttpRequestActionsRequest(EmptyPayload {}),
)
.await?;
@@ -460,6 +459,54 @@ impl PluginManager {
Ok(())
}
pub async fn get_http_authentication<R: Runtime>(
&self,
window: &WebviewWindow<R>,
) -> Result<Vec<GetHttpAuthenticationResponse>> {
let window_context = WindowContext::from_window(window);
let reply_events = self
.send_and_wait(
window_context,
&InternalEventPayload::GetHttpAuthenticationRequest(EmptyPayload {}),
)
.await?;
let mut results = Vec::new();
for event in reply_events {
if let InternalEventPayload::GetHttpAuthenticationResponse(resp) = event.payload {
results.push(resp.clone());
}
}
Ok(results)
}
pub async fn call_http_authentication<R: Runtime>(
&self,
window: &WebviewWindow<R>,
plugin_name: &str,
req: CallHttpAuthenticationRequest,
) -> Result<CallHttpAuthenticationResponse> {
let plugin = self
.get_plugin_by_name(plugin_name)
.await
.ok_or(PluginNotFoundErr(plugin_name.to_string()))?;
let event = self
.send_to_plugin_and_wait(
WindowContext::from_window(window),
&plugin,
&InternalEventPayload::CallHttpAuthenticationRequest(req),
)
.await?;
match event.payload {
InternalEventPayload::CallHttpAuthenticationResponse(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_template_function(
&self,
window_context: WindowContext,
@@ -552,7 +599,7 @@ impl PluginManager {
match event.payload {
InternalEventPayload::FilterResponse(resp) => Ok(resp),
InternalEventPayload::EmptyResponse => {
InternalEventPayload::EmptyResponse(_) => {
Err(PluginErr("Filter returned empty".to_string()))
}
e => Err(PluginErr(format!("Export returned invalid event {:?}", e))),

View File

@@ -6,15 +6,15 @@ export type EnvironmentVariable = { enabled?: boolean, name: string, value: stri
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, };
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id: string, };
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, };
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id: string, };
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id: string, };
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environment" } & Environment | { "type": "folder" } & Folder | { "type": "http_request" } & HttpRequest | { "type": "grpc_request" } & GrpcRequest;

View File

@@ -0,0 +1,251 @@
import type { Folder, HttpRequest } from '@yaakapp-internal/models';
import type {
FormInput,
FormInputCheckbox,
FormInputFile,
FormInputHttpRequest,
FormInputSelect,
FormInputText,
} from '@yaakapp-internal/plugins';
import { useCallback } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useFolders } from '../hooks/useFolders';
import { useHttpRequests } from '../hooks/useHttpRequests';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { Checkbox } from './core/Checkbox';
import { Input } from './core/Input';
import { Select } from './core/Select';
import { SelectFile } from './SelectFile';
// eslint-disable-next-line react-refresh/only-export-components
export const DYNAMIC_FORM_NULL_ARG = '__NULL__';
export function DynamicForm<T extends Record<string, string | boolean>>({
config,
data,
onChange,
useTemplating,
stateKey,
}: {
config: FormInput[];
onChange: (value: T) => void;
data: T;
useTemplating?: boolean;
stateKey: string;
}) {
const setDataAttr = useCallback(
(name: string, value: string | boolean | null) => {
onChange({ ...data, [name]: value == null ? '__NULL__' : value });
},
[data, onChange],
);
return (
<div>
{config.map((a, i) => {
switch (a.type) {
case 'select':
return (
<SelectArg
key={i + stateKey}
arg={a}
onChange={(v) => setDataAttr(a.name, v)}
value={data[a.name] ? String(data[a.name]) : '__ERROR__'}
/>
);
case 'text':
return (
<TextArg
key={i}
stateKey={stateKey}
arg={a}
useTemplating={useTemplating || false}
onChange={(v) => setDataAttr(a.name, v)}
value={data[a.name] ? String(data[a.name]) : ''}
/>
);
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}
/>
);
case 'http_request':
return (
<HttpRequestArg
key={i + stateKey}
arg={a}
onChange={(v) => setDataAttr(a.name, v)}
value={data[a.name] ? String(data[a.name]) : '__ERROR__'}
/>
);
case 'file':
return (
<FileArg
key={i + stateKey}
arg={a}
onChange={(v) => setDataAttr(a.name, v)}
filePath={data[a.name] ? String(data[a.name]) : '__ERROR__'}
/>
);
}
})}
</div>
);
}
function TextArg({
arg,
onChange,
value,
useTemplating,
stateKey,
}: {
arg: FormInputText;
value: string;
onChange: (v: string) => void;
useTemplating: boolean;
stateKey: string;
}) {
const handleChange = useCallback(
(value: string) => {
onChange(value === '' ? DYNAMIC_FORM_NULL_ARG : value);
},
[onChange],
);
return (
<Input
name={arg.name}
onChange={handleChange}
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? '' : value}
require={!arg.optional}
label={
<>
{arg.label ?? arg.name}
{arg.optional && <span> (optional)</span>}
</>
}
hideLabel={arg.label == null}
placeholder={arg.placeholder ?? arg.defaultValue ?? ''}
useTemplating={useTemplating}
stateKey={stateKey}
forceUpdateKey={stateKey}
/>
);
}
function SelectArg({
arg,
value,
onChange,
}: {
arg: FormInputSelect;
value: string;
onChange: (v: string) => void;
}) {
return (
<Select
label={arg.label ?? arg.name}
name={arg.name}
onChange={onChange}
value={value}
options={[
...arg.options.map((a) => ({
label: a.name + (arg.defaultValue === a.value ? ' (default)' : ''),
value: a.value === arg.defaultValue ? DYNAMIC_FORM_NULL_ARG : a.value,
})),
]}
/>
);
}
function FileArg({
arg,
filePath,
onChange,
}: {
arg: FormInputFile;
filePath: string;
onChange: (v: string | null) => void;
}) {
return (
<SelectFile
onChange={({ filePath }) => onChange(filePath)}
filePath={filePath === '__NULL__' ? null : filePath}
directory={!!arg.directory}
/>
);
}
function HttpRequestArg({
arg,
value,
onChange,
}: {
arg: FormInputHttpRequest;
value: string;
onChange: (v: string) => void;
}) {
const folders = useFolders();
const httpRequests = useHttpRequests();
const activeRequest = useActiveRequest();
return (
<Select
label={arg.label ?? arg.name}
name={arg.name}
onChange={onChange}
value={value}
options={[
...httpRequests.map((r) => {
return {
label:
buildRequestBreadcrumbs(r, folders).join(' / ') +
(r.id == activeRequest?.id ? ' (current)' : ''),
value: r.id,
};
}),
]}
/>
);
}
function buildRequestBreadcrumbs(request: HttpRequest, folders: Folder[]): string[] {
const ancestors: (HttpRequest | Folder)[] = [request];
const next = () => {
const latest = ancestors[0];
if (latest == null) return [];
const parent = folders.find((f) => f.id === latest.folderId);
if (parent == null) return;
ancestors.unshift(parent);
next();
};
next();
return ancestors.map((a) => (a.model === 'folder' ? a.name : fallbackRequestName(a)));
}
function CheckboxArg({
arg,
onChange,
value,
}: {
arg: FormInputCheckbox;
value: boolean;
onChange: (v: boolean) => void;
}) {
return (
<Checkbox
onChange={onChange}
checked={value}
title={arg.label ?? arg.name}
hideLabel={arg.label == null}
/>
);
}

View File

@@ -4,6 +4,7 @@ import type { ShowToastRequest } from '@yaakapp/api';
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import { useGenerateThemeCss } from '../hooks/useGenerateThemeCss';
import { useSubscribeHttpAuthentication } from '../hooks/useHttpAuthentication';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useNotificationToast } from '../hooks/useNotificationToast';
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
@@ -24,6 +25,7 @@ export function GlobalHooks() {
useSyncWorkspaceChildModels();
useSubscribeTemplateFunctions();
useSubscribeHttpAuthentication();
// Other useful things
useNotificationToast();

View File

@@ -6,12 +6,10 @@ 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 { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { AUTH_TYPE_BASIC, AUTH_TYPE_BEARER, AUTH_TYPE_NONE } from '../lib/model_util';
import { BasicAuth } from './BasicAuth';
import { BearerAuth } from './BearerAuth';
import { Button } from './core/Button';
import { CountBadge } from './core/CountBadge';
import { Icon } from './core/Icon';
@@ -22,8 +20,8 @@ import { RadioDropdown } from './core/RadioDropdown';
import { HStack, VStack } from './core/Stacks';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { GrpcEditor } from './GrpcEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor';
import { UrlBar } from './UrlBar';
@@ -71,6 +69,7 @@ export function GrpcConnectionSetupPane({
onSend,
}: Props) {
const updateRequest = useUpdateAnyGrpcRequest();
const authentication = useHttpAuthentication();
const [activeTabs, setActiveTabs] = useAtom(tabsAtom);
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
@@ -136,11 +135,6 @@ export function GrpcConnectionSetupPane({
const tabs: TabItem[] = useMemo(
() => [
{
value: TAB_DESCRIPTION,
label: 'Info',
rightSlot: activeRequest.description && <CountBadge count={true} />,
},
{ value: TAB_MESSAGE, label: 'Message' },
{
value: TAB_AUTH,
@@ -148,24 +142,21 @@ export function GrpcConnectionSetupPane({
options: {
value: activeRequest.authenticationType,
items: [
{ label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC },
{ label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER },
...authentication.map((a) => ({
label: a.name,
value: a.pluginName,
})),
{ type: 'separator' },
{ label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE },
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
],
onChange: async (authenticationType) => {
onChange: (authenticationType) => {
let authentication: GrpcRequest['authentication'] = activeRequest.authentication;
if (authenticationType === AUTH_TYPE_BASIC) {
if (activeRequest.authenticationType !== authenticationType) {
authentication = {
username: authentication.username ?? '',
password: authentication.password ?? '',
};
} else if (authenticationType === AUTH_TYPE_BEARER) {
authentication = {
token: authentication.token ?? '',
// Reset auth if changing types
};
}
await updateRequest.mutateAsync({
updateRequest.mutate({
id: activeRequest.id,
update: { authenticationType, authentication },
});
@@ -173,12 +164,18 @@ export function GrpcConnectionSetupPane({
},
},
{ value: TAB_METADATA, label: 'Metadata' },
{
value: TAB_DESCRIPTION,
label: 'Info',
rightSlot: activeRequest.description && <CountBadge count={true} />,
},
],
[
activeRequest.authentication,
activeRequest.authenticationType,
activeRequest.description,
activeRequest.id,
authentication,
updateRequest,
],
);
@@ -213,6 +210,7 @@ export function GrpcConnectionSetupPane({
)}
>
<UrlBar
key={forceUpdateKey}
url={activeRequest.url ?? ''}
method={null}
submitIcon={null}
@@ -321,16 +319,10 @@ export function GrpcConnectionSetupPane({
protoFiles={protoFiles}
/>
</TabContent>
<TabContent value="auth">
{activeRequest.authenticationType === AUTH_TYPE_BASIC ? (
<BasicAuth key={forceUpdateKey} request={activeRequest} />
) : activeRequest.authenticationType === AUTH_TYPE_BEARER ? (
<BearerAuth key={forceUpdateKey} request={activeRequest} />
) : (
<EmptyStateText>No Authentication {activeRequest.authenticationType}</EmptyStateText>
)}
<TabContent value={TAB_AUTH}>
<HttpAuthenticationEditor request={activeRequest} />
</TabContent>
<TabContent value="metadata">
<TabContent value={TAB_METADATA}>
<PairOrBulkEditor
preferenceName="grpc_metadata"
valueAutocompleteVariables

View File

@@ -0,0 +1,48 @@
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import React, { useCallback } from 'react';
import { useHttpAuthentication } from '../hooks/useHttpAuthentication';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { DynamicForm } from './DynamicForm';
import { EmptyStateText } from './EmptyStateText';
interface Props {
request: HttpRequest | GrpcRequest;
}
export function HttpAuthenticationEditor({ request }: Props) {
const updateHttpRequest = useUpdateAnyHttpRequest();
const updateGrpcRequest = useUpdateAnyGrpcRequest();
const auths = useHttpAuthentication();
const auth = auths.find((a) => a.pluginName === request.authenticationType);
const handleChange = useCallback(
(authentication: Record<string, boolean>) => {
if (request.model === 'http_request') {
updateHttpRequest.mutate({
id: request.id,
update: (r) => ({ ...r, authentication }),
});
} else {
updateGrpcRequest.mutate({
id: request.id,
update: (r) => ({ ...r, authentication }),
});
}
},
[request.id, request.model, updateGrpcRequest, updateHttpRequest],
);
if (auth == null) {
return <EmptyStateText>No Authentication {request.authenticationType}</EmptyStateText>;
}
return (
<DynamicForm
stateKey={`auth.${request.id}.${request.authenticationType}`}
config={auth.config}
data={request.authentication}
onChange={handleChange}
/>
);
}

View File

@@ -32,8 +32,9 @@ export function ImportCurlButton() {
variant="border"
color="success"
className="rounded-full"
leftSlot={<Icon icon="paste" size="sm" />}
rightSlot={<Icon icon="import" size="sm" />}
isLoading={isLoading}
title="Import Curl command from clipboard"
onClick={async () => {
setIsLoading(true);
try {

View File

@@ -1,11 +1,11 @@
import classNames from 'classnames';
import { useMemo, useRef } from 'react';
import { useKeyPressEvent } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
import { useHotKey } from '../hooks/useHotKey';
import { httpRequestsAtom } from '../hooks/useHttpRequests';
import { useKeyboardEvent } from '../hooks/useKeyboardEvent';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { jotaiStore } from '../lib/jotai';
@@ -27,16 +27,16 @@ export function RecentRequestsDropdown({ className }: Props) {
// Handle key-up
// TODO: Somehow make useHotKey have this functionality. Note: e.key does not work
// on Linux, for example, when Control is mapped to CAPS. This will never fire.
useKeyPressEvent('Control', undefined, () => {
if (!dropdownRef.current?.isOpen) return;
dropdownRef.current?.select?.();
useKeyboardEvent('keyup', 'Control', () => {
if (dropdownRef.current?.isOpen) {
dropdownRef.current?.select?.();
}
});
useHotKey('request_switcher.prev', () => {
if (!dropdownRef.current?.isOpen) {
dropdownRef.current?.open();
// Select the second because the first is the current request
dropdownRef.current?.next?.(2);
dropdownRef.current?.open(1);
} else {
dropdownRef.current?.next?.();
}

View File

@@ -52,7 +52,6 @@ export const RecentResponsesDropdown = function ResponsePane({
label: 'Delete',
leftSlot: <Icon icon="trash" />,
onSelect: deleteResponse.mutate,
disabled: activeResponse.state !== 'closed',
},
{
key: 'unpin',

View File

@@ -8,6 +8,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 { httpRequestsAtom } from '../hooks/useHttpRequests';
import { useImportCurl } from '../hooks/useImportCurl';
import { useImportQuerystring } from '../hooks/useImportQuerystring';
@@ -23,9 +24,6 @@ import { fallbackRequestName } from '../lib/fallbackRequestName';
import { tryFormatJson } from '../lib/formatters';
import { generateId } from '../lib/generateId';
import {
AUTH_TYPE_BASIC,
AUTH_TYPE_BEARER,
AUTH_TYPE_NONE,
BODY_TYPE_BINARY,
BODY_TYPE_FORM_MULTIPART,
BODY_TYPE_FORM_URLENCODED,
@@ -36,8 +34,6 @@ import {
BODY_TYPE_XML,
} from '../lib/model_util';
import { showToast } from '../lib/toast';
import { BasicAuth } from './BasicAuth';
import { BearerAuth } from './BearerAuth';
import { BinaryFileEditor } from './BinaryFileEditor';
import { CountBadge } from './core/CountBadge';
import { Editor } from './core/Editor/Editor';
@@ -55,6 +51,7 @@ import { FormMultipartEditor } from './FormMultipartEditor';
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
import { GraphQLEditor } from './GraphQLEditor';
import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor';
import { UrlBar } from './UrlBar';
import { UrlParametersEditor } from './UrlParameterEditor';
@@ -97,6 +94,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 handleContentTypeChange = useCallback(
async (contentType: string | null) => {
@@ -236,21 +234,18 @@ export const RequestPane = memo(function RequestPane({
options: {
value: activeRequest.authenticationType,
items: [
{ label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC },
{ label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER },
...authentication.map((a) => ({
label: a.name,
value: a.pluginName,
})),
{ type: 'separator' },
{ label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE },
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
],
onChange: async (authenticationType) => {
let authentication: HttpRequest['authentication'] = activeRequest.authentication;
if (authenticationType === AUTH_TYPE_BASIC) {
if (activeRequest.authenticationType !== authenticationType) {
authentication = {
username: authentication.username ?? '',
password: authentication.password ?? '',
};
} else if (authenticationType === AUTH_TYPE_BEARER) {
authentication = {
token: authentication.token ?? '',
// Reset auth if changing types
};
}
updateRequest({
@@ -272,6 +267,7 @@ export const RequestPane = memo(function RequestPane({
activeRequest.headers,
activeRequest.method,
activeRequestId,
authentication,
handleContentTypeChange,
numParams,
updateRequest,
@@ -384,15 +380,7 @@ export const RequestPane = memo(function RequestPane({
tabListClassName="mt-2 !mb-1.5"
>
<TabContent value={TAB_AUTH}>
{activeRequest.authenticationType === AUTH_TYPE_BASIC ? (
<BasicAuth key={forceUpdateKey} request={activeRequest} />
) : activeRequest.authenticationType === AUTH_TYPE_BEARER ? (
<BearerAuth key={forceUpdateKey} request={activeRequest} />
) : (
<EmptyStateText>
No Authentication {activeRequest.authenticationType}
</EmptyStateText>
)}
<HttpAuthenticationEditor request={activeRequest} />
</TabContent>
<TabContent value={TAB_HEADERS}>
<HeadersEditor

View File

@@ -1,32 +1,14 @@
import type { Folder, HttpRequest } from '@yaakapp-internal/models';
import type {
TemplateFunction,
TemplateFunctionArg,
TemplateFunctionCheckboxArg,
TemplateFunctionFileArg,
TemplateFunctionHttpRequestArg,
TemplateFunctionSelectArg,
TemplateFunctionTextArg,
} from '@yaakapp-internal/plugins';
import type { TemplateFunction } from '@yaakapp-internal/plugins';
import type { FnArg, Tokens } from '@yaakapp-internal/templates';
import classNames from 'classnames';
import { useCallback, useMemo, useState } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useMemo, useState } from 'react';
import { useDebouncedValue } from '../hooks/useDebouncedValue';
import { useFolders } from '../hooks/useFolders';
import { useHttpRequests } from '../hooks/useHttpRequests';
import { useRenderTemplate } from '../hooks/useRenderTemplate';
import { useTemplateTokensToString } from '../hooks/useTemplateTokensToString';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { Button } from './core/Button';
import { Checkbox } from './core/Checkbox';
import { InlineCode } from './core/InlineCode';
import { PlainInput } from './core/PlainInput';
import { Select } from './core/Select';
import { VStack } from './core/Stacks';
import { SelectFile } from './SelectFile';
const NULL_ARG = '__NULL__';
import { DYNAMIC_FORM_NULL_ARG, DynamicForm } from './DynamicForm';
interface Props {
templateFunction: TemplateFunction;
@@ -49,21 +31,17 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
? initialArg?.value.text
: // TODO: Implement variable-based args
'__NULL__';
initial[arg.name] = initialArgValue ?? NULL_ARG;
initial[arg.name] = initialArgValue ?? DYNAMIC_FORM_NULL_ARG;
}
return initial;
});
const setArgValue = useCallback((name: string, value: string | boolean | null) => {
setArgValues((v) => ({ ...v, [name]: value == null ? '__NULL__' : value }));
}, []);
const tokens: Tokens = useMemo(() => {
const argTokens: FnArg[] = Object.keys(argValues).map((name) => ({
name,
value:
argValues[name] === NULL_ARG
argValues[name] === DYNAMIC_FORM_NULL_ARG
? { type: 'null' }
: typeof argValues[name] === 'boolean'
? { type: 'bool', value: argValues[name] === true }
@@ -100,57 +78,12 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
return (
<VStack className="pb-3" space={4}>
<h1 className="font-mono !text-base">{templateFunction.name}()</h1>
<VStack space={2}>
{templateFunction.args.map((a: TemplateFunctionArg, i: number) => {
switch (a.type) {
case 'select':
return (
<SelectArg
key={i}
arg={a}
onChange={(v) => setArgValue(a.name, v)}
value={argValues[a.name] ? String(argValues[a.name]) : '__ERROR__'}
/>
);
case 'text':
return (
<TextArg
key={i}
arg={a}
onChange={(v) => setArgValue(a.name, v)}
value={argValues[a.name] ? String(argValues[a.name]) : '__ERROR__'}
/>
);
case 'checkbox':
return (
<CheckboxArg
key={i}
arg={a}
onChange={(v) => setArgValue(a.name, v)}
value={argValues[a.name] !== undefined ? argValues[a.name] === true : false}
/>
);
case 'http_request':
return (
<HttpRequestArg
key={i}
arg={a}
onChange={(v) => setArgValue(a.name, v)}
value={argValues[a.name] ? String(argValues[a.name]) : '__ERROR__'}
/>
);
case 'file':
return (
<FileArg
key={i}
arg={a}
onChange={(v) => setArgValue(a.name, v)}
filePath={argValues[a.name] ? String(argValues[a.name]) : '__ERROR__'}
/>
);
}
})}
</VStack>
<DynamicForm
config={templateFunction.args}
data={argValues}
onChange={setArgValues}
stateKey={`template_function.${templateFunction.name}`}
/>
<VStack className="w-full">
<div className="text-sm text-text-subtle">Preview</div>
<InlineCode
@@ -168,147 +101,3 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
</VStack>
);
}
function TextArg({
arg,
onChange,
value,
}: {
arg: TemplateFunctionTextArg;
value: string;
onChange: (v: string) => void;
}) {
const handleChange = useCallback(
(value: string) => {
onChange(value === '' ? NULL_ARG : value);
},
[onChange],
);
return (
<PlainInput
name={arg.name}
onChange={handleChange}
defaultValue={value === NULL_ARG ? '' : value}
require={!arg.optional}
label={
<>
{arg.label ?? arg.name}
{arg.optional && <span> (optional)</span>}
</>
}
hideLabel={arg.label == null}
placeholder={arg.placeholder ?? arg.defaultValue ?? ''}
/>
);
}
function SelectArg({
arg,
value,
onChange,
}: {
arg: TemplateFunctionSelectArg;
value: string;
onChange: (v: string) => void;
}) {
return (
<Select
label={arg.label ?? arg.name}
name={arg.name}
onChange={onChange}
value={value}
options={[
...arg.options.map((a) => ({
label: a.label + (arg.defaultValue === a.value ? ' (default)' : ''),
value: a.value === arg.defaultValue ? NULL_ARG : a.value,
})),
]}
/>
);
}
function FileArg({
arg,
filePath,
onChange,
}: {
arg: TemplateFunctionFileArg;
filePath: string;
onChange: (v: string | null) => void;
}) {
return (
<SelectFile
onChange={({ filePath }) => onChange(filePath)}
filePath={filePath === '__NULL__' ? null : filePath}
directory={!!arg.directory}
/>
);
}
function HttpRequestArg({
arg,
value,
onChange,
}: {
arg: TemplateFunctionHttpRequestArg;
value: string;
onChange: (v: string) => void;
}) {
const folders = useFolders();
const httpRequests = useHttpRequests();
const activeRequest = useActiveRequest();
return (
<Select
label={arg.label ?? arg.name}
name={arg.name}
onChange={onChange}
value={value}
options={[
...httpRequests.map((r) => {
return {
label: buildRequestBreadcrumbs(r, folders).join(' / ') + (r.id == activeRequest?.id ? ' (current)' : ''),
value: r.id,
};
}),
]}
/>
);
}
function buildRequestBreadcrumbs(request: HttpRequest, folders: Folder[]): string[] {
const ancestors: (HttpRequest | Folder)[] = [request];
const next = () => {
const latest = ancestors[0];
if (latest == null) return [];
const parent = folders.find((f) => f.id === latest.folderId);
if (parent == null) return;
ancestors.unshift(parent);
next();
};
next();
return ancestors.map((a) => (a.model === 'folder' ? a.name : fallbackRequestName(a)));
}
function CheckboxArg({
arg,
onChange,
value,
}: {
arg: TemplateFunctionCheckboxArg;
value: boolean;
onChange: (v: boolean) => void;
}) {
return (
<Checkbox
onChange={onChange}
checked={value}
title={arg.label ?? arg.name}
hideLabel={arg.label == null}
/>
);
}

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo } from 'react';
import { generateId } from '../../lib/generateId';
import { Editor } from './Editor/Editor';
import type { PairEditorProps } from './PairEditor';
import type { PairEditorProps, PairWithId } from './PairEditor';
type Props = PairEditorProps;
@@ -45,14 +45,12 @@ export function BulkPairEditor({
);
}
function lineToPair(line: string): PairEditorProps['pairs'][0] {
function lineToPair(line: string): PairWithId {
const [, name, value] = line.match(/^(:?[^:]+):\s+(.*)$/) ?? [];
const pair: PairEditorProps['pairs'][0] = {
return {
enabled: true,
name: (name ?? '').trim(),
value: (value ?? '').trim(),
id: generateId(),
};
return pair;
}

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import { motion } from 'framer-motion';
import { atom, useAtom } from 'jotai';
import { atom } from 'jotai';
import type {
CSSProperties,
FocusEvent as ReactFocusEvent,
@@ -12,11 +12,11 @@ import type {
SetStateAction,
} from 'react';
import React, {
useEffect,
Children,
cloneElement,
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
@@ -29,6 +29,7 @@ import { useHotKey } from '../../hooks/useHotKey';
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
import { generateId } from '../../lib/generateId';
import { getNodeText } from '../../lib/getNodeText';
import { jotaiStore } from '../../lib/jotai';
import { Overlay } from '../Overlay';
import { Button } from './Button';
import { HotKey } from './HotKey';
@@ -62,15 +63,13 @@ export type DropdownItem = DropdownItemDefault | DropdownItemSeparator;
export interface DropdownProps {
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
items: DropdownItem[];
onOpen?: () => void;
onClose?: () => void;
fullWidth?: boolean;
hotKeyAction?: HotkeyAction;
}
export interface DropdownRef {
isOpen: boolean;
open: () => void;
open: (index?: number) => void;
toggle: () => void;
close?: () => void;
next?: (incrBy?: number) => void;
@@ -84,46 +83,52 @@ export interface DropdownRef {
const openAtom = atom<string | null>(null);
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
{ children, items, onOpen, onClose, hotKeyAction, fullWidth }: DropdownProps,
{ children, items, hotKeyAction, fullWidth }: DropdownProps,
ref,
) {
const id = useRef(generateId()).current;
const [openId, setOpenId] = useAtom(openAtom);
const isOpen = openId === id;
const id = useRef(generateId());
const [isOpen, _setIsOpen] = useState<boolean>(false);
useEffect(() => {
return jotaiStore.sub(openAtom, () => {
const globalOpenId = jotaiStore.get(openAtom);
const newIsOpen = globalOpenId === id.current;
if (newIsOpen !== isOpen) {
_setIsOpen(newIsOpen);
}
});
}, [isOpen, _setIsOpen]);
// const [isOpen, _setIsOpen] = useState<boolean>(false);
const [defaultSelectedIndex, setDefaultSelectedIndex] = useState<number | null>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<Omit<DropdownRef, 'open'>>(null);
const setIsOpen = useCallback(
(o: SetStateAction<boolean>) => {
setOpenId((prevId) => {
const prevIsOpen = prevId === id;
const newIsOpen = typeof o === 'function' ? o(prevIsOpen) : o;
return newIsOpen ? id : null; // Set global atom to current ID to signify open state
});
},
[id, setOpenId],
);
useEffect(() => {
if (isOpen) {
const setIsOpen = useCallback((o: SetStateAction<boolean>) => {
jotaiStore.set(openAtom, (prevId) => {
const prevIsOpen = prevId === id.current;
const newIsOpen = typeof o === 'function' ? o(prevIsOpen) : o;
// Persist background color of button until we close the dropdown
buttonRef.current!.style.backgroundColor = window
.getComputedStyle(buttonRef.current!)
.getPropertyValue('background-color');
onOpen?.();
} else {
onClose?.();
if (newIsOpen) {
buttonRef.current!.style.backgroundColor = window
.getComputedStyle(buttonRef.current!)
.getPropertyValue('background-color');
}
return newIsOpen ? id.current : null; // Set global atom to current ID to signify open state
});
}, []);
// Because a different dropdown can cause ours to close, a useEffect([isOpen]) is the only method
// we have of detecting the dropdown closed, to do cleanup.
useEffect(() => {
if (!isOpen) {
buttonRef.current?.focus(); // Focus button
buttonRef.current!.style.backgroundColor = ''; // Clear persisted BG
// Set to different value when opened and closed to force it to update. This is to force
// <Menu/> to reset its selected-index state, which it does when this prop changes
setDefaultSelectedIndex(null);
}
// Set to different value when opened and closed to force it to update. This is to force
// <Menu/> to reset its selected-index state, which it does when this prop changes
setDefaultSelectedIndex(isOpen ? -1 : null);
}, [isOpen, onClose, onOpen]);
}, [isOpen]);
// Pull into variable so linter forces us to add it as a hook dep to useImperativeHandle. If we don't,
// the ref will not update when menuRef updates, causing stale callback state to be used.
@@ -138,8 +143,9 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
if (!isOpen) this.open();
else this.close();
},
open() {
open(index?: number) {
setIsOpen(true);
setDefaultSelectedIndex(index ?? -1);
},
close() {
setIsOpen(false);
@@ -264,11 +270,18 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
ref,
) {
const [selectedIndex, setSelectedIndex] = useStateWithDeps<number | null>(
defaultSelectedIndex ?? null,
defaultSelectedIndex ?? -1,
[defaultSelectedIndex],
);
const [filter, setFilter] = useState<string>('');
// HACK: Use a ref to track selectedIndex so our closure functions (eg. select()) can
// have access to the latest value.
const selectedIndexRef = useRef(selectedIndex);
useEffect(() => {
selectedIndexRef.current = selectedIndex;
}, [selectedIndex]);
const handleClose = useCallback(() => {
onClose();
setFilter('');
@@ -380,12 +393,12 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
prev: handlePrev,
next: handleNext,
select() {
const item = items[selectedIndex ?? -1] ?? null;
const item = items[selectedIndexRef.current ?? -1] ?? null;
if (!item) return;
handleSelect(item);
},
};
}, [handleClose, handleNext, handlePrev, handleSelect, items, selectedIndex]);
}, [handleClose, handleNext, handlePrev, handleSelect, items]);
const styles = useMemo<{
container: CSSProperties;

View File

@@ -51,14 +51,8 @@
}
}
/* Don't show selection on blurred input */
.cm-selectionBackground {
@apply bg-transparent;
}
&.cm-focused .cm-selectionBackground {
@apply bg-selection;
@apply bg-selection !important;
}
/* Style gutters */

View File

@@ -26,10 +26,7 @@ import { useActiveEnvironmentVariables } from '../../../hooks/useActiveEnvironme
import { parseTemplate } from '../../../hooks/useParseTemplate';
import { useRequestEditor } from '../../../hooks/useRequestEditor';
import { useSettings } from '../../../hooks/useSettings';
import {
useTemplateFunctions,
useTwigCompletionOptions,
} from '../../../hooks/useTemplateFunctions';
import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions';
import { showDialog } from '../../../lib/dialog';
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
import { TemplateVariableDialog } from '../../TemplateVariableDialog';
@@ -93,6 +90,10 @@ const stateFields = { history: historyField, folds: foldState };
const emptyVariables: EnvironmentVariable[] = [];
const emptyExtension: Extension = [];
// NOTE: For some reason, the cursor doesn't appear if the field is empty and there is no
// placeholder. So we set it to a space to force it to show.
const emptyPlaceholder = ' ';
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
{
readOnly,
@@ -126,7 +127,6 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
) {
const settings = useSettings();
const templateFunctions = useTemplateFunctions();
const allEnvironmentVariables = useActiveEnvironmentVariables();
const environmentVariables = autocompleteVariables ? allEnvironmentVariables : emptyVariables;
@@ -178,7 +178,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
useEffect(
function configurePlaceholder() {
if (cm.current === null) return;
const ext = placeholderExt(placeholderElFromText(placeholder ?? ''));
const ext = placeholderExt(placeholderElFromText(placeholder || emptyPlaceholder));
const effect = placeholderCompartment.current.reconfigure(ext);
cm.current?.view.dispatch({ effects: effect });
},
@@ -300,7 +300,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
[focusParamValue],
);
const completionOptions = useTwigCompletionOptions(onClickFunction);
const completionOptions = useTemplateFunctionCompletionOptions(onClickFunction);
// Update the language extension when the language changes
useEffect(() => {
@@ -322,7 +322,6 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
autocomplete,
useTemplating,
environmentVariables,
templateFunctions,
onClickFunction,
onClickVariable,
onClickMissingVariable,
@@ -355,7 +354,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
const extensions = [
languageCompartment.of(langExt),
placeholderCompartment.current.of(
placeholderExt(placeholderElFromText(placeholder ?? '')),
placeholderExt(placeholderElFromText(placeholder || emptyPlaceholder)),
),
wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : []),
keymapCompartment.current.of(
@@ -601,13 +600,16 @@ const placeholderElFromText = (text: string) => {
function saveCachedEditorState(stateKey: string | null, state: EditorState | null) {
if (!stateKey || state == null) return;
sessionStorage.setItem(stateKey, JSON.stringify(state.toJSON(stateFields)));
sessionStorage.setItem(
computeFullStateKey(stateKey),
JSON.stringify(state.toJSON(stateFields)),
);
}
function getCachedEditorState(doc: string, stateKey: string | null) {
if (stateKey == null) return;
const stateStr = sessionStorage.getItem(stateKey);
const stateStr = sessionStorage.getItem(computeFullStateKey(stateKey));
if (stateStr == null) return null;
try {
@@ -621,3 +623,7 @@ function getCachedEditorState(doc: string, stateKey: string | null) {
return null;
}
function computeFullStateKey(stateKey: string): string {
return `editor.${stateKey}`;
}

View File

@@ -56,6 +56,7 @@ const icons = {
help: lucide.CircleHelpIcon,
history: lucide.HistoryIcon,
house: lucide.HomeIcon,
import: lucide.ImportIcon,
info: lucide.InfoIcon,
keyboard: lucide.KeyboardIcon,
left_panel_hidden: lucide.PanelLeftOpenIcon,

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames';
import type { EditorView } from 'codemirror';
import type { ReactNode } from 'react';
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
import type { EditorProps } from './Editor/Editor';
import { Editor } from './Editor/Editor';
@@ -46,7 +46,7 @@ export type InputProps = Pick<
stateKey: EditorProps['stateKey'];
};
export const Input = forwardRef<EditorView | undefined, InputProps>(function Input(
export const Input = forwardRef<EditorView, InputProps>(function Input(
{
className,
containerClassName,
@@ -79,6 +79,8 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
const [focused, setFocused] = useState(false);
const editorRef = useRef<EditorView | null>(null);
useImperativeHandle<EditorView | null, EditorView | null>(ref, () => editorRef.current);
const handleFocus = useCallback(() => {
if (readOnly) return;
@@ -88,6 +90,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
const handleBlur = useCallback(() => {
setFocused(false);
editorRef.current?.dispatch({ selection: { anchor: 0 } });
onBlur?.();
}, [onBlur]);
@@ -164,7 +167,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
)}
>
<Editor
ref={ref}
ref={editorRef}
id={id}
singleLine
stateKey={stateKey}

View File

@@ -43,7 +43,7 @@ export type PairEditorProps = {
namePlaceholder?: string;
nameValidate?: InputProps['validate'];
noScroll?: boolean;
onChange: (pairs: Pair[]) => void;
onChange: (pairs: PairWithId[]) => void;
pairs: Pair[];
stateKey: InputProps['stateKey'];
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
@@ -54,7 +54,7 @@ export type PairEditorProps = {
};
export type Pair = {
id: string;
id?: string;
enabled?: boolean;
name: string;
value: string;
@@ -63,6 +63,10 @@ export type Pair = {
readOnlyName?: boolean;
};
export type PairWithId = Pair & {
id: string;
};
/** Max number of pairs to show before prompting the user to reveal the rest */
const MAX_INITIAL_PAIRS = 50;
@@ -90,7 +94,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
const [forceFocusNamePairId, setForceFocusNamePairId] = useState<string | null>(null);
const [forceFocusValuePairId, setForceFocusValuePairId] = useState<string | null>(null);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [pairs, setPairs] = useState<Pair[]>([]);
const [pairs, setPairs] = useState<PairWithId[]>([]);
const [showAll, toggleShowAll] = useToggle(false);
useImperativeHandle(
@@ -105,14 +109,13 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
);
useEffect(() => {
// Remove empty headers on initial render and ensure they all have valid ids (pairs didn't used to have IDs)
const newPairs = [];
// Remove empty headers on initial render and ensure they all have valid ids (pairs didn't use to have IDs)
const newPairs: PairWithId[] = [];
for (let i = 0; i < originalPairs.length; i++) {
const p = originalPairs[i];
if (!p) continue; // Make TS happy
if (isPairEmpty(p)) continue;
if (!p.id) p.id = generateId();
newPairs.push(p);
newPairs.push({ ...p, id: p.id ?? generateId() });
}
// Add empty last pair if there is none
@@ -127,7 +130,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
}, [forceUpdateKey]);
const setPairsAndSave = useCallback(
(fn: (pairs: Pair[]) => Pair[]) => {
(fn: (pairs: PairWithId[]) => PairWithId[]) => {
setPairs((oldPairs) => {
const pairs = fn(oldPairs);
onChange(pairs);
@@ -165,7 +168,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
);
const handleChange = useCallback(
(pair: Pair) => 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],
);
@@ -267,15 +270,15 @@ enum ItemTypes {
type PairEditorRowProps = {
className?: string;
pair: Pair;
pair: PairWithId;
forceFocusNamePairId?: string | null;
forceFocusValuePairId?: string | null;
onMove: (id: string, side: 'above' | 'below') => void;
onEnd: (id: string) => void;
onChange: (pair: Pair) => void;
onDelete?: (pair: Pair, focusPrevious: boolean) => void;
onFocus?: (pair: Pair) => void;
onSubmit?: (pair: Pair) => void;
onChange: (pair: PairWithId) => void;
onDelete?: (pair: PairWithId, focusPrevious: boolean) => void;
onFocus?: (pair: PairWithId) => void;
onSubmit?: (pair: PairWithId) => void;
isLast?: boolean;
index: number;
} & Pick<
@@ -618,7 +621,7 @@ function FileActionsDropdown({
);
}
function emptyPair(): Pair {
function emptyPair(): PairWithId {
return {
enabled: true,
name: '',

View File

@@ -0,0 +1,38 @@
import { useQuery } from '@tanstack/react-query';
import type { GetHttpAuthenticationResponse } from '@yaakapp-internal/plugins';
import { useAtomValue } from 'jotai';
import { atom, useSetAtom } from 'jotai/index';
import { useState } from 'react';
import { invokeCmd } from '../lib/tauri';
const httpAuthenticationAtom = atom<GetHttpAuthenticationResponse[]>([]);
const orderedHttpAuthenticationAtom = atom((get) =>
get(httpAuthenticationAtom).sort((a, b) => a.name.localeCompare(b.name)),
);
export function useHttpAuthentication() {
return useAtomValue(orderedHttpAuthenticationAtom);
}
export function useSubscribeHttpAuthentication() {
const [numResults, setNumResults] = useState<number>(0);
const setAtom = useSetAtom(httpAuthenticationAtom);
useQuery({
queryKey: ['http_authentication'],
// 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,
queryFn: async () => {
const result = await invokeCmd<GetHttpAuthenticationResponse[]>(
'cmd_get_http_authentication',
);
setNumResults(result.length);
setAtom(result);
return result;
},
});
}

View File

@@ -0,0 +1,18 @@
import { useEffect } from 'react';
export function useKeyboardEvent(
event: 'keyup' | 'keydown',
key: KeyboardEvent['key'],
cb: () => void,
) {
useEffect(() => {
const fn = (e: KeyboardEvent) => {
if (e.key === key) cb();
};
document.addEventListener(event, fn);
return () => document.removeEventListener(event, fn);
// Don't have `cb` as a dep for caller convenience
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [event]);
}

View File

@@ -1,5 +1,4 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import { showAlert } from '../lib/alert';
import { trackEvent } from '../lib/analytics';
import { getHttpRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
@@ -23,6 +22,5 @@ export function useSendAnyHttpRequest() {
});
},
onSettled: () => trackEvent('http_request', 'send'),
onError: (err) => showAlert({ id: 'send-failed', title: 'Send Failed', body: err }),
});
}

View File

@@ -9,10 +9,10 @@ import { usePluginsKey } from './usePlugins';
const templateFunctionsAtom = atom<TemplateFunction[]>([]);
export function useTwigCompletionOptions(
export function useTemplateFunctionCompletionOptions(
onClick: (fn: TemplateFunction, ragTag: string, pos: number) => void,
) {
const templateFunctions = useTemplateFunctions();
const templateFunctions = useAtomValue(templateFunctionsAtom);
return useMemo<TwigCompletionOption[]>(() => {
return (
templateFunctions.map((fn) => {
@@ -37,10 +37,6 @@ export function useTwigCompletionOptions(
}, [onClick, templateFunctions]);
}
export function useTemplateFunctions() {
return useAtomValue(templateFunctionsAtom);
}
export function useSubscribeTemplateFunctions() {
const pluginsKey = usePluginsKey();
const [numFns, setNumFns] = useState<number>(0);

View File

@@ -16,10 +16,6 @@ export const BODY_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded';
export const BODY_TYPE_FORM_MULTIPART = 'multipart/form-data';
export const BODY_TYPE_XML = 'text/xml';
export const AUTH_TYPE_NONE = null;
export const AUTH_TYPE_BASIC = 'basic';
export const AUTH_TYPE_BEARER = 'bearer';
export function cookieDomain(cookie: Cookie): string {
if (cookie.domain === 'NotPresent' || cookie.domain === 'Empty') {
return 'n/a';

View File

@@ -32,6 +32,7 @@ type TauriCmd =
| 'cmd_get_environment'
| 'cmd_get_folder'
| 'cmd_get_grpc_request'
| 'cmd_get_http_authentication'
| 'cmd_get_http_request'
| 'cmd_get_sse_events'
| 'cmd_get_key_value'

View File

@@ -13,9 +13,8 @@
font-variant-ligatures: none;
}
::selection,
.cm-selectionBackground {
@apply bg-selection !important;
::selection {
@apply bg-selection;
}
/* Disable user selection to make it more "app-like" */
@@ -26,7 +25,7 @@
@apply select-none cursor-default;
}
input,
input,
textarea {
&::placeholder {
@apply text-placeholder;