Compare commits

..

36 Commits

Author SHA1 Message Date
Gregory Schier
72dd768f55 Proper handling of boolean template function args 2025-05-28 13:08:43 -07:00
Gregory Schier
862d85e48d Better inheritance empty state 2025-05-28 10:42:57 -07:00
Gregory Schier
a6d03cbeeb Fix context menu closing immediately when using ctrl+click
https://feedback.yaak.app/p/right-click-on-mac-automatically-closes
2025-05-28 07:36:18 -07:00
Gregory Schier
7d1ca1c232 Render inherited auth and headers (#217) 2025-05-26 07:18:57 -07:00
Gregory Schier
261911b57e Fix weird import 2025-05-25 20:45:12 -07:00
Gregory Schier
245054cd7d Move react-pdf dynamic import 2025-05-25 20:39:14 -07:00
Gregory Schier
101582e540 Merge remote-tracking branch 'origin/master' 2025-05-25 20:25:22 -07:00
Gregory Schier
0a932798a0 API support for added template functions (eg. cookies) 2025-05-25 20:25:13 -07:00
Gregory Schier
4609c95ad5 Fix env editor switching (#216) 2025-05-25 08:03:29 -07:00
Gregory Schier
9d54e40aa8 Add list/get cookie plugin APIs 2025-05-25 08:02:36 -07:00
Gregory Schier
9ec9222216 Fix cookie jar not updating during chained requests
https://feedback.yaak.app/p/request-chaining-cookie-not-appear
2025-05-25 07:04:40 -07:00
Gregory Schier
4d1dda0786 Fix auth none 2025-05-23 08:43:52 -07:00
Gregory Schier
31605881ac Render inherited headers in UI 2025-05-23 08:18:29 -07:00
Gregory Schier
4cd2e9cd31 Request Inheritance (#209) 2025-05-23 08:13:25 -07:00
nguyen
13d959799a fix: prevent button stealing focus from url input (#212) 2025-05-23 08:12:06 -07:00
Pannawich Lohanimit
a6b18c23e1 fix: change incorrect default GraphQL request name (#213) 2025-05-23 08:11:16 -07:00
Gregory Schier
041298b3f8 Detect JSON language if application/javascript returns JSON 2025-05-21 11:05:20 -07:00
Gregory Schier
b400940f0e Fix import curl 2025-05-21 11:04:57 -07:00
Gregory Schier
2e144f064d Fix syntax highlighting 2025-05-21 08:26:15 -07:00
Gregory Schier
d8b1cadae6 Fix model deletion 2025-05-21 08:25:12 -07:00
Gregory Schier
c2f9760d08 Fix template parsing 2025-05-21 08:18:09 -07:00
Gregory Schier
a4c600cb48 Lint errors 2025-05-20 08:15:19 -07:00
Gregory Schier
bc3a5e3e58 Include license status in notification endpoint 2025-05-20 08:13:57 -07:00
Gregory Schier
4c3a02ac53 Show decrypt error in secure input 2025-05-20 07:41:32 -07:00
Gregory Schier
1974d61aa4 Fix syntax highlighting 2025-05-19 15:41:19 -07:00
Gregory Schier
0bcb092854 Update README.md 2025-05-19 15:10:56 -07:00
Gregory Schier
409620f533 More advanced template grammar
Fixes https://feedback.yaak.app/p/cannot-escape-call-to-variable-in-json-body
2025-05-19 13:37:12 -07:00
Gregory Schier
3e9037f70a No longer mark environments as external in Git 2025-05-17 06:06:36 -07:00
Desperate Necromancer
be82b67ed3 Allow disabling window decorations/controls (#176)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-05-16 13:33:59 -07:00
Gregory Schier
432b366105 Fix grpc/ws events error 2025-05-16 13:00:50 -07:00
Hao Xiang
42e70b941d fix proto to json-schema (#194) 2025-05-16 12:53:53 -07:00
Gregory Schier
3808215210 Better unicode un-escaping 2025-05-16 12:42:08 -07:00
Walyson G Oliveira
763a60982a Adjusting the JSON viewing response to accept accentuation (#203) 2025-05-16 12:37:00 -07:00
dependabot[bot]
a05679fd93 Bump vite from 6.2.6 to 6.2.7 in /src-web (#205)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-16 12:31:46 -07:00
Gregory Schier
73c366dc27 Hopefully fix weird env routing issue 2025-05-16 09:12:36 -07:00
Gregory Schier
0be7d0283b Add ref=<app_id> to external links pointing to yaak.app 2025-05-16 07:53:22 -07:00
128 changed files with 14358 additions and 1000 deletions

View File

@@ -5,6 +5,11 @@ APIs. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
![366149288-f18e963f-0b68-4ecb-b8b8-cb71aa9aec02](https://github.com/user-attachments/assets/ca83b7ad-5708-411b-8faf-e36b365841a4)
## Contribution Policy
Yaak is open source, but only accepting contributions for bug fixes. To get started,
visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment.
## Feature Overview
- 🪂 Import data from Postman, Insomnia, OpenAPI, Swagger, or Curl.<br/>
@@ -14,6 +19,7 @@ APIs. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
- ⛓️ Chain together multiple requests to dynamically reference values.<br/>
- 📂 Organize requests into workspaces and nested folders.<br/>
- 🧮 Use environment variables to easily switch between Prod and Dev.<br/>
- 🛡️ Secure arbitrary text values with end-to-end encryption<br/>
- 🏷️ Send dynamic values like UUIDs or timestamps using template tags.<br/>
- 🎨 Choose from many of the included themes, or make your own.<br/>
- 💽 Mirror workspace data to a directory for integration with Git or Dropbox.<br/>
@@ -21,17 +27,8 @@ APIs. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
- 🔌 Create your own plugins for authentication, template tags, and more!<br/>
- 🛜 Configure a proxy to access firewall-blocked APIs
## Feedback and Bug Reports
## Useful Resources
All feedback, bug reports, questions, and feature requests should be reported to
[feedback.yaak.app](https://feedback.yaak.app).
## Community Projects
- [`yaak2postman`](https://github.com/BiteCraft/yaak2postman) CLI for converting Yaak data
exports to Postman-compatible collections
## Contribution Policy
Yaak is open source, but only accepting contributions for bug fixes. To get started,
visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment.
- [Feedback and Bug Reports](https://feedback.yaak.app)
- [Documentation](https://feedback.yaak.app/help)
- [Yaak vs Postman](https://yaak.app/blog/postman-alternative)

8
package-lock.json generated
View File

@@ -14044,7 +14044,7 @@
"postcss": "^8.4.45",
"postcss-nesting": "^13.0.0",
"tailwindcss": "^3.4.10",
"vite": "6.2.6",
"vite": "6.2.7",
"vite-plugin-static-copy": "^2.2.0",
"vite-plugin-svgr": "^4.3.0",
"vite-plugin-top-level-await": "^1.5.0",
@@ -14584,9 +14584,9 @@
}
},
"src-web/node_modules/vite": {
"version": "6.2.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
"version": "6.2.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.7.tgz",
"integrity": "sha512-qg3LkeuinTrZoJHHF94coSaTfIPyBYoywp+ys4qu20oSJFbKMYoIJo0FWJT9q6Vp49l6z9IsJRbHdcGtiKbGoQ==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

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

View File

@@ -104,7 +104,11 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputCheckbox = {
/**
@@ -131,7 +135,11 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputEditor = {
/**
@@ -170,7 +178,11 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputFile = {
/**
@@ -205,7 +217,11 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputHttpRequest = {
/**
@@ -232,7 +248,11 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputMarkdown = { content: string, hidden?: boolean, };
@@ -265,7 +285,11 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputSelectOption = { label: string, value: string, };
@@ -306,10 +330,18 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type GenericCompletionOption = { label: string, detail?: string, info?: string, type?: CompletionOptionType, boost?: number, };
export type GetCookieValueRequest = { name: string, };
export type GetCookieValueResponse = { value: string | null, };
export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
@@ -346,10 +378,14 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type 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": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;
export type ListCookieNamesRequest = {};
export type ListCookieNamesResponse = { names: Array<string>, };
export type OpenWindowRequest = { url: string,
/**
* Label for the window. If not provided, a random one will be generated.

View File

@@ -4,11 +4,9 @@ export type Environment = { model: "environment", id: string, workspaceId: strin
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 Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
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 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<HttpRequestHeader>, 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>, };
@@ -24,4 +22,4 @@ export type HttpUrlParameter = { enabled?: boolean, name: string, value: string,
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };

View File

@@ -1,8 +1,11 @@
import type {
FindHttpResponsesRequest,
FindHttpResponsesResponse,
GetCookieValueRequest,
GetCookieValueResponse,
GetHttpRequestByIdRequest,
GetHttpRequestByIdResponse,
ListCookieNamesResponse,
OpenWindowRequest,
PromptTextRequest,
PromptTextResponse,
@@ -38,6 +41,10 @@ export interface Context {
},
): Promise<{ close: () => void }>;
};
cookies: {
listNames(): Promise<ListCookieNamesResponse['names']>;
getValue(args: GetCookieValueRequest): Promise<GetCookieValueResponse['value']>;
};
httpRequest: {
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;

View File

@@ -1,10 +1,10 @@
import { PluginWindowContext, TemplateFunctionArg } from '@yaakapp-internal/plugins';
import type {
import {
BootRequest,
Context,
DeleteKeyValueResponse,
FindHttpResponsesResponse,
FormInput,
GetCookieValueRequest,
GetCookieValueResponse,
GetHttpRequestByIdResponse,
GetKeyValueResponse,
HttpAuthenticationAction,
@@ -12,19 +12,20 @@ import type {
InternalEvent,
InternalEventPayload,
JsonPrimitive,
PluginDefinition,
ListCookieNamesResponse,
PluginWindowContext,
PromptTextResponse,
RenderHttpRequestResponse,
SendHttpRequestResponse,
TemplateFunction,
TemplateFunctionArg,
TemplateRenderResponse,
} from '@yaakapp/api';
} from '@yaakapp-internal/plugins';
import { Context, PluginDefinition } from '@yaakapp/api';
import console from 'node:console';
import { readFileSync, type Stats, statSync, watch } from 'node:fs';
import path from 'node:path';
// import util from 'node:util';
import { EventChannel } from './EventChannel';
// import { interceptStdout } from './interceptStdout';
import { migrateTemplateFunctionSelectOptions } from './migrations';
export interface PluginWorkerData {
@@ -495,6 +496,27 @@ export class PluginInstance {
return httpRequest;
},
},
cookies: {
getValue: async (args: GetCookieValueRequest) => {
const payload = {
type: 'get_cookie_value_request',
...args,
} as const;
const { value } = await this.#sendAndWaitForReply<GetCookieValueResponse>(
event.windowContext,
payload,
);
return value;
},
listNames: async () => {
const payload = { type: 'list_cookie_names_request' } as const;
const { names } = await this.#sendAndWaitForReply<ListCookieNamesResponse>(
event.windowContext,
payload,
);
return names;
},
},
templates: {
/**
* Invoke Yaak's template engine to render a value. If the value is a nested type

1
src-tauri/Cargo.lock generated
View File

@@ -8041,6 +8041,7 @@ name = "yaak-app"
version = "0.0.0"
dependencies = [
"chrono",
"cookie",
"encoding_rs",
"eventsource-client",
"http",

View File

@@ -40,6 +40,7 @@ openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu inst
[dependencies]
chrono = { version = "0.4.31", features = ["serde"] }
cookie = "0.18.1"
encoding_rs = "0.8.35"
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
http = { version = "1.2.0", default-features = false }

View File

@@ -7,6 +7,7 @@
"*"
],
"permissions": [
"core:app:allow-identifier",
"core:event:allow-emit",
"core:event:allow-listen",
"core:event:allow-unlisten",

View File

@@ -0,0 +1,2 @@
ALTER TABLE settings
ADD COLUMN hide_window_controls BOOLEAN DEFAULT FALSE NOT NULL;

View File

@@ -0,0 +1,15 @@
-- Auth
ALTER TABLE workspaces
ADD COLUMN authentication TEXT NOT NULL DEFAULT '{}';
ALTER TABLE folders
ADD COLUMN authentication TEXT NOT NULL DEFAULT '{}';
ALTER TABLE workspaces
ADD COLUMN authentication_type TEXT;
ALTER TABLE folders
ADD COLUMN authentication_type TEXT;
-- Headers
ALTER TABLE workspaces
ADD COLUMN headers TEXT NOT NULL DEFAULT '[]';
ALTER TABLE folders
ADD COLUMN headers TEXT NOT NULL DEFAULT '[]';

View File

@@ -1,12 +1,13 @@
use std::collections::BTreeMap;
use crate::error::Result;
use KeyAndValueRef::{Ascii, Binary};
use tauri::{Manager, Runtime, WebviewWindow};
use yaak_grpc::{KeyAndValueRef, MetadataMap};
use yaak_models::models::GrpcRequest;
use yaak_models::query_manager::QueryManagerExt;
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader};
use yaak_plugins::manager::PluginManager;
use KeyAndValueRef::{Ascii, Binary};
pub(crate) fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String> {
let mut entries = BTreeMap::new();
@@ -19,6 +20,23 @@ pub(crate) fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String>
entries
}
pub(crate) fn resolve_grpc_request<R: Runtime>(
window: &WebviewWindow<R>,
request: &GrpcRequest,
) -> Result<GrpcRequest> {
let mut new_request = request.clone();
let (authentication_type, authentication) =
window.db().resolve_auth_for_grpc_request(request)?;
new_request.authentication_type = authentication_type;
new_request.authentication = authentication;
let metadata = window.db().resolve_metadata_for_grpc_request(request)?;
new_request.metadata = metadata;
Ok(new_request)
}
pub(crate) async fn build_metadata<R: Runtime>(
window: &WebviewWindow<R>,
request: &GrpcRequest,
@@ -27,7 +45,7 @@ pub(crate) async fn build_metadata<R: Runtime>(
let mut metadata = BTreeMap::new();
// Add the rest of metadata
for h in request.clone().metadata {
for h in request.metadata.clone() {
if h.name.is_empty() && h.value.is_empty() {
continue;
}
@@ -39,25 +57,34 @@ pub(crate) async fn build_metadata<R: Runtime>(
metadata.insert(h.name, h.value);
}
if let Some(auth_name) = request.authentication_type.clone() {
let auth = request.authentication.clone();
let plugin_req = CallHttpAuthenticationRequest {
context_id: format!("{:x}", md5::compute(request.id.clone())),
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
method: "POST".to_string(),
url: request.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, &auth_name, plugin_req).await?;
for header in plugin_result.set_headers {
metadata.insert(header.name, header.value);
match request.authentication_type.clone() {
None => {
// No authentication found. Not even inherited
}
Some(authentication_type) if authentication_type == "none" => {
// Explicitly no authentication
}
Some(authentication_type) => {
let auth = request.authentication.clone();
let plugin_req = CallHttpAuthenticationRequest {
context_id: format!("{:x}", md5::compute(request.id.clone())),
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
method: "POST".to_string(),
url: request.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, &authentication_type, plugin_req)
.await?;
for header in plugin_result.set_headers {
metadata.insert(header.name, header.value);
}
}
}

View File

@@ -65,14 +65,7 @@ pub async fn send_http_request<R: Runtime>(
);
let update_source = UpdateSource::from_window(window);
let request = match render_http_request(
&unrendered_request,
&base_environment,
environment.as_ref(),
&cb,
)
.await
{
let resolved_request = match resolve_http_request(window, unrendered_request) {
Ok(r) => r,
Err(e) => {
return Ok(response_err(
@@ -84,7 +77,22 @@ pub async fn send_http_request<R: Runtime>(
}
};
let mut url_string = request.url;
let request =
match render_http_request(&resolved_request, &base_environment, environment.as_ref(), &cb)
.await
{
Ok(r) => r,
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
&update_source,
));
}
};
let mut url_string = request.url.clone();
url_string = ensure_proto(&url_string);
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
@@ -153,7 +161,10 @@ pub async fn send_http_request<R: Runtime>(
// Add cookie store if specified
let maybe_cookie_manager = match cookie_jar.clone() {
Some(cj) => {
Some(CookieJar { id, .. }) => {
// NOTE: WE need to refetch the cookie jar because a chained request might have
// updated cookies when we rendered the request.
let cj = window.db().get_cookie_jar(&id)?;
// HACK: Can't construct Cookie without serde, so we have to do this
let cookies = cj
.cookies
@@ -255,7 +266,7 @@ pub async fn send_http_request<R: Runtime>(
}
let request_body = request.body.clone();
if let Some(body_type) = &request.body_type {
if let Some(body_type) = &request.body_type.clone() {
if body_type == "graphql" {
let query = get_str_h(&request_body, "query");
let variables = get_str_h(&request_body, "variables");
@@ -376,7 +387,7 @@ pub async fn send_http_request<R: Runtime>(
};
}
// Set file path if it is not empty
// Set a file path if it is not empty
if !file_path.is_empty() {
let filename = PathBuf::from(file_path)
.file_name()
@@ -426,43 +437,52 @@ pub async fn send_http_request<R: Runtime>(
}
};
// Apply authentication
if let Some(auth_name) = request.authentication_type.to_owned() {
let req = CallHttpAuthenticationRequest {
context_id: format!("{:x}", md5::compute(request.id)),
values: serde_json::from_value(serde_json::to_value(&request.authentication).unwrap())
match request.authentication_type {
None => {
// No authentication found. Not even inherited
}
Some(authentication_type) if authentication_type == "none" => {
// Explicitly no authentication
}
Some(authentication_type) => {
let req = CallHttpAuthenticationRequest {
context_id: format!("{:x}", md5::compute(request.id)),
values: serde_json::from_value(
serde_json::to_value(&request.authentication).unwrap(),
)
.unwrap(),
url: sendable_req.url().to_string(),
method: sendable_req.method().to_string(),
headers: sendable_req
.headers()
.iter()
.map(|(name, value)| HttpHeader {
name: name.to_string(),
value: value.to_str().unwrap_or_default().to_string(),
})
.collect(),
};
let auth_result = plugin_manager.call_http_authentication(&window, &auth_name, req).await;
let plugin_result = match auth_result {
Ok(r) => r,
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
&update_source,
));
}
};
url: sendable_req.url().to_string(),
method: sendable_req.method().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 auth_result =
plugin_manager.call_http_authentication(&window, &authentication_type, req).await;
let plugin_result = match auth_result {
Ok(r) => r,
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
&update_source,
));
}
};
let headers = sendable_req.headers_mut();
for header in plugin_result.set_headers {
headers.insert(
HeaderName::from_str(&header.name).unwrap(),
HeaderValue::from_str(&header.value).unwrap(),
);
let headers = sendable_req.headers_mut();
for header in plugin_result.set_headers {
headers.insert(
HeaderName::from_str(&header.name).unwrap(),
HeaderValue::from_str(&header.value).unwrap(),
);
}
}
}
@@ -661,6 +681,23 @@ pub async fn send_http_request<R: Runtime>(
})
}
fn resolve_http_request<R: Runtime>(
window: &WebviewWindow<R>,
request: &HttpRequest,
) -> Result<HttpRequest> {
let mut new_request = request.clone();
let (authentication_type, authentication) =
window.db().resolve_auth_for_http_request(request)?;
new_request.authentication_type = authentication_type;
new_request.authentication = authentication;
let headers = window.db().resolve_headers_for_http_request(request)?;
new_request.headers = headers;
Ok(new_request)
}
fn ensure_proto(url_str: &str) -> String {
if url_str.starts_with("http://") || url_str.starts_with("https://") {
return url_str.to_string();

View File

@@ -1,7 +1,7 @@
extern crate core;
use crate::encoding::read_response_body;
use crate::error::Error::GenericError;
use crate::grpc::{build_metadata, metadata_to_map};
use crate::grpc::{build_metadata, metadata_to_map, resolve_grpc_request};
use crate::http_request::send_http_request;
use crate::notifications::YaakNotifier;
use crate::render::{render_grpc_request, render_template};
@@ -151,10 +151,13 @@ async fn cmd_grpc_reflect<R: Runtime>(
None => None,
};
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
let resolved_request = resolve_grpc_request(&window, &unrendered_request)?;
let base_environment =
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
let req = render_grpc_request(
&unrendered_request,
&resolved_request,
&base_environment,
environment.as_ref(),
&PluginTemplateCallback::new(
@@ -195,10 +198,12 @@ async fn cmd_grpc_go<R: Runtime>(
None => None,
};
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
let resolved_request = resolve_grpc_request(&window, &unrendered_request)?;
let base_environment =
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
let request = render_grpc_request(
&unrendered_request,
&resolved_request,
&base_environment,
environment.as_ref(),
&PluginTemplateCallback::new(
@@ -917,10 +922,10 @@ async fn cmd_call_http_authentication_action<R: Runtime>(
auth_name: &str,
action_index: i32,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
model_id: &str,
) -> YaakResult<()> {
Ok(plugin_manager
.call_http_authentication_action(&window, auth_name, action_index, values, request_id)
.call_http_authentication_action(&window, auth_name, action_index, values, model_id)
.await?)
}

View File

@@ -8,6 +8,7 @@ use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
use yaak_license::{LicenseCheckStatus, check_license};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
@@ -70,6 +71,13 @@ impl YaakNotifier {
self.last_check = SystemTime::now();
let license_check = match check_license(window).await? {
LicenseCheckStatus::PersonalUse { .. } => "personal".to_string(),
LicenseCheckStatus::CommercialUse => "commercial".to_string(),
LicenseCheckStatus::InvalidLicense => "invalid_license".to_string(),
LicenseCheckStatus::Trialing { .. } => "trialing".to_string(),
};
let settings = window.db().get_settings();
let num_launches = get_num_launches(app_handle).await;
let info = app_handle.package_info().clone();
let req = reqwest::Client::default()
@@ -77,6 +85,8 @@ impl YaakNotifier {
.query(&[
("version", info.version.to_string().as_str()),
("launches", num_launches.to_string().as_str()),
("installed", settings.created_at.format("%Y-%m-%d").to_string().as_str()),
("license", &license_check),
("platform", get_os()),
]);
let resp = req.send().await?;

View File

@@ -6,6 +6,7 @@ use crate::{
workspace_from_window,
};
use chrono::Utc;
use cookie::Cookie;
use log::warn;
use tauri::{AppHandle, Emitter, Manager, Runtime, State};
use tauri_plugin_clipboard_manager::ClipboardExt;
@@ -13,10 +14,11 @@ use yaak_models::models::{HttpResponse, Plugin};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
use yaak_plugins::events::{
Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse,
Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse, GetCookieValueResponse,
GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, InternalEventPayload,
PluginWindowContext, RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse,
ShowToastRequest, TemplateRenderResponse, WindowNavigateEvent,
ListCookieNamesResponse, PluginWindowContext, RenderHttpRequestResponse,
SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse,
WindowNavigateEvent,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle;
@@ -269,6 +271,33 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
let deleted = app_handle.db().delete_plugin_key_value(&name, &req.key).unwrap();
Some(InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted }))
}
InternalEventPayload::ListCookieNamesRequest(_req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for listing cookies");
let names = match cookie_jar_from_window(&window) {
None => Vec::new(),
Some(j) => j
.cookies
.into_iter()
.filter_map(|c| Cookie::parse(c.raw_cookie).ok().map(|c| c.name().to_string()))
.collect(),
};
Some(InternalEventPayload::ListCookieNamesResponse(ListCookieNamesResponse { names }))
}
InternalEventPayload::GetCookieValueRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for listing cookies");
let value = match cookie_jar_from_window(&window) {
None => None,
Some(j) => j.cookies.into_iter().find_map(|c| match Cookie::parse(c.raw_cookie) {
Ok(c) if c.name().to_string().eq(&req.name) => {
Some(c.value_trimmed().to_string())
}
_ => None,
}),
};
Some(InternalEventPayload::GetCookieValueResponse(GetCookieValueResponse { value }))
}
_ => None,
};

View File

@@ -2,7 +2,7 @@ use serde_json::Value;
use std::collections::{BTreeMap, HashMap};
use yaak_http::apply_path_placeholders;
use yaak_models::models::{
Environment, GrpcMetadataEntry, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter,
Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter,
};
use yaak_models::render::make_vars_hashmap;
use yaak_templates::{parse_and_render, render_json_value_raw, TemplateCallback};
@@ -37,7 +37,7 @@ pub async fn render_grpc_request<T: TemplateCallback>(
let mut metadata = Vec::new();
for p in r.metadata.clone() {
metadata.push(GrpcMetadataEntry {
metadata.push(HttpRequestHeader {
enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await?,

View File

@@ -32,6 +32,7 @@ var import_node_fs = require("node:fs");
async function getAccessToken(ctx, {
accessTokenUrl,
scope,
audience,
params,
grantType,
credentialsInBody,
@@ -56,6 +57,7 @@ async function getAccessToken(ctx, {
]
};
if (scope) httpRequest.body.form.push({ name: "scope", value: scope });
if (scope) httpRequest.body.form.push({ name: "audience", value: audience });
if (credentialsInBody) {
httpRequest.body.form.push({ name: "client_id", value: clientId });
httpRequest.body.form.push({ name: "client_secret", value: clientSecret });
@@ -64,10 +66,10 @@ async function getAccessToken(ctx, {
httpRequest.headers.push({ name: "Authorization", value });
}
const resp = await ctx.httpRequest.send({ httpRequest });
const body = resp.bodyPath ? (0, import_node_fs.readFileSync)(resp.bodyPath, "utf8") : "";
if (resp.status < 200 || resp.status >= 300) {
throw new Error("Failed to fetch access token with status=" + resp.status);
throw new Error("Failed to fetch access token with status=" + resp.status + " and body=" + body);
}
const body = (0, import_node_fs.readFileSync)(resp.bodyPath ?? "", "utf8");
let response;
try {
response = JSON.parse(body);
@@ -168,10 +170,10 @@ async function getOrRefreshAccessToken(ctx, contextId, {
await deleteToken(ctx, contextId);
return null;
}
const body = resp.bodyPath ? (0, import_node_fs2.readFileSync)(resp.bodyPath, "utf8") : "";
if (resp.status < 200 || resp.status >= 300) {
throw new Error("Failed to fetch access token with status=" + resp.status);
throw new Error("Failed to refresh access token with status=" + resp.status + " and body=" + body);
}
const body = (0, import_node_fs2.readFileSync)(resp.bodyPath ?? "", "utf8");
let response;
try {
response = JSON.parse(body);
@@ -201,6 +203,7 @@ async function getAuthorizationCode(ctx, contextId, {
redirectUri,
scope,
state,
audience,
credentialsInBody,
pkce
}) {
@@ -220,6 +223,7 @@ async function getAuthorizationCode(ctx, contextId, {
if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri);
if (scope) authorizationUrl.searchParams.set("scope", scope);
if (state) authorizationUrl.searchParams.set("state", state);
if (audience) authorizationUrl.searchParams.set("audience", audience);
if (pkce) {
const verifier = pkce.codeVerifier || createPkceCodeVerifier();
const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD;
@@ -256,6 +260,7 @@ async function getAuthorizationCode(ctx, contextId, {
clientId,
clientSecret,
scope,
audience,
credentialsInBody,
params: [
{ name: "code", value: code },
@@ -291,6 +296,7 @@ async function getClientCredentials(ctx, contextId, {
clientId,
clientSecret,
scope,
audience,
credentialsInBody
}) {
const token = await getToken(ctx, contextId);
@@ -299,6 +305,7 @@ async function getClientCredentials(ctx, contextId, {
const response = await getAccessToken(ctx, {
grantType: "client_credentials",
accessTokenUrl,
audience,
clientId,
clientSecret,
scope,
@@ -315,38 +322,46 @@ function getImplicit(ctx, contextId, {
clientId,
redirectUri,
scope,
state
state,
audience
}) {
return new Promise(async (resolve, reject) => {
const token = await getToken(ctx, contextId);
if (token) {
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ""}`);
authorizationUrl.searchParams.set("response_type", "code");
authorizationUrl.searchParams.set("response_type", "token");
authorizationUrl.searchParams.set("client_id", clientId);
if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri);
if (scope) authorizationUrl.searchParams.set("scope", scope);
if (state) authorizationUrl.searchParams.set("state", state);
if (audience) authorizationUrl.searchParams.set("audience", audience);
if (responseType.includes("id_token")) {
authorizationUrl.searchParams.set("nonce", String(Math.floor(Math.random() * 9999999999999) + 1));
}
const authorizationUrlStr = authorizationUrl.toString();
let foundAccessToken = false;
let { close } = await ctx.window.openUrl({
url: authorizationUrlStr,
label: "oauth-authorization-url",
async onClose() {
if (!foundAccessToken) {
reject(new Error("Authorization window closed"));
}
},
async onNavigate({ url: urlStr }) {
const url = new URL(urlStr);
if (url.searchParams.has("error")) {
return reject(Error(`Failed to authorize: ${url.searchParams.get("error")}`));
}
close();
const hash = url.hash.slice(1);
const params = new URLSearchParams(hash);
const idToken = params.get("id_token");
if (idToken) {
params.set("access_token", idToken);
params.delete("id_token");
const accessToken = params.get("access_token");
if (!accessToken) {
return;
}
foundAccessToken = true;
close();
const response = Object.fromEntries(params);
try {
resolve(await storeToken(ctx, contextId, response));
@@ -366,6 +381,7 @@ async function getPassword(ctx, contextId, {
username,
password,
credentialsInBody,
audience,
scope
}) {
const token = await getOrRefreshAccessToken(ctx, contextId, {
@@ -383,6 +399,7 @@ async function getPassword(ctx, contextId, {
clientId,
clientSecret,
scope,
audience,
grantType: "password",
credentialsInBody,
params: [
@@ -530,6 +547,12 @@ var plugin = {
optional: true,
dynamic: hiddenIfNot(["authorization_code", "implicit"])
},
{
type: "text",
name: "audience",
label: "Audience",
optional: true
},
{
type: "checkbox",
name: "usePkce",
@@ -635,6 +658,7 @@ var plugin = {
clientSecret: stringArg(values, "clientSecret"),
redirectUri: stringArgOrNull(values, "redirectUri"),
scope: stringArgOrNull(values, "scope"),
audience: stringArgOrNull(values, "audience"),
state: stringArgOrNull(values, "state"),
credentialsInBody,
pkce: values.usePkce ? {
@@ -650,6 +674,7 @@ var plugin = {
redirectUri: stringArgOrNull(values, "redirectUri"),
responseType: stringArg(values, "responseType"),
scope: stringArgOrNull(values, "scope"),
audience: stringArgOrNull(values, "audience"),
state: stringArgOrNull(values, "state")
});
} else if (grantType === "client_credentials") {
@@ -659,6 +684,7 @@ var plugin = {
clientId: stringArg(values, "clientId"),
clientSecret: stringArg(values, "clientSecret"),
scope: stringArgOrNull(values, "scope"),
audience: stringArgOrNull(values, "audience"),
credentialsInBody
});
} else if (grantType === "password") {
@@ -670,6 +696,7 @@ var plugin = {
username: stringArg(values, "username"),
password: stringArg(values, "password"),
scope: stringArgOrNull(values, "scope"),
audience: stringArgOrNull(values, "audience"),
credentialsInBody
});
} else {

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
"dev": "yaakcli dev ./src/index.js"
},
"dependencies": {
"jsonpath-plus": "^9.0.0"
"jsonpath-plus": "^10.3.0"
},
"devDependencies": {
"@types/jsonpath": "^0.2.4"

View File

@@ -542,20 +542,23 @@ function pairsToDataParameters(keyedPairs) {
}
for (const p of pairs) {
if (typeof p !== "string") continue;
const [name, value] = p.split("=");
if (p.startsWith("@")) {
dataParameters.push({
name: name ?? "",
value: "",
filePath: p.slice(1),
enabled: true
});
} else {
dataParameters.push({
name: name ?? "",
value: flagName === "data-urlencode" ? encodeURIComponent(value ?? "") : value ?? "",
enabled: true
});
let params = p.split("&");
for (const param of params) {
const [name, value] = param.split("=");
if (param.startsWith("@")) {
dataParameters.push({
name: name ?? "",
value: "",
filePath: param.slice(1),
enabled: true
});
} else {
dataParameters.push({
name: name ?? "",
value: flagName === "data-urlencode" ? encodeURIComponent(value ?? "") : value ?? "",
enabled: true
});
}
}
}
}

View File

@@ -7564,7 +7564,7 @@ function importHttpRequest2(r, workspaceId, parentId) {
const sortKey = r.meta?.sortKey ?? r.sortKey;
let bodyType = null;
let body = {};
if (r.body.mimeType === "application/octet-stream") {
if (r.body?.mimeType === "application/octet-stream") {
bodyType = "binary";
body = { filePath: r.body.fileName ?? "" };
} else if (r.body?.mimeType === "application/x-www-form-urlencoded") {
@@ -7720,7 +7720,7 @@ function importEnvironment2(e, workspaceId, isParent) {
base: isParent ?? e.parentId === workspaceId,
model: "environment",
name: e.name,
variables: Object.entries(e.data).map(([name, value]) => ({
variables: Object.entries(e.data ?? {}).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`

View File

@@ -0,0 +1,47 @@
"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 = {
templateFunctions: [
{
name: "cookie.value",
description: "Read the value of a cookie in the jar, by name",
args: [
{
type: "text",
name: "cookie_name",
label: "Cookie Name"
}
],
async onRender(ctx, args) {
return ctx.cookies.getValue({ name: String(args.values.cookie_name) });
}
}
]
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

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

View File

@@ -0,0 +1,69 @@
"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 = {
templateFunctions: [
{
name: "base64.encode",
description: "Encode a value to base64",
args: [{ label: "Plain Text", type: "text", name: "value", multiLine: true }],
async onRender(_ctx, args) {
return Buffer.from(args.values.value ?? "").toString("base64");
}
},
{
name: "base64.decode",
description: "Decode a value from base64",
args: [{ label: "Encoded Value", type: "text", name: "value", multiLine: true }],
async onRender(_ctx, args) {
return Buffer.from(args.values.value ?? "", "base64").toString("utf-8");
}
},
{
name: "url.encode",
description: "Encode a value for use in a URL (percent-encoding)",
args: [{ label: "Plain Text", type: "text", name: "value", multiLine: true }],
async onRender(_ctx, args) {
return encodeURIComponent(args.values.value ?? "");
}
},
{
name: "url.decode",
description: "Decode a percent-encoded URL value",
args: [{ label: "Encoded Value", type: "text", name: "value", multiLine: true }],
async onRender(_ctx, args) {
try {
return decodeURIComponent(args.values.value ?? "");
} catch {
return "";
}
}
}
]
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

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

View File

@@ -25,24 +25,76 @@ __export(src_exports, {
module.exports = __toCommonJS(src_exports);
var import_node_crypto = require("node:crypto");
var algorithms = ["md5", "sha1", "sha256", "sha512"];
var plugin = {
templateFunctions: algorithms.map((algorithm) => ({
name: `hash.${algorithm}`,
description: "Hash a value to its hexidecimal representation",
args: [
{
name: "input",
label: "Input",
placeholder: "input text",
type: "text"
}
],
async onRender(_ctx, args) {
if (!args.values.input) return "";
return (0, import_node_crypto.createHash)(algorithm).update(args.values.input, "utf-8").digest("hex");
var encodings = ["base64", "hex"];
var hashFunctions = algorithms.map((algorithm) => ({
name: `hash.${algorithm}`,
description: "Hash a value to its hexidecimal representation",
args: [
{
type: "text",
name: "input",
label: "Input",
placeholder: "input text",
multiLine: true
},
{
type: "select",
name: "encoding",
label: "Encoding",
defaultValue: "base64",
options: encodings.map((encoding) => ({
label: capitalize(encoding),
value: encoding
}))
}
}))
],
async onRender(_ctx, args) {
const input = String(args.values.input);
const encoding = String(args.values.encoding);
return (0, import_node_crypto.createHash)(algorithm).update(input, "utf-8").digest(encoding);
}
}));
var hmacFunctions = algorithms.map((algorithm) => ({
name: `hmac.${algorithm}`,
description: "Compute the HMAC of a value",
args: [
{
type: "text",
name: "input",
label: "Input",
placeholder: "input text",
multiLine: true
},
{
type: "text",
name: "key",
label: "Key",
password: true
},
{
type: "select",
name: "encoding",
label: "Encoding",
defaultValue: "base64",
options: encodings.map((encoding) => ({
value: encoding,
label: capitalize(encoding)
}))
}
],
async onRender(_ctx, args) {
const input = String(args.values.input);
const key = String(args.values.key);
const encoding = String(args.values.encoding);
return (0, import_node_crypto.createHmac)(algorithm, key, {}).update(input).digest(encoding);
}
}));
var plugin = {
templateFunctions: [...hashFunctions, ...hmacFunctions]
};
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
{
"name": "@yaakapp/template-function-json",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
},
"dependencies": {
"jsonpath-plus": "^10.3.0"
},
"devDependencies": {
"@types/jsonpath": "^0.2.4"
}
}

View File

@@ -0,0 +1,52 @@
"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 = {
templateFunctions: [{
name: "regex.match",
description: "Extract",
args: [
{
type: "text",
name: "regex",
label: "Regular Expression",
placeholder: "^w+=(?<value>w*)$",
defaultValue: "^(.*)$",
description: "A JavaScript regular expression, evaluated using the Node.js RegExp engine. Capture groups or named groups can be used to extract values."
},
{ type: "text", name: "input", label: "Input Text", multiLine: true }
],
async onRender(_ctx, args) {
if (!args.values.regex) return "";
const regex = new RegExp(String(args.values.regex));
const match = args.values.input?.match(regex);
return match?.groups ? Object.values(match.groups)[0] ?? "" : match?.[1] ?? match?.[0] ?? "";
}
}]
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

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

View File

@@ -0,0 +1,417 @@
"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);
// node_modules/uuid/dist/esm/regex.js
var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
// node_modules/uuid/dist/esm/validate.js
function validate(uuid) {
return typeof uuid === "string" && regex_default.test(uuid);
}
var validate_default = validate;
// node_modules/uuid/dist/esm/parse.js
function parse(uuid) {
if (!validate_default(uuid)) {
throw TypeError("Invalid UUID");
}
let v;
return Uint8Array.of((v = parseInt(uuid.slice(0, 8), 16)) >>> 24, v >>> 16 & 255, v >>> 8 & 255, v & 255, (v = parseInt(uuid.slice(9, 13), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(14, 18), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(19, 23), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(24, 36), 16)) / 1099511627776 & 255, v / 4294967296 & 255, v >>> 24 & 255, v >>> 16 & 255, v >>> 8 & 255, v & 255);
}
var parse_default = parse;
// node_modules/uuid/dist/esm/stringify.js
var byteToHex = [];
for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 256).toString(16).slice(1));
}
function unsafeStringify(arr, offset = 0) {
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
}
// node_modules/uuid/dist/esm/rng.js
var import_crypto = require("crypto");
var rnds8Pool = new Uint8Array(256);
var poolPtr = rnds8Pool.length;
function rng() {
if (poolPtr > rnds8Pool.length - 16) {
(0, import_crypto.randomFillSync)(rnds8Pool);
poolPtr = 0;
}
return rnds8Pool.slice(poolPtr, poolPtr += 16);
}
// node_modules/uuid/dist/esm/v1.js
var _state = {};
function v1(options, buf, offset) {
let bytes;
const isV6 = options?._v6 ?? false;
if (options) {
const optionsKeys = Object.keys(options);
if (optionsKeys.length === 1 && optionsKeys[0] === "_v6") {
options = void 0;
}
}
if (options) {
bytes = v1Bytes(options.random ?? options.rng?.() ?? rng(), options.msecs, options.nsecs, options.clockseq, options.node, buf, offset);
} else {
const now = Date.now();
const rnds = rng();
updateV1State(_state, now, rnds);
bytes = v1Bytes(rnds, _state.msecs, _state.nsecs, isV6 ? void 0 : _state.clockseq, isV6 ? void 0 : _state.node, buf, offset);
}
return buf ?? unsafeStringify(bytes);
}
function updateV1State(state, now, rnds) {
state.msecs ??= -Infinity;
state.nsecs ??= 0;
if (now === state.msecs) {
state.nsecs++;
if (state.nsecs >= 1e4) {
state.node = void 0;
state.nsecs = 0;
}
} else if (now > state.msecs) {
state.nsecs = 0;
} else if (now < state.msecs) {
state.node = void 0;
}
if (!state.node) {
state.node = rnds.slice(10, 16);
state.node[0] |= 1;
state.clockseq = (rnds[8] << 8 | rnds[9]) & 16383;
}
state.msecs = now;
return state;
}
function v1Bytes(rnds, msecs, nsecs, clockseq, node, buf, offset = 0) {
if (rnds.length < 16) {
throw new Error("Random bytes length must be >= 16");
}
if (!buf) {
buf = new Uint8Array(16);
offset = 0;
} else {
if (offset < 0 || offset + 16 > buf.length) {
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
}
}
msecs ??= Date.now();
nsecs ??= 0;
clockseq ??= (rnds[8] << 8 | rnds[9]) & 16383;
node ??= rnds.slice(10, 16);
msecs += 122192928e5;
const tl = ((msecs & 268435455) * 1e4 + nsecs) % 4294967296;
buf[offset++] = tl >>> 24 & 255;
buf[offset++] = tl >>> 16 & 255;
buf[offset++] = tl >>> 8 & 255;
buf[offset++] = tl & 255;
const tmh = msecs / 4294967296 * 1e4 & 268435455;
buf[offset++] = tmh >>> 8 & 255;
buf[offset++] = tmh & 255;
buf[offset++] = tmh >>> 24 & 15 | 16;
buf[offset++] = tmh >>> 16 & 255;
buf[offset++] = clockseq >>> 8 | 128;
buf[offset++] = clockseq & 255;
for (let n = 0; n < 6; ++n) {
buf[offset++] = node[n];
}
return buf;
}
var v1_default = v1;
// node_modules/uuid/dist/esm/v1ToV6.js
function v1ToV6(uuid) {
const v1Bytes2 = typeof uuid === "string" ? parse_default(uuid) : uuid;
const v6Bytes = _v1ToV6(v1Bytes2);
return typeof uuid === "string" ? unsafeStringify(v6Bytes) : v6Bytes;
}
function _v1ToV6(v1Bytes2) {
return Uint8Array.of((v1Bytes2[6] & 15) << 4 | v1Bytes2[7] >> 4 & 15, (v1Bytes2[7] & 15) << 4 | (v1Bytes2[4] & 240) >> 4, (v1Bytes2[4] & 15) << 4 | (v1Bytes2[5] & 240) >> 4, (v1Bytes2[5] & 15) << 4 | (v1Bytes2[0] & 240) >> 4, (v1Bytes2[0] & 15) << 4 | (v1Bytes2[1] & 240) >> 4, (v1Bytes2[1] & 15) << 4 | (v1Bytes2[2] & 240) >> 4, 96 | v1Bytes2[2] & 15, v1Bytes2[3], v1Bytes2[8], v1Bytes2[9], v1Bytes2[10], v1Bytes2[11], v1Bytes2[12], v1Bytes2[13], v1Bytes2[14], v1Bytes2[15]);
}
// node_modules/uuid/dist/esm/md5.js
var import_crypto2 = require("crypto");
function md5(bytes) {
if (Array.isArray(bytes)) {
bytes = Buffer.from(bytes);
} else if (typeof bytes === "string") {
bytes = Buffer.from(bytes, "utf8");
}
return (0, import_crypto2.createHash)("md5").update(bytes).digest();
}
var md5_default = md5;
// node_modules/uuid/dist/esm/v35.js
function stringToBytes(str) {
str = unescape(encodeURIComponent(str));
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; ++i) {
bytes[i] = str.charCodeAt(i);
}
return bytes;
}
var DNS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
var URL = "6ba7b811-9dad-11d1-80b4-00c04fd430c8";
function v35(version, hash, value, namespace, buf, offset) {
const valueBytes = typeof value === "string" ? stringToBytes(value) : value;
const namespaceBytes = typeof namespace === "string" ? parse_default(namespace) : namespace;
if (typeof namespace === "string") {
namespace = parse_default(namespace);
}
if (namespace?.length !== 16) {
throw TypeError("Namespace must be array-like (16 iterable integer values, 0-255)");
}
let bytes = new Uint8Array(16 + valueBytes.length);
bytes.set(namespaceBytes);
bytes.set(valueBytes, namespaceBytes.length);
bytes = hash(bytes);
bytes[6] = bytes[6] & 15 | version;
bytes[8] = bytes[8] & 63 | 128;
if (buf) {
offset = offset || 0;
for (let i = 0; i < 16; ++i) {
buf[offset + i] = bytes[i];
}
return buf;
}
return unsafeStringify(bytes);
}
// node_modules/uuid/dist/esm/v3.js
function v3(value, namespace, buf, offset) {
return v35(48, md5_default, value, namespace, buf, offset);
}
v3.DNS = DNS;
v3.URL = URL;
var v3_default = v3;
// node_modules/uuid/dist/esm/native.js
var import_crypto3 = require("crypto");
var native_default = { randomUUID: import_crypto3.randomUUID };
// node_modules/uuid/dist/esm/v4.js
function v4(options, buf, offset) {
if (native_default.randomUUID && !buf && !options) {
return native_default.randomUUID();
}
options = options || {};
const rnds = options.random ?? options.rng?.() ?? rng();
if (rnds.length < 16) {
throw new Error("Random bytes length must be >= 16");
}
rnds[6] = rnds[6] & 15 | 64;
rnds[8] = rnds[8] & 63 | 128;
if (buf) {
offset = offset || 0;
if (offset < 0 || offset + 16 > buf.length) {
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
}
for (let i = 0; i < 16; ++i) {
buf[offset + i] = rnds[i];
}
return buf;
}
return unsafeStringify(rnds);
}
var v4_default = v4;
// node_modules/uuid/dist/esm/sha1.js
var import_crypto4 = require("crypto");
function sha1(bytes) {
if (Array.isArray(bytes)) {
bytes = Buffer.from(bytes);
} else if (typeof bytes === "string") {
bytes = Buffer.from(bytes, "utf8");
}
return (0, import_crypto4.createHash)("sha1").update(bytes).digest();
}
var sha1_default = sha1;
// node_modules/uuid/dist/esm/v5.js
function v5(value, namespace, buf, offset) {
return v35(80, sha1_default, value, namespace, buf, offset);
}
v5.DNS = DNS;
v5.URL = URL;
var v5_default = v5;
// node_modules/uuid/dist/esm/v6.js
function v6(options, buf, offset) {
options ??= {};
offset ??= 0;
let bytes = v1_default({ ...options, _v6: true }, new Uint8Array(16));
bytes = v1ToV6(bytes);
if (buf) {
for (let i = 0; i < 16; i++) {
buf[offset + i] = bytes[i];
}
return buf;
}
return unsafeStringify(bytes);
}
var v6_default = v6;
// node_modules/uuid/dist/esm/v7.js
var _state2 = {};
function v7(options, buf, offset) {
let bytes;
if (options) {
bytes = v7Bytes(options.random ?? options.rng?.() ?? rng(), options.msecs, options.seq, buf, offset);
} else {
const now = Date.now();
const rnds = rng();
updateV7State(_state2, now, rnds);
bytes = v7Bytes(rnds, _state2.msecs, _state2.seq, buf, offset);
}
return buf ?? unsafeStringify(bytes);
}
function updateV7State(state, now, rnds) {
state.msecs ??= -Infinity;
state.seq ??= 0;
if (now > state.msecs) {
state.seq = rnds[6] << 23 | rnds[7] << 16 | rnds[8] << 8 | rnds[9];
state.msecs = now;
} else {
state.seq = state.seq + 1 | 0;
if (state.seq === 0) {
state.msecs++;
}
}
return state;
}
function v7Bytes(rnds, msecs, seq, buf, offset = 0) {
if (rnds.length < 16) {
throw new Error("Random bytes length must be >= 16");
}
if (!buf) {
buf = new Uint8Array(16);
offset = 0;
} else {
if (offset < 0 || offset + 16 > buf.length) {
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
}
}
msecs ??= Date.now();
seq ??= rnds[6] * 127 << 24 | rnds[7] << 16 | rnds[8] << 8 | rnds[9];
buf[offset++] = msecs / 1099511627776 & 255;
buf[offset++] = msecs / 4294967296 & 255;
buf[offset++] = msecs / 16777216 & 255;
buf[offset++] = msecs / 65536 & 255;
buf[offset++] = msecs / 256 & 255;
buf[offset++] = msecs & 255;
buf[offset++] = 112 | seq >>> 28 & 15;
buf[offset++] = seq >>> 20 & 255;
buf[offset++] = 128 | seq >>> 14 & 63;
buf[offset++] = seq >>> 6 & 255;
buf[offset++] = seq << 2 & 255 | rnds[10] & 3;
buf[offset++] = rnds[11];
buf[offset++] = rnds[12];
buf[offset++] = rnds[13];
buf[offset++] = rnds[14];
buf[offset++] = rnds[15];
return buf;
}
var v7_default = v7;
// src/index.ts
var plugin = {
templateFunctions: [
{
name: "uuid.v1",
description: "Generate a UUID V1",
args: [],
async onRender(_ctx, _args) {
return v1_default();
}
},
{
name: "uuid.v3",
description: "Generate a UUID V3",
args: [
{ type: "text", name: "name", label: "Name" },
{
type: "text",
name: "namespace",
label: "Namespace UUID",
description: "A valid UUID to use as the namespace",
placeholder: "24ced880-3bf4-11f0-8329-cd053d577f0e"
}
],
async onRender(_ctx, args) {
return v3_default(String(args.values.name), String(args.values.namespace));
}
},
{
name: "uuid.v4",
description: "Generate a UUID V4",
args: [],
async onRender(_ctx, _args) {
return v4_default();
}
},
{
name: "uuid.v5",
description: "Generate a UUID V5",
args: [
{ type: "text", name: "name", label: "Name" },
{ type: "text", name: "namespace", label: "Namespace" }
],
async onRender(_ctx, args) {
return v5_default(String(args.values.name), String(args.values.namespace));
}
},
{
name: "uuid.v6",
description: "Generate a UUID V6",
args: [
{
type: "text",
name: "timestamp",
label: "Timestamp",
optional: true,
description: "Can be any format that can be parsed by JavaScript new Date(...)",
placeholder: "2025-05-28T11:15:00Z"
}
],
async onRender(_ctx, args) {
return v6_default({ msecs: new Date(String(args.values.timestamp)).getTime() });
}
},
{
name: "uuid.v7",
description: "Generate a UUID V7",
args: [],
async onRender(_ctx, _args) {
return v7_default();
}
}
]
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

@@ -0,0 +1,12 @@
{
"name": "@yaakapp/template-function-uuid",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
},
"dependencies": {
"uuid": "^11.1.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"name": "@yaakapp/template-function-xml",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
},
"dependencies": {
"@xmldom/xmldom": "^0.8.10",
"xpath": "^0.0.34"
}
}

View File

@@ -4,11 +4,9 @@ export type Environment = { model: "environment", id: string, workspaceId: strin
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 Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
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 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<HttpRequestHeader>, 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>, };
@@ -20,4 +18,4 @@ export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environ
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };

View File

@@ -1,16 +1,256 @@
use prost_reflect::{DescriptorPool, MessageDescriptor};
use prost_types::field_descriptor_proto;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use prost_reflect::{DescriptorPool, FieldDescriptor, MessageDescriptor};
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Default, Serialize, Deserialize)]
pub fn message_to_json_schema(_: &DescriptorPool, root_msg: MessageDescriptor) -> JsonSchemaEntry {
JsonSchemaGenerator::generate_json_schema(root_msg)
}
struct JsonSchemaGenerator {
msg_mapping: HashMap<String, JsonSchemaEntry>,
}
impl JsonSchemaGenerator {
pub fn new() -> Self {
JsonSchemaGenerator {
msg_mapping: HashMap::new(),
}
}
pub fn generate_json_schema(msg: MessageDescriptor) -> JsonSchemaEntry {
let generator = JsonSchemaGenerator::new();
generator.scan_root(msg)
}
fn add_message(&mut self, msg: &MessageDescriptor) {
let name = msg.full_name().to_string();
if self.msg_mapping.contains_key(&name) {
return;
}
self.msg_mapping.insert(name.clone(), JsonSchemaEntry::object());
}
pub fn scan_root(mut self, root_msg: MessageDescriptor) -> JsonSchemaEntry {
self.init_structure(root_msg.clone());
self.fill_properties(root_msg.clone());
let mut root = self.msg_mapping.remove(root_msg.full_name()).unwrap();
if self.msg_mapping.len() > 0 {
root.defs = Some(self.msg_mapping);
}
root
}
fn fill_properties(&mut self, root_msg: MessageDescriptor) {
let root_name = root_msg.full_name().to_string();
let mut visited = HashSet::new();
let mut msg_queue = VecDeque::new();
msg_queue.push_back(root_msg);
while !msg_queue.is_empty() {
let msg = msg_queue.pop_front().unwrap();
let msg_name = msg.full_name();
if visited.contains(msg_name) {
continue;
}
visited.insert(msg_name.to_string());
let entry = self.msg_mapping.get_mut(msg_name).unwrap();
for field in msg.fields() {
let field_name = field.name().to_string();
if matches!(field.cardinality(), prost_reflect::Cardinality::Required) {
entry.add_required(field_name.clone());
}
if let Some(oneof) = field.containing_oneof() {
for oneof_field in oneof.fields() {
if let Some(fm) = is_message_field(&oneof_field) {
msg_queue.push_back(fm);
}
entry.add_property(
oneof_field.name().to_string(),
field_to_type_or_ref(&root_name, oneof_field),
);
}
continue;
}
let (field_type, nest_msg) = {
if let Some(fm) = is_message_field(&field) {
if field.is_list() {
// repeated message type
(
JsonSchemaEntry::array(field_to_type_or_ref(&root_name, field)),
Some(fm),
)
} else if field.is_map() {
let value_field = fm.get_field_by_name("value").unwrap();
if let Some(fm) = is_message_field(&value_field) {
(
JsonSchemaEntry::map(field_to_type_or_ref(
&root_name,
value_field,
)),
Some(fm),
)
} else {
(
JsonSchemaEntry::map(field_to_type_or_ref(
&root_name,
value_field,
)),
None,
)
}
} else {
(field_to_type_or_ref(&root_name, field), Some(fm))
}
} else {
if field.is_list() {
// repeated scalar type
(JsonSchemaEntry::array(field_to_type_or_ref(&root_name, field)), None)
} else {
(field_to_type_or_ref(&root_name, field), None)
}
}
};
if let Some(fm) = nest_msg {
msg_queue.push_back(fm);
}
entry.add_property(field_name, field_type);
}
}
}
fn init_structure(&mut self, root_msg: MessageDescriptor) {
let mut visited = HashSet::new();
let mut msg_queue = VecDeque::new();
msg_queue.push_back(root_msg.clone());
// level traversal, to make sure all message type is defined before used
while !msg_queue.is_empty() {
let msg = msg_queue.pop_front().unwrap();
let name = msg.full_name();
if visited.contains(name) {
continue;
}
visited.insert(name.to_string());
self.add_message(&msg);
for child in msg.child_messages() {
if child.is_map_entry() {
// for field with map<key, value> type, there will be a child message type *Entry generated
// just skip it
continue;
}
self.add_message(&child);
msg_queue.push_back(child);
}
for field in msg.fields() {
if let Some(oneof) = field.containing_oneof() {
for oneof_field in oneof.fields() {
if let Some(fm) = is_message_field(&oneof_field) {
self.add_message(&fm);
msg_queue.push_back(fm);
}
}
continue;
}
if field.is_map() {
// key is always scalar type, so no need to process
// value can be any type, so need to unpack value type
let map_field_msg = is_message_field(&field).unwrap();
let map_value_field = map_field_msg.get_field_by_name("value").unwrap();
if let Some(value_fm) = is_message_field(&map_value_field) {
self.add_message(&value_fm);
msg_queue.push_back(value_fm);
}
continue;
}
if let Some(fm) = is_message_field(&field) {
self.add_message(&fm);
msg_queue.push_back(fm);
}
}
}
}
}
fn field_to_type_or_ref(root_name: &str, field: FieldDescriptor) -> JsonSchemaEntry {
match field.kind() {
prost_reflect::Kind::Bool => JsonSchemaEntry::boolean(),
prost_reflect::Kind::Double => JsonSchemaEntry::number("double"),
prost_reflect::Kind::Float => JsonSchemaEntry::number("float"),
prost_reflect::Kind::Int32 => JsonSchemaEntry::number("int32"),
prost_reflect::Kind::Int64 => JsonSchemaEntry::string_with_format("int64"),
prost_reflect::Kind::Uint32 => JsonSchemaEntry::number("int64"),
prost_reflect::Kind::Uint64 => JsonSchemaEntry::string_with_format("uint64"),
prost_reflect::Kind::Sint32 => JsonSchemaEntry::number("sint32"),
prost_reflect::Kind::Sint64 => JsonSchemaEntry::string_with_format("sint64"),
prost_reflect::Kind::Fixed32 => JsonSchemaEntry::number("int64"),
prost_reflect::Kind::Fixed64 => JsonSchemaEntry::string_with_format("fixed64"),
prost_reflect::Kind::Sfixed32 => JsonSchemaEntry::number("sfixed32"),
prost_reflect::Kind::Sfixed64 => JsonSchemaEntry::string_with_format("sfixed64"),
prost_reflect::Kind::String => JsonSchemaEntry::string(),
prost_reflect::Kind::Bytes => JsonSchemaEntry::string_with_format("byte"),
prost_reflect::Kind::Enum(enums) => {
let values = enums.values().map(|v| v.name().to_string()).collect::<Vec<_>>();
JsonSchemaEntry::enums(values)
}
prost_reflect::Kind::Message(fm) => {
let field_type_full_name = fm.full_name();
match field_type_full_name {
// [Protocol Buffers Well-Known Types]: https://protobuf.dev/reference/protobuf/google.protobuf/
"google.protobuf.FieldMask" => JsonSchemaEntry::string(),
"google.protobuf.Timestamp" => JsonSchemaEntry::string_with_format("date-time"),
"google.protobuf.Duration" => JsonSchemaEntry::string(),
"google.protobuf.StringValue" => JsonSchemaEntry::string(),
"google.protobuf.BytesValue" => JsonSchemaEntry::string_with_format("byte"),
"google.protobuf.Int32Value" => JsonSchemaEntry::number("int32"),
"google.protobuf.UInt32Value" => JsonSchemaEntry::string_with_format("int64"),
"google.protobuf.Int64Value" => JsonSchemaEntry::string_with_format("int64"),
"google.protobuf.UInt64Value" => JsonSchemaEntry::string_with_format("uint64"),
"google.protobuf.FloatValue" => JsonSchemaEntry::number("float"),
"google.protobuf.DoubleValue" => JsonSchemaEntry::number("double"),
"google.protobuf.BoolValue" => JsonSchemaEntry::boolean(),
"google.protobuf.Empty" => JsonSchemaEntry::default(),
"google.protobuf.Struct" => JsonSchemaEntry::object(),
"google.protobuf.ListValue" => JsonSchemaEntry::array(JsonSchemaEntry::default()),
"google.protobuf.NullValue" => JsonSchemaEntry::null(),
name @ _ if name == root_name => JsonSchemaEntry::root_reference(),
_ => JsonSchemaEntry::reference(fm.full_name()),
}
}
}
}
fn is_message_field(field: &FieldDescriptor) -> Option<MessageDescriptor> {
match field.kind() {
prost_reflect::Kind::Message(m) => Some(m),
_ => None,
}
}
#[derive(Default, serde::Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct JsonSchemaEntry {
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(rename = "type")]
type_: JsonType,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
type_: Option<JsonType>,
#[serde(skip_serializing_if = "Option::is_none")]
format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
@@ -21,15 +261,115 @@ pub struct JsonSchemaEntry {
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
enum_: Option<Vec<String>>,
/// Don't allow any other properties in the object
additional_properties: bool,
// for map type
#[serde(skip_serializing_if = "Option::is_none")]
additional_properties: Option<Box<JsonSchemaEntry>>,
/// Set all properties to required
// Set all properties to required
#[serde(skip_serializing_if = "Option::is_none")]
required: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
items: Option<Box<JsonSchemaEntry>>,
#[serde(skip_serializing_if = "Option::is_none", rename = "$defs")]
defs: Option<HashMap<String, JsonSchemaEntry>>,
#[serde(skip_serializing_if = "Option::is_none", rename = "$ref")]
ref_: Option<String>,
}
impl JsonSchemaEntry {
pub fn add_property(&mut self, name: String, entry: JsonSchemaEntry) {
if self.properties.is_none() {
self.properties = Some(HashMap::new());
}
self.properties.as_mut().unwrap().insert(name, entry);
}
pub fn add_required(&mut self, name: String) {
if self.required.is_none() {
self.required = Some(Vec::new());
}
self.required.as_mut().unwrap().push(name);
}
}
impl JsonSchemaEntry {
pub fn object() -> Self {
JsonSchemaEntry {
type_: Some(JsonType::Object),
..Default::default()
}
}
pub fn boolean() -> Self {
JsonSchemaEntry {
type_: Some(JsonType::Boolean),
..Default::default()
}
}
pub fn number<S: Into<String>>(format: S) -> Self {
JsonSchemaEntry {
type_: Some(JsonType::Number),
format: Some(format.into()),
..Default::default()
}
}
pub fn string() -> Self {
JsonSchemaEntry {
type_: Some(JsonType::String),
..Default::default()
}
}
pub fn string_with_format<S: Into<String>>(format: S) -> Self {
JsonSchemaEntry {
type_: Some(JsonType::String),
format: Some(format.into()),
..Default::default()
}
}
pub fn reference<S: AsRef<str>>(ref_: S) -> Self {
JsonSchemaEntry {
ref_: Some(format!("#/$defs/{}", ref_.as_ref())),
..Default::default()
}
}
pub fn root_reference() -> Self{
JsonSchemaEntry {
ref_: Some("#".to_string()),
..Default::default()
}
}
pub fn array(item: JsonSchemaEntry) -> Self {
JsonSchemaEntry {
type_: Some(JsonType::Array),
items: Some(Box::new(item)),
..Default::default()
}
}
pub fn enums(enums: Vec<String>) -> Self {
JsonSchemaEntry {
type_: Some(JsonType::String),
enum_: Some(enums),
..Default::default()
}
}
pub fn map(value_type: JsonSchemaEntry) -> Self {
JsonSchemaEntry {
type_: Some(JsonType::Object),
additional_properties: Some(Box::new(value_type)),
..Default::default()
}
}
pub fn null() -> Self {
JsonSchemaEntry {
type_: Some(JsonType::Null),
..Default::default()
}
}
}
enum JsonType {
@@ -49,7 +389,7 @@ impl Default for JsonType {
}
impl serde::Serialize for JsonType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
@@ -64,116 +404,3 @@ impl serde::Serialize for JsonType {
}
}
}
impl<'de> serde::Deserialize<'de> for JsonType {
fn deserialize<D>(deserializer: D) -> Result<JsonType, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"string" => Ok(JsonType::String),
"number" => Ok(JsonType::Number),
"object" => Ok(JsonType::Object),
"array" => Ok(JsonType::Array),
"boolean" => Ok(JsonType::Boolean),
"null" => Ok(JsonType::Null),
_ => Ok(JsonType::_UNKNOWN),
}
}
}
pub fn message_to_json_schema(
pool: &DescriptorPool,
message: MessageDescriptor,
) -> JsonSchemaEntry {
let mut schema = JsonSchemaEntry {
title: Some(message.name().to_string()),
type_: JsonType::Object, // Messages are objects
..Default::default()
};
let mut properties = HashMap::new();
message.fields().for_each(|f| match f.kind() {
prost_reflect::Kind::Message(m) => {
properties.insert(f.name().to_string(), message_to_json_schema(pool, m));
}
prost_reflect::Kind::Enum(e) => {
properties.insert(
f.name().to_string(),
JsonSchemaEntry {
type_: map_proto_type_to_json_type(f.field_descriptor_proto().r#type()),
enum_: Some(e.values().map(|v| v.name().to_string()).collect::<Vec<_>>()),
..Default::default()
},
);
}
_ => {
// TODO: Handle repeated label
match f.field_descriptor_proto().label() {
field_descriptor_proto::Label::Repeated => {
// TODO: Handle more complex repeated types. This just handles primitives for now
properties.insert(
f.name().to_string(),
JsonSchemaEntry {
type_: JsonType::Array,
items: Some(Box::new(JsonSchemaEntry {
type_: map_proto_type_to_json_type(
f.field_descriptor_proto().r#type(),
),
..Default::default()
})),
..Default::default()
},
);
}
_ => {
// Regular JSON field
properties.insert(
f.name().to_string(),
JsonSchemaEntry {
type_: map_proto_type_to_json_type(f.field_descriptor_proto().r#type()),
..Default::default()
},
);
}
};
}
});
schema.properties = Some(properties);
// All proto 3 fields are optional, so maybe we could
// make this a setting?
// schema.required = Some(
// message
// .fields()
// .map(|f| f.name().to_string())
// .collect::<Vec<_>>(),
// );
schema
}
fn map_proto_type_to_json_type(proto_type: field_descriptor_proto::Type) -> JsonType {
match proto_type {
field_descriptor_proto::Type::Double => JsonType::Number,
field_descriptor_proto::Type::Float => JsonType::Number,
field_descriptor_proto::Type::Int64 => JsonType::Number,
field_descriptor_proto::Type::Uint64 => JsonType::Number,
field_descriptor_proto::Type::Int32 => JsonType::Number,
field_descriptor_proto::Type::Fixed64 => JsonType::Number,
field_descriptor_proto::Type::Fixed32 => JsonType::Number,
field_descriptor_proto::Type::Bool => JsonType::Boolean,
field_descriptor_proto::Type::String => JsonType::String,
field_descriptor_proto::Type::Group => JsonType::_UNKNOWN,
field_descriptor_proto::Type::Message => JsonType::Object,
field_descriptor_proto::Type::Bytes => JsonType::String,
field_descriptor_proto::Type::Uint32 => JsonType::Number,
field_descriptor_proto::Type::Enum => JsonType::String,
field_descriptor_proto::Type::Sfixed32 => JsonType::Number,
field_descriptor_proto::Type::Sfixed64 => JsonType::Number,
field_descriptor_proto::Type::Sint32 => JsonType::Number,
field_descriptor_proto::Type::Sint64 => JsonType::Number,
}
}

View File

@@ -1,60 +1,22 @@
use crate::error::Result;
use crate::{
activate_license, check_license, deactivate_license, ActivateLicenseRequestPayload,
CheckActivationRequestPayload, DeactivateLicenseRequestPayload, LicenseCheckStatus,
};
use crate::{LicenseCheckStatus, activate_license, check_license, deactivate_license};
use log::{debug, info};
use std::string::ToString;
use tauri::{command, Manager, Runtime, WebviewWindow};
use tauri::{Runtime, WebviewWindow, command};
#[command]
pub async fn check<R: Runtime>(window: WebviewWindow<R>) -> Result<LicenseCheckStatus> {
debug!("Checking license");
check_license(
&window,
CheckActivationRequestPayload {
app_platform: get_os().to_string(),
app_version: window.package_info().version.to_string(),
},
)
.await
check_license(&window).await
}
#[command]
pub async fn activate<R: Runtime>(license_key: &str, window: WebviewWindow<R>) -> Result<()> {
info!("Activating license {}", license_key);
activate_license(
&window,
ActivateLicenseRequestPayload {
license_key: license_key.to_string(),
app_platform: get_os().to_string(),
app_version: window.app_handle().package_info().version.to_string(),
},
)
.await
activate_license(&window, license_key).await
}
#[command]
pub async fn deactivate<R: Runtime>(window: WebviewWindow<R>) -> Result<()> {
info!("Deactivating activation");
deactivate_license(
&window,
DeactivateLicenseRequestPayload {
app_platform: get_os().to_string(),
app_version: window.app_handle().package_info().version.to_string(),
},
)
.await
}
fn get_os() -> &'static str {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
"unknown"
}
deactivate_license(&window).await
}

View File

@@ -16,3 +16,15 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.invoke_handler(generate_handler![check, activate, deactivate])
.build()
}
pub(crate) fn get_os() -> &'static str {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
"unknown"
}
}

View File

@@ -5,7 +5,7 @@ use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use std::ops::Add;
use std::time::Duration;
use tauri::{is_dev, AppHandle, Emitter, Manager, Runtime, WebviewWindow};
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev};
use ts_rs::TS;
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
@@ -63,10 +63,15 @@ pub struct APIErrorResponsePayload {
pub async fn activate_license<R: Runtime>(
window: &WebviewWindow<R>,
p: ActivateLicenseRequestPayload,
license_key: &str,
) -> Result<()> {
let client = reqwest::Client::new();
let response = client.post(build_url("/licenses/activate")).json(&p).send().await?;
let payload = ActivateLicenseRequestPayload {
license_key: license_key.to_string(),
app_platform: crate::get_os().to_string(),
app_version: window.app_handle().package_info().version.to_string(),
};
let response = client.post(build_url("/licenses/activate")).json(&payload).send().await?;
if response.status().is_client_error() {
let body: APIErrorResponsePayload = response.json().await?;
@@ -95,16 +100,17 @@ pub async fn activate_license<R: Runtime>(
Ok(())
}
pub async fn deactivate_license<R: Runtime>(
window: &WebviewWindow<R>,
p: DeactivateLicenseRequestPayload,
) -> Result<()> {
pub async fn deactivate_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<()> {
let app_handle = window.app_handle();
let activation_id = get_activation_id(app_handle).await;
let client = reqwest::Client::new();
let path = format!("/licenses/activations/{}/deactivate", activation_id);
let response = client.post(build_url(&path)).json(&p).send().await?;
let payload = DeactivateLicenseRequestPayload {
app_platform: crate::get_os().to_string(),
app_version: window.app_handle().package_info().version.to_string(),
};
let response = client.post(build_url(&path)).json(&payload).send().await?;
if response.status().is_client_error() {
let body: APIErrorResponsePayload = response.json().await?;
@@ -141,10 +147,11 @@ pub enum LicenseCheckStatus {
Trialing { end: NaiveDateTime },
}
pub async fn check_license<R: Runtime>(
window: &WebviewWindow<R>,
payload: CheckActivationRequestPayload,
) -> Result<LicenseCheckStatus> {
pub async fn check_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<LicenseCheckStatus> {
let payload = CheckActivationRequestPayload {
app_platform: crate::get_os().to_string(),
app_version: window.package_info().version.to_string(),
};
let activation_id = get_activation_id(window.app_handle()).await;
let settings = window.db().get_settings();
let trial_end = settings.created_at.add(Duration::from_secs(TRIAL_SECONDS));
@@ -197,9 +204,5 @@ fn build_url(path: &str) -> String {
}
pub async fn get_activation_id<R: Runtime>(app_handle: &AppHandle<R>) -> String {
app_handle.db().get_key_value_string(
KV_ACTIVATION_ID_KEY,
KV_NAMESPACE,
"",
)
app_handle.db().get_key_value_string(KV_ACTIVATION_ID_KEY, KV_NAMESPACE, "")
}

View File

@@ -18,7 +18,7 @@ export type Environment = { model: "environment", id: string, workspaceId: strin
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 Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, };
@@ -28,9 +28,7 @@ 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 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 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<HttpRequestHeader>, 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>, };
@@ -50,6 +48,10 @@ export type ModelChangeEvent = { "type": "upsert" } | { "type": "delete" };
export type ModelPayload = { model: AnyModel, updateSource: UpdateSource, change: ModelChangeEvent, };
export type ParentAuthentication = { authentication: Record<string, any>, authenticationType: string | null, };
export type ParentHeaders = { headers: Array<HttpRequestHeader>, };
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
@@ -58,7 +60,7 @@ export type ProxySetting = { "type": "enabled", disabled: boolean, http: string,
export type ProxySettingAuth = { user: string, password: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
@@ -76,6 +78,6 @@ export type WebsocketMessageType = "text" | "binary";
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };

View File

@@ -1,9 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models";
import type { Folder } from "./gen_models";
import type { GrpcRequest } from "./gen_models";
import type { HttpRequest } from "./gen_models";
import type { WebsocketRequest } from "./gen_models";
import type { Workspace } from "./gen_models";
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { WebsocketRequest } from "./gen_models.js";
import type { Workspace } from "./gen_models.js";
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };

View File

@@ -105,6 +105,7 @@ pub struct Settings {
pub appearance: String,
pub editor_font_size: i32,
pub editor_soft_wrap: bool,
pub hide_window_controls: bool,
pub interface_font_size: i32,
pub interface_scale: f32,
pub open_workspace_new_window: Option<bool>,
@@ -154,6 +155,7 @@ impl UpsertModelInfo for Settings {
(EditorSoftWrap, self.editor_soft_wrap.into()),
(InterfaceFontSize, self.interface_font_size.into()),
(InterfaceScale, self.interface_scale.into()),
(HideWindowControls, self.hide_window_controls.into()),
(OpenWorkspaceNewWindow, self.open_workspace_new_window.into()),
(ThemeDark, self.theme_dark.as_str().into()),
(ThemeLight, self.theme_light.as_str().into()),
@@ -171,6 +173,7 @@ impl UpsertModelInfo for Settings {
SettingsIden::EditorSoftWrap,
SettingsIden::InterfaceFontSize,
SettingsIden::InterfaceScale,
SettingsIden::HideWindowControls,
SettingsIden::OpenWorkspaceNewWindow,
SettingsIden::Proxy,
SettingsIden::ThemeDark,
@@ -200,6 +203,7 @@ impl UpsertModelInfo for Settings {
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
theme_dark: row.get("theme_dark")?,
theme_light: row.get("theme_light")?,
hide_window_controls: row.get("hide_window_controls")?,
update_channel: row.get("update_channel")?,
})
}
@@ -215,8 +219,13 @@ pub struct Workspace {
pub id: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub name: String,
#[ts(type = "Record<string, any>")]
pub authentication: BTreeMap<String, Value>,
pub authentication_type: Option<String>,
pub description: String,
pub headers: Vec<HttpRequestHeader>,
pub name: String,
pub encryption_key_challenge: Option<String>,
// Settings
@@ -257,6 +266,9 @@ impl UpsertModelInfo for Workspace {
(CreatedAt, upsert_date(source, self.created_at)),
(UpdatedAt, upsert_date(source, self.updated_at)),
(Name, self.name.trim().into()),
(Authentication, serde_json::to_string(&self.authentication)?.into()),
(AuthenticationType, self.authentication_type.into()),
(Headers, serde_json::to_string(&self.headers)?.into()),
(Description, self.description.into()),
(EncryptionKeyChallenge, self.encryption_key_challenge.into()),
(SettingFollowRedirects, self.setting_follow_redirects.into()),
@@ -269,6 +281,9 @@ impl UpsertModelInfo for Workspace {
vec![
WorkspaceIden::UpdatedAt,
WorkspaceIden::Name,
WorkspaceIden::Authentication,
WorkspaceIden::AuthenticationType,
WorkspaceIden::Headers,
WorkspaceIden::Description,
WorkspaceIden::EncryptionKeyChallenge,
WorkspaceIden::SettingRequestTimeout,
@@ -282,6 +297,8 @@ impl UpsertModelInfo for Workspace {
where
Self: Sized,
{
let headers: String = row.get("headers")?;
let authentication: String = row.get("authentication")?;
Ok(Self {
id: row.get("id")?,
model: row.get("model")?,
@@ -290,6 +307,9 @@ impl UpsertModelInfo for Workspace {
name: row.get("name")?,
description: row.get("description")?,
encryption_key_challenge: row.get("encryption_key_challenge")?,
headers: serde_json::from_str(&headers).unwrap_or_default(),
authentication: serde_json::from_str(&authentication).unwrap_or_default(),
authentication_type: row.get("authentication_type")?,
setting_follow_redirects: row.get("setting_follow_redirects")?,
setting_request_timeout: row.get("setting_request_timeout")?,
setting_validate_certificates: row.get("setting_validate_certificates")?,
@@ -381,7 +401,7 @@ impl UpsertModelInfo for WorkspaceMeta {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "gen_models.ts")]
enum CookieDomain {
pub enum CookieDomain {
HostOnly(String),
Suffix(String),
NotPresent,
@@ -390,7 +410,7 @@ enum CookieDomain {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "gen_models.ts")]
enum CookieExpires {
pub enum CookieExpires {
AtUtc(String),
SessionEnd,
}
@@ -398,10 +418,10 @@ enum CookieExpires {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "gen_models.ts")]
pub struct Cookie {
raw_cookie: String,
domain: CookieDomain,
expires: CookieExpires,
path: (String, bool),
pub raw_cookie: String,
pub domain: CookieDomain,
pub expires: CookieExpires,
pub path: (String, bool),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
@@ -577,6 +597,22 @@ pub struct EnvironmentVariable {
pub id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
pub struct ParentAuthentication {
#[ts(type = "Record<string, any>")]
pub authentication: BTreeMap<String, Value>,
pub authentication_type: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
pub struct ParentHeaders {
pub headers: Vec<HttpRequestHeader>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
@@ -590,8 +626,12 @@ pub struct Folder {
pub workspace_id: String,
pub folder_id: Option<String>,
pub name: String,
#[ts(type = "Record<string, any>")]
pub authentication: BTreeMap<String, Value>,
pub authentication_type: Option<String>,
pub description: String,
pub headers: Vec<HttpRequestHeader>,
pub name: String,
pub sort_priority: f32,
}
@@ -626,8 +666,11 @@ impl UpsertModelInfo for Folder {
(UpdatedAt, upsert_date(source, self.updated_at)),
(WorkspaceId, self.workspace_id.into()),
(FolderId, self.folder_id.into()),
(Name, self.name.trim().into()),
(Authentication, serde_json::to_string(&self.authentication)?.into()),
(AuthenticationType, self.authentication_type.into()),
(Headers, serde_json::to_string(&self.headers)?.into()),
(Description, self.description.into()),
(Name, self.name.trim().into()),
(SortPriority, self.sort_priority.into()),
])
}
@@ -636,6 +679,9 @@ impl UpsertModelInfo for Folder {
vec![
FolderIden::UpdatedAt,
FolderIden::Name,
FolderIden::Authentication,
FolderIden::AuthenticationType,
FolderIden::Headers,
FolderIden::Description,
FolderIden::FolderId,
FolderIden::SortPriority,
@@ -646,6 +692,8 @@ impl UpsertModelInfo for Folder {
where
Self: Sized,
{
let headers: String = row.get("headers")?;
let authentication: String = row.get("authentication")?;
Ok(Self {
id: row.get("id")?,
model: row.get("model")?,
@@ -656,6 +704,9 @@ impl UpsertModelInfo for Folder {
folder_id: row.get("folder_id")?,
name: row.get("name")?,
description: row.get("description")?,
headers: serde_json::from_str(&headers).unwrap_or_default(),
authentication_type: row.get("authentication_type")?,
authentication: serde_json::from_str(&authentication).unwrap_or_default(),
})
}
}
@@ -778,28 +829,28 @@ impl UpsertModelInfo for HttpRequest {
]
}
fn from_row(r: &Row) -> rusqlite::Result<Self> {
let url_parameters: String = r.get("url_parameters")?;
let body: String = r.get("body")?;
let authentication: String = r.get("authentication")?;
let headers: String = r.get("headers")?;
fn from_row(row: &Row) -> rusqlite::Result<Self> {
let url_parameters: String = row.get("url_parameters")?;
let body: String = row.get("body")?;
let authentication: String = row.get("authentication")?;
let headers: String = row.get("headers")?;
Ok(Self {
id: r.get("id")?,
model: r.get("model")?,
workspace_id: r.get("workspace_id")?,
created_at: r.get("created_at")?,
updated_at: r.get("updated_at")?,
id: row.get("id")?,
model: row.get("model")?,
workspace_id: row.get("workspace_id")?,
created_at: row.get("created_at")?,
updated_at: row.get("updated_at")?,
authentication: serde_json::from_str(authentication.as_str()).unwrap_or_default(),
authentication_type: r.get("authentication_type")?,
authentication_type: row.get("authentication_type")?,
body: serde_json::from_str(body.as_str()).unwrap_or_default(),
body_type: r.get("body_type")?,
description: r.get("description")?,
folder_id: r.get("folder_id")?,
body_type: row.get("body_type")?,
description: row.get("description")?,
folder_id: row.get("folder_id")?,
headers: serde_json::from_str(headers.as_str()).unwrap_or_default(),
method: r.get("method")?,
name: r.get("name")?,
sort_priority: r.get("sort_priority")?,
url: r.get("url")?,
method: row.get("method")?,
name: row.get("name")?,
sort_priority: row.get("sort_priority")?,
url: row.get("url")?,
url_parameters: serde_json::from_str(url_parameters.as_str()).unwrap_or_default(),
})
}
@@ -988,7 +1039,7 @@ impl UpsertModelInfo for WebsocketRequest {
(WorkspaceId, self.workspace_id.into()),
(FolderId, self.folder_id.as_ref().map(|s| s.as_str()).into()),
(Authentication, serde_json::to_string(&self.authentication)?.into()),
(AuthenticationType, self.authentication_type.as_ref().map(|s| s.as_str()).into()),
(AuthenticationType, self.authentication_type.into()),
(Description, self.description.into()),
(Headers, serde_json::to_string(&self.headers)?.into()),
(Message, self.message.into()),
@@ -1291,19 +1342,6 @@ impl UpsertModelInfo for HttpResponse {
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
pub struct GrpcMetadataEntry {
#[serde(default = "default_true")]
#[ts(optional, as = "Option<bool>")]
pub enabled: bool,
pub name: String,
pub value: String,
#[ts(optional, as = "Option<String>")]
pub id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
@@ -1322,7 +1360,7 @@ pub struct GrpcRequest {
pub authentication: BTreeMap<String, Value>,
pub description: String,
pub message: String,
pub metadata: Vec<GrpcMetadataEntry>,
pub metadata: Vec<HttpRequestHeader>,
pub method: Option<String>,
pub name: String,
pub service: Option<String>,

View File

@@ -2,10 +2,12 @@ use crate::connection_or_tx::ConnectionOrTx;
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{
Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestIden,
WebsocketRequest, WebsocketRequestIden,
Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestHeader,
HttpRequestIden, WebsocketRequest, WebsocketRequestIden,
};
use crate::util::UpdateSource;
use serde_json::Value;
use std::collections::BTreeMap;
impl<'a> DbContext<'a> {
pub fn get_folder(&self, id: &str) -> Result<Folder> {
@@ -110,4 +112,40 @@ impl<'a> DbContext<'a> {
Ok(new_folder)
}
pub fn resolve_auth_for_folder(
&self,
folder: Folder,
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
if let Some(at) = folder.authentication_type {
return Ok((Some(at), folder.authentication));
}
if let Some(folder_id) = folder.folder_id {
let folder = self.get_folder(&folder_id)?;
return self.resolve_auth_for_folder(folder);
}
let workspace = self.get_workspace(&folder.workspace_id)?;
Ok(self.resolve_auth_for_workspace(&workspace))
}
pub fn resolve_headers_for_folder(&self, folder: &Folder) -> Result<Vec<HttpRequestHeader>> {
let mut headers = Vec::new();
if let Some(folder_id) = folder.folder_id.clone() {
let parent_folder = self.get_folder(&folder_id)?;
let mut folder_headers = self.resolve_headers_for_folder(&parent_folder)?;
// NOTE: Add parent headers first, so overrides are logical
headers.append(&mut folder_headers);
} else {
let workspace = self.get_workspace(&folder.workspace_id)?;
let mut workspace_headers = self.resolve_headers_for_workspace(&workspace);
headers.append(&mut workspace_headers);
}
headers.append(&mut folder.headers.clone());
Ok(headers)
}
}

View File

@@ -1,7 +1,9 @@
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{GrpcRequest, GrpcRequestIden};
use crate::models::{GrpcRequest, GrpcRequestIden, HttpRequestHeader};
use crate::util::UpdateSource;
use serde_json::Value;
use std::collections::BTreeMap;
impl<'a> DbContext<'a> {
pub fn get_grpc_request(&self, id: &str) -> Result<GrpcRequest> {
@@ -48,4 +50,43 @@ impl<'a> DbContext<'a> {
) -> Result<GrpcRequest> {
self.upsert(grpc_request, source)
}
pub fn resolve_auth_for_grpc_request(
&self,
grpc_request: &GrpcRequest,
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
if let Some(at) = grpc_request.authentication_type.clone() {
return Ok((Some(at), grpc_request.authentication.clone()));
}
if let Some(folder_id) = grpc_request.folder_id.clone() {
let folder = self.get_folder(&folder_id)?;
return self.resolve_auth_for_folder(folder);
}
let workspace = self.get_workspace(&grpc_request.workspace_id)?;
Ok(self.resolve_auth_for_workspace(&workspace))
}
pub fn resolve_metadata_for_grpc_request(
&self,
grpc_request: &GrpcRequest,
) -> Result<Vec<HttpRequestHeader>> {
// Resolved headers should be from furthest to closest ancestor, to override logically.
let mut metadata = Vec::new();
if let Some(folder_id) = grpc_request.folder_id.clone() {
let parent_folder = self.get_folder(&folder_id)?;
let mut folder_headers = self.resolve_headers_for_folder(&parent_folder)?;
metadata.append(&mut folder_headers);
} else {
let workspace = self.get_workspace(&grpc_request.workspace_id)?;
let mut workspace_metadata = self.resolve_headers_for_workspace(&workspace);
metadata.append(&mut workspace_metadata);
}
metadata.append(&mut grpc_request.metadata.clone());
Ok(metadata)
}
}

View File

@@ -1,7 +1,9 @@
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{HttpRequest, HttpRequestIden};
use crate::models::{HttpRequest, HttpRequestHeader, HttpRequestIden};
use crate::util::UpdateSource;
use serde_json::Value;
use std::collections::BTreeMap;
impl<'a> DbContext<'a> {
pub fn get_http_request(&self, id: &str) -> Result<HttpRequest> {
@@ -48,4 +50,43 @@ impl<'a> DbContext<'a> {
) -> Result<HttpRequest> {
self.upsert(http_request, source)
}
pub fn resolve_auth_for_http_request(
&self,
http_request: &HttpRequest,
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
if let Some(at) = http_request.authentication_type.clone() {
return Ok((Some(at), http_request.authentication.clone()));
}
if let Some(folder_id) = http_request.folder_id.clone() {
let folder = self.get_folder(&folder_id)?;
return self.resolve_auth_for_folder(folder);
}
let workspace = self.get_workspace(&http_request.workspace_id)?;
Ok(self.resolve_auth_for_workspace(&workspace))
}
pub fn resolve_headers_for_http_request(
&self,
http_request: &HttpRequest,
) -> Result<Vec<HttpRequestHeader>> {
// Resolved headers should be from furthest to closest ancestor, to override logically.
let mut headers = Vec::new();
if let Some(folder_id) = http_request.folder_id.clone() {
let parent_folder = self.get_folder(&folder_id)?;
let mut folder_headers = self.resolve_headers_for_folder(&parent_folder)?;
headers.append(&mut folder_headers);
} else {
let workspace = self.get_workspace(&http_request.workspace_id)?;
let mut workspace_headers = self.resolve_headers_for_workspace(&workspace);
headers.append(&mut workspace_headers);
}
headers.append(&mut http_request.headers.clone());
Ok(headers)
}
}

View File

@@ -23,6 +23,7 @@ impl<'a> DbContext<'a> {
editor_soft_wrap: true,
interface_font_size: 15,
interface_scale: 1.0,
hide_window_controls: false,
open_workspace_new_window: None,
proxy: None,
theme_dark: "yaak-dark".to_string(),

View File

@@ -1,7 +1,9 @@
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{WebsocketRequest, WebsocketRequestIden};
use crate::models::{HttpRequestHeader, WebsocketRequest, WebsocketRequestIden};
use crate::util::UpdateSource;
use serde_json::Value;
use std::collections::BTreeMap;
impl<'a> DbContext<'a> {
pub fn get_websocket_request(&self, id: &str) -> Result<WebsocketRequest> {
@@ -48,4 +50,47 @@ impl<'a> DbContext<'a> {
) -> Result<WebsocketRequest> {
self.upsert(websocket_request, source)
}
pub fn resolve_auth_for_websocket_request(
&self,
websocket_request: &WebsocketRequest,
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
if let Some(at) = websocket_request.authentication_type.clone() {
return Ok((Some(at), websocket_request.authentication.clone()));
}
if let Some(folder_id) = websocket_request.folder_id.clone() {
let folder = self.get_folder(&folder_id)?;
return self.resolve_auth_for_folder(folder);
}
let workspace = self.get_workspace(&websocket_request.workspace_id)?;
Ok(self.resolve_auth_for_workspace(&workspace))
}
pub fn resolve_headers_for_websocket_request(
&self,
websocket_request: &WebsocketRequest,
) -> Result<Vec<HttpRequestHeader>> {
let workspace = self.get_workspace(&websocket_request.workspace_id)?;
// Resolved headers should be from furthest to closest ancestor, to override logically.
let mut headers = Vec::new();
headers.append(&mut workspace.headers.clone());
if let Some(folder_id) = websocket_request.folder_id.clone() {
let parent_folder = self.get_folder(&folder_id)?;
let mut folder_headers = self.resolve_headers_for_folder(&parent_folder)?;
headers.append(&mut folder_headers);
} else {
let workspace = self.get_workspace(&websocket_request.workspace_id)?;
let mut workspace_headers = self.resolve_headers_for_workspace(&workspace);
headers.append(&mut workspace_headers);
}
headers.append(&mut websocket_request.headers.clone());
Ok(headers)
}
}

View File

@@ -1,10 +1,12 @@
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{
EnvironmentIden, FolderIden, GrpcRequestIden, HttpRequestIden, WebsocketRequestIden, Workspace,
WorkspaceIden,
EnvironmentIden, FolderIden, GrpcRequestIden, HttpRequestHeader, HttpRequestIden,
WebsocketRequestIden, Workspace, WorkspaceIden,
};
use crate::util::UpdateSource;
use serde_json::Value;
use std::collections::BTreeMap;
impl<'a> DbContext<'a> {
pub fn get_workspace(&self, id: &str) -> Result<Workspace> {
@@ -65,4 +67,15 @@ impl<'a> DbContext<'a> {
pub fn upsert_workspace(&self, w: &Workspace, source: &UpdateSource) -> Result<Workspace> {
self.upsert(w, source)
}
pub fn resolve_auth_for_workspace(
&self,
workspace: &Workspace,
) -> (Option<String>, BTreeMap<String, Value>) {
(workspace.authentication_type.clone(), workspace.authentication.clone())
}
pub fn resolve_headers_for_workspace(&self, workspace: &Workspace) -> Vec<HttpRequestHeader> {
workspace.headers.clone()
}
}

View File

@@ -104,7 +104,11 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputCheckbox = {
/**
@@ -131,7 +135,11 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputEditor = {
/**
@@ -170,7 +178,11 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputFile = {
/**
@@ -205,7 +217,11 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputHttpRequest = {
/**
@@ -232,7 +248,11 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputMarkdown = { content: string, hidden?: boolean, };
@@ -265,7 +285,11 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputSelectOption = { label: string, value: string, };
@@ -306,10 +330,18 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type GenericCompletionOption = { label: string, detail?: string, info?: string, type?: CompletionOptionType, boost?: number, };
export type GetCookieValueRequest = { name: string, };
export type GetCookieValueResponse = { value: string | null, };
export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
@@ -346,10 +378,14 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type 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": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;
export type ListCookieNamesRequest = {};
export type ListCookieNamesResponse = { names: Array<string>, };
export type OpenWindowRequest = { url: string,
/**
* Label for the window. If not provided, a random one will be generated.

View File

@@ -4,11 +4,9 @@ export type Environment = { model: "environment", id: string, workspaceId: strin
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 Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
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 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<HttpRequestHeader>, 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>, };
@@ -24,4 +22,4 @@ export type HttpUrlParameter = { enabled?: boolean, name: string, value: string,
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };

View File

@@ -84,6 +84,11 @@ pub enum InternalEventPayload {
SendHttpRequestRequest(SendHttpRequestRequest),
SendHttpRequestResponse(SendHttpRequestResponse),
ListCookieNamesRequest(ListCookieNamesRequest),
ListCookieNamesResponse(ListCookieNamesResponse),
GetCookieValueRequest(GetCookieValueRequest),
GetCookieValueResponse(GetCookieValueResponse),
// Request Actions
GetHttpRequestActionsRequest(EmptyPayload),
GetHttpRequestActionsResponse(GetHttpRequestActionsResponse),
@@ -231,6 +236,32 @@ pub struct SendHttpRequestResponse {
pub http_response: HttpResponse,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)]
#[ts(export, type = "{}", export_to = "gen_events.ts")]
pub struct ListCookieNamesRequest {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct ListCookieNamesResponse {
pub names: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetCookieValueRequest {
pub name: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetCookieValueResponse {
pub value: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
@@ -563,6 +594,10 @@ pub struct FormInputBase {
#[ts(optional)]
pub disabled: Option<bool>,
/// Longer description of the input, likely shown in a tooltip
#[ts(optional)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
@@ -805,7 +840,7 @@ pub struct CallTemplateFunctionResponse {
#[ts(export, export_to = "gen_events.ts")]
pub struct CallTemplateFunctionArgs {
pub purpose: RenderPurpose,
pub values: HashMap<String, String>,
pub values: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]

View File

@@ -11,6 +11,7 @@ use crate::events::{
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, ImportRequest, ImportResponse,
InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose,
};
use crate::native_template_functions::template_function_secure;
use crate::nodejs::start_nodejs_plugin_runtime;
use crate::plugin_handle::PluginHandle;
use crate::server_ws::PluginRuntimeServerWebsocket;
@@ -24,13 +25,12 @@ use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager, Runtime, WebviewWindow};
use tokio::fs::read_dir;
use tokio::net::TcpListener;
use tokio::sync::{mpsc, Mutex};
use tokio::time::{timeout, Instant};
use tokio::sync::{Mutex, mpsc};
use tokio::time::{Instant, timeout};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::generate_id;
use yaak_templates::error::Error::RenderError;
use yaak_templates::error::Result as TemplateResult;
use crate::native_template_functions::template_function_secure;
#[derive(Clone)]
pub struct PluginManager {
@@ -160,8 +160,7 @@ impl PluginManager {
})
.collect();
let plugins =
app_handle.db().list_plugins().unwrap_or_default();
let plugins = app_handle.db().list_plugins().unwrap_or_default();
let installed_plugin_dirs: Vec<PluginCandidate> = plugins
.iter()
.map(|p| PluginCandidate {
@@ -537,7 +536,7 @@ impl PluginManager {
auth_name: &str,
action_index: i32,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
model_id: &str,
) -> Result<()> {
let results = self.get_http_authentication_summaries(window).await?;
let plugin = results
@@ -545,7 +544,7 @@ impl PluginManager {
.find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None })
.ok_or(PluginNotFoundErr(auth_name.into()))?;
let context_id = format!("{:x}", md5::compute(request_id.to_string()));
let context_id = format!("{:x}", md5::compute(model_id.to_string()));
self.send_to_plugin_and_wait(
&PluginWindowContext::new(window),
&plugin,
@@ -606,15 +605,12 @@ impl PluginManager {
&self,
window_context: &PluginWindowContext,
fn_name: &str,
args: HashMap<String, String>,
values: HashMap<String, serde_json::Value>,
purpose: RenderPurpose,
) -> TemplateResult<String> {
let req = CallTemplateFunctionRequest {
name: fn_name.to_string(),
args: CallTemplateFunctionArgs {
purpose,
values: args,
},
args: CallTemplateFunctionArgs { purpose, values },
};
let events = self

View File

@@ -34,7 +34,7 @@ pub(crate) fn template_function_secure() -> TemplateFunction {
pub fn template_function_secure_run<R: Runtime>(
app_handle: &AppHandle<R>,
args: HashMap<String, String>,
args: HashMap<String, serde_json::Value>,
window_context: &PluginWindowContext,
) -> Result<String> {
match window_context.clone() {
@@ -43,9 +43,10 @@ pub fn template_function_secure_run<R: Runtime>(
..
} => {
let value = args.get("value").map(|v| v.to_owned()).unwrap_or_default();
if value.is_empty() {
return Ok("".to_string());
}
let value = match value {
serde_json::Value::String(s) => s,
_ => return Ok("".to_string()),
};
let value = match value.strip_prefix("YENC_") {
None => {
@@ -118,7 +119,7 @@ pub fn decrypt_secure_template_function<R: Runtime>(
for a in args {
match a.clone().value {
Val::Str { text } => {
args_map.insert(a.name.to_string(), text);
args_map.insert(a.name.to_string(), serde_json::Value::String(text));
}
_ => continue,
}

View File

@@ -30,7 +30,7 @@ impl<R: Runtime> PluginTemplateCallback<R> {
}
impl<R: Runtime> TemplateCallback for PluginTemplateCallback<R> {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
async fn run(&self, fn_name: &str, args: HashMap<String, serde_json::Value>) -> Result<String> {
// The beta named the function `Response` but was changed in stable.
// Keep this here for a while because there's no easy way to migrate
let fn_name = if fn_name == "Response" { "response" } else { fn_name };

View File

@@ -4,11 +4,9 @@ export type Environment = { model: "environment", id: string, workspaceId: strin
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 Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
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 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<HttpRequestHeader>, 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>, };
@@ -22,4 +20,4 @@ export type SyncState = { model: "sync_state", id: string, workspaceId: string,
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };

View File

@@ -11,7 +11,7 @@ pub trait TemplateCallback {
fn run(
&self,
fn_name: &str,
args: HashMap<String, String>,
args: HashMap<String, serde_json::Value>,
) -> impl Future<Output = Result<String>> + Send;
fn transform_arg(&self, fn_name: &str, arg_name: &str, arg_value: &str) -> Result<String>;
@@ -107,9 +107,15 @@ async fn render_value<T: TemplateCallback>(
None => return Err(VariableNotFound(name)),
},
Val::Fn { name, args } => {
let mut resolved_args: HashMap<String, String> = HashMap::new();
let mut resolved_args: HashMap<String, serde_json::Value> = HashMap::new();
for a in args {
let v = Box::pin(render_value(a.value, vars, cb, depth)).await?;
let v = match a.value.clone() {
Val::Bool { value } => serde_json::Value::Bool(value),
Val::Null => serde_json::Value::Null,
_ => serde_json::Value::String(
Box::pin(render_value(a.value, vars, cb, depth)).await?,
),
};
resolved_args.insert(a.name, v);
}
let result = cb.run(name.as_str(), resolved_args.clone()).await?;
@@ -133,7 +139,11 @@ mod parse_and_render_tests {
struct EmptyCB {}
impl TemplateCallback for EmptyCB {
async fn run(&self, _fn_name: &str, _args: HashMap<String, String>) -> Result<String> {
async fn run(
&self,
_fn_name: &str,
_args: HashMap<String, serde_json::Value>,
) -> Result<String> {
todo!()
}
@@ -236,7 +246,11 @@ mod parse_and_render_tests {
struct CB {}
impl TemplateCallback for CB {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
async fn run(
&self,
fn_name: &str,
args: HashMap<String, serde_json::Value>,
) -> Result<String> {
Ok(format!("{fn_name}: {}, {:?} {:?}", args.len(), args.get("a"), args.get("b")))
}
@@ -260,7 +274,11 @@ mod parse_and_render_tests {
let result = r#"BAR"#;
struct CB {}
impl TemplateCallback for CB {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
async fn run(
&self,
fn_name: &str,
args: HashMap<String, serde_json::Value>,
) -> Result<String> {
Ok(match fn_name {
"secret" => "abc".to_string(),
"upper" => args["foo"].to_string().to_uppercase(),
@@ -290,7 +308,7 @@ mod parse_and_render_tests {
let result = r#"FOO 'BAR' BAZ"#;
struct CB {}
impl TemplateCallback for CB {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
async fn run(&self, fn_name: &str, args: HashMap<String, serde_json::Value>) -> Result<String> {
Ok(match fn_name {
"upper" => args["foo"].to_string().to_uppercase(),
_ => "".to_string(),
@@ -319,7 +337,7 @@ mod parse_and_render_tests {
let result = r#"BAR"#;
struct CB {}
impl TemplateCallback for CB {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
async fn run(&self, fn_name: &str, args: HashMap<String, serde_json::Value>) -> Result<String> {
Ok(match fn_name {
"secret" => "abc".to_string(),
"upper" => args["foo"].to_string().to_uppercase(),
@@ -349,7 +367,7 @@ mod parse_and_render_tests {
let result = r#"bar"#;
struct CB {}
impl TemplateCallback for CB {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
async fn run(&self, fn_name: &str, args: HashMap<String, serde_json::Value>) -> Result<String> {
Ok(match fn_name {
"no_op" => args["inner"].to_string(),
_ => "".to_string(),
@@ -377,7 +395,7 @@ mod parse_and_render_tests {
let result = r#"ABC"#;
struct CB {}
impl TemplateCallback for CB {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
async fn run(&self, fn_name: &str, args: HashMap<String, serde_json::Value>) -> Result<String> {
Ok(match fn_name {
"secret" => "abc".to_string(),
"upper" => args["foo"].to_string().to_uppercase(),
@@ -406,7 +424,7 @@ mod parse_and_render_tests {
struct CB {}
impl TemplateCallback for CB {
async fn run(&self, _fn_name: &str, _args: HashMap<String, String>) -> Result<String> {
async fn run(&self, _fn_name: &str, _args: HashMap<String, serde_json::Value>) -> Result<String> {
Err(RenderError("Failed to do it!".to_string()))
}
@@ -438,7 +456,7 @@ mod render_json_value_raw_tests {
struct EmptyCB {}
impl TemplateCallback for EmptyCB {
async fn run(&self, _fn_name: &str, _args: HashMap<String, String>) -> Result<String> {
async fn run(&self, _fn_name: &str, _args: HashMap<String, serde_json::Value>) -> Result<String> {
todo!()
}

View File

@@ -1,6 +1,7 @@
use crate::error::Result;
use crate::manager::WebsocketManager;
use crate::render::render_request;
use crate::render::render_websocket_request;
use crate::resolve::resolve_websocket_request;
use log::{info, warn};
use std::str::FromStr;
use tauri::http::{HeaderMap, HeaderName};
@@ -119,8 +120,9 @@ pub(crate) async fn send<R: Runtime>(
};
let base_environment =
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
let request = render_request(
&unrendered_request,
let resolved_request = resolve_websocket_request(&window, &unrendered_request)?;
let request = render_websocket_request(
&resolved_request,
&base_environment,
environment.as_ref(),
&PluginTemplateCallback::new(
@@ -194,8 +196,9 @@ pub(crate) async fn connect<R: Runtime>(
};
let base_environment =
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
let request = render_request(
&unrendered_request,
let resolved_request = resolve_websocket_request(&window, &unrendered_request)?;
let request = render_websocket_request(
&resolved_request,
&base_environment,
environment.as_ref(),
&PluginTemplateCallback::new(
@@ -207,30 +210,55 @@ pub(crate) async fn connect<R: Runtime>(
.await?;
let mut headers = HeaderMap::new();
if let Some(auth_name) = request.authentication_type.clone() {
let auth = request.authentication.clone();
let plugin_req = CallHttpAuthenticationRequest {
context_id: format!("{:x}", md5::compute(request_id.to_string())),
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
method: "POST".to_string(),
url: request.url.clone(),
headers: request
.headers
.clone()
.into_iter()
.map(|h| HttpHeader {
name: h.name,
value: h.value,
})
.collect(),
};
let plugin_result =
plugin_manager.call_http_authentication(&window, &auth_name, plugin_req).await?;
for header in plugin_result.set_headers {
headers.insert(
HeaderName::from_str(&header.name).unwrap(),
HeaderValue::from_str(&header.value).unwrap(),
);
for h in request.headers.clone() {
if h.name.is_empty() && h.value.is_empty() {
continue;
}
if !h.enabled {
continue;
}
headers.insert(
HeaderName::from_str(&h.name).unwrap(),
HeaderValue::from_str(&h.value).unwrap(),
);
}
match request.authentication_type {
None => {
// No authentication found. Not even inherited
}
Some(authentication_type) if authentication_type == "none" => {
// Explicitly no authentication
}
Some(authentication_type) => {
let auth = request.authentication.clone();
let plugin_req = CallHttpAuthenticationRequest {
context_id: format!("{:x}", md5::compute(request_id.to_string())),
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
method: "POST".to_string(),
url: request.url.clone(),
headers: request
.headers
.clone()
.into_iter()
.map(|h| HttpHeader {
name: h.name,
value: h.value,
})
.collect(),
};
let plugin_result = plugin_manager
.call_http_authentication(&window, &authentication_type, plugin_req)
.await?;
for header in plugin_result.set_headers {
headers.insert(
HeaderName::from_str(&header.name).unwrap(),
HeaderValue::from_str(&header.value).unwrap(),
);
}
}
}

View File

@@ -3,6 +3,7 @@ mod connect;
pub mod error;
mod manager;
mod render;
mod resolve;
use crate::commands::{
close, connect, delete_connection, delete_connections, delete_request, duplicate_request,

View File

@@ -4,7 +4,7 @@ use yaak_models::models::{Environment, HttpRequestHeader, WebsocketRequest};
use yaak_models::render::make_vars_hashmap;
use yaak_templates::{parse_and_render, render_json_value_raw, TemplateCallback};
pub async fn render_request<T: TemplateCallback>(
pub async fn render_websocket_request<T: TemplateCallback>(
r: &WebsocketRequest,
base_environment: &Environment,
environment: Option<&Environment>,

View File

@@ -0,0 +1,21 @@
use crate::error::Result;
use tauri::{Runtime, WebviewWindow};
use yaak_models::models::WebsocketRequest;
use yaak_models::query_manager::QueryManagerExt;
pub(crate) fn resolve_websocket_request<R: Runtime>(
window: &WebviewWindow<R>,
request: &WebsocketRequest,
) -> Result<WebsocketRequest> {
let mut new_request = request.clone();
let (authentication_type, authentication) =
window.db().resolve_auth_for_websocket_request(request)?;
new_request.authentication_type = authentication_type;
new_request.authentication = authentication;
let headers = window.db().resolve_headers_for_websocket_request(request)?;
new_request.headers = headers;
Ok(new_request)
}

View File

@@ -0,0 +1,51 @@
import { createWorkspaceModel, type Environment } from '@yaakapp-internal/models';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { createFastMutation } from '../hooks/useFastMutation';
import { jotaiStore } from '../lib/jotai';
import { showPrompt } from '../lib/prompt';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
export const createEnvironmentAndActivate = createFastMutation<
string | null,
unknown,
Environment | null
>({
mutationKey: ['create_environment'],
mutationFn: async (baseEnvironment) => {
if (baseEnvironment == null) {
throw new Error('No base environment passed');
}
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) {
throw new Error('Cannot create environment when no active workspace');
}
const name = await showPrompt({
id: 'new-environment',
title: 'New Environment',
description: 'Create multiple environments with different sets of variables',
label: 'Name',
placeholder: 'My Environment',
defaultValue: 'My Environment',
confirmText: 'Create',
});
if (name == null) return null;
return createWorkspaceModel({
model: 'environment',
name,
variables: [],
workspaceId,
base: false,
});
},
onSuccess: async (environmentId) => {
if (environmentId == null) {
return; // Was not created
}
console.log('NAVIGATING', jotaiStore.get(activeWorkspaceIdAtom), environmentId);
setWorkspaceSearchParams({ environment_id: environmentId });
},
});

View File

@@ -0,0 +1,14 @@
import type { FolderSettingsTab } from '../components/FolderSettingsDialog';
import { FolderSettingsDialog } from '../components/FolderSettingsDialog';
import { showDialog } from '../lib/dialog';
export function openFolderSettings(folderId: string, tab?: FolderSettingsTab) {
showDialog({
id: 'folder-settings',
title: 'Folder Settings',
size: 'lg',
className: 'h-[50rem]',
noPadding: true,
render: () => <FolderSettingsDialog folderId={folderId} tab={tab} />,
});
}

View File

@@ -1,20 +1,22 @@
import { WorkspaceSettingsDialog } from '../components/WorkspaceSettingsDialog';
import type {
WorkspaceSettingsTab} from '../components/WorkspaceSettingsDialog';
import {
WorkspaceSettingsDialog
} from '../components/WorkspaceSettingsDialog';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { createFastMutation } from '../hooks/useFastMutation';
import { showDialog } from '../lib/dialog';
import { jotaiStore } from '../lib/jotai';
export const openWorkspaceSettings = createFastMutation<void, string>({
mutationKey: ['open_workspace_settings'],
async mutationFn() {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
showDialog({
id: 'workspace-settings',
title: 'Workspace Settings',
size: 'md',
render({ hide }) {
return <WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} />;
},
});
},
});
export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
showDialog({
id: 'workspace-settings',
title: 'Workspace Settings',
size: 'lg',
className: 'h-[50rem]',
noPadding: true,
render({ hide }) {
return <WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />;
},
});
}

View File

@@ -5,6 +5,7 @@ import { useAtomValue } from 'jotai';
import type { KeyboardEvent, ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createFolder } from '../commands/commands';
import { createEnvironmentAndActivate } from '../commands/createEnvironment';
import { openSettings } from '../commands/openSettings';
import { switchWorkspace } from '../commands/switchWorkspace';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
@@ -12,7 +13,6 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { useAllRequests } from '../hooks/useAllRequests';
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDebouncedState } from '../hooks/useDebouncedState';
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
@@ -72,7 +72,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const activeCookieJar = useActiveCookieJar();
const [recentRequests] = useRecentRequests();
const [, setSidebarHidden] = useSidebarHidden();
const { mutate: createEnvironment } = useCreateEnvironment();
const { mutate: sendRequest } = useSendAnyHttpRequest();
const workspaceCommands = useMemo<CommandPaletteItem[]>(() => {
@@ -139,7 +138,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
{
key: 'environment.create',
label: 'Create Environment',
onSelect: () => createEnvironment(baseEnvironment),
onSelect: () => createEnvironmentAndActivate.mutate(baseEnvironment),
},
{
key: 'sidebar.toggle',
@@ -190,7 +189,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
activeEnvironment,
activeRequest,
baseEnvironment,
createEnvironment,
createWorkspace,
httpRequestActions,
sendRequest,

View File

@@ -237,6 +237,7 @@ function TextArg({
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
required={!arg.optional}
disabled={arg.disabled}
help={arg.description}
type={arg.password ? 'password' : 'text'}
label={arg.label ?? arg.name}
size={INPUT_SIZE}
@@ -278,6 +279,7 @@ function EditorArg({
htmlFor={id}
required={!arg.optional}
visuallyHidden={arg.hideLabel}
help={arg.description}
tags={arg.language ? [capitalize(arg.language)] : undefined}
>
{arg.label}
@@ -319,6 +321,7 @@ function SelectArg({
<Select
label={arg.label ?? arg.name}
name={arg.name}
help={arg.description}
onChange={onChange}
hideLabel={arg.hideLabel}
value={value}
@@ -341,6 +344,7 @@ function FileArg({
return (
<SelectFile
disabled={arg.disabled}
help={arg.description}
onChange={({ filePath }) => onChange(filePath)}
filePath={filePath === '__NULL__' ? null : filePath}
directory={!!arg.directory}
@@ -365,6 +369,7 @@ function HttpRequestArg({
label={arg.label ?? arg.name}
name={arg.name}
onChange={onChange}
help={arg.description}
value={value}
disabled={arg.disabled}
options={[
@@ -412,6 +417,7 @@ function CheckboxArg({
<Checkbox
onChange={onChange}
checked={value}
help={arg.description}
disabled={arg.disabled}
title={arg.label ?? arg.name}
hideLabel={arg.label == null}

View File

@@ -4,7 +4,7 @@ import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { createEnvironmentAndActivate } from '../commands/createEnvironment';
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
import { useIsEncryptionEnabled } from '../hooks/useIsEncryptionEnabled';
import { useKeyValue } from '../hooks/useKeyValue';
@@ -41,7 +41,6 @@ interface Props {
}
export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
const createEnvironment = useCreateEnvironment();
const { baseEnvironment, otherBaseEnvironments, subEnvironments, allEnvironments } =
useEnvironmentsBreakdown();
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string | null>(
@@ -55,7 +54,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
const handleCreateEnvironment = async () => {
if (baseEnvironment == null) return;
const id = await createEnvironment.mutateAsync(baseEnvironment);
const id = await createEnvironmentAndActivate.mutateAsync(baseEnvironment);
if (id != null) setSelectedEnvironmentId(id);
};
@@ -162,30 +161,30 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
};
const EnvironmentEditor = function ({
environment: activeEnvironment,
environment: selectedEnvironment,
className,
}: {
environment: Environment;
className?: string;
}) {
const activeWorkspaceId = activeEnvironment.workspaceId;
const workspaceId = selectedEnvironment.workspaceId;
const isEncryptionEnabled = useIsEncryptionEnabled();
const valueVisibility = useKeyValue<boolean>({
namespace: 'global',
key: ['environmentValueVisibility', activeWorkspaceId],
key: ['environmentValueVisibility', workspaceId],
fallback: false,
});
const { allEnvironments } = useEnvironmentsBreakdown();
const handleChange = useCallback(
(variables: PairWithId[]) => patchModel(activeEnvironment, { variables }),
[activeEnvironment],
(variables: PairWithId[]) => patchModel(selectedEnvironment, { variables }),
[selectedEnvironment],
);
const [forceUpdateKey, regenerateForceUpdateKey] = useRandomKey();
// Gather a list of env names from other environments to help the user get them aligned
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
const options: GenericCompletionOption[] = [];
if (activeEnvironment.base) {
if (selectedEnvironment.base) {
return { options };
}
@@ -195,7 +194,7 @@ const EnvironmentEditor = function ({
const containingEnvs = allEnvironments.filter((e) =>
e.variables.some((v) => v.name === name),
);
const isAlreadyInActive = containingEnvs.find((e) => e.id === activeEnvironment.id);
const isAlreadyInActive = containingEnvs.find((e) => e.id === selectedEnvironment.id);
if (isAlreadyInActive) continue;
options.push({
label: name,
@@ -204,7 +203,7 @@ const EnvironmentEditor = function ({
});
}
return { options };
}, [activeEnvironment.base, activeEnvironment.id, allEnvironments]);
}, [selectedEnvironment.base, selectedEnvironment.id, allEnvironments]);
const validateName = useCallback((name: string) => {
// Empty just means the variable doesn't have a name yet and is unusable
@@ -217,11 +216,11 @@ const EnvironmentEditor = function ({
if (!isEncryptionEnabled) {
return true;
} else {
return !activeEnvironment.variables.every(
return !selectedEnvironment.variables.every(
(v) => v.value === '' || analyzeTemplate(v.value) !== 'insecure',
);
}
}, [activeEnvironment.variables, isEncryptionEnabled]);
}, [selectedEnvironment.variables, isEncryptionEnabled]);
const encryptEnvironment = (environment: Environment) => {
withEncryptionEnabled(async () => {
@@ -238,10 +237,10 @@ const EnvironmentEditor = function ({
return (
<VStack space={4} className={classNames(className, 'pl-4')}>
<Heading className="w-full flex items-center gap-0.5">
<div className="mr-2">{activeEnvironment?.name}</div>
<div className="mr-2">{selectedEnvironment?.name}</div>
{isEncryptionEnabled ? (
promptToEncrypt ? (
<BadgeButton color="notice" onClick={() => encryptEnvironment(activeEnvironment)}>
<BadgeButton color="notice" onClick={() => encryptEnvironment(selectedEnvironment)}>
Encrypt All Variables
</BadgeButton>
) : (
@@ -257,9 +256,9 @@ const EnvironmentEditor = function ({
</>
)}
</Heading>
{activeEnvironment.public && promptToEncrypt && (
{selectedEnvironment.public && promptToEncrypt && (
<DismissibleBanner
id={`warn-unencrypted-${activeEnvironment.id}`}
id={`warn-unencrypted-${selectedEnvironment.id}`}
color="notice"
className="mr-3"
>
@@ -277,11 +276,15 @@ const EnvironmentEditor = function ({
valueType={valueType}
valueAutocompleteVariables
valueAutocompleteFunctions
forcedEnvironmentId={activeEnvironment.id}
forceUpdateKey={`${activeEnvironment.id}::${forceUpdateKey}`}
pairs={activeEnvironment.variables}
forceUpdateKey={`${selectedEnvironment.id}::${forceUpdateKey}`}
pairs={selectedEnvironment.variables}
onChange={handleChange}
stateKey={`environment.${activeEnvironment.id}`}
stateKey={`environment.${selectedEnvironment.id}`}
forcedEnvironmentId={
// Editing the base environment should resolve variables using the active environment.
// Editing a sub environment should resolve variables as if it's the active environment
selectedEnvironment.base ? undefined : selectedEnvironment.id
}
/>
</div>
</VStack>

View File

@@ -1,36 +1,90 @@
import { foldersAtom, patchModel } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useMemo, useState } from 'react';
import { useAuthTab } from '../hooks/useAuthTab';
import { useHeadersTab } from '../hooks/useHeadersTab';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { Input } from './core/Input';
import { VStack } from './core/Stacks';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor';
interface Props {
folderId: string | null;
tab?: FolderSettingsTab;
}
export function FolderSettingsDialog({ folderId }: Props) {
const TAB_AUTH = 'auth';
const TAB_HEADERS = 'headers';
const TAB_GENERAL = 'general';
export type FolderSettingsTab = typeof TAB_AUTH | typeof TAB_HEADERS | typeof TAB_GENERAL;
export function FolderSettingsDialog({ folderId, tab }: Props) {
const folders = useAtomValue(foldersAtom);
const folder = folders.find((f) => f.id === folderId);
const folder = folders.find((f) => f.id === folderId) ?? null;
const [activeTab, setActiveTab] = useState<string>(tab ?? TAB_GENERAL);
const authTab = useAuthTab(TAB_AUTH, folder);
const headersTab = useHeadersTab(TAB_HEADERS, folder);
const inheritedHeaders = useInheritedHeaders(folder);
const tabs = useMemo<TabItem[]>(() => {
if (folder == null) return [];
return [
{
value: TAB_GENERAL,
label: 'General',
},
...authTab,
...headersTab,
];
}, [authTab, folder, headersTab]);
if (folder == null) return null;
return (
<VStack space={3} className="pb-3">
<Input
label="Folder Name"
defaultValue={folder.name}
onChange={(name) => patchModel(folder, { name })}
stateKey={`name.${folder.id}`}
/>
<MarkdownEditor
name="folder-description"
placeholder="Folder description"
className="min-h-[10rem] border border-border px-2"
defaultValue={folder.description}
stateKey={`description.${folder.id}`}
onChange={(description) => patchModel(folder, { description })}
/>
</VStack>
<Tabs
value={activeTab}
onChangeValue={setActiveTab}
label="Folder Settings"
className="px-1.5 pb-2"
addBorders
tabs={tabs}
>
<TabContent value={TAB_AUTH} className="pt-3 overflow-y-auto h-full px-4">
<HttpAuthenticationEditor model={folder} />
</TabContent>
<TabContent value={TAB_GENERAL} className="pt-3 overflow-y-auto h-full px-4">
<VStack space={3} className="pb-3 h-full">
<Input
label="Folder Name"
defaultValue={folder.name}
onChange={(name) => patchModel(folder, { name })}
stateKey={`name.${folder.id}`}
/>
<MarkdownEditor
name="folder-description"
placeholder="Folder description"
className="border border-border px-2"
defaultValue={folder.description}
stateKey={`description.${folder.id}`}
onChange={(description) => patchModel(folder, { description })}
/>
</VStack>
</TabContent>
<TabContent value={TAB_HEADERS} className="pt-3 overflow-y-auto h-full px-4">
<HeadersEditor
inheritedHeaders={inheritedHeaders}
forceUpdateKey={folder.id}
headers={folder.headers}
onChange={(headers) => patchModel(folder, { headers })}
stateKey={`headers.${folder.id}`}
/>
</TabContent>
</Tabs>
);
}

View File

@@ -70,8 +70,6 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
allEntries.push(entry);
if (entry.next == null && entry.prev == null) {
externalEntries.push(entry);
} else if (entry.next?.model === 'environment' || entry.prev?.model === 'environment') {
externalEntries.push(entry);
} else {
yaakEntries.push(entry);
}

View File

@@ -343,9 +343,7 @@ function SetupSyncDropdown({ workspaceMeta }: { workspaceMeta: WorkspaceMeta })
color: 'success',
label: 'Open Workspace Settings',
leftSlot: <Icon icon="settings" />,
onSelect() {
openWorkspaceSettings.mutate();
},
onSelect: openWorkspaceSettings,
},
{ type: 'separator' },
{

View File

@@ -1,10 +1,12 @@
import { type GrpcMetadataEntry, type GrpcRequest, patchModel } from '@yaakapp-internal/models';
import { type GrpcRequest, type HttpRequestHeader, patchModel } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { CSSProperties } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
import { useAuthTab } from '../hooks/useAuthTab';
import { useContainerSize } from '../hooks/useContainerQuery';
import type { ReflectResponseService } from '../hooks/useGrpc';
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
import { useHeadersTab } from '../hooks/useHeadersTab';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { useKeyValue } from '../hooks/useKeyValue';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { resolvedModelName } from '../lib/resolvedModelName';
@@ -12,13 +14,13 @@ import { Button } from './core/Button';
import { CountBadge } from './core/CountBadge';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { PlainInput } from './core/PlainInput';
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 { GrpcEditor } from './GrpcEditor';
import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor';
import { UrlBar } from './UrlBar';
@@ -64,7 +66,9 @@ export function GrpcRequestPane({
onCancel,
onSend,
}: Props) {
const authentication = useHttpAuthenticationSummaries();
const authTab = useAuthTab(TAB_AUTH, activeRequest);
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, 'Metadata');
const inheritedHeaders = useInheritedHeaders(activeRequest);
const { value: activeTabs, set: setActiveTabs } = useKeyValue<Record<string, string>>({
namespace: 'no_sync',
key: 'grpcRequestActiveTabs',
@@ -130,42 +134,15 @@ export function GrpcRequestPane({
const tabs: TabItem[] = useMemo(
() => [
{ value: TAB_MESSAGE, label: 'Message' },
{
value: TAB_AUTH,
label: 'Auth',
options: {
value: activeRequest.authenticationType,
items: [
...authentication.map((a) => ({
label: a.label || 'UNKNOWN',
shortLabel: a.shortLabel,
value: a.name,
})),
{ type: 'separator' },
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
],
onChange: async (authenticationType) => {
let authentication: GrpcRequest['authentication'] = activeRequest.authentication;
if (activeRequest.authenticationType !== authenticationType) {
authentication = {
// Reset auth if changing types
};
}
await patchModel(activeRequest, {
authenticationType,
authentication,
});
},
},
},
{ value: TAB_METADATA, label: 'Metadata' },
...metadataTab,
...authTab,
{
value: TAB_DESCRIPTION,
label: 'Info',
rightSlot: activeRequest.description && <CountBadge count={true} />,
},
],
[activeRequest, authentication],
[activeRequest.description, authTab, metadataTab],
);
const activeTab = activeTabs?.[activeRequest.id];
@@ -177,7 +154,7 @@ export function GrpcRequestPane({
);
const handleMetadataChange = useCallback(
(metadata: GrpcMetadataEntry[]) => patchModel(activeRequest, { metadata }),
(metadata: HttpRequestHeader[]) => patchModel(activeRequest, { metadata }),
[activeRequest],
);
@@ -307,17 +284,15 @@ export function GrpcRequestPane({
/>
</TabContent>
<TabContent value={TAB_AUTH}>
<HttpAuthenticationEditor request={activeRequest} />
<HttpAuthenticationEditor model={activeRequest} />
</TabContent>
<TabContent value={TAB_METADATA}>
<PairOrBulkEditor
preferenceName="grpc_metadata"
valueAutocompleteVariables
nameAutocompleteVariables
pairs={activeRequest.metadata}
onChange={handleMetadataChange}
<HeadersEditor
inheritedHeaders={inheritedHeaders}
forceUpdateKey={forceUpdateKey}
stateKey={`grpc_metadata.${activeRequest.id}`}
headers={activeRequest.metadata}
stateKey={`headers.${activeRequest.id}`}
onChange={handleMetadataChange}
/>
</TabContent>
<TabContent value={TAB_DESCRIPTION}>

View File

@@ -1,9 +1,8 @@
import { settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { HTMLAttributes, ReactNode } from 'react';
import React from 'react';
import { useOsInfo } from '../hooks/useOsInfo';
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
import React, { useMemo } from 'react';
import { useStoplightsVisible } from '../hooks/useStoplightsVisible';
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { WindowControls } from './WindowControls';
@@ -23,27 +22,42 @@ export function HeaderSize({
onlyXWindowControl,
children,
}: HeaderSizeProps) {
const osInfo = useOsInfo();
const settings = useAtomValue(settingsAtom);
const stoplightsVisible = useStoplightsVisible();
const finalStyle = useMemo<CSSProperties>(() => {
const s = { ...style };
// Set the height (use min-height because scaling font size may make it larger
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
// Add large padding for window controls
if (stoplightsVisible && !ignoreControlsSpacing) {
s.paddingLeft = 72 / settings.interfaceScale;
} else if (!stoplightsVisible && !ignoreControlsSpacing && !settings.hideWindowControls) {
s.paddingRight = WINDOW_CONTROLS_WIDTH;
}
return s;
}, [
ignoreControlsSpacing,
settings.hideWindowControls,
settings.interfaceScale,
size,
stoplightsVisible,
style,
]);
return (
<div
data-tauri-drag-region
style={{
...style,
// Add padding for macOS stoplights, but keep it the same width (account for the interface scale)
paddingLeft:
stoplightsVisible && !ignoreControlsSpacing ? 72 / settings.interfaceScale : undefined,
...(size === 'md' ? { minHeight: HEADER_SIZE_MD } : {}),
...(size === 'lg' ? { minHeight: HEADER_SIZE_LG } : {}),
...(osInfo.osType === 'macos' || ignoreControlsSpacing
? { paddingRight: '2px' }
: { paddingLeft: '2px', paddingRight: WINDOW_CONTROLS_WIDTH }),
}}
style={finalStyle}
className={classNames(
className,
'px-1', // Give it some space on either end
'pt-[1px]', // Make up for bottom border
'select-none relative',
'pt-[1px] w-full border-b border-border-subtle min-w-0',
'w-full border-b border-border-subtle min-w-0',
)}
>
{/* NOTE: This needs display:grid or else the element shrinks (even though scrollable) */}

View File

@@ -5,36 +5,85 @@ import { connections } from '../lib/data/connections';
import { encodings } from '../lib/data/encodings';
import { headerNames } from '../lib/data/headerNames';
import { mimeTypes } from '../lib/data/mimetypes';
import { Banner } from './core/Banner';
import { CountBadge } from './core/CountBadge';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import type { InputProps } from './core/Input';
import type { Pair, PairEditorProps } from './core/PairEditor';
import { ensurePairId, PairEditorRow } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { HStack } from './core/Stacks';
type Props = {
forceUpdateKey: string;
headers: HttpRequestHeader[];
inheritedHeaders?: HttpRequestHeader[];
stateKey: string;
onChange: (headers: HttpRequestHeader[]) => void;
label?: string;
};
export function HeadersEditor({ stateKey, headers, onChange, forceUpdateKey }: Props) {
export function HeadersEditor({
stateKey,
headers,
inheritedHeaders,
onChange,
forceUpdateKey,
}: Props) {
const validInheritedHeaders =
inheritedHeaders?.filter((pair) => pair.enabled && (pair.name || pair.value)) ?? [];
return (
<PairOrBulkEditor
forceUpdateKey={forceUpdateKey}
nameAutocomplete={nameAutocomplete}
nameAutocompleteFunctions
nameAutocompleteVariables
namePlaceholder="Header-Name"
nameValidate={validateHttpHeader}
onChange={onChange}
pairs={headers}
preferenceName="headers"
stateKey={stateKey}
valueType={valueType}
valueAutocomplete={valueAutocomplete}
valueAutocompleteFunctions
valueAutocompleteVariables
/>
<div className="@container w-full h-full grid grid-rows-[auto_minmax(0,1fr)]">
{validInheritedHeaders.length > 0 ? (
<Banner className="!py-0 mb-1.5 border-dashed" color="secondary">
<details>
<summary className="py-1.5 text-sm !cursor-default !select-none opacity-70 hover:opacity-100">
<HStack>
Inherited <CountBadge count={validInheritedHeaders.length} />
</HStack>
</summary>
<div className="pb-2">
{validInheritedHeaders?.map((pair, i) => (
<PairEditorRow
key={pair.id + '.' + i}
index={i}
disabled
disableDrag
className="py-1"
onChange={() => {}}
onEnd={() => {}}
onMove={() => {}}
pair={ensurePairId(pair)}
stateKey={null}
nameAutocompleteFunctions
nameAutocompleteVariables
valueAutocompleteFunctions
valueAutocompleteVariables
/>
))}
</div>
</details>
</Banner>
) : (
<span />
)}
<PairOrBulkEditor
forceUpdateKey={forceUpdateKey}
nameAutocomplete={nameAutocomplete}
nameAutocompleteFunctions
nameAutocompleteVariables
namePlaceholder="Header-Name"
nameValidate={validateHttpHeader}
onChange={onChange}
pairs={headers}
preferenceName="headers"
stateKey={stateKey}
valueType={valueType}
valueAutocomplete={valueAutocomplete}
valueAutocompleteFunctions
valueAutocompleteVariables
/>
</div>
);
}
@@ -51,14 +100,14 @@ const headerOptionsMap: Record<string, string[]> = {
const valueType = (pair: Pair): InputProps['type'] => {
const name = pair.name.toLowerCase().trim();
if (
name.includes('authorization') ||
name.includes('api-key') ||
name.includes('access-token') ||
name.includes('auth') ||
name.includes('secret') ||
name.includes('token') ||
name === 'cookie' ||
name === 'set-cookie'
name.includes('authorization') ||
name.includes('api-key') ||
name.includes('access-token') ||
name.includes('auth') ||
name.includes('secret') ||
name.includes('token') ||
name === 'cookie' ||
name === 'set-cookie'
) {
return 'password';
} else {

View File

@@ -1,34 +1,98 @@
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import type {
Folder,
GrpcRequest,
HttpRequest,
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import React, { useCallback } from 'react';
import { openFolderSettings } from '../commands/openFolderSettings';
import { openWorkspaceSettings } from '../commands/openWorkspaceSettings';
import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig';
import { useInheritedAuthentication } from '../hooks/useInheritedAuthentication';
import { resolvedModelName } from '../lib/resolvedModelName';
import { Checkbox } from './core/Checkbox';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import { Link } from './core/Link';
import { HStack } from './core/Stacks';
import { DynamicForm } from './DynamicForm';
import { EmptyStateText } from './EmptyStateText';
interface Props {
request: HttpRequest | GrpcRequest | WebsocketRequest;
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace;
}
export function HttpAuthenticationEditor({ request }: Props) {
export function HttpAuthenticationEditor({ model }: Props) {
const inheritedAuth = useInheritedAuthentication(model);
const authConfig = useHttpAuthenticationConfig(
request.authenticationType,
request.authentication,
request.id,
model.authenticationType,
model.authentication,
model.id,
);
const handleChange = useCallback(
(authentication: Record<string, boolean>) => patchModel(request, { authentication }),
[request],
async (authentication: Record<string, boolean>) => await patchModel(model, { authentication }),
[model],
);
if (authConfig.data == null) {
return <EmptyStateText>No Authentication {request.authenticationType}</EmptyStateText>;
if (model.authenticationType === 'none') {
return <EmptyStateText>No authentication</EmptyStateText>;
}
if (model.authenticationType != null && authConfig.data == null) {
return (
<EmptyStateText>
Unknown authentication <InlineCode>{authConfig.data}</InlineCode>
</EmptyStateText>
);
}
if (inheritedAuth == null) {
if (model.model === 'workspace' || model.model === 'folder') {
return (
<EmptyStateText className="flex-col gap-1">
<p>
Apply auth to all requests in <strong>{resolvedModelName(model)}</strong>
</p>
<Link href="https://feedback.yaak.app/help/articles/2112119-request-inheritance">
Documentation
</Link>
</EmptyStateText>
);
} else {
return <EmptyStateText>Authentication not configured</EmptyStateText>;
}
}
if (inheritedAuth.authenticationType === 'none') {
return <EmptyStateText>No authentication</EmptyStateText>;
}
const wasAuthInherited = inheritedAuth?.id !== model.id;
if (wasAuthInherited) {
const name = resolvedModelName(inheritedAuth);
const cta = inheritedAuth.model === 'workspace' ? 'Workspace' : name;
return (
<EmptyStateText>
<p>
Inherited from{' '}
<button
className="underline hover:text-text"
onClick={() => {
if (inheritedAuth.model === 'folder') openFolderSettings(inheritedAuth.id, 'auth');
else openWorkspaceSettings('auth');
}}
>
{cta}
</button>
</p>
</EmptyStateText>
);
}
return (
@@ -36,17 +100,17 @@ export function HttpAuthenticationEditor({ request }: Props) {
<HStack space={2} className="mb-2" alignItems="center">
<Checkbox
className="w-full"
checked={!request.authentication.disabled}
onChange={(disabled) => handleChange({ ...request.authentication, disabled: !disabled })}
checked={!model.authentication.disabled}
onChange={(disabled) => handleChange({ ...model.authentication, disabled: !disabled })}
title="Enabled"
/>
{authConfig.data.actions && authConfig.data.actions.length > 0 && (
{authConfig.data?.actions && authConfig.data.actions.length > 0 && (
<Dropdown
items={authConfig.data.actions.map(
(a): DropdownItem => ({
label: a.label,
leftSlot: a.icon ? <Icon icon={a.icon} /> : null,
onSelect: () => a.call(request),
onSelect: () => a.call(model),
}),
)}
>
@@ -55,12 +119,12 @@ export function HttpAuthenticationEditor({ request }: Props) {
)}
</HStack>
<DynamicForm
disabled={request.authentication.disabled}
disabled={model.authentication.disabled}
autocompleteVariables
autocompleteFunctions
stateKey={`auth.${request.id}.${request.authenticationType}`}
inputs={authConfig.data.args}
data={request.authentication}
stateKey={`auth.${model.id}.${model.authenticationType}`}
inputs={authConfig.data?.args ?? []}
data={model.authentication}
onChange={handleChange}
/>
</div>

View File

@@ -6,13 +6,15 @@ import { atom, useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useAuthTab } from '../hooks/useAuthTab';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
import { useHeadersTab } from '../hooks/useHeadersTab';
import { useImportCurl } from '../hooks/useImportCurl';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { useKeyValue } from '../hooks/useKeyValue';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { deepEqualAtom } from '../lib/atoms';
@@ -44,12 +46,12 @@ import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
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';
import { GraphQLEditor } from './GraphQLEditor';
interface Props {
style: CSSProperties;
@@ -85,7 +87,9 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
const contentType = getContentTypeFromHeaders(activeRequest.headers);
const authentication = useHttpAuthenticationSummaries();
const authTab = useAuthTab(TAB_AUTH, activeRequest);
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
const inheritedHeaders = useInheritedHeaders(activeRequest);
const handleContentTypeChange = useCallback(
async (contentType: string | null, patch: Partial<Omit<HttpRequest, 'headers'>> = {}) => {
@@ -214,42 +218,21 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
rightSlot: <CountBadge count={urlParameterPairs.length} />,
label: 'Params',
},
{
value: TAB_HEADERS,
label: 'Headers',
rightSlot: <CountBadge count={activeRequest.headers.filter((h) => h.name).length} />,
},
{
value: TAB_AUTH,
label: 'Auth',
options: {
value: activeRequest.authenticationType,
items: [
...authentication.map((a) => ({
label: a.label || 'UNKNOWN',
shortLabel: a.shortLabel,
value: a.name,
})),
{ type: 'separator' },
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
],
onChange: async (authenticationType) => {
let authentication: HttpRequest['authentication'] = activeRequest.authentication;
if (activeRequest.authenticationType !== authenticationType) {
authentication = {
// Reset auth if changing types
};
}
await patchModel(activeRequest, { authenticationType, authentication });
},
},
},
...headersTab,
...authTab,
{
value: TAB_DESCRIPTION,
label: 'Info',
},
],
[activeRequest, authentication, handleContentTypeChange, numParams, urlParameterPairs.length],
[
activeRequest,
authTab,
handleContentTypeChange,
headersTab,
numParams,
urlParameterPairs.length,
],
);
const { mutate: sendRequest } = useSendAnyHttpRequest();
@@ -372,10 +355,11 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
tabListClassName="mt-2 !mb-1.5"
>
<TabContent value={TAB_AUTH}>
<HttpAuthenticationEditor request={activeRequest} />
<HttpAuthenticationEditor model={activeRequest} />
</TabContent>
<TabContent value={TAB_HEADERS}>
<HeadersEditor
inheritedHeaders={inheritedHeaders}
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
headers={activeRequest.headers}
stateKey={`headers.${activeRequest.id}`}

View File

@@ -1,12 +1,11 @@
import type { ReactNode } from 'react';
import { useAppInfo } from '../hooks/useAppInfo';
import { appInfo } from '../lib/appInfo';
interface Props {
children: ReactNode;
}
export function IsDev({ children }: Props) {
const appInfo = useAppInfo();
if (!appInfo.isDev) {
return null;
}

View File

@@ -2,7 +2,7 @@ import type { LicenseCheckStatus } from '@yaakapp-internal/license';
import { useLicense } from '@yaakapp-internal/license';
import type { ReactNode } from 'react';
import { openSettings } from '../commands/openSettings';
import { appInfo } from '../hooks/useAppInfo';
import { appInfo } from '../lib/appInfo';
import { useLicenseConfirmation } from '../hooks/useLicenseConfirmation';
import { BadgeButton } from './core/BadgeButton';
import type { ButtonProps } from './core/Button';

View File

@@ -17,7 +17,7 @@ export default function RouteError({ error }: { error: unknown }) {
<FormattedError>
{message}
{stack && (
<details className="mt-3 select-autotext-xs">
<details className="mt-3 select-auto text-xs">
<summary className="!cursor-default !select-none">Stack Trace</summary>
<div className="mt-2 text-xs">{stack}</div>
</details>

View File

@@ -80,7 +80,7 @@ export function SelectFile({
<>
{filePath && (
<IconButton
size={size}
size={size === 'auto' ? 'md' : size}
variant="border"
icon="x"
title={'Unset ' + itemLabel}

View File

@@ -1,9 +1,9 @@
import { useSearch } from '@tanstack/react-router';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { type } from '@tauri-apps/plugin-os';
import classNames from 'classnames';
import React, { useState } from 'react';
import { useKeyPressEvent } from 'react-use';
import { useOsInfo } from '../../hooks/useOsInfo';
import { capitalize } from '../../lib/capitalize';
import { HStack } from '../core/Stacks';
import { TabContent, Tabs } from '../core/Tabs/Tabs';
@@ -27,7 +27,6 @@ const tabs = [TAB_GENERAL, TAB_APPEARANCE, TAB_PROXY, TAB_PLUGINS, TAB_LICENSE]
export type SettingsTab = (typeof tabs)[number];
export default function Settings({ hide }: Props) {
const osInfo = useOsInfo();
const { tab: tabFromQuery } = useSearch({ from: '/workspaces/$workspaceId/settings' });
const [tab, setTab] = useState<string | undefined>(tabFromQuery);
@@ -60,9 +59,7 @@ export default function Settings({ hide }: Props) {
justifyContent="center"
className="w-full h-full grid grid-cols-[1fr_auto] pointer-events-none"
>
<div className={classNames(osInfo?.osType === 'macos' ? 'text-center' : 'pl-2')}>
Settings
</div>
<div className={classNames(type() === 'macos' ? 'text-center' : 'pl-2')}>Settings</div>
</HStack>
</HeaderSize>
)}

View File

@@ -18,6 +18,7 @@ import type { SelectProps } from '../core/Select';
import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
import { HStack, VStack } from '../core/Stacks';
import { type } from '@tauri-apps/plugin-os';
const fontSizeOptions = [
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
@@ -122,6 +123,15 @@ export function SettingsAppearance() {
onChange={(editorSoftWrap) => patchModel(settings, { editorSoftWrap })}
/>
{type() !== 'macos' && (
<Checkbox
checked={settings.hideWindowControls}
title="Hide Window Controls"
help="Hide the close/maximize/minimize controls on Windows or Linux"
onChange={(hideWindowControls) => patchModel(settings, { hideWindowControls })}
/>
)}
<Separator className="my-4" />
<Select

View File

@@ -3,7 +3,7 @@ import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { useAppInfo } from '../../hooks/useAppInfo';
import { appInfo } from '../../lib/appInfo';
import { useCheckForUpdates } from '../../hooks/useCheckForUpdates';
import { revealInFinderText } from '../../lib/reveal';
import { Checkbox } from '../core/Checkbox';
@@ -18,7 +18,6 @@ import { VStack } from '../core/Stacks';
export function SettingsGeneral() {
const workspace = useAtomValue(activeWorkspaceAtom);
const settings = useAtomValue(settingsAtom);
const appInfo = useAppInfo();
const checkForUpdates = useCheckForUpdates();
if (settings == null || workspace == null) {

View File

@@ -2,7 +2,7 @@ import { openUrl } from '@tauri-apps/plugin-opener';
import { useLicense } from '@yaakapp-internal/license';
import { useRef } from 'react';
import { openSettings } from '../commands/openSettings';
import { useAppInfo } from '../hooks/useAppInfo';
import { appInfo } from '../lib/appInfo';
import { useCheckForUpdates } from '../hooks/useCheckForUpdates';
import { useExportData } from '../hooks/useExportData';
import { useImportData } from '../hooks/useImportData';
@@ -17,7 +17,6 @@ import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
export function SettingsDropdown() {
const importData = useImportData();
const exportData = useExportData();
const appInfo = useAppInfo();
const dropdownRef = useRef<DropdownRef>(null);
const checkForUpdates = useCheckForUpdates();
const { check } = useLicense();

View File

@@ -113,6 +113,10 @@ export const UrlBar = memo(function UrlBar({
iconColor="secondary"
icon={isLoading ? 'x' : submitIcon}
hotkeyAction="http_request.send"
onMouseDown={(e) => {
// Prevent the button from taking focus
e.preventDefault();
}}
/>
</div>
)}

View File

@@ -1,4 +1,4 @@
import type { HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import type { WebsocketRequest } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import { closeWebsocket, connectWebsocket, sendWebsocket } from '@yaakapp-internal/ws';
@@ -9,13 +9,15 @@ import React, { useCallback, useMemo } from 'react';
import { getActiveCookieJar } from '../hooks/useActiveCookieJar';
import { getActiveEnvironment } from '../hooks/useActiveEnvironment';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useAuthTab } from '../hooks/useAuthTab';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
import { useHeadersTab } from '../hooks/useHeadersTab';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { useKeyValue } from '../hooks/useKeyValue';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { activeWebsocketConnectionAtom } from '../hooks/usePinnedWebsocketConnection';
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
import {allRequestsAtom} from "../hooks/useAllRequests";
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { deepEqualAtom } from '../lib/atoms';
import { languageFromContentType } from '../lib/contentType';
@@ -69,7 +71,9 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
});
const forceUpdateKey = useRequestUpdateKey(activeRequest.id);
const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
const authentication = useHttpAuthenticationSummaries();
const authTab = useAuthTab(TAB_AUTH, activeRequest);
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
const inheritedHeaders = useInheritedHeaders(activeRequest);
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
@@ -99,45 +103,14 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
rightSlot: <CountBadge count={urlParameterPairs.length} />,
label: 'Params',
},
{
value: TAB_HEADERS,
label: 'Headers',
rightSlot: <CountBadge count={activeRequest.headers.filter((h) => h.name).length} />,
},
{
value: TAB_AUTH,
label: 'Auth',
options: {
value: activeRequest.authenticationType,
items: [
...authentication.map((a) => ({
label: a.label || 'UNKNOWN',
shortLabel: a.shortLabel,
value: a.name,
})),
{ type: 'separator' },
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
],
onChange: async (authenticationType) => {
let authentication: HttpRequest['authentication'] = activeRequest.authentication;
if (activeRequest.authenticationType !== authenticationType) {
authentication = {
// Reset auth if changing types
};
}
await patchModel(activeRequest, {
authenticationType,
authentication,
});
},
},
},
...headersTab,
...authTab,
{
value: TAB_DESCRIPTION,
label: 'Info',
},
];
}, [activeRequest, authentication, urlParameterPairs.length]);
}, [authTab, headersTab, urlParameterPairs.length]);
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
@@ -266,10 +239,11 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
tabListClassName="mt-2 !mb-1.5"
>
<TabContent value={TAB_AUTH}>
<HttpAuthenticationEditor request={activeRequest} />
<HttpAuthenticationEditor model={activeRequest} />
</TabContent>
<TabContent value={TAB_HEADERS}>
<HeadersEditor
inheritedHeaders={inheritedHeaders}
forceUpdateKey={forceUpdateKey}
headers={activeRequest.headers}
stateKey={`headers.${activeRequest.id}`}

View File

@@ -1,8 +1,10 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { type } from '@tauri-apps/plugin-os';
import { settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import React, { useState } from 'react';
import { useOsInfo } from '../hooks/useOsInfo';
import {WINDOW_CONTROLS_WIDTH} from "../lib/constants";
import { WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { Button } from './core/Button';
import { HStack } from './core/Stacks';
@@ -14,10 +16,9 @@ interface Props {
export function WindowControls({ className, onlyX }: Props) {
const [maximized, setMaximized] = useState<boolean>(false);
const osInfo = useOsInfo();
// Never show controls on macOS
if (osInfo.osType === 'macos') {
const settings = useAtomValue(settingsAtom);
// Never show controls on macOS or if hideWindowControls is true
if (type() === 'macos' || settings.hideWindowControls) {
return null;
}

View File

@@ -49,7 +49,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
label: 'Workspace Settings',
leftSlot: <Icon icon="settings" />,
hotKeyAction: 'workspace_settings.show',
onSelect: () => openWorkspaceSettings.mutate(),
onSelect: openWorkspaceSettings,
},
{
label: revealInFinderText,

View File

@@ -1,5 +1,9 @@
import { patchModel, workspaceMetasAtom, workspacesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useState } from 'react';
import { useAuthTab } from '../hooks/useAuthTab';
import { useHeadersTab } from '../hooks/useHeadersTab';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
import { router } from '../lib/router';
import { Banner } from './core/Banner';
@@ -8,6 +12,9 @@ import { InlineCode } from './core/InlineCode';
import { PlainInput } from './core/PlainInput';
import { Separator } from './core/Separator';
import { HStack, VStack } from './core/Stacks';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor';
import { SyncToFilesystemSetting } from './SyncToFilesystemSetting';
import { WorkspaceEncryptionSetting } from './WorkspaceEncryptionSetting';
@@ -15,11 +22,29 @@ import { WorkspaceEncryptionSetting } from './WorkspaceEncryptionSetting';
interface Props {
workspaceId: string | null;
hide: () => void;
tab?: WorkspaceSettingsTab;
}
export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) {
const TAB_AUTH = 'auth';
const TAB_DESCRIPTION = 'description';
const TAB_HEADERS = 'headers';
const TAB_GENERAL = 'general';
export type WorkspaceSettingsTab =
| typeof TAB_AUTH
| typeof TAB_HEADERS
| typeof TAB_GENERAL
| typeof TAB_DESCRIPTION;
const DEFAULT_TAB: WorkspaceSettingsTab = TAB_DESCRIPTION;
export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
const workspace = useAtomValue(workspacesAtom).find((w) => w.id === workspaceId);
const workspaceMeta = useAtomValue(workspaceMetasAtom).find((m) => m.workspaceId === workspaceId);
const [activeTab, setActiveTab] = useState<string>(tab ?? DEFAULT_TAB);
const authTab = useAuthTab(TAB_AUTH, workspace ?? null);
const headersTab = useHeadersTab(TAB_HEADERS, workspace ?? null);
const inheritedHeaders = useInheritedHeaders(workspace ?? null);
if (workspace == null) {
return (
@@ -37,53 +62,87 @@ export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) {
);
return (
<VStack space={4} alignItems="start" className="pb-3 h-full">
<PlainInput
required
hideLabel
placeholder="Workspace Name"
label="Name"
defaultValue={workspace.name}
className="!text-base font-sans"
onChange={(name) => patchModel(workspace, { name })}
/>
<Tabs
value={activeTab}
onChangeValue={setActiveTab}
label="Folder Settings"
className="px-1.5 pb-2"
addBorders
tabs={[
{ value: TAB_DESCRIPTION, label: 'Description' },
{
value: TAB_GENERAL,
label: 'General',
},
...authTab,
...headersTab,
]}
>
<TabContent value={TAB_AUTH} className="pt-3 overflow-y-auto h-full px-4">
<HttpAuthenticationEditor model={workspace} />
</TabContent>
<TabContent value={TAB_HEADERS} className="pt-3 overflow-y-auto h-full px-4">
<HeadersEditor
inheritedHeaders={inheritedHeaders}
forceUpdateKey={workspace.id}
headers={workspace.headers}
onChange={(headers) => patchModel(workspace, { headers })}
stateKey={`headers.${workspace.id}`}
/>
</TabContent>
<TabContent value={TAB_DESCRIPTION} className="pt-3 overflow-y-auto h-full px-4">
<VStack space={4} alignItems="start" className="pb-3 h-full">
<PlainInput
required
hideLabel
placeholder="Workspace Name"
label="Name"
defaultValue={workspace.name}
className="!text-base font-sans"
onChange={(name) => patchModel(workspace, { name })}
/>
<MarkdownEditor
name="workspace-description"
placeholder="Workspace description"
className="min-h-[3rem] max-h-[25rem] border border-border px-2"
defaultValue={workspace.description}
stateKey={`description.${workspace.id}`}
onChange={(description) => patchModel(workspace, { description })}
heightMode="auto"
/>
<MarkdownEditor
name="workspace-description"
placeholder="Workspace description"
className="border border-border px-2"
defaultValue={workspace.description}
stateKey={`description.${workspace.id}`}
onChange={(description) => patchModel(workspace, { description })}
heightMode="auto"
/>
</VStack>
</TabContent>
<TabContent value={TAB_GENERAL} className="pt-3 overflow-y-auto h-full px-4">
<VStack space={4} alignItems="start" className="pb-3 h-full">
<SyncToFilesystemSetting
value={{ filePath: workspaceMeta.settingSyncDir }}
onCreateNewWorkspace={hide}
onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })}
/>
<WorkspaceEncryptionSetting size="xs" />
<SyncToFilesystemSetting
value={{ filePath: workspaceMeta.settingSyncDir }}
onCreateNewWorkspace={hide}
onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })}
/>
<WorkspaceEncryptionSetting size="xs" />
<Separator className="my-4" />
<Separator className="my-4" />
<HStack alignItems="center" justifyContent="between" className="w-full">
<Button
onClick={async () => {
const didDelete = await deleteModelWithConfirm(workspace);
if (didDelete) {
hide(); // Only hide if actually deleted workspace
await router.navigate({ to: '/' });
}
}}
color="danger"
variant="border"
size="xs"
>
Delete Workspace
</Button>
<InlineCode className="select-text cursor-text">{workspaceId}</InlineCode>
</HStack>
</VStack>
<HStack alignItems="center" justifyContent="between" className="w-full">
<Button
onClick={async () => {
const didDelete = await deleteModelWithConfirm(workspace);
if (didDelete) {
hide(); // Only hide if actually deleted workspace
await router.navigate({ to: '/' });
}
}}
color="danger"
variant="border"
size="xs"
>
Delete Workspace
</Button>
<InlineCode className="select-text cursor-text">{workspaceId}</InlineCode>
</HStack>
</VStack>
</TabContent>
</Tabs>
);
}

View File

@@ -12,7 +12,7 @@ export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color' | 'onC
color?: Color | 'custom' | 'default';
variant?: 'border' | 'solid';
isLoading?: boolean;
size?: '2xs' | 'xs' | 'sm' | 'md';
size?: '2xs' | 'xs' | 'sm' | 'md' | 'auto';
justify?: 'start' | 'center';
type?: 'button' | 'submit';
forDropdown?: boolean;
@@ -114,7 +114,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
{...props}
>
{isLoading ? (
<LoadingIcon size={size} className="mr-1" />
<LoadingIcon size={size === 'auto' ? 'md' : size} className="mr-1" />
) : leftSlot ? (
<div className="mr-2">{leftSlot}</div>
) : null}
@@ -128,7 +128,9 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
{children}
</div>
{rightSlot && <div className="ml-1">{rightSlot}</div>}
{forDropdown && <Icon icon="chevron_down" size={size} className="ml-1 -mr-1" />}
{forDropdown && (
<Icon icon="chevron_down" size={size === 'auto' ? 'md' : size} className="ml-1 -mr-1" />
)}
</button>
);
});

View File

@@ -31,10 +31,10 @@ export function Checkbox({
<HStack
as="label"
alignItems="center"
space={3}
space={2}
className={classNames(className, 'text-text mr-auto')}
>
<div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex')}>
<div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex mr-0.5')}>
<input
aria-hidden
className={classNames(
@@ -52,6 +52,7 @@ export function Checkbox({
<div className="absolute inset-0 flex items-center justify-center">
<Icon
size="sm"
className={classNames(disabled && 'opacity-disabled')}
icon={checked === 'indeterminate' ? 'minus' : checked ? 'check' : 'empty'}
/>
</div>

View File

@@ -1,7 +1,7 @@
import { styleTags, tags as t } from '@lezer/highlight';
export const highlight = styleTags({
TagOpen: t.tagName,
TagClose: t.tagName,
TagOpen: t.bracket,
TagClose: t.bracket,
TagContent: t.keyword,
});

View File

@@ -80,6 +80,10 @@ function templateTags(
const inner = rawTag.replace(/^\$\{\[\s*/, '').replace(/\s*]}$/, '');
let name = inner.match(/([\w.]+)[(]/)?.[1] ?? inner;
if (inner.includes('\n')) {
return;
}
// The beta named the function `Response` but was changed in stable.
// Keep this here for a while because there's no easy way to migrate
if (name === 'Response') {

View File

@@ -1,7 +1,6 @@
import classNames from 'classnames';
import type { HotkeyAction } from '../../hooks/useHotKey';
import { useFormattedHotkey } from '../../hooks/useHotKey';
import { useOsInfo } from '../../hooks/useOsInfo';
import { HStack } from './Stacks';
interface Props {
@@ -11,9 +10,8 @@ interface Props {
}
export function HotKey({ action, className, variant }: Props) {
const osInfo = useOsInfo();
const labelParts = useFormattedHotkey(action);
if (labelParts === null || osInfo == null) {
if (labelParts === null) {
return null;
}

View File

@@ -11,6 +11,7 @@ import {
useRef,
useState,
} from 'react';
import { createFastMutation } from '../../hooks/useFastMutation';
import { useIsEncryptionEnabled } from '../../hooks/useIsEncryptionEnabled';
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
import { copyToClipboard } from '../../lib/copy';
@@ -20,7 +21,10 @@ import {
convertTemplateToSecure,
} from '../../lib/encryption';
import { generateId } from '../../lib/generateId';
import { withEncryptionEnabled } from '../../lib/setupOrConfigureEncryption';
import {
setupOrConfigureEncryption,
withEncryptionEnabled,
} from '../../lib/setupOrConfigureEncryption';
import { Button } from './Button';
import type { DropdownItem } from './Dropdown';
import { Dropdown } from './Dropdown';
@@ -29,6 +33,7 @@ import { Editor } from './Editor/Editor';
import type { IconProps } from './Icon';
import { Icon } from './Icon';
import { IconButton } from './IconButton';
import { IconTooltip } from './IconTooltip';
import { Label } from './Label';
import { HStack } from './Stacks';
@@ -53,6 +58,7 @@ export type InputProps = Pick<
fullHeight?: boolean;
hideLabel?: boolean;
inputWrapperClassName?: string;
help?: ReactNode;
label: ReactNode;
labelClassName?: string;
labelPosition?: 'top' | 'left';
@@ -67,7 +73,7 @@ export type InputProps = Pick<
placeholder?: string;
required?: boolean;
rightSlot?: ReactNode;
size?: 'xs' | 'sm' | 'md' | 'auto';
size?: '2xs' | 'xs' | 'sm' | 'md' | 'auto';
stateKey: EditorProps['stateKey'];
tint?: Color;
type?: 'text' | 'password';
@@ -89,33 +95,34 @@ const BaseInput = forwardRef<EditorView, InputProps>(function InputBase(
{
className,
containerClassName,
inputWrapperClassName,
defaultValue,
disableObscureToggle,
disabled,
forceUpdateKey,
fullHeight,
help,
hideLabel,
inputWrapperClassName,
label,
labelClassName,
labelPosition = 'top',
leftSlot,
multiLine,
onBlur,
onChange,
onFocus,
onPaste,
onPasteOverwrite,
placeholder,
readOnly,
required,
rightSlot,
wrapLines,
size = 'md',
type = 'text',
disableObscureToggle,
tint,
validate,
readOnly,
stateKey,
multiLine,
disabled,
tint,
type = 'text',
validate,
wrapLines,
...props
}: InputProps,
ref,
@@ -211,6 +218,7 @@ const BaseInput = forwardRef<EditorView, InputProps>(function InputBase(
>
<Label
htmlFor={id.current}
help={help}
required={required}
visuallyHidden={hideLabel}
className={classNames(labelClassName)}
@@ -231,6 +239,7 @@ const BaseInput = forwardRef<EditorView, InputProps>(function InputBase(
size === 'md' && 'min-h-md',
size === 'sm' && 'min-h-sm',
size === 'xs' && 'min-h-xs',
size === '2xs' && 'min-h-2xs',
)}
>
{tint != null && (
@@ -332,7 +341,10 @@ function EncryptionInput({
value: string | null;
security: ReturnType<typeof analyzeTemplate> | null;
obscured: boolean;
}>({ fieldType: 'encrypted', value: null, security: null, obscured: true }, [ogForceUpdateKey]);
error: string | null;
}>({ fieldType: 'encrypted', value: null, security: null, obscured: true, error: null }, [
ogForceUpdateKey,
]);
const forceUpdateKey = `${ogForceUpdateKey}::${state.fieldType}::${state.value === null}`;
@@ -345,25 +357,48 @@ function EncryptionInput({
const security = analyzeTemplate(defaultValue ?? '');
if (analyzeTemplate(defaultValue ?? '') === 'global_secured') {
// Lazily update value to decrypted representation
convertTemplateToInsecure(defaultValue ?? '').then((value) => {
setState({ fieldType: 'encrypted', security, value, obscured: true });
templateToInsecure.mutate(defaultValue ?? '', {
onSuccess: (value) => {
setState({ fieldType: 'encrypted', security, value, obscured: true, error: null });
},
onError: (value) => {
setState({
fieldType: 'encrypted',
security,
value: null,
error: String(value),
obscured: true,
});
},
});
} else if (isEncryptionEnabled && !defaultValue) {
// Default to encrypted field for new encrypted inputs
setState({ fieldType: 'encrypted', security, value: '', obscured: true });
setState({ fieldType: 'encrypted', security, value: '', obscured: true, error: null });
} else if (isEncryptionEnabled) {
// Don't obscure plain text when encryption is enabled
setState({ fieldType: 'text', security, value: defaultValue ?? '', obscured: false });
setState({
fieldType: 'text',
security,
value: defaultValue ?? '',
obscured: false,
error: null,
});
} else {
// Don't obscure plain text when encryption is disabled
setState({ fieldType: 'text', security, value: defaultValue ?? '', obscured: true });
setState({
fieldType: 'text',
security,
value: defaultValue ?? '',
obscured: true,
error: null,
});
}
}, [defaultValue, isEncryptionEnabled, setState, state.value]);
const handleChange = useCallback(
(value: string, fieldType: PasswordFieldType) => {
if (fieldType === 'encrypted') {
convertTemplateToSecure(value).then((value) => onChange?.(value));
templateToSecure.mutate(value, { onSuccess: (value) => onChange?.(value) });
} else {
onChange?.(value);
}
@@ -372,7 +407,7 @@ function EncryptionInput({
const security = fieldType === 'encrypted' ? 'global_secured' : analyzeTemplate(value);
// Reset obscured value when the field type is being changed
const obscured = fieldType === s.fieldType ? s.obscured : fieldType !== 'text';
return { fieldType, value, security, obscured };
return { fieldType, value, security, obscured, error: s.error };
});
},
[onChange, setState],
@@ -477,6 +512,23 @@ function EncryptionInput({
const type = state.obscured ? 'password' : 'text';
if (state.error) {
return (
<Button
variant="border"
color="danger"
size={props.size}
className="text-sm"
rightSlot={<IconTooltip content={state.error} icon="alert_triangle" />}
onClick={() => {
setupOrConfigureEncryption();
}}
>
{state.error.replace(/^Render Error: /i, '')}
</Button>
);
}
return (
<BaseInput
disableObscureToggle
@@ -488,8 +540,20 @@ function EncryptionInput({
tint={tint}
type={type}
rightSlot={rightSlot}
disabled={state.error != null}
className="pr-1.5" // To account for encryption dropdown
{...props}
/>
);
}
const templateToSecure = createFastMutation({
mutationKey: ['template-to-secure'],
mutationFn: convertTemplateToSecure,
});
const templateToInsecure = createFastMutation({
mutationKey: ['template-to-insecure'],
mutationFn: convertTemplateToInsecure,
disableToastError: true,
});

View File

@@ -1,6 +1,7 @@
import { Link as RouterLink } from '@tanstack/react-router';
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
import { appInfo } from '../../lib/appInfo';
import { Icon } from './Icon';
interface Props extends HTMLAttributes<HTMLAnchorElement> {
@@ -13,9 +14,15 @@ export function Link({ href, children, className, ...other }: Props) {
className = classNames(className, 'relative underline hover:text-violet-600');
if (isExternal) {
let finalHref = href;
if (href.startsWith('https://yaak.app')) {
const url = new URL(href);
url.searchParams.set('ref', appInfo.identifier);
finalHref = url.toString();
}
return (
<a
href={href}
href={finalHref}
target="_blank"
rel="noopener noreferrer"
className={classNames(className, 'pr-4 inline-flex items-center')}

View File

@@ -12,6 +12,7 @@ import {
} from 'react';
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { useRandomKey } from '../../hooks/useRandomKey';
import { useToggle } from '../../hooks/useToggle';
import { languageFromContentType } from '../../lib/contentType';
import { showDialog } from '../../lib/dialog';
@@ -107,6 +108,9 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [pairs, setPairs] = useState<PairWithId[]>([]);
const [showAll, toggleShowAll] = useToggle(false);
// NOTE: Use local force update key because we trigger an effect on forceUpdateKey change. If
// we simply pass forceUpdateKey to the editor, the data set by useEffect will be stale.
const [localForceUpdateKey, regenerateLocalForceUpdateKey] = useRandomKey();
useImperativeHandle(
ref,
@@ -136,6 +140,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
}
setPairs(newPairs);
regenerateLocalForceUpdateKey();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [forceUpdateKey]);
@@ -240,7 +245,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
forcedEnvironmentId={forcedEnvironmentId}
forceFocusNamePairId={forceFocusNamePairId}
forceFocusValuePairId={forceFocusValuePairId}
forceUpdateKey={forceUpdateKey}
forceUpdateKey={localForceUpdateKey}
index={i}
isLast={isLast}
nameAutocomplete={nameAutocomplete}
@@ -290,6 +295,8 @@ type PairEditorRowProps = {
onFocus?: (pair: PairWithId) => void;
onSubmit?: (pair: PairWithId) => void;
isLast?: boolean;
disabled?: boolean;
disableDrag?: boolean;
index: number;
} & Pick<
PairEditorProps,
@@ -311,21 +318,23 @@ type PairEditorRowProps = {
| 'valueValidate'
>;
function PairEditorRow({
export function PairEditorRow({
allowFileValues,
allowMultilineValues,
className,
forcedEnvironmentId,
disableDrag,
disabled,
forceFocusNamePairId,
forceFocusValuePairId,
forceUpdateKey,
forcedEnvironmentId,
index,
isLast,
nameAutocomplete,
namePlaceholder,
nameValidate,
nameAutocompleteFunctions,
nameAutocompleteVariables,
namePlaceholder,
nameValidate,
onChange,
onDelete,
onEnd,
@@ -461,12 +470,12 @@ function PairEditorRow({
<Checkbox
hideLabel
title={pair.enabled ? 'Disable item' : 'Enable item'}
disabled={isLast}
disabled={isLast || disabled}
checked={isLast ? false : !!pair.enabled}
className={classNames(isLast && '!opacity-disabled')}
onChange={handleChangeEnabled}
/>
{!isLast ? (
{!isLast && !disableDrag ? (
<div
className={classNames(
'py-2 h-7 w-4 flex items-center',
@@ -502,6 +511,7 @@ function PairEditorRow({
ref={nameInputRef}
hideLabel
stateKey={`name.${pair.id}.${stateKey}`}
disabled={disabled}
wrapLines={false}
readOnly={pair.readOnlyName}
size="sm"
@@ -523,12 +533,19 @@ function PairEditorRow({
)}
<div className="w-full grid grid-cols-[minmax(0,1fr)_auto] gap-1 items-center">
{pair.isFile ? (
<SelectFile inline size="xs" filePath={pair.value} onChange={handleChangeValueFile} />
<SelectFile
disabled={disabled}
inline
size="xs"
filePath={pair.value}
onChange={handleChangeValueFile}
/>
) : isLast ? (
// Use PlainInput for last ones because there's a unique bug where clicking below
// the Codemirror input focuses it.
<PlainInput
hideLabel
disabled={disabled}
size="sm"
containerClassName={classNames(isLast && 'border-dashed')}
label="Value"
@@ -553,6 +570,7 @@ function PairEditorRow({
stateKey={`value.${pair.id}.${stateKey}`}
wrapLines={false}
size="sm"
disabled={disabled}
containerClassName={classNames(isLast && 'border-dashed')}
validate={valueValidate}
forcedEnvironmentId={forcedEnvironmentId}
@@ -585,8 +603,9 @@ function PairEditorRow({
<IconButton
iconSize="sm"
size="xs"
icon={isLast ? 'empty' : 'chevron_down'}
icon={(isLast || disabled) ? 'empty' : 'chevron_down'}
title="Select form data type"
className="text-text-subtle"
/>
</Dropdown>
)}

View File

@@ -116,6 +116,7 @@ export function PlainInput({
size === 'md' && 'min-h-md',
size === 'sm' && 'min-h-sm',
size === 'xs' && 'min-h-xs',
size === '2xs' && 'min-h-2xs',
)}
>
{tint != null && (

View File

@@ -8,7 +8,7 @@ export type RadioDropdownItem<T = string | null> =
| {
type?: 'default';
label: ReactNode;
shortLabel?: string;
shortLabel?: ReactNode;
value: T;
rightSlot?: ReactNode;
}

View File

@@ -1,13 +1,13 @@
import classNames from 'classnames';
import type { CSSProperties, ReactNode } from 'react';
import { useState } from 'react';
import { useOsInfo } from '../../hooks/useOsInfo';
import type { ButtonProps } from './Button';
import { Button } from './Button';
import { Label } from './Label';
import type { RadioDropdownItem } from './RadioDropdown';
import { RadioDropdown } from './RadioDropdown';
import { HStack } from './Stacks';
import { type } from '@tauri-apps/plugin-os';
export interface SelectProps<T extends string> {
name: string;
@@ -16,6 +16,7 @@ export interface SelectProps<T extends string> {
labelClassName?: string;
hideLabel?: boolean;
value: T;
help?: ReactNode;
leftSlot?: ReactNode;
options: RadioDropdownItem<T>[];
onChange: (value: T) => void;
@@ -28,6 +29,7 @@ export interface SelectProps<T extends string> {
export function Select<T extends string>({
labelPosition = 'top',
name,
help,
labelClassName,
disabled,
hideLabel,
@@ -40,7 +42,6 @@ export function Select<T extends string>({
defaultValue,
size = 'md',
}: SelectProps<T>) {
const osInfo = useOsInfo();
const [focused, setFocused] = useState<boolean>(false);
const id = `input-${name}`;
const isInvalidSelection = options.find((o) => 'value' in o && o.value === value) == null;
@@ -60,10 +61,10 @@ export function Select<T extends string>({
labelPosition === 'top' && 'flex-row gap-0.5',
)}
>
<Label htmlFor={id} visuallyHidden={hideLabel} className={labelClassName}>
<Label htmlFor={id} visuallyHidden={hideLabel} className={labelClassName} help={help}>
{label}
</Label>
{osInfo?.osType === 'macos' ? (
{type() === 'macos' ? (
<HStack
space={2}
className={classNames(

Some files were not shown because too many files have changed in this diff Show More