Compare commits

..

37 Commits

Author SHA1 Message Date
Gregory Schier
badcbc7aef Special case Response()->response() 2024-08-26 15:26:43 -07:00
Gregory Schier
4b91601b98 Clean up code 2024-08-26 15:10:29 -07:00
Gregory Schier
93e0202b86 Default template fn args 2024-08-26 13:10:22 -07:00
Gregory Schier
e75d6abe33 Option to disable telemetry 2024-08-26 12:06:56 -07:00
Gregory Schier
24a4e3494e Node syntaxTree to parse template tags 2024-08-26 11:30:10 -07:00
Gregory Schier
124fb35dcd Force codemirror to parse more to be able to show code folding 2024-08-23 14:09:28 -07:00
Gregory Schier
1aa2839c51 Publish plugin-runtime-types 2024-08-23 13:37:47 -07:00
Gregory Schier
8d3260f394 Fix recursive plugin call locking 2024-08-23 13:20:48 -07:00
Gregory Schier
7e194b9148 Surface plugin error on import 2024-08-23 11:53:40 -07:00
Gregory Schier
f4abc1c7cb Fix initial apply text 2024-08-22 12:49:42 -07:00
Gregory Schier
6766bc8f59 Switch to single quotes for template strings 2024-08-22 12:48:14 -07:00
Gregory Schier
adda44e861 Pass render purpose to render 2024-08-22 11:27:55 -07:00
Gregory Schier
6aab017d3b Move to workspace crate 2024-08-22 10:49:51 -07:00
Gregory Schier
68c1dca9d1 Fix compile 2024-08-22 06:30:19 -07:00
Gregory Schier
066b7ea4f4 Fix deadlock on getting the focused window 2024-08-22 05:46:09 -07:00
Gregory Schier
0763c1b9b8 Some tweaks for beta 2024-08-19 19:10:08 -07:00
Gregory Schier
323e27a047 A bit more chaining cleanup 2024-08-19 16:38:28 -07:00
Gregory Schier
bd02206df2 A bunch of improvements to chaining 2024-08-19 14:10:44 -07:00
Gregory Schier
3411575ecc Actually call template functions 2024-08-19 10:34:22 -07:00
Gregory Schier
1193e1d7aa Async template functions working 2024-08-19 06:21:03 -07:00
Gregory Schier
1b63f33d43 Colored autocompletion icons 2024-08-17 05:47:05 -07:00
Gregory Schier
9f04cfb673 Template Tag Function Editor (#67)
![CleanShot 2024-08-15 at 16 53
09@2x](https://github.com/user-attachments/assets/8c0eb655-1daf-4dc8-811f-f606c770f7dc)
2024-08-16 08:31:19 -07:00
Gregory Schier
1fd1a901f8 Fix sidebar scroll into view 2024-08-15 09:09:18 -07:00
Gregory Schier
06517417b7 Fix some more styles 2024-08-15 05:58:09 -07:00
Gregory Schier
d924709b0c Keep sidebar and cmd+p items in view 2024-08-15 05:50:38 -07:00
Gregory Schier
6d274cea56 Request actions in command palette 2024-08-15 05:37:10 -07:00
Gregory Schier
b95fa25898 Request actions (#65) 2024-08-14 15:31:52 -07:00
Gregory Schier
12f4c2c668 Start on plugin ctx API (#64) 2024-08-14 06:42:54 -07:00
Gregory Schier
e47a2c5fab Variable value as title attr 2024-08-13 10:19:21 -07:00
Gregory Schier
fc279f67a1 Fix dollar sign in Twig grammar 2024-08-13 10:12:09 -07:00
Gregory Schier
6ed6e3e0e0 Fix rose pine themes 2024-08-13 07:51:26 -07:00
Gregory Schier
b5242b9a3f Use new theme vars (#63)
This PR swaps the theme to use the new stuff from the Theme Studio
2024-08-13 07:44:28 -07:00
Gregory Schier
a0950ce5b8 Don't persist settings tab 2024-08-10 08:10:14 -07:00
Gregory Schier
1dd8034cf9 Better curl import 2024-08-10 07:53:26 -07:00
Gregory Schier
7e73b680e6 Catch clipboard errors 2024-08-10 07:33:10 -07:00
Gregory Schier
352ffe9415 Don't show unnamed variables in autocomplete 2024-08-10 07:09:23 -07:00
Gregory Schier
e461851f6f Append [DEV] to window title in dev 2024-08-09 15:35:21 -07:00
274 changed files with 5613 additions and 3815 deletions

View File

@@ -1,43 +1,47 @@
module.exports = {
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:import/recommended",
"plugin:jsx-a11y/recommended",
"plugin:@typescript-eslint/recommended",
"eslint-config-prettier"
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:import/recommended',
'plugin:jsx-a11y/recommended',
'plugin:@typescript-eslint/recommended',
'eslint-config-prettier',
],
parser: "@typescript-eslint/parser",
parser: '@typescript-eslint/parser',
parserOptions: {
project: ["./tsconfig.json"]
project: ['./tsconfig.json'],
},
ignorePatterns: [
"scripts/**/*",
"plugin-runtime/**/*",
"plugin-runtime-types/**/*",
"src-tauri/**/*",
"plugins/**/*",
'scripts/**/*',
'plugin-runtime/**/*',
'plugin-runtime-types/**/*',
'src-tauri/**/*',
'plugins/**/*',
'tailwind.config.cjs',
],
settings: {
react: {
version: "detect"
version: 'detect',
},
"import/resolver": {
'import/resolver': {
node: {
paths: ["src-web"],
extensions: [".ts", ".tsx"]
}
}
paths: ['src-web'],
extensions: ['.ts', '.tsx'],
},
},
},
rules: {
"jsx-a11y/no-autofocus": "off",
"react/react-in-jsx-scope": "off",
"import/no-unresolved": "off",
"@typescript-eslint/consistent-type-imports": ["error", {
prefer: "type-imports",
disallowTypeAnnotations: true,
fixStyle: "separate-type-imports"
}]
}
'jsx-a11y/no-autofocus': 'off',
'react/react-in-jsx-scope': 'off',
'import/no-unresolved': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
disallowTypeAnnotations: true,
fixStyle: 'separate-type-imports',
},
],
},
};

View File

@@ -7,7 +7,8 @@
</scripts>
<node-interpreter value="project" />
<envs>
<env name="RUST_BACKTRACE" value="1" />
</envs>
<method v="2" />
</configuration>
</component>
</component>

View File

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

29
package-lock.json generated
View File

@@ -27,7 +27,7 @@
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@yaakapp/api": "^0.1.4",
"@yaakapp/api": "^0.1.13",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",
@@ -38,6 +38,7 @@
"focus-trap-react": "^10.1.1",
"format-graphql": "^1.4.0",
"framer-motion": "^9.0.4",
"jotai": "^2.9.3",
"lucide-react": "^0.309.0",
"mime": "^4.0.1",
"papaparse": "^5.4.1",
@@ -2989,9 +2990,9 @@
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="
},
"node_modules/@yaakapp/api": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.4.tgz",
"integrity": "sha512-dI5b2WPjTWXkaYBE/ltfxrJDIjIf/ETjMOzrfWDDcgT2GSBNlYmywZRTsk7j5cEbSQbSrDNgXYGJwG0I89aqSg==",
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.13.tgz",
"integrity": "sha512-FSYPHZV0mP967w63VXi9zYP81hPo3vjSW3/UElJLuF/8ig6WmG4p1q2oYos4Ik267Z3qSQAGN5dPMfuk3DAnBA==",
"dependencies": {
"@types/node": "^22.0.0"
}
@@ -7423,6 +7424,26 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/jotai": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.9.3.tgz",
"integrity": "sha512-IqMWKoXuEzWSShjd9UhalNsRGbdju5G2FrqNLQJT+Ih6p41VNYe2sav5hnwQx4HJr25jq9wRqvGSWGviGG6Gjw==",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=17.0.0",
"react": ">=17.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/js-base64": {
"version": "3.7.7",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz",

View File

@@ -39,10 +39,10 @@
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-rc.0",
"@tauri-apps/plugin-dialog": "^2.0.0-rc.0",
"@tauri-apps/plugin-fs": "^2.0.0-rc.0",
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
"@yaakapp/api": "^0.1.4",
"@yaakapp/api": "^0.1.13",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",
@@ -53,6 +53,7 @@
"focus-trap-react": "^10.1.1",
"format-graphql": "^1.4.0",
"framer-motion": "^9.0.4",
"jotai": "^2.9.3",
"lucide-react": "^0.309.0",
"mime": "^4.0.1",
"papaparse": "^5.4.1",

View File

@@ -1,12 +1,12 @@
{
"name": "@yaakapp/api",
"version": "0.1.0-beta.4",
"version": "0.1.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@yaakapp/api",
"version": "0.1.0-beta.4",
"version": "0.1.11",
"dependencies": {
"@types/node": "^22.0.0"
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty";
export type CallTemplateFunctionResponse = { value: string | null, };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,28 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BootRequest } from "./BootRequest";
import type { BootResponse } from "./BootResponse";
import type { CallHttpRequestActionRequest } from "./CallHttpRequestActionRequest";
import type { CallTemplateFunctionRequest } from "./CallTemplateFunctionRequest";
import type { CallTemplateFunctionResponse } from "./CallTemplateFunctionResponse";
import type { CopyTextRequest } from "./CopyTextRequest";
import type { EmptyResponse } from "./EmptyResponse";
import type { ExportHttpRequestRequest } from "./ExportHttpRequestRequest";
import type { ExportHttpRequestResponse } from "./ExportHttpRequestResponse";
import type { FilterRequest } from "./FilterRequest";
import type { FilterResponse } from "./FilterResponse";
import type { FindHttpResponsesRequest } from "./FindHttpResponsesRequest";
import type { FindHttpResponsesResponse } from "./FindHttpResponsesResponse";
import type { GetHttpRequestActionsRequest } from "./GetHttpRequestActionsRequest";
import type { GetHttpRequestActionsResponse } from "./GetHttpRequestActionsResponse";
import type { GetHttpRequestByIdRequest } from "./GetHttpRequestByIdRequest";
import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse";
import type { GetTemplateFunctionsResponse } from "./GetTemplateFunctionsResponse";
import type { ImportRequest } from "./ImportRequest";
import type { ImportResponse } from "./ImportResponse";
import type { RenderHttpRequestRequest } from "./RenderHttpRequestRequest";
import type { RenderHttpRequestResponse } from "./RenderHttpRequestResponse";
import type { SendHttpRequestRequest } from "./SendHttpRequestRequest";
import type { SendHttpRequestResponse } from "./SendHttpRequestResponse";
import type { ShowToastRequest } from "./ShowToastRequest";
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "empty_response" } & EmptyResponse;
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & GetHttpRequestActionsRequest | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "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" } & EmptyResponse;

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RenderPurpose = "send" | "preview";

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Settings = { id: string, model: "settings", createdAt: string, updatedAt: string, theme: string, appearance: string, themeDark: string, themeLight: string, updateChannel: string, interfaceFontSize: number, interfaceScale: number, editorFontSize: number, editorSoftWrap: boolean, openWorkspaceNewWindow: boolean | null, };
export type Settings = { id: string, model: "settings", createdAt: string, updatedAt: string, theme: string, appearance: string, themeDark: string, themeLight: string, updateChannel: string, interfaceFontSize: number, interfaceScale: number, editorFontSize: number, editorSoftWrap: boolean, telemetry: boolean, openWorkspaceNewWindow: boolean | null, };

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { TemplateFunctionCheckboxArg } from "./TemplateFunctionCheckboxArg";
import type { TemplateFunctionHttpRequestArg } from "./TemplateFunctionHttpRequestArg";
import type { TemplateFunctionSelectArg } from "./TemplateFunctionSelectArg";
import type { TemplateFunctionTextArg } from "./TemplateFunctionTextArg";
export type TemplateFunctionArg = { "type": "text" } & TemplateFunctionTextArg | { "type": "select" } & TemplateFunctionSelectArg | { "type": "checkbox" } & TemplateFunctionCheckboxArg | { "type": "http_request" } & TemplateFunctionHttpRequestArg;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { TemplateFunctionSelectOption } from "./TemplateFunctionSelectOption";
export type TemplateFunctionSelectArg = { options: Array<TemplateFunctionSelectOption>, name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,19 @@
export type * from './plugins';
export type * from './themes';
// TODO: The next ts-rs release includes the ability to put everything in 1 file!
export * from './gen/BootRequest';
export * from './gen/BootResponse';
export * from './gen/CallHttpRequestActionArgs';
export * from './gen/CallHttpRequestActionRequest';
export * from './gen/CallTemplateFunctionRequest';
export * from './gen/CallTemplateFunctionResponse';
export * from './gen/CallTemplateFunctionArgs';
export * from './gen/Cookie';
export * from './gen/CookieDomain';
export * from './gen/CookieExpires';
export * from './gen/CookieJar';
export * from './gen/CopyTextRequest';
export * from './gen/EmptyResponse';
export * from './gen/Environment';
export * from './gen/EnvironmentVariable';
@@ -15,11 +22,18 @@ export * from './gen/ExportHttpRequestResponse';
export * from './gen/FilterRequest';
export * from './gen/FilterResponse';
export * from './gen/Folder';
export * from './gen/FindHttpResponsesRequest';
export * from './gen/FindHttpResponsesResponse';
export * from './gen/GetHttpRequestActionsResponse';
export * from './gen/GetHttpRequestByIdRequest';
export * from './gen/GetHttpRequestByIdResponse';
export * from './gen/GetTemplateFunctionsResponse';
export * from './gen/GrpcConnection';
export * from './gen/GrpcEvent';
export * from './gen/GrpcMetadataEntry';
export * from './gen/GrpcRequest';
export * from './gen/HttpRequest';
export * from './gen/HttpRequestAction';
export * from './gen/HttpRequestHeader';
export * from './gen/HttpResponse';
export * from './gen/HttpResponseHeader';
@@ -31,5 +45,21 @@ export * from './gen/InternalEvent';
export * from './gen/InternalEventPayload';
export * from './gen/KeyValue';
export * from './gen/Model';
export * from './gen/RenderHttpRequestRequest';
export * from './gen/RenderHttpRequestResponse';
export * from './gen/RenderPurpose';
export * from './gen/SendHttpRequestRequest';
export * from './gen/SendHttpRequestResponse';
export * from './gen/SendHttpRequestResponse';
export * from './gen/Settings';
export * from './gen/ShowToastRequest';
export * from './gen/TemplateFunction';
export * from './gen/TemplateFunctionArg';
export * from './gen/TemplateFunctionBaseArg';
export * from './gen/TemplateFunctionCheckboxArg';
export * from './gen/TemplateFunctionHttpRequestArg';
export * from './gen/TemplateFunctionSelectArg';
export * from './gen/TemplateFunctionSelectOption';
export * from './gen/TemplateFunctionTextArg';
export * from './gen/ToastVariant';
export * from './gen/Workspace';

View File

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

View File

@@ -1,13 +1,13 @@
import { YaakContext } from './context';
import { Context } from './Context';
export type FilterPluginResponse = string[];
export type FilterPlugin = {
name: string;
description?: string;
canFilter(ctx: YaakContext, args: { mimeType: string }): Promise<boolean>;
canFilter(ctx: Context, args: { mimeType: string }): Promise<boolean>;
onFilter(
ctx: YaakContext,
ctx: Context,
args: { payload: string; mimeType: string },
): Promise<FilterPluginResponse>;
};

View File

@@ -0,0 +1,7 @@
import { CallHttpRequestActionArgs } from '../gen/CallHttpRequestActionArgs';
import { HttpRequestAction } from '../gen/HttpRequestAction';
import { Context } from './Context';
export type HttpRequestActionPlugin = HttpRequestAction & {
onSelect(ctx: Context, args: CallHttpRequestActionArgs): Promise<void> | void;
};

View File

@@ -3,7 +3,7 @@ import { Folder } from '../gen/Folder';
import { HttpRequest } from '../gen/HttpRequest';
import { Workspace } from '../gen/Workspace';
import { AtLeast } from '../helpers';
import { YaakContext } from './context';
import { Context } from './Context';
export type ImportPluginResponse = null | {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
@@ -15,5 +15,5 @@ export type ImportPluginResponse = null | {
export type ImporterPlugin = {
name: string;
description?: string;
onImport(ctx: YaakContext, args: { text: string }): Promise<ImportPluginResponse>;
onImport(ctx: Context, args: { text: string }): Promise<ImportPluginResponse>;
};

View File

@@ -0,0 +1,7 @@
import { CallTemplateFunctionArgs } from '../gen/CallTemplateFunctionArgs';
import { TemplateFunction } from '../gen/TemplateFunction';
import { Context } from './Context';
export type TemplateFunctionPlugin = TemplateFunction & {
onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null>;
};

View File

@@ -0,0 +1,8 @@
import { Theme } from '../themes';
import { Context } from './Context';
export type ThemePlugin = {
name: string;
description?: string;
getTheme(ctx: Context, fileContents: string): Promise<Theme>;
};

View File

@@ -1,12 +0,0 @@
import { HttpRequest } from '../gen/HttpRequest';
import { HttpResponse } from '../gen/HttpResponse';
export type YaakContext = {
metadata: {
getVersion(): Promise<string>;
};
httpRequest: {
send(id: string): Promise<HttpResponse>;
getById(id: string): Promise<HttpRequest | null>;
};
};

View File

@@ -1,8 +0,0 @@
import { HttpRequest } from '../gen/HttpRequest';
import { YaakContext } from './context';
export type HttpRequestActionPlugin = {
key: string;
label: string;
onSelect(ctx: YaakContext, args: { httpRequest: HttpRequest }): void;
};

View File

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

View File

@@ -1,8 +0,0 @@
import { Theme } from '../themes';
import { YaakContext } from './context';
export type ThemePlugin = {
name: string;
description?: string;
getTheme(ctx: YaakContext, fileContents: string): Promise<Theme>;
};

View File

@@ -10,369 +10,10 @@ import * as _m0 from "protobufjs/minimal";
export const protobufPackage = "yaak.plugins.runtime";
export interface PluginInfo {
plugin: string;
}
export interface HookResponse {
info: PluginInfo | undefined;
data: string;
}
export interface HookImportRequest {
data: string;
}
export interface HookResponseFilterRequest {
filter: string;
body: string;
contentType: string;
}
export interface HookExportRequest {
request: string;
}
export interface EventStreamEvent {
event: string;
}
function createBasePluginInfo(): PluginInfo {
return { plugin: "" };
}
export const PluginInfo = {
encode(message: PluginInfo, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.plugin !== "") {
writer.uint32(10).string(message.plugin);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): PluginInfo {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBasePluginInfo();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.plugin = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): PluginInfo {
return { plugin: isSet(object.plugin) ? globalThis.String(object.plugin) : "" };
},
toJSON(message: PluginInfo): unknown {
const obj: any = {};
if (message.plugin !== "") {
obj.plugin = message.plugin;
}
return obj;
},
create(base?: DeepPartial<PluginInfo>): PluginInfo {
return PluginInfo.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<PluginInfo>): PluginInfo {
const message = createBasePluginInfo();
message.plugin = object.plugin ?? "";
return message;
},
};
function createBaseHookResponse(): HookResponse {
return { info: undefined, data: "" };
}
export const HookResponse = {
encode(message: HookResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.info !== undefined) {
PluginInfo.encode(message.info, writer.uint32(10).fork()).ldelim();
}
if (message.data !== "") {
writer.uint32(18).string(message.data);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HookResponse {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHookResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.info = PluginInfo.decode(reader, reader.uint32());
continue;
case 2:
if (tag !== 18) {
break;
}
message.data = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HookResponse {
return {
info: isSet(object.info) ? PluginInfo.fromJSON(object.info) : undefined,
data: isSet(object.data) ? globalThis.String(object.data) : "",
};
},
toJSON(message: HookResponse): unknown {
const obj: any = {};
if (message.info !== undefined) {
obj.info = PluginInfo.toJSON(message.info);
}
if (message.data !== "") {
obj.data = message.data;
}
return obj;
},
create(base?: DeepPartial<HookResponse>): HookResponse {
return HookResponse.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<HookResponse>): HookResponse {
const message = createBaseHookResponse();
message.info = (object.info !== undefined && object.info !== null)
? PluginInfo.fromPartial(object.info)
: undefined;
message.data = object.data ?? "";
return message;
},
};
function createBaseHookImportRequest(): HookImportRequest {
return { data: "" };
}
export const HookImportRequest = {
encode(message: HookImportRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.data !== "") {
writer.uint32(10).string(message.data);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HookImportRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHookImportRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.data = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HookImportRequest {
return { data: isSet(object.data) ? globalThis.String(object.data) : "" };
},
toJSON(message: HookImportRequest): unknown {
const obj: any = {};
if (message.data !== "") {
obj.data = message.data;
}
return obj;
},
create(base?: DeepPartial<HookImportRequest>): HookImportRequest {
return HookImportRequest.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<HookImportRequest>): HookImportRequest {
const message = createBaseHookImportRequest();
message.data = object.data ?? "";
return message;
},
};
function createBaseHookResponseFilterRequest(): HookResponseFilterRequest {
return { filter: "", body: "", contentType: "" };
}
export const HookResponseFilterRequest = {
encode(message: HookResponseFilterRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.filter !== "") {
writer.uint32(10).string(message.filter);
}
if (message.body !== "") {
writer.uint32(18).string(message.body);
}
if (message.contentType !== "") {
writer.uint32(26).string(message.contentType);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HookResponseFilterRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHookResponseFilterRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.filter = reader.string();
continue;
case 2:
if (tag !== 18) {
break;
}
message.body = reader.string();
continue;
case 3:
if (tag !== 26) {
break;
}
message.contentType = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HookResponseFilterRequest {
return {
filter: isSet(object.filter) ? globalThis.String(object.filter) : "",
body: isSet(object.body) ? globalThis.String(object.body) : "",
contentType: isSet(object.contentType) ? globalThis.String(object.contentType) : "",
};
},
toJSON(message: HookResponseFilterRequest): unknown {
const obj: any = {};
if (message.filter !== "") {
obj.filter = message.filter;
}
if (message.body !== "") {
obj.body = message.body;
}
if (message.contentType !== "") {
obj.contentType = message.contentType;
}
return obj;
},
create(base?: DeepPartial<HookResponseFilterRequest>): HookResponseFilterRequest {
return HookResponseFilterRequest.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<HookResponseFilterRequest>): HookResponseFilterRequest {
const message = createBaseHookResponseFilterRequest();
message.filter = object.filter ?? "";
message.body = object.body ?? "";
message.contentType = object.contentType ?? "";
return message;
},
};
function createBaseHookExportRequest(): HookExportRequest {
return { request: "" };
}
export const HookExportRequest = {
encode(message: HookExportRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.request !== "") {
writer.uint32(10).string(message.request);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HookExportRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHookExportRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.request = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HookExportRequest {
return { request: isSet(object.request) ? globalThis.String(object.request) : "" };
},
toJSON(message: HookExportRequest): unknown {
const obj: any = {};
if (message.request !== "") {
obj.request = message.request;
}
return obj;
},
create(base?: DeepPartial<HookExportRequest>): HookExportRequest {
return HookExportRequest.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<HookExportRequest>): HookExportRequest {
const message = createBaseHookExportRequest();
message.request = object.request ?? "";
return message;
},
};
function createBaseEventStreamEvent(): EventStreamEvent {
return { event: "" };
}

View File

@@ -1,4 +1,17 @@
import { ImportResponse, InternalEvent, InternalEventPayload } from '@yaakapp/api';
import {
Context,
FindHttpResponsesResponse,
GetHttpRequestByIdResponse,
HttpRequestAction,
ImportResponse,
InternalEvent,
InternalEventPayload,
RenderHttpRequestResponse,
SendHttpRequestResponse,
TemplateFunction,
} from '@yaakapp/api';
import { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/httpRequestAction';
import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
import interceptStdout from 'intercept-stdout';
import * as console from 'node:console';
import { readFileSync } from 'node:fs';
@@ -7,7 +20,7 @@ import * as util from 'node:util';
import { parentPort, workerData } from 'node:worker_threads';
new Promise<void>(async (resolve, reject) => {
const { pluginDir /*, pluginRefId*/ } = workerData;
const { pluginDir, pluginRefId } = workerData;
const pathPkg = path.join(pluginDir, 'package.json');
// NOTE: Use POSIX join because require() needs forward slash
@@ -33,10 +46,93 @@ new Promise<void>(async (resolve, reject) => {
console.log('Plugin initialized', pkg.name, capabilities, Object.keys(mod));
// Message comes into the plugin to be processed
parentPort!.on('message', async ({ payload, pluginRefId, id: replyId }: InternalEvent) => {
console.log(`Received ${payload.type}`);
function buildEventToSend(
payload: InternalEventPayload,
replyId: string | null = null,
): InternalEvent {
return { pluginRefId, id: genId(), replyId, payload };
}
function sendEmpty(replyId: string | null = null): string {
return sendPayload({ type: 'empty_response' }, replyId);
}
function sendPayload(payload: InternalEventPayload, replyId: string | null): string {
const event = buildEventToSend(payload, replyId);
sendEvent(event);
return event.id;
}
function sendEvent(event: InternalEvent) {
if (event.payload.type !== 'empty_response') {
console.log('Sending event to app', event.id, event.payload.type);
}
parentPort!.postMessage(event);
}
async function sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
payload: InternalEventPayload,
): Promise<T> {
// 1. Build event to send
const eventToSend = buildEventToSend(payload, null);
// 2. Spawn listener in background
const promise = new Promise<InternalEventPayload>(async (resolve) => {
const cb = (event: InternalEvent) => {
if (event.replyId === eventToSend.id) {
parentPort!.off('message', cb); // Unlisten, now that we're done
resolve(event.payload); // Not type-safe but oh well
}
};
parentPort!.on('message', cb);
});
// 3. Send the event after we start listening (to prevent race)
sendEvent(eventToSend);
// 4. Return the listener promise
return promise as unknown as Promise<T>;
}
const ctx: Context = {
clipboard: {
async copyText(text) {
await sendAndWaitForReply({ type: 'copy_text_request', text });
},
},
toast: {
async show(args) {
await sendAndWaitForReply({ type: 'show_toast_request', ...args });
},
},
httpResponse: {
async find(args) {
const payload = { type: 'find_http_responses_request', ...args } as const;
const { httpResponses } = await sendAndWaitForReply<FindHttpResponsesResponse>(payload);
return httpResponses;
},
},
httpRequest: {
async getById(args) {
const payload = { type: 'get_http_request_by_id_request', ...args } as const;
const { httpRequest } = await sendAndWaitForReply<GetHttpRequestByIdResponse>(payload);
return httpRequest;
},
async send(args) {
const payload = { type: 'send_http_request_request', ...args } as const;
const { httpResponse } = await sendAndWaitForReply<SendHttpRequestResponse>(payload);
return httpResponse;
},
async render(args) {
const payload = { type: 'render_http_request_request', ...args } as const;
const result = await sendAndWaitForReply<RenderHttpRequestResponse>(payload);
return result.httpRequest;
},
},
};
// Message comes into the plugin to be processed
parentPort!.on('message', async ({ payload, id: replyId }: InternalEvent) => {
try {
if (payload.type === 'boot_request') {
const payload: InternalEventPayload = {
@@ -45,18 +141,18 @@ new Promise<void>(async (resolve, reject) => {
version: pkg.version,
capabilities,
};
sendToServer({ id: genId(), pluginRefId, replyId, payload });
sendPayload(payload, replyId);
return;
}
if (payload.type === 'import_request' && typeof mod.pluginHookImport === 'function') {
const reply: ImportResponse | null = await mod.pluginHookImport({}, payload.content);
const reply: ImportResponse | null = await mod.pluginHookImport(ctx, payload.content);
if (reply != null) {
const replyPayload: InternalEventPayload = {
type: 'import_response',
resources: reply?.resources,
};
sendToServer({ id: genId(), pluginRefId, replyId, payload: replyPayload });
sendPayload(replyPayload, replyId);
return;
} else {
// Continue, to send back an empty reply
@@ -67,36 +163,98 @@ new Promise<void>(async (resolve, reject) => {
payload.type === 'export_http_request_request' &&
typeof mod.pluginHookExport === 'function'
) {
const reply: string = await mod.pluginHookExport({}, payload.httpRequest);
const reply: string = await mod.pluginHookExport(ctx, payload.httpRequest);
const replyPayload: InternalEventPayload = {
type: 'export_http_request_response',
content: reply,
};
sendToServer({ id: genId(), pluginRefId, replyId, payload: replyPayload });
sendPayload(replyPayload, replyId);
return;
}
if (payload.type === 'filter_request' && typeof mod.pluginHookResponseFilter === 'function') {
const reply: string = await mod.pluginHookResponseFilter(
{},
{ filter: payload.filter, body: payload.content },
);
const reply: string = await mod.pluginHookResponseFilter(ctx, {
filter: payload.filter,
body: payload.content,
});
const replyPayload: InternalEventPayload = {
type: 'filter_response',
content: reply,
};
sendToServer({ id: genId(), pluginRefId, replyId, payload: replyPayload });
sendPayload(replyPayload, replyId);
return;
}
if (
payload.type === 'get_http_request_actions_request' &&
Array.isArray(mod.plugin?.httpRequestActions)
) {
const reply: HttpRequestAction[] = mod.plugin.httpRequestActions.map(
(a: HttpRequestActionPlugin) => ({
...a,
// Add everything except onSelect
onSelect: undefined,
}),
);
const replyPayload: InternalEventPayload = {
type: 'get_http_request_actions_response',
pluginRefId,
actions: reply,
};
sendPayload(replyPayload, replyId);
return;
}
if (
payload.type === 'get_template_functions_request' &&
Array.isArray(mod.plugin?.templateFunctions)
) {
const reply: TemplateFunction[] = mod.plugin.templateFunctions.map(
(a: TemplateFunctionPlugin) => ({
...a,
// Add everything except render
onRender: undefined,
}),
);
const replyPayload: InternalEventPayload = {
type: 'get_template_functions_response',
pluginRefId,
functions: reply,
};
sendPayload(replyPayload, replyId);
return;
}
if (
payload.type === 'call_http_request_action_request' &&
Array.isArray(mod.plugin?.httpRequestActions)
) {
const action = mod.plugin.httpRequestActions.find((a) => a.key === payload.key);
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
sendEmpty(replyId);
return;
}
}
if (
payload.type === 'call_template_function_request' &&
Array.isArray(mod.plugin?.templateFunctions)
) {
const action = mod.plugin.templateFunctions.find((a) => a.name === payload.name);
if (typeof action?.onRender === 'function') {
const result = await action.onRender(ctx, payload.args);
sendPayload({ type: 'call_template_function_response', value: result ?? null }, replyId);
return;
}
}
} catch (err) {
console.log('Plugin call threw exception', payload.type, err);
// TODO: Return errors to server
}
// No matches, so send back an empty response so the caller doesn't block forever
const id = genId();
console.log('Sending nothing back to', id, { replyId });
sendToServer({ id, pluginRefId, replyId, payload: { type: 'empty_response' } });
sendEmpty(replyId);
});
resolve();
@@ -104,10 +262,6 @@ new Promise<void>(async (resolve, reject) => {
console.log('failed to boot plugin', err);
});
function sendToServer(e: InternalEvent) {
parentPort!.postMessage(e);
}
function genId(len = 5): string {
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let id = '';

View File

@@ -1,16 +1,16 @@
const {readdirSync, cpSync} = require("node:fs");
const path = require("node:path");
const {execSync} = require("node:child_process");
const { readdirSync, cpSync } = require('node:fs');
const path = require('node:path');
const { execSync } = require('node:child_process');
const pluginsDir = process.env.YAAK_PLUGINS_DIR;
if (!pluginsDir) {
console.log("YAAK_PLUGINS_DIR is not set");
console.log('YAAK_PLUGINS_DIR is not set');
process.exit(1);
}
console.log('Installing Yaak plugins dependencies', pluginsDir);
execSync('npm ci', {cwd: pluginsDir});
execSync('npm ci', { cwd: pluginsDir });
console.log('Building Yaak plugins', pluginsDir);
execSync('npm run build', {cwd: pluginsDir});
execSync('npm run build', { cwd: pluginsDir });
console.log('Copying Yaak plugins to', pluginsDir);

90
src-tauri/Cargo.lock generated
View File

@@ -2052,30 +2052,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "grpc"
version = "0.1.0"
dependencies = [
"anyhow",
"dunce",
"hyper 0.14.30",
"hyper-rustls 0.24.2",
"log",
"md5",
"prost 0.12.6",
"prost-reflect",
"prost-types 0.12.6",
"serde",
"serde_json",
"tauri",
"tauri-plugin-shell",
"tokio",
"tokio-stream",
"tonic 0.10.2",
"tonic-reflection",
"uuid",
]
[[package]]
name = "gtk"
version = "0.18.1"
@@ -4943,9 +4919,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.205"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150"
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
dependencies = [
"serde_derive",
]
@@ -4973,9 +4949,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.205"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1"
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
dependencies = [
"proc-macro2",
"quote",
@@ -4995,9 +4971,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.122"
version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
dependencies = [
"itoa 1.0.11",
"memchr",
@@ -5868,9 +5844,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-clipboard-manager"
version = "2.0.0-rc.0"
version = "2.1.0-beta.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a26868f7e05a09673e4172d23acb82cd48911cca092f0e8d06179a69e5024c"
checksum = "becbc5a692e842f8d6a7ab5e490c3c36d267b5c3d5bf4b6a0cdd039d7df25569"
dependencies = [
"arboard",
"image 0.24.9",
@@ -6125,13 +6101,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "templates"
version = "0.1.0"
dependencies = [
"log",
]
[[package]]
name = "tendril"
version = "0.4.3"
@@ -6249,9 +6218,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.39.2"
version = "1.39.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
dependencies = [
"backtrace",
"bytes",
@@ -7573,7 +7542,6 @@ dependencies = [
"chrono",
"cocoa",
"datetime",
"grpc",
"hex_color",
"http 1.1.0",
"log",
@@ -7597,13 +7565,38 @@ dependencies = [
"tauri-plugin-shell",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"templates",
"thiserror",
"tokio",
"tokio-stream",
"uuid",
"yaak_grpc",
"yaak_models",
"yaak_plugin_runtime",
"yaak_templates",
]
[[package]]
name = "yaak_grpc"
version = "0.1.0"
dependencies = [
"anyhow",
"dunce",
"hyper 0.14.30",
"hyper-rustls 0.24.2",
"log",
"md5",
"prost 0.12.6",
"prost-reflect",
"prost-types 0.12.6",
"serde",
"serde_json",
"tauri",
"tauri-plugin-shell",
"tokio",
"tokio-stream",
"tonic 0.10.2",
"tonic-reflection",
"uuid",
]
[[package]]
@@ -7651,6 +7644,17 @@ dependencies = [
"yaak_models",
]
[[package]]
name = "yaak_templates"
version = "0.1.0"
dependencies = [
"log",
"serde",
"serde_json",
"tokio",
"ts-rs",
]
[[package]]
name = "zbus"
version = "4.0.1"

View File

@@ -1,5 +1,5 @@
[workspace]
members = ["grpc", "templates", "yaak_plugin_runtime", "yaak_models"]
members = ["yaak_grpc", "yaak_templates", "yaak_plugin_runtime", "yaak_models"]
[package]
@@ -26,9 +26,10 @@ cocoa = "0.25.0"
openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
[dependencies]
grpc = { path = "./grpc" }
templates = { path = "./templates" }
yaak_plugin_runtime = { path = "yaak_plugin_runtime" }
yaak_grpc = { path = "yaak_grpc" }
yaak_templates = { path = "yaak_templates" }
yaak_plugin_runtime = { workspace = true }
yaak_models = { workspace = true }
anyhow = "1.0.86"
base64 = "0.22.0"
chrono = { version = "0.4.31", features = ["serde"] }
@@ -43,9 +44,9 @@ reqwest_cookie_store = "0.8.0"
serde = { version = "1.0.198", features = ["derive"] }
serde_json = { version = "1.0.116", features = ["raw_value"] }
serde_yaml = "0.9.34"
tauri = { workspace = true }
tauri = { workspace = true, features = ["unstable"] }
tauri-plugin-shell = { workspace = true }
tauri-plugin-clipboard-manager = "2.0.0-rc.0"
tauri-plugin-clipboard-manager = "2.1.0-beta.7"
tauri-plugin-dialog = "2.0.0-rc.0"
tauri-plugin-fs = "2.0.0-rc.0"
tauri-plugin-log = { version = "2.0.0-rc.0", features = ["colored"] }
@@ -54,12 +55,12 @@ tauri-plugin-updater = "2.0.0-rc.0"
tauri-plugin-window-state = "2.0.0-rc.0"
tokio = { version = "1.36.0", features = ["sync"] }
tokio-stream = "0.1.15"
yaak_models = {workspace = true}
uuid = "1.7.0"
thiserror = "1.0.61"
mime_guess = "2.0.5"
[workspace.dependencies]
yaak_models = { path = "yaak_models" }
yaak_plugin_runtime = { path = "yaak_plugin_runtime" }
tauri = { version = "2.0.0-rc.2", features = ["devtools", "protocol-asset"] }
tauri-plugin-shell = "2.0.0-rc.0"

View File

@@ -11,6 +11,7 @@
"core:event:allow-listen",
"core:event:allow-unlisten",
"os:allow-os-type",
"clipboard-manager:allow-clear",
"clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-text",
"dialog:allow-open",

View File

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

View File

@@ -0,0 +1,2 @@
ALTER TABLE environments DROP COLUMN model;
ALTER TABLE environments ADD COLUMN model TEXT DEFAULT 'environment';

View File

@@ -0,0 +1 @@
ALTER TABLE settings ADD COLUMN telemetry BOOLEAN DEFAULT TRUE;

View File

@@ -3,9 +3,9 @@ use std::fmt::Display;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tauri::{AppHandle, Manager};
use tauri::{Manager, Runtime, WebviewWindow};
use yaak_models::queries::{generate_id, get_key_value_int, get_key_value_string, set_key_value_int, set_key_value_string};
use yaak_models::queries::{generate_id, get_key_value_int, get_key_value_string, get_or_create_settings, set_key_value_int, set_key_value_string};
use crate::is_dev;
@@ -36,7 +36,7 @@ pub enum AnalyticsResource {
impl AnalyticsResource {
pub fn from_str(s: &str) -> serde_json::Result<AnalyticsResource> {
return serde_json::from_str(format!("\"{s}\"").as_str());
serde_json::from_str(format!("\"{s}\"").as_str())
}
}
@@ -74,7 +74,7 @@ pub enum AnalyticsAction {
impl AnalyticsAction {
pub fn from_str(s: &str) -> serde_json::Result<AnalyticsAction> {
return serde_json::from_str(format!("\"{s}\"").as_str());
serde_json::from_str(format!("\"{s}\"").as_str())
}
}
@@ -96,19 +96,18 @@ pub struct LaunchEventInfo {
pub num_launches: i32,
}
pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo {
pub async fn track_launch_event<R: Runtime>(w: &WebviewWindow<R>) -> LaunchEventInfo {
let last_tracked_version_key = "last_tracked_version";
let mut info = LaunchEventInfo::default();
info.num_launches = get_num_launches(app).await + 1;
info.previous_version =
get_key_value_string(app, NAMESPACE, last_tracked_version_key, "").await;
info.current_version = app.package_info().version.to_string();
info.num_launches = get_num_launches(w).await + 1;
info.previous_version = get_key_value_string(w, NAMESPACE, last_tracked_version_key, "").await;
info.current_version = w.package_info().version.to_string();
if info.previous_version.is_empty() {
track_event(
app,
w,
AnalyticsResource::App,
AnalyticsAction::LaunchFirst,
None,
@@ -118,7 +117,7 @@ pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo {
info.launched_after_update = info.current_version != info.previous_version;
if info.launched_after_update {
track_event(
app,
w,
AnalyticsResource::App,
AnalyticsAction::LaunchUpdate,
Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })),
@@ -129,7 +128,7 @@ pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo {
// Track a launch event in all cases
track_event(
app,
w,
AnalyticsResource::App,
AnalyticsAction::Launch,
Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })),
@@ -139,27 +138,28 @@ pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo {
// Update key values
set_key_value_string(
app,
w,
NAMESPACE,
last_tracked_version_key,
info.current_version.as_str(),
)
.await;
set_key_value_int(app, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches).await;
set_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches).await;
info
}
pub async fn track_event(
app_handle: &AppHandle,
pub async fn track_event<R: Runtime>(
w: &WebviewWindow<R>,
resource: AnalyticsResource,
action: AnalyticsAction,
attributes: Option<Value>,
) {
let id = get_id(app_handle).await;
let id = get_id(w).await;
let event = format!("{}.{}", resource, action);
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
let info = app_handle.package_info();
let info = w.app_handle().package_info();
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
let site = match is_dev() {
true => "site_TkHWjoXwZPq3HfhERb",
@@ -177,7 +177,7 @@ pub async fn track_event(
("v", info.version.clone().to_string()),
("os", get_os().to_string()),
("tz", tz),
("xy", get_window_size(app_handle)),
("xy", get_window_size(w)),
];
let req = reqwest::Client::builder()
.build()
@@ -185,9 +185,15 @@ pub async fn track_event(
.get(format!("{base_url}/t/e"))
.query(&params);
let settings = get_or_create_settings(w).await;
if !settings.telemetry {
info!("Track event (disabled): {}", event);
return
}
// Disable analytics actual sending in dev
if is_dev() {
debug!("track: {} {}", event, attributes_json);
debug!("Track event: {} {}", event, attributes_json);
return;
}
@@ -208,13 +214,8 @@ fn get_os() -> &'static str {
}
}
fn get_window_size(app_handle: &AppHandle) -> String {
let window = match app_handle.webview_windows().into_values().next() {
Some(w) => w,
None => return "unknown".to_string(),
};
let current_monitor = match window.current_monitor() {
fn get_window_size<R: Runtime>(w: &WebviewWindow<R>) -> String {
let current_monitor = match w.current_monitor() {
Ok(Some(m)) => m,
_ => return "unknown".to_string(),
};
@@ -231,17 +232,17 @@ fn get_window_size(app_handle: &AppHandle) -> String {
)
}
async fn get_id(app_handle: &AppHandle) -> String {
let id = get_key_value_string(app_handle, "analytics", "id", "").await;
async fn get_id<R: Runtime>(w: &WebviewWindow<R>) -> String {
let id = get_key_value_string(w, "analytics", "id", "").await;
if id.is_empty() {
let new_id = generate_id();
set_key_value_string(app_handle, "analytics", "id", new_id.as_str()).await;
set_key_value_string(w, "analytics", "id", new_id.as_str()).await;
new_id
} else {
id
}
}
pub async fn get_num_launches(app: &AppHandle) -> i32 {
get_key_value_int(app, NAMESPACE, NUM_LAUNCHES_KEY, 0).await
pub async fn get_num_launches<R: Runtime>(w: &WebviewWindow<R>) -> i32 {
get_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, 0).await
}

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use KeyAndValueRef::{Ascii, Binary};
use grpc::{KeyAndValueRef, MetadataMap};
use yaak_grpc::{KeyAndValueRef, MetadataMap};
pub fn metadata_to_map(metadata: MetadataMap) -> HashMap<String, String> {
let mut entries = HashMap::new();

View File

@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::fs;
use std::fs::{create_dir_all, File};
use std::io::Write;
@@ -6,38 +7,48 @@ use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use crate::render::variables_from_environment;
use crate::{render, response_err};
use crate::render::render_http_request;
use crate::response_err;
use crate::template_callback::PluginTemplateCallback;
use base64::Engine;
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue};
use log::{error, info, warn};
use log::{error, warn};
use mime_guess::Mime;
use reqwest::redirect::Policy;
use reqwest::Method;
use reqwest::{multipart, Url};
use tauri::{Manager, WebviewWindow};
use serde_json::Value;
use tauri::{Manager, Runtime, WebviewWindow};
use tokio::sync::oneshot;
use tokio::sync::watch::Receiver;
use yaak_models::models::{Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader};
use yaak_models::models::{
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader,
};
use yaak_models::queries::{get_workspace, update_response_if_id, upsert_cookie_jar};
pub async fn send_http_request(
window: &WebviewWindow,
request: HttpRequest,
pub async fn send_http_request<R: Runtime>(
window: &WebviewWindow<R>,
request: &HttpRequest,
response: &HttpResponse,
environment: Option<Environment>,
cookie_jar: Option<CookieJar>,
download_path: Option<PathBuf>,
cancel_rx: &mut Receiver<bool>,
) -> Result<HttpResponse, String> {
let environment_ref = environment.as_ref();
let workspace = get_workspace(window, &request.workspace_id)
.await
.expect("Failed to get Workspace");
let vars = variables_from_environment(&workspace, environment_ref);
let cb = &*window.app_handle().state::<PluginTemplateCallback>();
let cb = cb.for_send();
let rendered_request = render_http_request(
&request,
&workspace,
environment.as_ref(),
&cb,
)
.await;
let mut url_string = render::render(&request.url, &vars);
let mut url_string = rendered_request.url;
url_string = ensure_proto(&url_string);
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
@@ -114,7 +125,7 @@ pub async fn send_http_request(
}
};
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
let m = Method::from_bytes(rendered_request.method.to_uppercase().as_bytes())
.expect("Failed to create method");
let mut request_builder = client.request(m, url);
@@ -137,7 +148,7 @@ pub async fn send_http_request(
// );
// }
for h in request.headers {
for h in rendered_request.headers {
if h.name.is_empty() && h.value.is_empty() {
continue;
}
@@ -146,17 +157,14 @@ pub async fn send_http_request(
continue;
}
let name = render::render(&h.name, &vars);
let value = render::render(&h.value, &vars);
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
let header_name = match HeaderName::from_bytes(h.name.as_bytes()) {
Ok(n) => n,
Err(e) => {
error!("Failed to create header name: {}", e);
continue;
}
};
let header_value = match HeaderValue::from_str(value.as_str()) {
let header_value = match HeaderValue::from_str(h.value.as_str()) {
Ok(n) => n,
Err(e) => {
error!("Failed to create header value: {}", e);
@@ -167,23 +175,21 @@ pub async fn send_http_request(
headers.insert(header_name, header_value);
}
if let Some(b) = &request.authentication_type {
if let Some(b) = &rendered_request.authentication_type {
let empty_value = &serde_json::to_value("").unwrap();
let a = request.authentication;
let a = rendered_request.authentication;
if b == "basic" {
let raw_username = a
let username = a
.get("username")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let raw_password = a
.unwrap_or_default();
let password = a
.get("password")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let username = render::render(raw_username, &vars);
let password = render::render(raw_password, &vars);
.unwrap_or_default();
let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
@@ -192,8 +198,11 @@ pub async fn send_http_request(
HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
);
} else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
let token = render::render(raw_token, &vars);
let token = a
.get("token")
.unwrap_or(empty_value)
.as_str()
.unwrap_or_default();
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
@@ -202,56 +211,38 @@ pub async fn send_http_request(
}
let mut query_params = Vec::new();
for p in request.url_parameters {
for p in rendered_request.url_parameters {
if !p.enabled || p.name.is_empty() {
continue;
}
query_params.push((
render::render(&p.name, &vars),
render::render(&p.value, &vars),
));
query_params.push((p.name, p.value));
}
request_builder = request_builder.query(&query_params);
if let Some(body_type) = &request.body_type {
let empty_string = &serde_json::to_value("").unwrap();
let empty_bool = &serde_json::to_value(false).unwrap();
let request_body = request.body;
let request_body = rendered_request.body;
if let Some(body_type) = &rendered_request.body_type {
if request_body.contains_key("text") {
let raw_text = request_body
.get("text")
.unwrap_or(empty_string)
.as_str()
.unwrap_or("");
let body = render::render(raw_text, &vars);
request_builder = request_builder.body(body);
let body = get_str_h(&request_body, "text");
request_builder = request_builder.body(body.to_owned());
} else if body_type == "application/x-www-form-urlencoded"
&& request_body.contains_key("form")
{
let mut form_params = Vec::new();
let form = request_body.get("form");
if let Some(f) = form {
for p in f.as_array().unwrap_or(&Vec::new()) {
let enabled = p
.get("enabled")
.unwrap_or(empty_bool)
.as_bool()
.unwrap_or(false);
let name = p
.get("name")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
if !enabled || name.is_empty() {
continue;
match f.as_array() {
None => {}
Some(a) => {
for p in a {
let enabled = get_bool(p, "enabled");
let name = get_str(p, "name");
if !enabled || name.is_empty() {
continue;
}
let value = get_str(p, "value");
form_params.push((name, value));
}
}
let value = p
.get("value")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
form_params.push((render::render(name, &vars), render::render(value, &vars)));
}
}
request_builder = request_builder.form(&form_params);
@@ -273,77 +264,59 @@ pub async fn send_http_request(
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
let mut multipart_form = multipart::Form::new();
if let Some(form_definition) = request_body.get("form") {
for p in form_definition.as_array().unwrap_or(&Vec::new()) {
let enabled = p
.get("enabled")
.unwrap_or(empty_bool)
.as_bool()
.unwrap_or(false);
let name_raw = p
.get("name")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
match form_definition.as_array() {
None => {}
Some(fd) => {
for p in fd {
let enabled = get_bool(p, "enabled");
let name = get_str(p, "name").to_string();
if !enabled || name_raw.is_empty() {
continue;
}
let file_path = p
.get("file")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
let value_raw = p
.get("value")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
let name = render::render(name_raw, &vars);
let mut part = if file_path.is_empty() {
multipart::Part::text(render::render(value_raw, &vars))
} else {
match fs::read(file_path) {
Ok(f) => multipart::Part::bytes(f),
Err(e) => {
return response_err(response, e.to_string(), window).await;
if !enabled || name.is_empty() {
continue;
}
let file_path = get_str(p, "file").to_owned();
let value = get_str(p, "value").to_owned();
let mut part = if file_path.is_empty() {
multipart::Part::text(value.clone())
} else {
match fs::read(file_path.clone()) {
Ok(f) => multipart::Part::bytes(f),
Err(e) => {
return response_err(response, e.to_string(), window).await;
}
}
};
let content_type = get_str(p, "contentType");
// Set or guess mimetype
if !content_type.is_empty() {
part = part.mime_str(content_type).map_err(|e| e.to_string())?;
} else if !file_path.is_empty() {
let default_mime =
Mime::from_str("application/octet-stream").unwrap();
let mime =
mime_guess::from_path(file_path.clone()).first_or(default_mime);
part = part
.mime_str(mime.essence_str())
.map_err(|e| e.to_string())?;
}
// Set file path if not empty
if !file_path.is_empty() {
let filename = PathBuf::from(file_path)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
part = part.file_name(filename);
}
multipart_form = multipart_form.part(name, part);
}
};
let ct_raw = p
.get("contentType")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
// Set or guess mimetype
if !ct_raw.is_empty() {
let content_type = render::render(ct_raw, &vars);
part = part
.mime_str(content_type.as_str())
.map_err(|e| e.to_string())?;
} else if !file_path.is_empty() {
let default_mime = Mime::from_str("application/octet-stream").unwrap();
let mime = mime_guess::from_path(file_path).first_or(default_mime);
part = part
.mime_str(mime.essence_str())
.map_err(|e| e.to_string())?;
}
// Set fil path if not empty
if !file_path.is_empty() {
let filename = PathBuf::from(file_path)
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string();
part = part.file_name(filename);
}
multipart_form = multipart_form.part(name, part);
}
}
headers.remove("Content-Type"); // reqwest will add this automatically
@@ -442,16 +415,6 @@ pub async fn send_http_request(
.await
.expect("Failed to update response");
// Copy response to the download path, if specified
match (download_path, response.body_path.clone()) {
(Some(dl_path), Some(body_path)) => {
info!("Downloading response body to {}", dl_path.display());
fs::copy(body_path, dl_path)
.expect("Failed to copy file for response download");
}
_ => {}
};
// Add cookie store if specified
if let Some((cookie_store, mut cookie_jar)) = maybe_cookie_manager {
// let cookies = response_headers.get_all(SET_COOKIE).iter().map(|h| {
@@ -466,7 +429,8 @@ pub async fn send_http_request(
.unwrap()
.iter_any()
.map(|c| {
let json_cookie = serde_json::to_value(&c).expect("Failed to serialize cookie");
let json_cookie =
serde_json::to_value(&c).expect("Failed to serialize cookie");
serde_json::from_value(json_cookie).expect("Failed to deserialize cookie")
})
.collect::<Vec<_>>();
@@ -504,3 +468,24 @@ fn ensure_proto(url_str: &str) -> String {
format!("http://{url_str}")
}
fn get_bool(v: &Value, key: &str) -> bool {
match v.get(key) {
None => false,
Some(v) => v.as_bool().unwrap_or_default(),
}
}
fn get_str<'a>(v: &'a Value, key: &str) -> &'a str {
match v.get(key) {
None => "",
Some(v) => v.as_str().unwrap_or_default(),
}
}
fn get_str_h<'a>(v: &'a HashMap<String, Value>, key: &str) -> &'a str {
match v.get(key) {
None => "",
Some(v) => v.as_str().unwrap_or_default(),
}
}

View File

@@ -16,17 +16,18 @@ use fern::colors::ColoredLevelConfig;
use log::{debug, error, info, warn};
use rand::random;
use serde_json::{json, Value};
use tauri::Listener;
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::{AppHandle, Emitter, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{Listener, Runtime};
use tauri::{Manager, WindowEvent};
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_log::{fern, Target, TargetKind};
use tauri_plugin_shell::ShellExt;
use tokio::sync::Mutex;
use ::grpc::manager::{DynamicMessage, GrpcHandle};
use ::grpc::{deserialize_message, serialize_message, Code, ServiceDefinition};
use yaak_grpc::manager::{DynamicMessage, GrpcHandle};
use yaak_grpc::{deserialize_message, serialize_message, Code, ServiceDefinition};
use yaak_plugin_runtime::manager::PluginManager;
use crate::analytics::{AnalyticsAction, AnalyticsResource};
@@ -34,7 +35,8 @@ use crate::export_resources::{get_workspace_export_resources, WorkspaceExportRes
use crate::grpc::metadata_to_map;
use crate::http_request::send_http_request;
use crate::notifications::YaakNotifier;
use crate::render::{render_request, variables_from_environment};
use crate::render::{render_grpc_request, render_http_request, render_template};
use crate::template_callback::PluginTemplateCallback;
use crate::updates::{UpdateMode, YaakUpdater};
use crate::window_menu::app_menu;
use yaak_models::models::{
@@ -42,7 +44,7 @@ use yaak_models::models::{
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Settings, Workspace,
};
use yaak_models::queries::{
cancel_pending_grpc_connections, cancel_pending_responses, create_http_response,
cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response,
delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment,
delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request,
delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request,
@@ -50,11 +52,16 @@ use yaak_models::queries::{
get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
get_or_create_settings, get_workspace, list_cookie_jars, list_environments, list_folders,
list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests,
list_responses, list_workspaces, set_key_value_raw, update_response_if_id, update_settings,
upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
list_http_responses, list_workspaces, set_key_value_raw, update_response_if_id,
update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace,
};
use yaak_plugin_runtime::events::FilterResponse;
use yaak_plugin_runtime::events::{
CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse,
InternalEvent, InternalEventPayload, RenderHttpRequestResponse, SendHttpRequestResponse,
};
use yaak_templates::{Parser, Tokens};
mod analytics;
mod export_resources;
@@ -64,7 +71,7 @@ mod notifications;
mod render;
#[cfg(target_os = "macos")]
mod tauri_plugin_mac_window;
mod template_fns;
mod template_callback;
mod updates;
mod window_menu;
@@ -94,13 +101,55 @@ async fn cmd_metadata(app_handle: AppHandle) -> Result<AppMetaData, ()> {
})
}
#[tauri::command]
async fn cmd_parse_template(template: &str) -> Result<Tokens, String> {
Ok(Parser::new(template).parse())
}
#[tauri::command]
async fn cmd_template_tokens_to_string(tokens: Tokens) -> Result<String, String> {
Ok(tokens.to_string())
}
#[tauri::command]
async fn cmd_render_template(
window: WebviewWindow,
template: &str,
workspace_id: &str,
environment_id: Option<&str>,
) -> Result<String, String> {
let environment = match environment_id {
Some(id) => Some(
get_environment(&window, id)
.await
.map_err(|e| e.to_string())?,
),
None => None,
};
let workspace = get_workspace(&window, &workspace_id)
.await
.map_err(|e| e.to_string())?;
let rendered = render_template(
window.app_handle(),
template,
&workspace,
environment.as_ref(),
)
.await;
Ok(rendered)
}
#[tauri::command]
async fn cmd_dismiss_notification(
app: AppHandle,
window: WebviewWindow,
notification_id: &str,
yaak_notifier: State<'_, Mutex<YaakNotifier>>,
) -> Result<(), String> {
yaak_notifier.lock().await.seen(&app, notification_id).await
yaak_notifier
.lock()
.await
.seen(&window, notification_id)
.await
}
#[tauri::command]
@@ -135,23 +184,28 @@ async fn cmd_grpc_go(
request_id: &str,
environment_id: Option<&str>,
proto_files: Vec<String>,
w: WebviewWindow,
window: WebviewWindow,
grpc_handle: State<'_, Mutex<GrpcHandle>>,
) -> Result<String, String> {
let req = get_grpc_request(&w, request_id)
.await
.map_err(|e| e.to_string())?;
let environment = match environment_id {
Some(id) => Some(get_environment(&w, id).await.map_err(|e| e.to_string())?),
Some(id) => Some(
get_environment(&window, id)
.await
.map_err(|e| e.to_string())?,
),
None => None,
};
let workspace = get_workspace(&w, &req.workspace_id)
let req = get_grpc_request(&window, request_id)
.await
.map_err(|e| e.to_string())?;
let workspace = get_workspace(&window, &req.workspace_id)
.await
.map_err(|e| e.to_string())?;
let req =
render_grpc_request(window.app_handle(), &req, &workspace, environment.as_ref()).await;
let mut metadata = HashMap::new();
let vars = variables_from_environment(&workspace, environment.as_ref());
// Add rest of metadata
// Add the rest of metadata
for h in req.clone().metadata {
if h.name.is_empty() && h.value.is_empty() {
continue;
@@ -161,10 +215,7 @@ async fn cmd_grpc_go(
continue;
}
let name = render::render(&h.name, &vars);
let value = render::render(&h.value, &vars);
metadata.insert(name, value);
metadata.insert(h.name, h.value);
}
if let Some(b) = &req.authentication_type {
@@ -173,25 +224,22 @@ async fn cmd_grpc_go(
let a = req.authentication;
if b == "basic" {
let raw_username = a
let username = a
.get("username")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let raw_password = a
let password = a
.get("password")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let username = render::render(raw_username, &vars);
let password = render::render(raw_password, &vars);
let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
metadata.insert("Authorization".to_string(), format!("Basic {}", encoded));
} else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
let token = render::render(raw_token, &vars);
let token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
metadata.insert("Authorization".to_string(), format!("Bearer {token}"));
}
}
@@ -199,7 +247,7 @@ async fn cmd_grpc_go(
let conn = {
let req = req.clone();
upsert_grpc_connection(
&w,
&window,
&GrpcConnection {
workspace_id: req.workspace_id,
request_id: req.id,
@@ -256,7 +304,7 @@ async fn cmd_grpc_go(
Ok(c) => c,
Err(err) => {
upsert_grpc_connection(
&w,
&window,
&GrpcConnection {
elapsed: start.elapsed().as_millis() as i32,
error: Some(err.clone()),
@@ -282,10 +330,9 @@ async fn cmd_grpc_go(
let cb = {
let cancelled_rx = cancelled_rx.clone();
let w = w.clone();
let w = window.clone();
let base_msg = base_msg.clone();
let method_desc = method_desc.clone();
let vars = vars.clone();
move |ev: tauri::Event| {
if *cancelled_rx.borrow() {
@@ -305,11 +352,10 @@ async fn cmd_grpc_go(
};
match serde_json::from_str::<IncomingMsg>(ev.payload()) {
Ok(IncomingMsg::Message(raw_msg)) => {
Ok(IncomingMsg::Message(msg)) => {
let w = w.clone();
let base_msg = base_msg.clone();
let method_desc = method_desc.clone();
let msg = render::render(raw_msg.as_str(), &vars);
let d_msg: DynamicMessage = match deserialize_message(msg.as_str(), method_desc)
{
Ok(d_msg) => d_msg,
@@ -355,19 +401,17 @@ async fn cmd_grpc_go(
}
}
};
let event_handler = w.listen_any(format!("grpc_client_msg_{}", conn.id).as_str(), cb);
let event_handler = window.listen_any(format!("grpc_client_msg_{}", conn.id).as_str(), cb);
let grpc_listen = {
let w = w.clone();
let w = window.clone();
let base_event = base_msg.clone();
let req = req.clone();
let vars = vars.clone();
let raw_msg = if req.message.is_empty() {
let msg = if req.message.is_empty() {
"{}".to_string()
} else {
req.message
};
let msg = render::render(&raw_msg, &vars);
upsert_grpc_event(
&w,
@@ -603,7 +647,7 @@ async fn cmd_grpc_go(
{
let conn_id = conn_id.clone();
tauri::async_runtime::spawn(async move {
let w = w.clone();
let w = window.clone();
tokio::select! {
_ = grpc_listen => {
let events = list_grpc_events(&w, &conn_id)
@@ -687,11 +731,10 @@ async fn cmd_send_ephemeral_request(
send_http_request(
&window,
request,
&request,
&response,
environment,
cookie_jar,
None,
&mut cancel_rx,
)
.await
@@ -701,7 +744,7 @@ async fn cmd_send_ephemeral_request(
async fn cmd_filter_response(
w: WebviewWindow,
response_id: &str,
plugin_manager: State<'_, Mutex<PluginManager>>,
plugin_manager: State<'_, PluginManager>,
filter: &str,
) -> Result<FilterResponse, String> {
let response = get_http_response(&w, response_id)
@@ -724,9 +767,7 @@ async fn cmd_filter_response(
// TODO: Have plugins register their own content type (regex?)
plugin_manager
.lock()
.await
.run_filter(filter, &body, &content_type)
.filter_data(filter, &body, &content_type)
.await
.map_err(|e| e.to_string())
}
@@ -734,16 +775,14 @@ async fn cmd_filter_response(
#[tauri::command]
async fn cmd_import_data(
w: WebviewWindow,
plugin_manager: State<'_, Mutex<PluginManager>>,
plugin_manager: State<'_, PluginManager>,
file_path: &str,
) -> Result<WorkspaceExportResources, String> {
let file =
read_to_string(file_path).unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
let file_contents = file.as_str();
let (import_result, plugin_name) = plugin_manager
.lock()
.await
.run_import(file_contents)
.import_data(file_contents)
.await
.map_err(|e| e.to_string())?;
@@ -853,7 +892,7 @@ async fn cmd_import_data(
);
analytics::track_event(
&w.app_handle(),
&w,
AnalyticsResource::App,
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
@@ -864,49 +903,52 @@ async fn cmd_import_data(
}
#[tauri::command]
async fn cmd_request_to_curl(
app: AppHandle,
request_id: &str,
plugin_manager: State<'_, Mutex<PluginManager>>,
environment_id: Option<&str>,
) -> Result<String, String> {
let request = get_http_request(&app, request_id)
async fn cmd_http_request_actions(
plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<GetHttpRequestActionsResponse>, String> {
plugin_manager
.get_http_request_actions()
.await
.map_err(|e| e.to_string())?;
let environment = match environment_id {
Some(id) => Some(get_environment(&app, id).await.map_err(|e| e.to_string())?),
None => None,
};
let workspace = get_workspace(&app, &request.workspace_id)
.await
.map_err(|e| e.to_string())?;
let rendered = render_request(&request, &workspace, environment.as_ref());
.map_err(|e| e.to_string())
}
let import_response = plugin_manager
.lock()
#[tauri::command]
async fn cmd_template_functions(
plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<GetTemplateFunctionsResponse>, String> {
plugin_manager
.get_template_functions()
.await
.run_export_curl(&rendered)
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_call_http_request_action(
req: CallHttpRequestActionRequest,
plugin_manager: State<'_, PluginManager>,
) -> Result<(), String> {
plugin_manager
.call_http_request_action(req)
.await
.map_err(|e| e.to_string())?;
Ok(import_response.content)
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_curl_to_request(
command: &str,
plugin_manager: State<'_, Mutex<PluginManager>>,
plugin_manager: State<'_, PluginManager>,
workspace_id: &str,
w: WebviewWindow,
) -> Result<HttpRequest, String> {
let (import_result, plugin_name) = plugin_manager
.lock()
.await
.run_import(command)
.await
.map_err(|e| e.to_string())?;
let (import_result, plugin_name) = {
plugin_manager
.import_data(command)
.await
.map_err(|e| e.to_string())?
};
analytics::track_event(
&w.app_handle(),
&w,
AnalyticsResource::App,
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
@@ -947,7 +989,7 @@ async fn cmd_export_data(
f.sync_all().expect("Failed to sync");
analytics::track_event(
&window.app_handle(),
&window,
AnalyticsResource::App,
AnalyticsAction::Export,
None,
@@ -984,7 +1026,6 @@ async fn cmd_send_http_request(
window: WebviewWindow,
environment_id: Option<&str>,
cookie_jar_id: Option<&str>,
download_dir: Option<&str>,
// NOTE: We receive the entire request because to account for the race
// condition where the user may have just edited a field before sending
// that has not yet been saved in the DB.
@@ -1010,28 +1051,9 @@ async fn cmd_send_http_request(
None => None,
};
let response = create_http_response(
&window,
&request.id,
0,
0,
"",
0,
None,
None,
None,
vec![],
None,
None,
)
.await
.expect("Failed to create response");
let download_path = if let Some(p) = download_dir {
Some(std::path::Path::new(p).to_path_buf())
} else {
None
};
let response = create_default_http_response(&window, &request.id)
.await
.map_err(|e| e.to_string())?;
let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
window.listen_any(
@@ -1043,20 +1065,19 @@ async fn cmd_send_http_request(
send_http_request(
&window,
request.clone(),
&request,
&response,
environment,
cookie_jar,
download_path,
&mut cancel_rx,
)
.await
}
async fn response_err(
async fn response_err<R: Runtime>(
response: &HttpResponse,
error: String,
w: &WebviewWindow,
w: &WebviewWindow<R>,
) -> Result<HttpResponse, String> {
warn!("Failed to send request: {}", error);
let mut response = response.clone();
@@ -1080,7 +1101,7 @@ async fn cmd_track_event(
AnalyticsAction::from_str(action),
) {
(Ok(resource), Ok(action)) => {
analytics::track_event(&window.app_handle(), resource, action, attributes).await;
analytics::track_event(&window, resource, action, attributes).await;
}
(r, a) => {
error!(
@@ -1475,7 +1496,7 @@ async fn cmd_list_http_responses(
limit: Option<i64>,
w: WebviewWindow,
) -> Result<Vec<HttpResponse>, String> {
list_responses(&w, request_id, limit)
list_http_responses(&w, request_id, limit)
.await
.map_err(|e| e.to_string())
}
@@ -1635,13 +1656,17 @@ pub fn run() {
let grpc_handle = GrpcHandle::new(&app.app_handle());
app.manage(Mutex::new(grpc_handle));
// Add plugin manager
let grpc_handle = GrpcHandle::new(&app.app_handle());
app.manage(Mutex::new(grpc_handle));
// Plugin template callback
let plugin_cb = PluginTemplateCallback::new(app.app_handle().clone());
app.manage(plugin_cb);
let app_handle = app.app_handle().clone();
monitor_plugin_events(&app_handle);
Ok(())
})
.invoke_handler(tauri::generate_handler![
cmd_call_http_request_action,
cmd_check_for_updates,
cmd_create_cookie_jar,
cmd_create_environment,
@@ -1660,6 +1685,10 @@ pub fn run() {
cmd_delete_http_request,
cmd_delete_http_response,
cmd_delete_workspace,
cmd_dismiss_notification,
cmd_parse_template,
cmd_template_tokens_to_string,
cmd_render_template,
cmd_duplicate_grpc_request,
cmd_duplicate_http_request,
cmd_export_data,
@@ -1674,6 +1703,8 @@ pub fn run() {
cmd_get_workspace,
cmd_grpc_go,
cmd_grpc_reflect,
cmd_http_request_actions,
cmd_template_functions,
cmd_import_data,
cmd_list_cookie_jars,
cmd_list_environments,
@@ -1687,8 +1718,6 @@ pub fn run() {
cmd_metadata,
cmd_new_nested_window,
cmd_new_window,
cmd_request_to_curl,
cmd_dismiss_notification,
cmd_save_response,
cmd_send_ephemeral_request,
cmd_send_http_request,
@@ -1715,10 +1744,9 @@ pub fn run() {
.run(|app_handle, event| {
match event {
RunEvent::Ready => {
create_window(app_handle, "/");
let h = app_handle.clone();
let w = create_window(app_handle, "/");
tauri::async_runtime::spawn(async move {
let info = analytics::track_launch_event(&h).await;
let info = analytics::track_launch_event(&w).await;
debug!("Launched Yaak {:?}", info);
});
@@ -1743,10 +1771,12 @@ pub fn run() {
let h = app_handle.clone();
tauri::async_runtime::spawn(async move {
let windows = h.webview_windows();
let w = windows.values().next().unwrap();
tokio::time::sleep(Duration::from_millis(4000)).await;
let val: State<'_, Mutex<YaakNotifier>> = h.state();
let val: State<'_, Mutex<YaakNotifier>> = w.state();
let mut n = val.lock().await;
if let Err(e) = n.check(&h).await {
if let Err(e) = n.check(&w).await {
warn!("Failed to check for notifications {}", e)
}
});
@@ -1905,3 +1935,147 @@ fn safe_uri(endpoint: &str) -> String {
format!("http://{}", endpoint)
}
}
fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
let (_rx_id, mut rx) = plugin_manager.subscribe().await;
while let Some(event) = rx.recv().await {
let app_handle = app_handle.clone();
// We might have recursive back-and-forth calls between app and plugin, so we don't
// want to block here
tauri::async_runtime::spawn(async move {
handle_plugin_event(&app_handle, &event).await;
});
}
});
}
async fn handle_plugin_event<R: Runtime>(app_handle: &AppHandle<R>, event: &InternalEvent) {
info!("Got event to app {}", event.id);
let response_event: Option<InternalEventPayload> = match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => {
app_handle
.clipboard()
.write_text(req.text.as_str())
.expect("Failed to write text to clipboard");
None
}
InternalEventPayload::ShowToastRequest(req) => {
app_handle
.emit("show_toast", req)
.expect("Failed to emit show_toast");
None
}
InternalEventPayload::FindHttpResponsesRequest(req) => {
let http_responses = list_http_responses(
app_handle,
req.request_id.as_str(),
req.limit.map(|l| l as i64),
)
.await
.unwrap_or_default();
Some(InternalEventPayload::FindHttpResponsesResponse(
FindHttpResponsesResponse { http_responses },
))
}
InternalEventPayload::GetHttpRequestByIdRequest(req) => {
let http_request = get_http_request(app_handle, req.id.as_str()).await.ok();
Some(InternalEventPayload::GetHttpRequestByIdResponse(
GetHttpRequestByIdResponse { http_request },
))
}
InternalEventPayload::RenderHttpRequestRequest(req) => {
let w = get_focused_window_no_lock(app_handle).expect("No focused window");
let workspace = get_workspace(app_handle, req.http_request.workspace_id.as_str())
.await
.expect("Failed to get workspace for request");
let url = w.url().unwrap();
let mut query_pairs = url.query_pairs();
let environment_id = query_pairs
.find(|(k, _v)| k == "environment_id")
.map(|(_k, v)| v.to_string());
let environment = match environment_id {
None => None,
Some(id) => get_environment(&w, id.as_str()).await.ok(),
};
let cb = &*app_handle.state::<PluginTemplateCallback>();
let rendered_http_request =
render_http_request(&req.http_request, &workspace, environment.as_ref(), cb).await;
Some(InternalEventPayload::RenderHttpRequestResponse(
RenderHttpRequestResponse {
http_request: rendered_http_request,
},
))
}
InternalEventPayload::SendHttpRequestRequest(req) => {
let w = get_focused_window_no_lock(app_handle).expect("No focused window");
let url = w.url().unwrap();
let mut query_pairs = url.query_pairs();
let cookie_jar_id = query_pairs
.find(|(k, _v)| k == "cookie_jar_id")
.map(|(_k, v)| v.to_string());
let cookie_jar = match cookie_jar_id {
None => None,
Some(id) => get_cookie_jar(app_handle, id.as_str()).await.ok(),
};
let environment_id = query_pairs
.find(|(k, _v)| k == "environment_id")
.map(|(_k, v)| v.to_string());
let environment = match environment_id {
None => None,
Some(id) => get_environment(app_handle, id.as_str()).await.ok(),
};
let resp = create_default_http_response(&w, req.http_request.id.as_str())
.await
.unwrap();
let result = send_http_request(
&w,
&req.http_request,
&resp,
environment,
cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel
)
.await;
let http_response = match result {
Ok(r) => r,
Err(_e) => return,
};
Some(InternalEventPayload::SendHttpRequestResponse(
SendHttpRequestResponse { http_response },
))
}
_ => None,
};
if let Some(e) = response_event {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
if let Err(e) = plugin_manager.reply(&event, &e).await {
warn!("Failed to reply to plugin manager: {}", e)
}
}
}
// app_handle.get_focused_window locks, so this one is a non-locking version, safe for use in async context
fn get_focused_window_no_lock<R: Runtime>(app_handle: &AppHandle<R>) -> Option<WebviewWindow<R>> {
app_handle
.windows()
.iter()
.find(|w| w.1.is_focused().unwrap_or(false))
.map(|w| w.1.clone())?
.webview_windows()
.iter()
.next()
.map(|(_, w)| w.to_owned())
}

View File

@@ -5,7 +5,7 @@ use chrono::{DateTime, Duration, Utc};
use log::debug;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter};
use tauri::{Emitter, Manager, Runtime, WebviewWindow};
use yaak_models::queries::{get_key_value_raw, set_key_value_raw};
// Check for updates every hour
@@ -42,16 +42,16 @@ impl YaakNotifier {
}
}
pub async fn seen(&mut self, app: &AppHandle, id: &str) -> Result<(), String> {
let mut seen = get_kv(app).await?;
pub async fn seen<R: Runtime>(&mut self, w: &WebviewWindow<R>, id: &str) -> Result<(), String> {
let mut seen = get_kv(w).await?;
seen.push(id.to_string());
debug!("Marked notification as seen {}", id);
let seen_json = serde_json::to_string(&seen).map_err(|e| e.to_string())?;
set_key_value_raw(app, KV_NAMESPACE, KV_KEY, seen_json.as_str()).await;
set_key_value_raw(w, KV_NAMESPACE, KV_KEY, seen_json.as_str()).await;
Ok(())
}
pub async fn check(&mut self, app: &AppHandle) -> Result<(), String> {
pub async fn check<R: Runtime>(&mut self, w: &WebviewWindow<R>) -> Result<(), String> {
let ignore_check = self.last_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
if ignore_check {
@@ -60,8 +60,8 @@ impl YaakNotifier {
self.last_check = SystemTime::now();
let num_launches = get_num_launches(app).await;
let info = app.package_info().clone();
let num_launches = get_num_launches(w).await;
let info = w.app_handle().package_info().clone();
let req = reqwest::Client::default()
.request(Method::GET, "https://notify.yaak.app/notifications")
.query(&[
@@ -80,21 +80,21 @@ impl YaakNotifier {
.map_err(|e| e.to_string())?;
let age = notification.timestamp.signed_duration_since(Utc::now());
let seen = get_kv(app).await?;
let seen = get_kv(w).await?;
if seen.contains(&notification.id) || (age > Duration::days(2)) {
debug!("Already seen notification {}", notification.id);
return Ok(());
}
debug!("Got notification {:?}", notification);
let _ = app.emit("notification", notification.clone());
let _ = w.emit("notification", notification.clone());
Ok(())
}
}
async fn get_kv(app: &AppHandle) -> Result<Vec<String>, String> {
match get_key_value_raw(app, "notifications", "seen").await {
async fn get_kv<R: Runtime>(w: &WebviewWindow<R>) -> Result<Vec<String>, String> {
match get_key_value_raw(w, "notifications", "seen").await {
None => Ok(Vec::new()),
Some(v) => serde_json::from_str(&v.value).map_err(|e| e.to_string()),
}

View File

@@ -1,71 +1,113 @@
use crate::template_callback::PluginTemplateCallback;
use serde_json::{json, Map, Value};
use std::collections::HashMap;
use serde_json::Value;
use crate::template_fns::timestamp;
use templates::parse_and_render;
use tauri::{AppHandle, Manager, Runtime};
use yaak_models::models::{
Environment, EnvironmentVariable, HttpRequest, HttpRequestHeader, HttpUrlParameter, Workspace,
Environment, EnvironmentVariable, GrpcMetadataEntry, GrpcRequest, HttpRequest,
HttpRequestHeader, HttpUrlParameter, Workspace,
};
use yaak_templates::{parse_and_render, TemplateCallback};
pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -> HttpRequest {
let r = r.clone();
let vars = &variables_from_environment(w, e);
pub async fn render_template<R: Runtime>(
app_handle: &AppHandle<R>,
template: &str,
w: &Workspace,
e: Option<&Environment>,
) -> String {
let cb = &*app_handle.state::<PluginTemplateCallback>();
let vars = &variables_from_environment(w, e, cb).await;
render(template, vars, cb).await
}
HttpRequest {
url: render(r.url.as_str(), vars),
url_parameters: r
.url_parameters
.iter()
.map(|p| HttpUrlParameter {
enabled: p.enabled,
name: render(p.name.as_str(), vars),
value: render(p.value.as_str(), vars),
})
.collect::<Vec<HttpUrlParameter>>(),
headers: r
.headers
.iter()
.map(|p| HttpRequestHeader {
enabled: p.enabled,
name: render(p.name.as_str(), vars),
value: render(p.value.as_str(), vars),
})
.collect::<Vec<HttpRequestHeader>>(),
body: r
.body
.iter()
.map(|(k, v)| {
let v = if v.is_string() {
render(v.as_str().unwrap(), vars)
} else {
v.to_string()
};
(render(k, vars), Value::from(v))
})
.collect::<HashMap<String, Value>>(),
authentication: r
.authentication
.iter()
.map(|(k, v)| {
let v = if v.is_string() {
render(v.as_str().unwrap(), vars)
} else {
v.to_string()
};
(render(k, vars), Value::from(v))
})
.collect::<HashMap<String, Value>>(),
..r
pub async fn render_grpc_request<R: Runtime>(
app_handle: &AppHandle<R>,
r: &GrpcRequest,
w: &Workspace,
e: Option<&Environment>,
) -> GrpcRequest {
let cb = &*app_handle.state::<PluginTemplateCallback>();
let vars = &variables_from_environment(w, e, cb).await;
let mut metadata = Vec::new();
for p in r.metadata.clone() {
metadata.push(GrpcMetadataEntry {
enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await,
value: render(p.value.as_str(), vars, cb).await,
})
}
let mut authentication = HashMap::new();
for (k, v) in r.authentication.clone() {
authentication.insert(k, render_json_value(v, vars, cb).await);
}
let url = render(r.url.as_str(), vars, cb).await;
GrpcRequest {
url,
metadata,
authentication,
..r.to_owned()
}
}
pub fn recursively_render_variables<'s>(
pub async fn render_http_request(
r: &HttpRequest,
w: &Workspace,
e: Option<&Environment>,
cb: &PluginTemplateCallback,
) -> HttpRequest {
let vars = &variables_from_environment(w, e, cb).await;
let mut url_parameters = Vec::new();
for p in r.url_parameters.clone() {
url_parameters.push(HttpUrlParameter {
enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await,
value: render(p.value.as_str(), vars, cb).await,
})
}
let mut headers = Vec::new();
for p in r.headers.clone() {
headers.push(HttpRequestHeader {
enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await,
value: render(p.value.as_str(), vars, cb).await,
})
}
let mut body = HashMap::new();
for (k, v) in r.body.clone() {
body.insert(k, render_json_value(v, vars, cb).await);
}
let mut authentication = HashMap::new();
for (k, v) in r.authentication.clone() {
authentication.insert(k, render_json_value(v, vars, cb).await);
}
let url = render(r.url.clone().as_str(), vars, cb).await;
HttpRequest {
url,
url_parameters,
headers,
body,
authentication,
..r.to_owned()
}
}
pub async fn recursively_render_variables<'s, T: TemplateCallback>(
m: &HashMap<String, String>,
render_count: usize,
cb: &T,
) -> HashMap<String, String> {
let mut did_render = false;
let mut new_map = m.clone();
for (k, v) in m.clone() {
let rendered = render(v.as_str(), m);
let rendered = Box::pin(render(v.as_str(), m, cb)).await;
if rendered != v {
did_render = true
}
@@ -73,15 +115,16 @@ pub fn recursively_render_variables<'s>(
}
if did_render && render_count <= 3 {
new_map = recursively_render_variables(&new_map, render_count + 1);
new_map = Box::pin(recursively_render_variables(&new_map, render_count + 1, cb)).await;
}
new_map
}
pub fn variables_from_environment(
pub async fn variables_from_environment<T: TemplateCallback>(
workspace: &Workspace,
environment: Option<&Environment>,
cb: &T,
) -> HashMap<String, String> {
let mut variables = HashMap::new();
variables = add_variable_to_map(variables, &workspace.variables);
@@ -90,18 +133,15 @@ pub fn variables_from_environment(
variables = add_variable_to_map(variables, &e.variables);
}
recursively_render_variables(&variables, 0)
recursively_render_variables(&variables, 0, cb).await
}
pub fn render(template: &str, vars: &HashMap<String, String>) -> String {
parse_and_render(template, vars, Some(template_callback))
}
fn template_callback(name: &str, args: HashMap<String, String>) -> Result<String, String> {
match name {
"timestamp" => timestamp(args),
_ => Err(format!("Unknown template function {name}")),
}
pub async fn render<T: TemplateCallback>(
template: &str,
vars: &HashMap<String, String>,
cb: &T,
) -> String {
parse_and_render(template, vars, cb).await
}
fn add_variable_to_map(
@@ -120,3 +160,103 @@ fn add_variable_to_map(
map
}
pub async fn render_json_value<T: TemplateCallback>(
v: Value,
vars: &HashMap<String, String>,
cb: &T,
) -> Value {
match v {
Value::String(s) => json!(render(s.as_str(), vars, cb).await),
Value::Array(a) => {
let mut new_a = Vec::new();
for v in a {
new_a.push(Box::pin(render_json_value(v, vars, cb)).await)
}
json!(new_a)
}
Value::Object(o) => {
let mut new_o = Map::new();
for (k, v) in o {
let key = Box::pin(render(k.as_str(), vars, cb)).await;
let value = Box::pin(render_json_value(v, vars, cb)).await;
new_o.insert(key, value);
}
json!(new_o)
}
v => v,
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use std::collections::HashMap;
use yaak_templates::TemplateCallback;
struct EmptyCB {}
impl TemplateCallback for EmptyCB {
async fn run(
&self,
_fn_name: &str,
_args: HashMap<String, String>,
) -> Result<String, String> {
todo!()
}
}
#[tokio::test]
async fn render_json_value_string() {
let v = json!("${[a]}");
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = super::render_json_value(v, &vars, &EmptyCB {}).await;
assert_eq!(result, json!("aaa"))
}
#[tokio::test]
async fn render_json_value_array() {
let v = json!(["${[a]}", "${[a]}"]);
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = super::render_json_value(v, &vars, &EmptyCB {}).await;
assert_eq!(result, json!(["aaa", "aaa"]))
}
#[tokio::test]
async fn render_json_value_object() {
let v = json!({"${[a]}": "${[a]}"});
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = super::render_json_value(v, &vars, &EmptyCB {}).await;
assert_eq!(result, json!({"aaa": "aaa"}))
}
#[tokio::test]
async fn render_json_value_nested() {
let v = json!([
123,
{"${[a]}": "${[a]}"},
null,
"${[a]}",
false,
{"x": ["${[a]}"]}
]);
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = super::render_json_value(v, &vars, &EmptyCB {}).await;
assert_eq!(result, json!([
123,
{"aaa": "aaa"},
null,
"aaa",
false,
{"x": ["aaa"]}
]))
}
}

View File

@@ -0,0 +1,69 @@
use std::collections::HashMap;
use tauri::{AppHandle, Manager};
use yaak_plugin_runtime::events::{RenderPurpose, TemplateFunctionArg};
use yaak_plugin_runtime::manager::PluginManager;
use yaak_templates::TemplateCallback;
#[derive(Clone)]
pub struct PluginTemplateCallback {
app_handle: AppHandle,
purpose: RenderPurpose,
}
impl PluginTemplateCallback {
pub fn new(app_handle: AppHandle) -> PluginTemplateCallback {
PluginTemplateCallback {
app_handle,
purpose: RenderPurpose::Preview,
}
}
pub fn for_send(&self) -> PluginTemplateCallback {
let mut v = self.clone();
v.purpose = RenderPurpose::Send;
v
}
}
impl TemplateCallback for PluginTemplateCallback {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String, 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
};
let plugin_manager = self.app_handle.state::<PluginManager>();
let function = plugin_manager
.get_template_functions()
.await
.map_err(|e| e.to_string())?
.iter()
.flat_map(|f| f.functions.clone())
.find(|f| f.name == fn_name)
.ok_or("")?;
let mut args_with_defaults = args.clone();
// Fill in default values for all args
for a_def in function.args {
let base = match a_def {
TemplateFunctionArg::Text(a) => a.base,
TemplateFunctionArg::Select(a) => a.base,
TemplateFunctionArg::Checkbox(a) => a.base,
TemplateFunctionArg::HttpRequest(a) => a.base,
};
if let None = args_with_defaults.get(base.name.as_str()) {
args_with_defaults.insert(base.name, base.default_value.unwrap_or_default());
}
}
let resp = plugin_manager
.call_template_function(fn_name, args_with_defaults, self.purpose.clone())
.await
.map_err(|e| e.to_string())?;
Ok(resp.unwrap_or_default())
}
}

View File

@@ -1,70 +0,0 @@
use chrono::{DateTime, Utc};
use std::collections::HashMap;
pub fn timestamp(args: HashMap<String, String>) -> Result<String, String> {
let from = args.get("from").map(|v| v.as_str()).unwrap_or("now");
let format = args.get("format").map(|v| v.as_str()).unwrap_or("rfc3339");
let dt = match from {
"now" => {
let now = Utc::now();
now
}
_ => {
let json_from = serde_json::to_string(from).unwrap_or_default();
let now: DateTime<Utc> = match serde_json::from_str(json_from.as_str()) {
Ok(r) => r,
Err(e) => return Err(e.to_string()),
};
now
}
};
let result = match format {
"rfc3339" => dt.to_rfc3339(),
"unix" => dt.timestamp().to_string(),
"unix_millis" => dt.timestamp_millis().to_string(),
_ => "".to_string(),
};
Ok(result)
}
// Test it
#[cfg(test)]
mod tests {
use crate::template_fns::timestamp;
use std::collections::HashMap;
#[test]
fn timestamp_empty() {
let args = HashMap::new();
assert_ne!(timestamp(args), Ok("".to_string()));
}
#[test]
fn timestamp_from() {
let mut args = HashMap::new();
args.insert("from".to_string(), "2024-07-31T14:16:41.983Z".to_string());
assert_eq!(
timestamp(args),
Ok("2024-07-31T14:16:41.983+00:00".to_string())
);
}
#[test]
fn timestamp_format_unix() {
let mut args = HashMap::new();
args.insert("from".to_string(), "2024-07-31T14:16:41.983Z".to_string());
args.insert("format".to_string(), "unix".to_string());
assert_eq!(timestamp(args), Ok("1722435401".to_string()));
}
#[test]
fn timestamp_format_unix_millis() {
let mut args = HashMap::new();
args.insert("from".to_string(), "2024-07-31T14:16:41.983Z".to_string());
args.insert("format".to_string(), "unix_millis".to_string());
assert_eq!(timestamp(args), Ok("1722435401983".to_string()));
}
}

View File

@@ -1,7 +0,0 @@
[package]
name = "templates"
version = "0.1.0"
edition = "2021"
[dependencies]
log = "0.4.22"

View File

@@ -1,494 +0,0 @@
#[derive(Clone, PartialEq, Debug)]
pub struct FnArg {
pub name: String,
pub value: Val,
}
#[derive(Clone, PartialEq, Debug)]
pub enum Val {
Str(String),
Var(String),
Fn { name: String, args: Vec<FnArg> },
}
#[derive(Clone, PartialEq, Debug)]
pub enum Token {
Raw(String),
Tag(Val),
Eof,
}
// Template Syntax
//
// ${[ my_var ]}
// ${[ my_fn() ]}
// ${[ my_fn(my_var) ]}
// ${[ my_fn(my_var, "A String") ]}
// default
#[derive(Default)]
pub struct Parser {
tokens: Vec<Token>,
chars: Vec<char>,
pos: usize,
curr_text: String,
}
impl Parser {
pub fn new(text: &str) -> Parser {
Parser {
chars: text.chars().collect(),
..Parser::default()
}
}
pub fn parse(&mut self) -> Vec<Token> {
let start_pos = self.pos;
while self.pos < self.chars.len() {
if self.match_str("${[") {
let start_curr = self.pos;
if let Some(t) = self.parse_tag() {
self.push_token(t);
} else {
self.pos = start_curr;
self.curr_text += "${[";
}
} else {
let ch = self.next_char();
self.curr_text.push(ch);
}
if start_pos == self.pos {
panic!("Parser stuck!");
}
}
self.push_token(Token::Eof);
self.tokens.clone()
}
fn parse_tag(&mut self) -> Option<Token> {
// Parse up to first identifier
// ${[ my_var...
self.skip_whitespace();
let val = match self.parse_value() {
Some(v) => v,
None => return None,
};
// Parse to closing tag
// ${[ my_var(a, b, c) ]}
self.skip_whitespace();
if !self.match_str("]}") {
return None;
}
Some(Token::Tag(val))
}
#[allow(dead_code)]
fn debug_pos(&self, x: &str) {
println!(
r#"Position: {x}: text[{}]='{}' → "{}" → {:?}"#,
self.pos,
self.chars[self.pos],
self.chars.iter().collect::<String>(),
self.tokens,
);
}
fn parse_value(&mut self) -> Option<Val> {
if let Some((name, args)) = self.parse_fn() {
Some(Val::Fn { name, args })
} else if let Some(v) = self.parse_ident() {
Some(Val::Var(v))
} else if let Some(v) = self.parse_string() {
Some(Val::Str(v))
} else {
None
}
}
fn parse_fn(&mut self) -> Option<(String, Vec<FnArg>)> {
let start_pos = self.pos;
let name = match self.parse_ident() {
Some(v) => v,
None => {
self.pos = start_pos;
return None;
}
};
let args = match self.parse_fn_args() {
Some(args) => args,
None => {
self.pos = start_pos;
return None;
}
};
Some((name, args))
}
fn parse_fn_args(&mut self) -> Option<Vec<FnArg>> {
if !self.match_str("(") {
return None;
}
let start_pos = self.pos;
let mut args: Vec<FnArg> = Vec::new();
// Fn closed immediately
self.skip_whitespace();
if self.match_str(")") {
return Some(args)
}
while self.pos < self.chars.len() {
self.skip_whitespace();
let name = self.parse_ident();
self.skip_whitespace();
self.match_str("=");
self.skip_whitespace();
let value = self.parse_value();
self.skip_whitespace();
if let (Some(name), Some(value)) = (name.clone(), value.clone()) {
args.push(FnArg { name, value });
} else {
// Didn't find valid thing, so return
self.pos = start_pos;
return None;
}
if self.match_str(")") {
break;
}
self.skip_whitespace();
// If we don't find a comma, that's bad
if !args.is_empty() && !self.match_str(",") {
self.pos = start_pos;
return None;
}
if start_pos == self.pos {
panic!("Parser stuck!");
}
}
return Some(args);
}
fn parse_ident(&mut self) -> Option<String> {
let start_pos = self.pos;
let mut text = String::new();
while self.pos < self.chars.len() {
let ch = self.peek_char();
if ch.is_alphanumeric() || ch == '_' {
text.push(ch);
self.pos += 1;
} else {
break;
}
if start_pos == self.pos {
panic!("Parser stuck!");
}
}
if text.is_empty() {
self.pos = start_pos;
return None;
}
return Some(text);
}
fn parse_string(&mut self) -> Option<String> {
let start_pos = self.pos;
let mut text = String::new();
if !self.match_str("\"") {
return None;
}
let mut found_closing = false;
while self.pos < self.chars.len() {
let ch = self.next_char();
match ch {
'\\' => {
text.push(self.next_char());
}
'"' => {
found_closing = true;
break;
}
_ => {
text.push(ch);
}
}
if start_pos == self.pos {
panic!("Parser stuck!");
}
}
if !found_closing {
self.pos = start_pos;
return None;
}
return Some(text);
}
fn skip_whitespace(&mut self) {
while self.pos < self.chars.len() {
if self.peek_char().is_whitespace() {
self.pos += 1;
} else {
break;
}
}
}
fn next_char(&mut self) -> char {
let ch = self.peek_char();
self.pos += 1;
ch
}
fn peek_char(&self) -> char {
let ch = self.chars[self.pos];
ch
}
fn push_token(&mut self, token: Token) {
// Push any text we've accumulated
if !self.curr_text.is_empty() {
let text_token = Token::Raw(self.curr_text.clone());
self.tokens.push(text_token);
self.curr_text.clear();
}
self.tokens.push(token);
}
fn match_str(&mut self, value: &str) -> bool {
if self.pos + value.len() > self.chars.len() {
return false;
}
let cmp = self.chars[self.pos..self.pos + value.len()]
.iter()
.collect::<String>();
if cmp == value {
// We have a match, so advance the current index
self.pos += value.len();
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn var_simple() {
let mut p = Parser::new("${[ foo ]}");
assert_eq!(
p.parse(),
vec![Token::Tag(Val::Var("foo".into())), Token::Eof]
);
}
#[test]
fn var_multiple_names_invalid() {
let mut p = Parser::new("${[ foo bar ]}");
assert_eq!(
p.parse(),
vec![Token::Raw("${[ foo bar ]}".into()), Token::Eof]
);
}
#[test]
fn tag_string() {
let mut p = Parser::new(r#"${[ "foo \"bar\" baz" ]}"#);
assert_eq!(
p.parse(),
vec![Token::Tag(Val::Str(r#"foo "bar" baz"#.into())), Token::Eof]
);
}
#[test]
fn var_surrounded() {
let mut p = Parser::new("Hello ${[ foo ]}!");
assert_eq!(
p.parse(),
vec![
Token::Raw("Hello ".to_string()),
Token::Tag(Val::Var("foo".into())),
Token::Raw("!".to_string()),
Token::Eof,
]
);
}
#[test]
fn fn_simple() {
let mut p = Parser::new("${[ foo() ]}");
assert_eq!(
p.parse(),
vec![
Token::Tag(Val::Fn {
name: "foo".into(),
args: Vec::new(),
}),
Token::Eof
]
);
}
#[test]
fn fn_ident_arg() {
let mut p = Parser::new("${[ foo(a=bar) ]}");
assert_eq!(
p.parse(),
vec![
Token::Tag(Val::Fn {
name: "foo".into(),
args: vec![FnArg {
name: "a".into(),
value: Val::Var("bar".into())
}],
}),
Token::Eof
]
);
}
#[test]
fn fn_ident_args() {
let mut p = Parser::new("${[ foo(a=bar,b = baz, c =qux ) ]}");
assert_eq!(
p.parse(),
vec![
Token::Tag(Val::Fn {
name: "foo".into(),
args: vec![
FnArg {
name: "a".into(),
value: Val::Var("bar".into())
},
FnArg {
name: "b".into(),
value: Val::Var("baz".into())
},
FnArg {
name: "c".into(),
value: Val::Var("qux".into())
},
],
}),
Token::Eof
]
);
}
#[test]
fn fn_mixed_args() {
let mut p = Parser::new(r#"${[ foo(aaa=bar,bb="baz \"hi\"", c=qux ) ]}"#);
assert_eq!(
p.parse(),
vec![
Token::Tag(Val::Fn {
name: "foo".into(),
args: vec![
FnArg {
name: "aaa".into(),
value: Val::Var("bar".into())
},
FnArg {
name: "bb".into(),
value: Val::Str(r#"baz "hi""#.into())
},
FnArg {
name: "c".into(),
value: Val::Var("qux".into())
},
],
}),
Token::Eof
]
);
}
#[test]
fn fn_nested() {
let mut p = Parser::new("${[ foo(b=bar()) ]}");
assert_eq!(
p.parse(),
vec![
Token::Tag(Val::Fn {
name: "foo".into(),
args: vec![FnArg {
name: "b".into(),
value: Val::Fn {
name: "bar".into(),
args: vec![],
}
}],
}),
Token::Eof
]
);
}
#[test]
fn fn_nested_args() {
let mut p = Parser::new(r#"${[ outer(a=inner(a=foo, b="i"), c="o") ]}"#);
assert_eq!(
p.parse(),
vec![
Token::Tag(Val::Fn {
name: "outer".into(),
args: vec![
FnArg {
name: "a".into(),
value: Val::Fn {
name: "inner".into(),
args: vec![
FnArg {
name: "a".into(),
value: Val::Var("foo".into())
},
FnArg {
name: "b".into(),
value: Val::Str("i".into()),
},
],
}
},
FnArg {
name: "c".into(),
value: Val::Str("o".into())
},
],
}),
Token::Eof
]
);
}
}

View File

@@ -1,165 +0,0 @@
use crate::{FnArg, Parser, Token, Val};
use log::warn;
use std::collections::HashMap;
type TemplateCallback = fn(name: &str, args: HashMap<String, String>) -> Result<String, String>;
pub fn parse_and_render(
template: &str,
vars: &HashMap<String, String>,
cb: Option<TemplateCallback>,
) -> String {
let mut p = Parser::new(template);
let tokens = p.parse();
render(tokens, vars, cb)
}
pub fn render(
tokens: Vec<Token>,
vars: &HashMap<String, String>,
cb: Option<TemplateCallback>,
) -> String {
let mut doc_str: Vec<String> = Vec::new();
for t in tokens {
match t {
Token::Raw(s) => doc_str.push(s),
Token::Tag(val) => doc_str.push(render_tag(val, &vars, cb)),
Token::Eof => {}
}
}
return doc_str.join("");
}
fn render_tag(val: Val, vars: &HashMap<String, String>, cb: Option<TemplateCallback>) -> String {
match val {
Val::Str(s) => s.into(),
Val::Var(name) => match vars.get(name.as_str()) {
Some(v) => v.to_string(),
None => "".into(),
},
Val::Fn { name, args } => {
let empty = "".to_string();
let resolved_args = args
.iter()
.map(|a| match a {
FnArg {
name,
value: Val::Str(s),
} => (name.to_string(), s.to_string()),
FnArg {
name,
value: Val::Var(i),
} => (
name.to_string(),
vars.get(i.as_str()).unwrap_or(&empty).to_string(),
),
FnArg { name, value: val } => {
(name.to_string(), render_tag(val.clone(), vars, cb))
}
})
.collect::<HashMap<String, String>>();
match cb {
Some(cb) => match cb(name.as_str(), resolved_args.clone()) {
Ok(s) => s,
Err(e) => {
warn!("Failed to run template callback {}({:?}): {}", name, resolved_args, e);
"".to_string()
}
},
None => "".into(),
}
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::*;
#[test]
fn render_empty() {
let template = "";
let vars = HashMap::new();
let result = "";
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
}
#[test]
fn render_text_only() {
let template = "Hello World!";
let vars = HashMap::new();
let result = "Hello World!";
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
}
#[test]
fn render_simple() {
let template = "${[ foo ]}";
let vars = HashMap::from([("foo".to_string(), "bar".to_string())]);
let result = "bar";
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
}
#[test]
fn render_surrounded() {
let template = "hello ${[ word ]} world!";
let vars = HashMap::from([("word".to_string(), "cruel".to_string())]);
let result = "hello cruel world!";
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
}
#[test]
fn render_valid_fn() {
let vars = HashMap::new();
let template = r#"${[ say_hello(a="John", b="Kate") ]}"#;
let result = r#"say_hello: 2, Some("John") Some("Kate")"#;
fn cb(name: &str, args: HashMap<String, String>) -> Result<String, String> {
Ok(format!(
"{name}: {}, {:?} {:?}",
args.len(),
args.get("a"),
args.get("b")
))
}
assert_eq!(parse_and_render(template, &vars, Some(cb)), result);
}
#[test]
fn render_nested_fn() {
let vars = HashMap::new();
let template = r#"${[ upper(foo=secret()) ]}"#;
let result = r#"ABC"#;
fn cb(name: &str, args: HashMap<String, String>) -> Result<String, String> {
Ok(match name {
"secret" => "abc".to_string(),
"upper" => args["foo"].to_string().to_uppercase(),
_ => "".to_string(),
})
}
assert_eq!(
parse_and_render(template, &vars, Some(cb)),
result.to_string()
);
}
#[test]
fn render_fn_err() {
let vars = HashMap::new();
let template = r#"${[ error() ]}"#;
let result = r#""#;
fn cb(_name: &str, _args: HashMap<String, String>) -> Result<String, String> {
Err("Failed to do it!".to_string())
}
assert_eq!(
parse_and_render(template, &vars, Some(cb)),
result.to_string()
);
}
}

View File

@@ -1,5 +1,5 @@
[package]
name = "grpc"
name = "yaak_grpc"
version = "0.1.0"
edition = "2021"

View File

@@ -23,6 +23,7 @@ pub struct Settings {
pub interface_scale: i32,
pub editor_font_size: i32,
pub editor_soft_wrap: bool,
pub telemetry: bool,
pub open_workspace_new_window: Option<bool>,
}
@@ -43,6 +44,7 @@ pub enum SettingsIden {
InterfaceScale,
EditorFontSize,
EditorSoftWrap,
Telemetry,
OpenWorkspaceNewWindow,
}
@@ -64,6 +66,7 @@ impl<'s> TryFrom<&Row<'s>> for Settings {
interface_scale: r.get("interface_scale")?,
editor_font_size: r.get("editor_font_size")?,
editor_soft_wrap: r.get("editor_soft_wrap")?,
telemetry: r.get("telemetry")?,
open_workspace_new_window: r.get("open_workspace_new_window")?,
})
}

View File

@@ -15,10 +15,10 @@ use sea_query::Keyword::CurrentTimestamp;
use sea_query::{Cond, Expr, OnConflict, Order, Query, SqliteQueryBuilder};
use sea_query_rusqlite::RusqliteBinder;
use serde::Serialize;
use tauri::{AppHandle, Emitter, Manager, WebviewWindow, Wry};
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
pub async fn set_key_value_string(
mgr: &impl Manager<Wry>,
pub async fn set_key_value_string<R: Runtime>(
mgr: &WebviewWindow<R>,
namespace: &str,
key: &str,
value: &str,
@@ -27,8 +27,8 @@ pub async fn set_key_value_string(
set_key_value_raw(mgr, namespace, key, &encoded.unwrap()).await
}
pub async fn set_key_value_int(
mgr: &impl Manager<Wry>,
pub async fn set_key_value_int<R: Runtime>(
mgr: &WebviewWindow<R>,
namespace: &str,
key: &str,
value: i32,
@@ -37,8 +37,8 @@ pub async fn set_key_value_int(
set_key_value_raw(mgr, namespace, key, &encoded.unwrap()).await
}
pub async fn get_key_value_string(
mgr: &impl Manager<Wry>,
pub async fn get_key_value_string<R: Runtime>(
mgr: &impl Manager<R>,
namespace: &str,
key: &str,
default: &str,
@@ -58,8 +58,8 @@ pub async fn get_key_value_string(
}
}
pub async fn get_key_value_int(
mgr: &impl Manager<Wry>,
pub async fn get_key_value_int<R: Runtime>(
mgr: &impl Manager<R>,
namespace: &str,
key: &str,
default: i32,
@@ -79,15 +79,15 @@ pub async fn get_key_value_int(
}
}
pub async fn set_key_value_raw(
mgr: &impl Manager<Wry>,
pub async fn set_key_value_raw<R: Runtime>(
w: &WebviewWindow<R>,
namespace: &str,
key: &str,
value: &str,
) -> (KeyValue, bool) {
let existing = get_key_value_raw(mgr, namespace, key).await;
let existing = get_key_value_raw(w, namespace, key).await;
let dbm = &*mgr.state::<SqliteConnection>();
let dbm = &*w.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::insert()
.into_table(KeyValueIden::Table)
@@ -119,11 +119,11 @@ pub async fn set_key_value_raw(
let kv = stmt
.query_row(&*params.as_params(), |row| row.try_into())
.expect("Failed to upsert KeyValue");
(kv, existing.is_none())
(emit_upserted_model(w, kv), existing.is_none())
}
pub async fn get_key_value_raw(
mgr: &impl Manager<Wry>,
pub async fn get_key_value_raw<R: Runtime>(
mgr: &impl Manager<R>,
namespace: &str,
key: &str,
) -> Option<KeyValue> {
@@ -143,7 +143,7 @@ pub async fn get_key_value_raw(
.ok()
}
pub async fn list_workspaces(mgr: &impl Manager<Wry>) -> Result<Vec<Workspace>> {
pub async fn list_workspaces<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<Workspace>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
@@ -155,7 +155,7 @@ pub async fn list_workspaces(mgr: &impl Manager<Wry>) -> Result<Vec<Workspace>>
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn get_workspace(mgr: &impl Manager<Wry>, id: &str) -> Result<Workspace> {
pub async fn get_workspace<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<Workspace> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
@@ -167,7 +167,10 @@ pub async fn get_workspace(mgr: &impl Manager<Wry>, id: &str) -> Result<Workspac
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn upsert_workspace(window: &WebviewWindow, workspace: Workspace) -> Result<Workspace> {
pub async fn upsert_workspace<R: Runtime>(
window: &WebviewWindow<R>,
workspace: Workspace,
) -> Result<Workspace> {
let id = match workspace.id.as_str() {
"" => generate_model_id(ModelType::TypeWorkspace),
_ => workspace.id.to_string(),
@@ -222,7 +225,10 @@ pub async fn upsert_workspace(window: &WebviewWindow, workspace: Workspace) -> R
Ok(emit_upserted_model(window, m))
}
pub async fn delete_workspace(window: &WebviewWindow, id: &str) -> Result<Workspace> {
pub async fn delete_workspace<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<Workspace> {
let workspace = get_workspace(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -241,7 +247,7 @@ pub async fn delete_workspace(window: &WebviewWindow, id: &str) -> Result<Worksp
emit_deleted_model(window, workspace)
}
pub async fn get_cookie_jar(mgr: &impl Manager<Wry>, id: &str) -> Result<CookieJar> {
pub async fn get_cookie_jar<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<CookieJar> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -254,8 +260,8 @@ pub async fn get_cookie_jar(mgr: &impl Manager<Wry>, id: &str) -> Result<CookieJ
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_cookie_jars(
mgr: &impl Manager<Wry>,
pub async fn list_cookie_jars<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<CookieJar>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -270,7 +276,10 @@ pub async fn list_cookie_jars(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn delete_cookie_jar(window: &WebviewWindow, id: &str) -> Result<CookieJar> {
pub async fn delete_cookie_jar<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<CookieJar> {
let cookie_jar = get_cookie_jar(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -284,13 +293,19 @@ pub async fn delete_cookie_jar(window: &WebviewWindow, id: &str) -> Result<Cooki
emit_deleted_model(window, cookie_jar)
}
pub async fn duplicate_grpc_request(window: &WebviewWindow, id: &str) -> Result<GrpcRequest> {
pub async fn duplicate_grpc_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<GrpcRequest> {
let mut request = get_grpc_request(window, id).await?.clone();
request.id = "".to_string();
upsert_grpc_request(window, &request).await
}
pub async fn delete_grpc_request(window: &WebviewWindow, id: &str) -> Result<GrpcRequest> {
pub async fn delete_grpc_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<GrpcRequest> {
let req = get_grpc_request(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -304,8 +319,8 @@ pub async fn delete_grpc_request(window: &WebviewWindow, id: &str) -> Result<Grp
emit_deleted_model(window, req)
}
pub async fn upsert_grpc_request(
window: &WebviewWindow,
pub async fn upsert_grpc_request<R: Runtime>(
window: &WebviewWindow<R>,
request: &GrpcRequest,
) -> Result<GrpcRequest> {
let id = match request.id.as_str() {
@@ -346,7 +361,11 @@ pub async fn upsert_grpc_request(
request.service.as_ref().map(|s| s.as_str()).into(),
request.method.as_ref().map(|s| s.as_str()).into(),
request.message.as_str().into(),
request .authentication_type .as_ref() .map(|s| s.as_str()) .into(),
request
.authentication_type
.as_ref()
.map(|s| s.as_str())
.into(),
serde_json::to_string(&request.authentication)?.into(),
serde_json::to_string(&request.metadata)?.into(),
])
@@ -376,7 +395,7 @@ pub async fn upsert_grpc_request(
Ok(emit_upserted_model(window, m))
}
pub async fn get_grpc_request(mgr: &impl Manager<Wry>, id: &str) -> Result<GrpcRequest> {
pub async fn get_grpc_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<GrpcRequest> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -389,8 +408,8 @@ pub async fn get_grpc_request(mgr: &impl Manager<Wry>, id: &str) -> Result<GrpcR
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_grpc_requests(
mgr: &impl Manager<Wry>,
pub async fn list_grpc_requests<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<GrpcRequest>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -405,8 +424,8 @@ pub async fn list_grpc_requests(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn upsert_grpc_connection(
window: &WebviewWindow,
pub async fn upsert_grpc_connection<R: Runtime>(
window: &WebviewWindow<R>,
connection: &GrpcConnection,
) -> Result<GrpcConnection> {
let id = match connection.id.as_str() {
@@ -467,7 +486,10 @@ pub async fn upsert_grpc_connection(
Ok(emit_upserted_model(window, m))
}
pub async fn get_grpc_connection(mgr: &impl Manager<Wry>, id: &str) -> Result<GrpcConnection> {
pub async fn get_grpc_connection<R: Runtime>(
mgr: &impl Manager<R>,
id: &str,
) -> Result<GrpcConnection> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
@@ -479,8 +501,8 @@ pub async fn get_grpc_connection(mgr: &impl Manager<Wry>, id: &str) -> Result<Gr
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_grpc_connections(
mgr: &impl Manager<Wry>,
pub async fn list_grpc_connections<R: Runtime>(
mgr: &impl Manager<R>,
request_id: &str,
) -> Result<Vec<GrpcConnection>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -497,7 +519,10 @@ pub async fn list_grpc_connections(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn delete_grpc_connection(window: &WebviewWindow, id: &str) -> Result<GrpcConnection> {
pub async fn delete_grpc_connection<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<GrpcConnection> {
let resp = get_grpc_connection(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -512,14 +537,20 @@ pub async fn delete_grpc_connection(window: &WebviewWindow, id: &str) -> Result<
emit_deleted_model(window, resp)
}
pub async fn delete_all_grpc_connections(window: &WebviewWindow, request_id: &str) -> Result<()> {
pub async fn delete_all_grpc_connections<R: Runtime>(
window: &WebviewWindow<R>,
request_id: &str,
) -> Result<()> {
for r in list_grpc_connections(window, request_id).await? {
delete_grpc_connection(window, &r.id).await?;
}
Ok(())
}
pub async fn upsert_grpc_event(window: &WebviewWindow, event: &GrpcEvent) -> Result<GrpcEvent> {
pub async fn upsert_grpc_event<R: Runtime>(
window: &WebviewWindow<R>,
event: &GrpcEvent,
) -> Result<GrpcEvent> {
let id = match event.id.as_str() {
"" => generate_model_id(ModelType::TypeGrpcEvent),
_ => event.id.to_string(),
@@ -575,7 +606,7 @@ pub async fn upsert_grpc_event(window: &WebviewWindow, event: &GrpcEvent) -> Res
Ok(emit_upserted_model(window, m))
}
pub async fn get_grpc_event(mgr: &impl Manager<Wry>, id: &str) -> Result<GrpcEvent> {
pub async fn get_grpc_event<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<GrpcEvent> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
@@ -587,8 +618,8 @@ pub async fn get_grpc_event(mgr: &impl Manager<Wry>, id: &str) -> Result<GrpcEve
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_grpc_events(
mgr: &impl Manager<Wry>,
pub async fn list_grpc_events<R: Runtime>(
mgr: &impl Manager<R>,
connection_id: &str,
) -> Result<Vec<GrpcEvent>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -605,8 +636,8 @@ pub async fn list_grpc_events(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn upsert_cookie_jar(
window: &WebviewWindow,
pub async fn upsert_cookie_jar<R: Runtime>(
window: &WebviewWindow<R>,
cookie_jar: &CookieJar,
) -> Result<CookieJar> {
let id = match cookie_jar.id.as_str() {
@@ -653,8 +684,8 @@ pub async fn upsert_cookie_jar(
Ok(emit_upserted_model(window, m))
}
pub async fn list_environments(
mgr: &impl Manager<Wry>,
pub async fn list_environments<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<Environment>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -671,7 +702,10 @@ pub async fn list_environments(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn delete_environment(window: &WebviewWindow, id: &str) -> Result<Environment> {
pub async fn delete_environment<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<Environment> {
let env = get_environment(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -686,20 +720,22 @@ pub async fn delete_environment(window: &WebviewWindow, id: &str) -> Result<Envi
emit_deleted_model(window, env)
}
async fn get_settings(mgr: &impl Manager<Wry>) -> Result<Settings> {
const SETTINGS_ID: &str = "default";
async fn get_settings<R: Runtime>(mgr: &impl Manager<R>) -> Result<Settings> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
.from(SettingsIden::Table)
.column(Asterisk)
.cond_where(Expr::col(SettingsIden::Id).eq("default"))
.cond_where(Expr::col(SettingsIden::Id).eq(SETTINGS_ID))
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn get_or_create_settings(mgr: &impl Manager<Wry>) -> Settings {
pub async fn get_or_create_settings<R: Runtime>(mgr: &impl Manager<R>) -> Settings {
if let Ok(settings) = get_settings(mgr).await {
return settings;
}
@@ -710,7 +746,7 @@ pub async fn get_or_create_settings(mgr: &impl Manager<Wry>) -> Settings {
let (sql, params) = Query::insert()
.into_table(SettingsIden::Table)
.columns([SettingsIden::Id])
.values_panic(["default".into()])
.values_panic([SETTINGS_ID.into()])
.returning_all()
.build_rusqlite(SqliteQueryBuilder);
@@ -721,7 +757,10 @@ pub async fn get_or_create_settings(mgr: &impl Manager<Wry>) -> Settings {
.expect("Failed to insert Settings")
}
pub async fn update_settings(window: &WebviewWindow, settings: Settings) -> Result<Settings> {
pub async fn update_settings<R: Runtime>(
window: &WebviewWindow<R>,
settings: Settings,
) -> Result<Settings> {
let dbm = &*window.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -760,6 +799,10 @@ pub async fn update_settings(window: &WebviewWindow, settings: Settings) -> Resu
SettingsIden::EditorSoftWrap,
settings.editor_soft_wrap.into(),
),
(
SettingsIden::Telemetry,
settings.telemetry.into(),
),
(
SettingsIden::OpenWorkspaceNewWindow,
settings.open_workspace_new_window.into(),
@@ -773,8 +816,8 @@ pub async fn update_settings(window: &WebviewWindow, settings: Settings) -> Resu
Ok(emit_upserted_model(window, m))
}
pub async fn upsert_environment(
window: &WebviewWindow,
pub async fn upsert_environment<R: Runtime>(
window: &WebviewWindow<R>,
environment: Environment,
) -> Result<Environment> {
let id = match environment.id.as_str() {
@@ -821,7 +864,7 @@ pub async fn upsert_environment(
Ok(emit_upserted_model(window, m))
}
pub async fn get_environment(mgr: &impl Manager<Wry>, id: &str) -> Result<Environment> {
pub async fn get_environment<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<Environment> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -834,7 +877,7 @@ pub async fn get_environment(mgr: &impl Manager<Wry>, id: &str) -> Result<Enviro
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn get_folder(mgr: &impl Manager<Wry>, id: &str) -> Result<Folder> {
pub async fn get_folder<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<Folder> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -847,7 +890,10 @@ pub async fn get_folder(mgr: &impl Manager<Wry>, id: &str) -> Result<Folder> {
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_folders(mgr: &impl Manager<Wry>, workspace_id: &str) -> Result<Vec<Folder>> {
pub async fn list_folders<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<Folder>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -862,7 +908,7 @@ pub async fn list_folders(mgr: &impl Manager<Wry>, workspace_id: &str) -> Result
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn delete_folder(window: &WebviewWindow, id: &str) -> Result<Folder> {
pub async fn delete_folder<R: Runtime>(window: &WebviewWindow<R>, id: &str) -> Result<Folder> {
let folder = get_folder(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -877,7 +923,7 @@ pub async fn delete_folder(window: &WebviewWindow, id: &str) -> Result<Folder> {
emit_deleted_model(window, folder)
}
pub async fn upsert_folder(window: &WebviewWindow, r: Folder) -> Result<Folder> {
pub async fn upsert_folder<R: Runtime>(window: &WebviewWindow<R>, r: Folder) -> Result<Folder> {
let id = match r.id.as_str() {
"" => generate_model_id(ModelType::TypeFolder),
_ => r.id.to_string(),
@@ -925,13 +971,19 @@ pub async fn upsert_folder(window: &WebviewWindow, r: Folder) -> Result<Folder>
Ok(emit_upserted_model(window, m))
}
pub async fn duplicate_http_request(window: &WebviewWindow, id: &str) -> Result<HttpRequest> {
pub async fn duplicate_http_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<HttpRequest> {
let mut request = get_http_request(window, id).await?.clone();
request.id = "".to_string();
upsert_http_request(window, request).await
}
pub async fn upsert_http_request(window: &WebviewWindow, r: HttpRequest) -> Result<HttpRequest> {
pub async fn upsert_http_request<R: Runtime>(
window: &WebviewWindow<R>,
r: HttpRequest,
) -> Result<HttpRequest> {
let id = match r.id.as_str() {
"" => generate_model_id(ModelType::TypeHttpRequest),
_ => r.id.to_string(),
@@ -1004,8 +1056,8 @@ pub async fn upsert_http_request(window: &WebviewWindow, r: HttpRequest) -> Resu
Ok(emit_upserted_model(window, m))
}
pub async fn list_http_requests(
mgr: &impl Manager<Wry>,
pub async fn list_http_requests<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<HttpRequest>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -1021,7 +1073,7 @@ pub async fn list_http_requests(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn get_http_request(mgr: &impl Manager<Wry>, id: &str) -> Result<HttpRequest> {
pub async fn get_http_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<HttpRequest> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -1034,7 +1086,10 @@ pub async fn get_http_request(mgr: &impl Manager<Wry>, id: &str) -> Result<HttpR
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn delete_http_request(window: &WebviewWindow, id: &str) -> Result<HttpRequest> {
pub async fn delete_http_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<HttpRequest> {
let req = get_http_request(window, id).await?;
// DB deletes will cascade but this will delete the files
@@ -1051,9 +1106,30 @@ pub async fn delete_http_request(window: &WebviewWindow, id: &str) -> Result<Htt
emit_deleted_model(window, req)
}
pub async fn create_default_http_response<R: Runtime>(
window: &WebviewWindow<R>,
request_id: &str,
) -> Result<HttpResponse> {
create_http_response(
&window,
request_id,
0,
0,
"",
0,
None,
None,
None,
vec![],
None,
None,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn create_http_response(
window: &WebviewWindow,
pub async fn create_http_response<R: Runtime>(
window: &WebviewWindow<R>,
request_id: &str,
elapsed: i64,
elapsed_headers: i64,
@@ -1146,8 +1222,8 @@ pub async fn cancel_pending_responses(app: &AppHandle) -> Result<()> {
Ok(())
}
pub async fn update_response_if_id(
window: &WebviewWindow,
pub async fn update_response_if_id<R: Runtime>(
window: &WebviewWindow<R>,
response: &HttpResponse,
) -> Result<HttpResponse> {
if response.id.is_empty() {
@@ -1157,8 +1233,8 @@ pub async fn update_response_if_id(
}
}
pub async fn update_response(
window: &WebviewWindow,
pub async fn update_response<R: Runtime>(
window: &WebviewWindow<R>,
response: &HttpResponse,
) -> Result<HttpResponse> {
let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -1211,7 +1287,10 @@ pub async fn update_response(
Ok(emit_upserted_model(window, m))
}
pub async fn get_http_response(mgr: &impl Manager<Wry>, id: &str) -> Result<HttpResponse> {
pub async fn get_http_response<R: Runtime>(
mgr: &impl Manager<R>,
id: &str,
) -> Result<HttpResponse> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
@@ -1223,7 +1302,10 @@ pub async fn get_http_response(mgr: &impl Manager<Wry>, id: &str) -> Result<Http
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn delete_http_response(window: &WebviewWindow, id: &str) -> Result<HttpResponse> {
pub async fn delete_http_response<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<HttpResponse> {
let resp = get_http_response(window, id).await?;
// Delete the body file if it exists
@@ -1244,15 +1326,18 @@ pub async fn delete_http_response(window: &WebviewWindow, id: &str) -> Result<Ht
emit_deleted_model(window, resp)
}
pub async fn delete_all_http_responses(window: &WebviewWindow, request_id: &str) -> Result<()> {
for r in list_responses(window, request_id, None).await? {
pub async fn delete_all_http_responses<R: Runtime>(
window: &WebviewWindow<R>,
request_id: &str,
) -> Result<()> {
for r in list_http_responses(window, request_id, None).await? {
delete_http_response(window, &r.id).await?;
}
Ok(())
}
pub async fn list_responses(
mgr: &impl Manager<Wry>,
pub async fn list_http_responses<R: Runtime>(
mgr: &impl Manager<R>,
request_id: &str,
limit: Option<i64>,
) -> Result<Vec<HttpResponse>> {
@@ -1271,8 +1356,8 @@ pub async fn list_responses(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn list_responses_by_workspace_id(
mgr: &impl Manager<Wry>,
pub async fn list_responses_by_workspace_id<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<HttpResponse>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -1288,7 +1373,7 @@ pub async fn list_responses_by_workspace_id(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn debug_pool(mgr: &impl Manager<Wry>) {
pub async fn debug_pool<R: Runtime>(mgr: &impl Manager<R>) {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await;
debug!("Debug database state: {:?}", db.state());
@@ -1310,7 +1395,7 @@ struct ModelPayload<M: Serialize + Clone> {
pub window_label: String,
}
fn emit_upserted_model<M: Serialize + Clone>(window: &WebviewWindow, model: M) -> M {
fn emit_upserted_model<M: Serialize + Clone, R: Runtime>(window: &WebviewWindow<R>, model: M) -> M {
let payload = ModelPayload {
model: model.clone(),
window_label: window.label().to_string(),
@@ -1320,7 +1405,10 @@ fn emit_upserted_model<M: Serialize + Clone>(window: &WebviewWindow, model: M) -
model
}
fn emit_deleted_model<M: Serialize + Clone>(window: &WebviewWindow, model: M) -> Result<M> {
fn emit_deleted_model<M: Serialize + Clone, R: Runtime>(
window: &WebviewWindow<R>,
model: M,
) -> Result<M> {
let payload = ModelPayload {
model: model.clone(),
window_label: window.label().to_string(),

View File

@@ -17,18 +17,10 @@ pub enum Error {
GrpcSendErr(#[from] SendError<tonic::Result<EventStreamEvent>>),
#[error("JSON error")]
JsonErr(#[from] serde_json::Error),
#[error("Plugin not found error")]
#[error("Plugin not found: {0}")]
PluginNotFoundErr(String),
#[error("unknown error")]
MissingCallbackIdErr(String),
#[error("Missing callback ID error")]
MissingCallbackErr(String),
#[error("No plugins found")]
NoPluginsErr(String),
#[error("Plugin error")]
#[error("Plugin error: {0}")]
PluginErr(String),
#[error("Unknown error")]
UnknownErr(String),
}
impl Into<String> for Error {

View File

@@ -1,7 +1,11 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use ts_rs::TS;
use yaak_models::models::{CookieJar, Environment, Folder, GrpcConnection, GrpcEvent, GrpcRequest, HttpRequest, HttpResponse, KeyValue, Settings, Workspace};
use yaak_models::models::{
CookieJar, Environment, Folder, GrpcConnection, GrpcEvent, GrpcRequest, HttpRequest,
HttpResponse, KeyValue, Settings, Workspace,
};
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -14,18 +18,46 @@ pub struct InternalEvent {
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export)]
pub enum InternalEventPayload {
BootRequest(BootRequest),
BootResponse(BootResponse),
ImportRequest(ImportRequest),
ImportResponse(ImportResponse),
FilterRequest(FilterRequest),
FilterResponse(FilterResponse),
ExportHttpRequestRequest(ExportHttpRequestRequest),
ExportHttpRequestResponse(ExportHttpRequestResponse),
SendHttpRequestRequest(SendHttpRequestRequest),
SendHttpRequestResponse(SendHttpRequestResponse),
GetHttpRequestActionsRequest(GetHttpRequestActionsRequest),
GetHttpRequestActionsResponse(GetHttpRequestActionsResponse),
CallHttpRequestActionRequest(CallHttpRequestActionRequest),
GetTemplateFunctionsRequest,
GetTemplateFunctionsResponse(GetTemplateFunctionsResponse),
CallTemplateFunctionRequest(CallTemplateFunctionRequest),
CallTemplateFunctionResponse(CallTemplateFunctionResponse),
CopyTextRequest(CopyTextRequest),
RenderHttpRequestRequest(RenderHttpRequestRequest),
RenderHttpRequestResponse(RenderHttpRequestResponse),
ShowToastRequest(ShowToastRequest),
GetHttpRequestByIdRequest(GetHttpRequestByIdRequest),
GetHttpRequestByIdResponse(GetHttpRequestByIdResponse),
FindHttpResponsesRequest(FindHttpResponsesRequest),
FindHttpResponsesResponse(FindHttpResponsesResponse),
/// Returned when a plugin doesn't get run, just so the server
/// has something to listen for
EmptyResponse(EmptyResponse),
@@ -95,17 +127,253 @@ pub struct ExportHttpRequestResponse {
pub content: String,
}
// TODO: Migrate plugins to return this type
// #[derive(Debug, Clone, Serialize, Deserialize, TS)]
// #[serde(rename_all = "camelCase", untagged)]
// #[ts(export)]
// pub enum ExportableModel {
// Workspace(Workspace),
// Environment(Environment),
// Folder(Folder),
// HttpRequest(HttpRequest),
// GrpcRequest(GrpcRequest),
// }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct SendHttpRequestRequest {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct SendHttpRequestResponse {
pub http_response: HttpResponse,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct CopyTextRequest {
pub text: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct RenderHttpRequestRequest {
pub http_request: HttpRequest,
pub purpose: RenderPurpose,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct RenderHttpRequestResponse {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct ShowToastRequest {
pub message: String,
pub variant: ToastVariant,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export)]
pub enum ToastVariant {
Custom,
Copied,
Success,
Info,
Warning,
Error,
}
impl Default for ToastVariant {
fn default() -> Self {
ToastVariant::Info
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct GetTemplateFunctionsResponse {
pub functions: Vec<TemplateFunction>,
pub plugin_ref_id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct TemplateFunction {
pub name: String,
pub args: Vec<TemplateFunctionArg>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export)]
pub enum TemplateFunctionArg {
Text(TemplateFunctionTextArg),
Select(TemplateFunctionSelectArg),
Checkbox(TemplateFunctionCheckboxArg),
HttpRequest(TemplateFunctionHttpRequestArg),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct TemplateFunctionBaseArg {
pub name: String,
#[ts(optional = nullable)]
pub optional: Option<bool>,
#[ts(optional = nullable)]
pub label: Option<String>,
#[ts(optional = nullable)]
pub default_value: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct TemplateFunctionTextArg {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
#[ts(optional = nullable)]
pub placeholder: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct TemplateFunctionHttpRequestArg {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct TemplateFunctionSelectArg {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
pub options: Vec<TemplateFunctionSelectOption>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct TemplateFunctionCheckboxArg {
#[serde(flatten)]
pub base: TemplateFunctionBaseArg,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct TemplateFunctionSelectOption {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct CallTemplateFunctionRequest {
pub name: String,
pub args: CallTemplateFunctionArgs,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct CallTemplateFunctionResponse {
pub value: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct CallTemplateFunctionArgs {
pub purpose: RenderPurpose,
pub values: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export)]
pub enum RenderPurpose {
Send,
Preview,
}
impl Default for RenderPurpose {
fn default() -> Self {
RenderPurpose::Preview
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)]
#[ts(export)]
pub struct GetHttpRequestActionsRequest {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct GetHttpRequestActionsResponse {
pub actions: Vec<HttpRequestAction>,
pub plugin_ref_id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct HttpRequestAction {
pub key: String,
pub label: String,
pub icon: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct CallHttpRequestActionRequest {
pub key: String,
pub plugin_ref_id: String,
pub args: CallHttpRequestActionArgs,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct CallHttpRequestActionArgs {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct GetHttpRequestByIdRequest {
pub id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct GetHttpRequestByIdResponse {
pub http_request: Option<HttpRequest>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct FindHttpResponsesRequest {
pub request_id: String,
pub limit: Option<i32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct FindHttpResponsesResponse {
pub http_responses: Vec<HttpResponse>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]

View File

@@ -1,8 +1,11 @@
use crate::error::Result;
use crate::events::{
ExportHttpRequestRequest, ExportHttpRequestResponse, FilterRequest, FilterResponse,
ImportRequest, ImportResponse, InternalEventPayload,
CallHttpRequestActionRequest, CallTemplateFunctionArgs, RenderPurpose,
CallTemplateFunctionRequest, CallTemplateFunctionResponse, FilterRequest, FilterResponse,
GetHttpRequestActionsRequest, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse,
ImportRequest, ImportResponse, InternalEvent, InternalEventPayload,
};
use std::collections::HashMap;
use crate::error::Error::PluginErr;
use crate::nodejs::start_nodejs_plugin_runtime;
@@ -10,8 +13,8 @@ use crate::plugin::start_server;
use crate::server::PluginRuntimeGrpcServer;
use std::time::Duration;
use tauri::{AppHandle, Runtime};
use tokio::sync::mpsc;
use tokio::sync::watch::Sender;
use yaak_models::models::HttpRequest;
pub struct PluginManager {
kill_tx: Sender<bool>,
@@ -35,14 +38,109 @@ impl PluginManager {
PluginManager { kill_tx, server }
}
pub async fn cleanup(&mut self) {
pub async fn subscribe(&self) -> (String, mpsc::Receiver<InternalEvent>) {
self.server.subscribe().await
}
pub async fn unsubscribe(&self, rx_id: &str) {
self.server.unsubscribe(rx_id).await
}
pub async fn cleanup(&self) {
self.kill_tx.send_replace(true);
// Give it a bit of time to kill
tokio::time::sleep(Duration::from_millis(500)).await;
}
pub async fn run_import(&mut self, content: &str) -> Result<(ImportResponse, String)> {
pub async fn reply(
&self,
source_event: &InternalEvent,
payload: &InternalEventPayload,
) -> Result<()> {
let reply_id = Some(source_event.clone().id);
self.server
.send(&payload, source_event.plugin_ref_id.as_str(), reply_id)
.await
}
pub async fn get_http_request_actions(&self) -> Result<Vec<GetHttpRequestActionsResponse>> {
let reply_events = self
.server
.send_and_wait(&InternalEventPayload::GetHttpRequestActionsRequest(
GetHttpRequestActionsRequest {},
))
.await?;
let mut all_actions = Vec::new();
for event in reply_events {
if let InternalEventPayload::GetHttpRequestActionsResponse(resp) = event.payload {
all_actions.push(resp.clone());
}
}
Ok(all_actions)
}
pub async fn get_template_functions(&self) -> Result<Vec<GetTemplateFunctionsResponse>> {
let reply_events = self
.server
.send_and_wait(&InternalEventPayload::GetTemplateFunctionsRequest)
.await?;
let mut all_actions = Vec::new();
for event in reply_events {
if let InternalEventPayload::GetTemplateFunctionsResponse(resp) = event.payload {
all_actions.push(resp.clone());
}
}
Ok(all_actions)
}
pub async fn call_http_request_action(&self, req: CallHttpRequestActionRequest) -> Result<()> {
let plugin = self
.server
.plugin_by_ref_id(req.plugin_ref_id.as_str())
.await?;
let event = plugin.build_event_to_send(
&InternalEventPayload::CallHttpRequestActionRequest(req),
None,
);
plugin.send(&event).await?;
Ok(())
}
pub async fn call_template_function(
&self,
fn_name: &str,
args: HashMap<String, String>,
purpose: RenderPurpose,
) -> Result<Option<String>> {
let req = CallTemplateFunctionRequest {
name: fn_name.to_string(),
args: CallTemplateFunctionArgs {
purpose,
values: args,
},
};
let events = self
.server
.send_and_wait(&InternalEventPayload::CallTemplateFunctionRequest(req))
.await?;
let value = events.into_iter().find_map(|e| match e.payload {
InternalEventPayload::CallTemplateFunctionResponse(CallTemplateFunctionResponse {
value,
}) => value,
_ => None,
});
Ok(value)
}
pub async fn import_data(&self, content: &str) -> Result<(ImportResponse, String)> {
let reply_events = self
.server
.send_and_wait(&InternalEventPayload::ImportRequest(ImportRequest {
@@ -51,53 +149,31 @@ impl PluginManager {
.await?;
// TODO: Don't just return the first valid response
for event in reply_events {
match event.payload {
InternalEventPayload::ImportResponse(resp) => {
let ref_id = event.plugin_ref_id.as_str();
let plugin = self.server.plugin_by_ref_id(ref_id).await?;
let plugin_name = plugin.name().await;
return Ok((resp, plugin_name));
}
_ => {}
let result = reply_events.into_iter().find_map(|e| match e.payload {
InternalEventPayload::ImportResponse(resp) => Some((resp, e.plugin_ref_id)),
_ => None,
});
match result {
None => Err(PluginErr("No importers found for file contents".to_string())),
Some((resp, ref_id)) => {
let plugin = self.server.plugin_by_ref_id(ref_id.as_str()).await?;
let plugin_name = plugin.name().await;
Ok((resp, plugin_name))
}
}
Err(PluginErr("No import responses found".to_string()))
}
pub async fn run_export_curl(
&mut self,
request: &HttpRequest,
) -> Result<ExportHttpRequestResponse> {
let event = self
.server
.send_to_plugin_and_wait(
"exporter-curl",
&InternalEventPayload::ExportHttpRequestRequest(ExportHttpRequestRequest {
http_request: request.to_owned(),
}),
)
.await?;
match event.payload {
InternalEventPayload::ExportHttpRequestResponse(resp) => Ok(resp),
InternalEventPayload::EmptyResponse(_) => {
Err(PluginErr("Export returned empty".to_string()))
}
e => Err(PluginErr(format!("Export returned invalid event {:?}", e))),
}
}
pub async fn run_filter(
&mut self,
pub async fn filter_data(
&self,
filter: &str,
content: &str,
content_type: &str,
) -> Result<FilterResponse> {
let plugin_name = match content_type {
"application/json" => "filter-jsonpath",
_ => "filter-xpath",
let plugin_name = if content_type.to_lowercase().contains("json") {
"filter-jsonpath"
} else {
"filter-xpath"
};
let event = self

View File

@@ -13,7 +13,6 @@ use tauri::plugin::{Builder, TauriPlugin};
use tauri::{Manager, RunEvent, Runtime, State};
use tokio::fs::read_dir;
use tokio::net::TcpListener;
use tokio::sync::Mutex;
use tonic::codegen::tokio_stream;
use tonic::transport::Server;
@@ -30,8 +29,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.await
.expect("Failed to read plugins dir");
let manager = PluginManager::new(&app, plugin_dirs).await;
let manager_state = Mutex::new(manager);
app.manage(manager_state);
app.manage(manager);
Ok(())
})
})
@@ -41,8 +39,8 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
api.prevent_exit();
tauri::async_runtime::block_on(async move {
info!("Exiting plugin runtime due to app exit");
let manager: State<Mutex<PluginManager>> = app.state();
manager.lock().await.cleanup().await;
let manager: State<PluginManager> = app.state();
manager.cleanup().await;
exit(0);
});
}
@@ -79,7 +77,7 @@ pub async fn start_server(
_ => {}
};
}
server.unsubscribe(rx_id).await;
server.unsubscribe(rx_id.as_str()).await;
});
};

View File

@@ -9,7 +9,7 @@ use tonic::codegen::tokio_stream::wrappers::ReceiverStream;
use tonic::codegen::tokio_stream::{Stream, StreamExt};
use tonic::{Request, Response, Status, Streaming};
use crate::error::Error::{NoPluginsErr, PluginNotFoundErr};
use crate::error::Error::PluginNotFoundErr;
use crate::error::Result;
use crate::events::{BootRequest, BootResponse, InternalEvent, InternalEventPayload};
use crate::server::plugin_runtime::plugin_runtime_server::PluginRuntime;
@@ -53,7 +53,11 @@ impl PluginHandle {
}
pub async fn send(&self, event: &InternalEvent) -> Result<()> {
info!("Sending event {} {:?}", event.id, self.name().await);
info!(
"Sending event to plugin {} {:?}",
event.id,
self.name().await
);
self.to_plugin_tx
.lock()
.await
@@ -90,13 +94,13 @@ impl PluginRuntimeGrpcServer {
pub async fn subscribe(&self) -> (String, Receiver<InternalEvent>) {
let (tx, rx) = mpsc::channel(128);
let id = generate_id();
self.subscribers.lock().await.insert(id.clone(), tx);
(id, rx)
let rx_id = generate_id();
self.subscribers.lock().await.insert(rx_id.clone(), tx);
(rx_id, rx)
}
pub async fn unsubscribe(&self, rx_id: String) {
self.subscribers.lock().await.remove(rx_id.as_str());
pub async fn unsubscribe(&self, rx_id: &str) {
self.subscribers.lock().await.remove(rx_id);
}
pub async fn remove_plugins(&self, plugin_ids: Vec<String>) {
@@ -187,14 +191,11 @@ impl PluginRuntimeGrpcServer {
pub async fn plugin_by_ref_id(&self, ref_id: &str) -> Result<PluginHandle> {
let plugins = self.plugin_ref_to_plugin.lock().await;
if plugins.is_empty() {
return Err(NoPluginsErr("Send failed because no plugins exist".into()));
return Err(PluginNotFoundErr(ref_id.into()));
}
match plugins.get(ref_id) {
None => {
let msg = format!("Failed to find plugin for id {ref_id}");
Err(PluginNotFoundErr(msg))
}
None => Err(PluginNotFoundErr(ref_id.into())),
Some(p) => Ok(p.to_owned()),
}
}
@@ -202,7 +203,7 @@ impl PluginRuntimeGrpcServer {
pub async fn plugin_by_name(&self, plugin_name: &str) -> Result<PluginHandle> {
let plugins = self.plugin_ref_to_plugin.lock().await;
if plugins.is_empty() {
return Err(NoPluginsErr("Send failed because no plugins exist".into()));
return Err(PluginNotFoundErr(plugin_name.into()));
}
for p in plugins.values() {
@@ -211,8 +212,18 @@ impl PluginRuntimeGrpcServer {
}
}
let msg = format!("Failed to find plugin for {plugin_name}");
Err(PluginNotFoundErr(msg))
Err(PluginNotFoundErr(plugin_name.into()))
}
pub async fn send(
&self,
payload: &InternalEventPayload,
plugin_ref_id: &str,
reply_id: Option<String>,
) -> Result<()> {
let plugin = self.plugin_by_ref_id(plugin_ref_id).await?;
let event = plugin.build_event_to_send(payload, reply_id);
plugin.send(&event).await
}
pub async fn send_to_plugin(
@@ -222,7 +233,7 @@ impl PluginRuntimeGrpcServer {
) -> Result<InternalEvent> {
let plugins = self.plugin_ref_to_plugin.lock().await;
if plugins.is_empty() {
return Err(NoPluginsErr("Send failed because no plugins exist".into()));
return Err(PluginNotFoundErr(plugin_name.into()));
}
let mut plugin = None;
@@ -239,10 +250,7 @@ impl PluginRuntimeGrpcServer {
plugin.send(&event).await?;
Ok(event)
}
None => {
let msg = format!("Failed to find plugin for {plugin_name}");
Err(PluginNotFoundErr(msg))
}
None => Err(PluginNotFoundErr(plugin_name.into())),
}
}
@@ -301,7 +309,7 @@ impl PluginRuntimeGrpcServer {
break;
}
}
server.unsubscribe(rx_id).await;
server.unsubscribe(rx_id.as_str()).await;
found_events
})
@@ -321,30 +329,6 @@ impl PluginRuntimeGrpcServer {
Ok(events)
}
pub async fn send(&self, payload: InternalEventPayload) -> Result<Vec<InternalEvent>> {
let mut events: Vec<InternalEvent> = Vec::new();
let plugins = self.plugin_ref_to_plugin.lock().await;
if plugins.is_empty() {
return Err(NoPluginsErr("Send failed because no plugins exist".into()));
}
for ph in plugins.values() {
let event = ph.build_event_to_send(&payload, None);
self.send_to_plugin_handle(ph, &event).await?;
events.push(event);
}
Ok(events)
}
async fn send_to_plugin_handle(
&self,
plugin: &PluginHandle,
event: &InternalEvent,
) -> Result<()> {
plugin.send(event).await
}
async fn load_plugins(
&self,
to_plugin_tx: mpsc::Sender<tonic::Result<EventStreamEvent>>,
@@ -414,7 +398,7 @@ impl PluginRuntime for PluginRuntimeGrpcServer {
for tx in subscribers.values() {
// Emit event to the channel for server to handle
if let Err(e) = tx.try_send(event.clone()) {
println!("Failed to send to server channel. Receiver probably isn't listening: {:?}", e);
println!("Failed to send to server channel (n={}). Receiver probably isn't listening: {:?}", subscribers.len(), e);
}
}

View File

@@ -0,0 +1,11 @@
[package]
name = "yaak_templates"
version = "0.1.0"
edition = "2021"
[dependencies]
log = "0.4.22"
serde = { version = "1.0.208", features = ["derive"] }
serde_json = "1.0.125"
ts-rs = { version = "9.0.1" }
tokio = { version = "1.39.3", features = ["macros", "rt"] }

View File

@@ -0,0 +1,6 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Tell ts-rs where to generate types to
println!("cargo:rustc-env=TS_RS_EXPORT_DIR=../../src-web/gen");
Ok(())
}

View File

@@ -2,6 +2,4 @@ pub mod parser;
pub mod renderer;
pub use parser::*;
pub use renderer::*;
pub fn template_foo() {}
pub use renderer::*;

View File

@@ -0,0 +1,733 @@
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use ts_rs::TS;
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct Tokens {
pub tokens: Vec<Token>,
}
impl Display for Tokens {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = self
.tokens
.iter()
.map(|t| t.to_string())
.collect::<Vec<String>>()
.join("");
write!(f, "{}", str)
}
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct FnArg {
pub name: String,
pub value: Val,
}
impl Display for FnArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = format!("{}={}", self.name, self.value);
write!(f, "{}", str)
}
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export)]
pub enum Val {
Str { text: String },
Var { name: String },
Bool { value: bool },
Fn { name: String, args: Vec<FnArg> },
Null,
}
impl Display for Val {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
Val::Str { text } => format!("'{}'", text.to_string().replace("'", "\'")),
Val::Var { name } => name.to_string(),
Val::Bool { value } => value.to_string(),
Val::Fn { name, args } => {
format!(
"{name}({})",
args.iter()
.filter_map(|a| match a.value.clone() {
Val::Null => None,
_ => Some(a.to_string()),
})
.collect::<Vec<String>>()
.join(", ")
)
}
Val::Null => "null".to_string(),
};
write!(f, "{}", str)
}
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export)]
pub enum Token {
Raw { text: String },
Tag { val: Val },
Eof,
}
impl Display for Token {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
Token::Raw { text } => text.to_string(),
Token::Tag { val } => format!("${{[ {} ]}}", val.to_string()),
Token::Eof => "".to_string(),
};
write!(f, "{}", str)
}
}
// Template Syntax
//
// ${[ my_var ]}
// ${[ my_fn() ]}
// ${[ my_fn(my_var) ]}
// ${[ my_fn(my_var, "A String") ]}
// default
#[derive(Default)]
pub struct Parser {
tokens: Vec<Token>,
chars: Vec<char>,
pos: usize,
curr_text: String,
}
impl Parser {
pub fn new(text: &str) -> Parser {
Parser {
chars: text.chars().collect(),
..Parser::default()
}
}
pub fn parse(&mut self) -> Tokens {
let start_pos = self.pos;
while self.pos < self.chars.len() {
if self.match_str("${[") {
let start_curr = self.pos;
if let Some(t) = self.parse_tag() {
self.push_token(t);
} else {
self.pos = start_curr;
self.curr_text += "${[";
}
} else {
let ch = self.next_char();
self.curr_text.push(ch);
}
if start_pos == self.pos {
panic!("Parser stuck!");
}
}
self.push_token(Token::Eof);
Tokens {
tokens: self.tokens.clone(),
}
}
fn parse_tag(&mut self) -> Option<Token> {
// Parse up to first identifier
// ${[ my_var...
self.skip_whitespace();
let val = match self.parse_value() {
Some(v) => v,
None => return None,
};
// Parse to closing tag
// ${[ my_var(a, b, c) ]}
self.skip_whitespace();
if !self.match_str("]}") {
return None;
}
Some(Token::Tag { val })
}
#[allow(dead_code)]
fn debug_pos(&self, x: &str) {
println!(
r#"Position: {x}: text[{}]='{}' → "{}" → {:?}"#,
self.pos,
self.chars[self.pos],
self.chars.iter().collect::<String>(),
self.tokens,
);
}
fn parse_value(&mut self) -> Option<Val> {
if let Some((name, args)) = self.parse_fn() {
Some(Val::Fn { name, args })
} else if let Some(v) = self.parse_ident() {
if v == "null" {
Some(Val::Null)
} else if v == "true" {
Some(Val::Bool { value: true })
} else if v == "false" {
Some(Val::Bool { value: false })
} else {
Some(Val::Var { name: v })
}
} else if let Some(v) = self.parse_string() {
Some(Val::Str { text: v })
} else {
None
}
}
fn parse_fn(&mut self) -> Option<(String, Vec<FnArg>)> {
let start_pos = self.pos;
let name = match self.parse_ident() {
Some(v) => v,
None => {
self.pos = start_pos;
return None;
}
};
let args = match self.parse_fn_args() {
Some(args) => args,
None => {
self.pos = start_pos;
return None;
}
};
Some((name, args))
}
fn parse_fn_args(&mut self) -> Option<Vec<FnArg>> {
if !self.match_str("(") {
return None;
}
let start_pos = self.pos;
let mut args: Vec<FnArg> = Vec::new();
// Fn closed immediately
self.skip_whitespace();
if self.match_str(")") {
return Some(args);
}
while self.pos < self.chars.len() {
self.skip_whitespace();
let name = self.parse_ident();
self.skip_whitespace();
self.match_str("=");
self.skip_whitespace();
let value = self.parse_value();
self.skip_whitespace();
if let (Some(name), Some(value)) = (name.clone(), value.clone()) {
args.push(FnArg { name, value });
} else {
// Didn't find valid thing, so return
self.pos = start_pos;
return None;
}
if self.match_str(")") {
break;
}
self.skip_whitespace();
// If we don't find a comma, that's bad
if !args.is_empty() && !self.match_str(",") {
self.pos = start_pos;
return None;
}
if start_pos == self.pos {
panic!("Parser stuck!");
}
}
Some(args)
}
fn parse_ident(&mut self) -> Option<String> {
let start_pos = self.pos;
let mut text = String::new();
while self.pos < self.chars.len() {
let ch = self.peek_char();
if ch.is_alphanumeric() || ch == '_' {
text.push(ch);
self.pos += 1;
} else {
break;
}
if start_pos == self.pos {
panic!("Parser stuck!");
}
}
if text.is_empty() {
self.pos = start_pos;
return None;
}
Some(text)
}
fn parse_string(&mut self) -> Option<String> {
let start_pos = self.pos;
let mut text = String::new();
if !self.match_str("'") {
return None;
}
let mut found_closing = false;
while self.pos < self.chars.len() {
let ch = self.next_char();
match ch {
'\\' => {
text.push(self.next_char());
}
'\'' => {
found_closing = true;
break;
}
_ => {
text.push(ch);
}
}
if start_pos == self.pos {
panic!("Parser stuck!");
}
}
if !found_closing {
self.pos = start_pos;
return None;
}
Some(text)
}
fn skip_whitespace(&mut self) {
while self.pos < self.chars.len() {
if self.peek_char().is_whitespace() {
self.pos += 1;
} else {
break;
}
}
}
fn next_char(&mut self) -> char {
let ch = self.peek_char();
self.pos += 1;
ch
}
fn peek_char(&self) -> char {
let ch = self.chars[self.pos];
ch
}
fn push_token(&mut self, token: Token) {
// Push any text we've accumulated
if !self.curr_text.is_empty() {
let text_token = Token::Raw {
text: self.curr_text.clone(),
};
self.tokens.push(text_token);
self.curr_text.clear();
}
self.tokens.push(token);
}
fn match_str(&mut self, value: &str) -> bool {
if self.pos + value.len() > self.chars.len() {
return false;
}
let cmp = self.chars[self.pos..self.pos + value.len()]
.iter()
.collect::<String>();
if cmp == value {
// We have a match, so advance the current index
self.pos += value.len();
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use crate::Val::Null;
use crate::*;
#[test]
fn var_simple() {
let mut p = Parser::new("${[ foo ]}");
assert_eq!(
p.parse().tokens,
vec![
Token::Tag {
val: Val::Var { name: "foo".into() }
},
Token::Eof
]
);
}
#[test]
fn var_boolean() {
let mut p = Parser::new("${[ true ]}${[ false ]}");
assert_eq!(
p.parse().tokens,
vec![
Token::Tag {
val: Val::Bool { value: true },
},
Token::Tag {
val: Val::Bool { value: false },
},
Token::Eof
]
);
}
#[test]
fn var_multiple_names_invalid() {
let mut p = Parser::new("${[ foo bar ]}");
assert_eq!(
p.parse().tokens,
vec![
Token::Raw {
text: "${[ foo bar ]}".into()
},
Token::Eof
]
);
}
#[test]
fn tag_string() {
let mut p = Parser::new(r#"${[ 'foo \'bar\' baz' ]}"#);
assert_eq!(
p.parse().tokens,
vec![
Token::Tag {
val: Val::Str {
text: r#"foo 'bar' baz"#.into()
}
},
Token::Eof
]
);
}
#[test]
fn var_surrounded() {
let mut p = Parser::new("Hello ${[ foo ]}!");
assert_eq!(
p.parse().tokens,
vec![
Token::Raw {
text: "Hello ".to_string()
},
Token::Tag {
val: Val::Var { name: "foo".into() }
},
Token::Raw {
text: "!".to_string()
},
Token::Eof,
]
);
}
#[test]
fn fn_simple() {
let mut p = Parser::new("${[ foo() ]}");
assert_eq!(
p.parse().tokens,
vec![
Token::Tag {
val: Val::Fn {
name: "foo".into(),
args: Vec::new(),
}
},
Token::Eof
]
);
}
#[test]
fn fn_ident_arg() {
let mut p = Parser::new("${[ foo(a=bar) ]}");
assert_eq!(
p.parse().tokens,
vec![
Token::Tag {
val: Val::Fn {
name: "foo".into(),
args: vec![FnArg {
name: "a".into(),
value: Val::Var { name: "bar".into() }
}],
}
},
Token::Eof
]
);
}
#[test]
fn fn_ident_args() {
let mut p = Parser::new("${[ foo(a=bar,b = baz, c =qux ) ]}");
assert_eq!(
p.parse().tokens,
vec![
Token::Tag {
val: Val::Fn {
name: "foo".into(),
args: vec![
FnArg {
name: "a".into(),
value: Val::Var { name: "bar".into() }
},
FnArg {
name: "b".into(),
value: Val::Var { name: "baz".into() }
},
FnArg {
name: "c".into(),
value: Val::Var { name: "qux".into() }
},
],
}
},
Token::Eof
]
);
}
#[test]
fn fn_mixed_args() {
let mut p = Parser::new(r#"${[ foo(aaa=bar,bb='baz \'hi\'', c=qux, z=true ) ]}"#);
assert_eq!(
p.parse().tokens,
vec![
Token::Tag {
val: Val::Fn {
name: "foo".into(),
args: vec![
FnArg {
name: "aaa".into(),
value: Val::Var { name: "bar".into() }
},
FnArg {
name: "bb".into(),
value: Val::Str {
text: r#"baz 'hi'"#.into()
}
},
FnArg {
name: "c".into(),
value: Val::Var { name: "qux".into() }
},
FnArg {
name: "z".into(),
value: Val::Bool { value: true }
},
],
}
},
Token::Eof
]
);
}
#[test]
fn fn_nested() {
let mut p = Parser::new("${[ foo(b=bar()) ]}");
assert_eq!(
p.parse().tokens,
vec![
Token::Tag {
val: Val::Fn {
name: "foo".into(),
args: vec![FnArg {
name: "b".into(),
value: Val::Fn {
name: "bar".into(),
args: vec![],
}
}],
}
},
Token::Eof
]
);
}
#[test]
fn fn_nested_args() {
let mut p = Parser::new(r#"${[ outer(a=inner(a=foo, b='i'), c='o') ]}"#);
assert_eq!(
p.parse().tokens,
vec![
Token::Tag {
val: Val::Fn {
name: "outer".into(),
args: vec![
FnArg {
name: "a".into(),
value: Val::Fn {
name: "inner".into(),
args: vec![
FnArg {
name: "a".into(),
value: Val::Var { name: "foo".into() }
},
FnArg {
name: "b".into(),
value: Val::Str { text: "i".into() },
},
],
}
},
FnArg {
name: "c".into(),
value: Val::Str { text: "o".into() }
},
],
}
},
Token::Eof
]
);
}
#[test]
fn token_display_var() {
assert_eq!(
Val::Var {
name: "foo".to_string()
}
.to_string(),
"foo"
);
}
#[test]
fn token_display_str() {
assert_eq!(
Val::Str {
text: "Hello 'You'".to_string()
}
.to_string(),
"'Hello \'You\''"
);
}
#[test]
fn token_null_fn_arg() {
assert_eq!(
Val::Fn {
name: "fn".to_string(),
args: vec![
FnArg {
name: "n".to_string(),
value: Null,
},
FnArg {
name: "a".to_string(),
value: Val::Str {
text: "aaa".to_string()
}
}
]
}
.to_string(),
r#"fn(a='aaa')"#
);
}
#[test]
fn token_display_fn() {
assert_eq!(
Token::Tag {
val: Val::Fn {
name: "foo".to_string(),
args: vec![
FnArg {
name: "arg".to_string(),
value: Val::Str {
text: "v".to_string()
}
},
FnArg {
name: "arg2".to_string(),
value: Val::Var {
name: "my_var".to_string()
}
}
]
}
}
.to_string(),
r#"${[ foo(arg='v', arg2=my_var) ]}"#
);
}
#[test]
fn tokens_display() {
assert_eq!(
Tokens {
tokens: vec![
Token::Tag {
val: Val::Var {
name: "my_var".to_string()
}
},
Token::Raw {
text: " Some cool text ".to_string(),
},
Token::Tag {
val: Val::Str {
text: "Hello World".to_string()
}
}
]
}
.to_string(),
r#"${[ my_var ]} Some cool text ${[ 'Hello World' ]}"#
);
}
}

View File

@@ -0,0 +1,230 @@
use crate::{FnArg, Parser, Token, Tokens, Val};
use log::warn;
use std::collections::HashMap;
use std::future::Future;
pub trait TemplateCallback {
fn run(
&self,
fn_name: &str,
args: HashMap<String, String>,
) -> impl Future<Output = Result<String, String>> + Send;
}
pub async fn parse_and_render<T: TemplateCallback>(
template: &str,
vars: &HashMap<String, String>,
cb: &T,
) -> String {
let mut p = Parser::new(template);
let tokens = p.parse();
render(tokens, vars, cb).await
}
pub async fn render<T: TemplateCallback>(
tokens: Tokens,
vars: &HashMap<String, String>,
cb: &T,
) -> String {
let mut doc_str: Vec<String> = Vec::new();
for t in tokens.tokens {
match t {
Token::Raw { text } => doc_str.push(text),
Token::Tag { val } => doc_str.push(render_tag(val, &vars, cb).await),
Token::Eof => {}
}
}
doc_str.join("")
}
async fn render_tag<T: TemplateCallback>(
val: Val,
vars: &HashMap<String, String>,
cb: &T,
) -> String {
match val {
Val::Str { text } => text.into(),
Val::Var { name } => match vars.get(name.as_str()) {
Some(v) => v.to_string(),
None => "".into(),
},
Val::Bool { value } => value.to_string(),
Val::Fn { name, args } => {
let empty = "".to_string();
let mut resolved_args: HashMap<String, String> = HashMap::new();
for a in args {
let (k, v) = match a {
FnArg {
name,
value: Val::Str { text },
} => (name.to_string(), text.to_string()),
FnArg {
name,
value: Val::Var { name: var_name },
} => (
name.to_string(),
vars.get(var_name.as_str()).unwrap_or(&empty).to_string(),
),
FnArg { name, value: val } => {
let r = Box::pin(render_tag(val.clone(), vars, cb)).await;
(name.to_string(), r)
}
};
resolved_args.insert(k, v);
}
match cb.run(name.as_str(), resolved_args.clone()).await {
Ok(s) => s,
Err(e) => {
warn!(
"Failed to run template callback {}({:?}): {}",
name, resolved_args, e
);
"".to_string()
}
}
}
Val::Null => "".into(),
}
}
#[cfg(test)]
mod tests {
use crate::renderer::TemplateCallback;
use crate::*;
use std::collections::HashMap;
struct EmptyCB {}
impl TemplateCallback for EmptyCB {
async fn run(
&self,
_fn_name: &str,
_args: HashMap<String, String>,
) -> Result<String, String> {
todo!()
}
}
#[tokio::test]
async fn render_empty() {
let empty_cb = EmptyCB {};
let template = "";
let vars = HashMap::new();
let result = "";
assert_eq!(
parse_and_render(template, &vars, &empty_cb).await,
result.to_string()
);
}
#[tokio::test]
async fn render_text_only() {
let empty_cb = EmptyCB {};
let template = "Hello World!";
let vars = HashMap::new();
let result = "Hello World!";
assert_eq!(
parse_and_render(template, &vars, &empty_cb).await,
result.to_string()
);
}
#[tokio::test]
async fn render_simple() {
let empty_cb = EmptyCB {};
let template = "${[ foo ]}";
let vars = HashMap::from([("foo".to_string(), "bar".to_string())]);
let result = "bar";
assert_eq!(
parse_and_render(template, &vars, &empty_cb).await,
result.to_string()
);
}
#[tokio::test]
async fn render_surrounded() {
let empty_cb = EmptyCB {};
let template = "hello ${[ word ]} world!";
let vars = HashMap::from([("word".to_string(), "cruel".to_string())]);
let result = "hello cruel world!";
assert_eq!(
parse_and_render(template, &vars, &empty_cb).await,
result.to_string()
);
}
#[tokio::test]
async fn render_valid_fn() {
let vars = HashMap::new();
let template = r#"${[ say_hello(a='John', b='Kate') ]}"#;
let result = r#"say_hello: 2, Some("John") Some("Kate")"#;
struct CB {}
impl TemplateCallback for CB {
async fn run(
&self,
fn_name: &str,
args: HashMap<String, String>,
) -> Result<String, String> {
Ok(format!(
"{fn_name}: {}, {:?} {:?}",
args.len(),
args.get("a"),
args.get("b")
))
}
}
assert_eq!(parse_and_render(template, &vars, &CB {}).await, result);
}
#[tokio::test]
async fn render_nested_fn() {
let vars = HashMap::new();
let template = r#"${[ upper(foo=secret()) ]}"#;
let result = r#"ABC"#;
struct CB {}
impl TemplateCallback for CB {
async fn run(
&self,
fn_name: &str,
args: HashMap<String, String>,
) -> Result<String, String> {
Ok(match fn_name {
"secret" => "abc".to_string(),
"upper" => args["foo"].to_string().to_uppercase(),
_ => "".to_string(),
})
}
}
assert_eq!(
parse_and_render(template, &vars, &CB {}).await,
result.to_string()
);
}
#[tokio::test]
async fn render_fn_err() {
let vars = HashMap::new();
let template = r#"${[ error() ]}"#;
let result = r#""#;
struct CB {}
impl TemplateCallback for CB {
async fn run(
&self,
_fn_name: &str,
_args: HashMap<String, String>,
) -> Result<String, String> {
Err("Failed to do it!".to_string())
}
}
assert_eq!(
parse_and_render(template, &vars, &CB {}).await,
result.to_string()
);
}
}

View File

@@ -7,6 +7,8 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import { HelmetProvider } from 'react-helmet-async';
import { AppRouter } from './AppRouter';
const ENABLE_REACT_QUERY_DEVTOOLS = false;
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@@ -20,7 +22,7 @@ const queryClient = new QueryClient({
export function App() {
return (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools buttonPosition="bottom-left" />
{ENABLE_REACT_QUERY_DEVTOOLS && <ReactQueryDevtools buttonPosition="bottom-left" />}
<MotionConfig transition={{ duration: 0.1 }}>
<HelmetProvider>
<DndProvider backend={HTML5Backend}>

View File

@@ -1,12 +1,11 @@
import classNames from 'classnames';
import { search } from 'fast-fuzzy';
import type { KeyboardEvent, ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useRef, useCallback, useMemo, useState } from 'react';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest';
@@ -17,12 +16,14 @@ import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useEnvironments } from '../hooks/useEnvironments';
import type { HotkeyAction } from '../hooks/useHotKey';
import { useHotKey } from '../hooks/useHotKey';
import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useRenameRequest } from '../hooks/useRenameRequest';
import { useRequests } from '../hooks/useRequests';
import { useScrollIntoView } from '../hooks/useScrollIntoView';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useWorkspaces } from '../hooks/useWorkspaces';
@@ -56,8 +57,9 @@ const MAX_PER_GROUP = 8;
export function CommandPalette({ onClose }: { onClose: () => void }) {
const [command, setCommand] = useDebouncedState<string>('', 150);
const [selectedItemKey, setSelectedItemKey] = useState<string | null>(null);
const [activeEnvironment, setActiveEnvironmentId] = useActiveEnvironment();
const httpRequestActions = useHttpRequestActions();
const routes = useAppRoutes();
const activeEnvironmentId = useActiveEnvironmentId();
const workspaces = useWorkspaces();
const environments = useEnvironments();
const recentEnvironments = useRecentEnvironments();
@@ -68,12 +70,11 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
const openWorkspace = useOpenWorkspace();
const createWorkspace = useCreateWorkspace();
const createHttpRequest = useCreateHttpRequest();
const { activeCookieJar } = useActiveCookieJar();
const [activeCookieJar] = useActiveCookieJar();
const createGrpcRequest = useCreateGrpcRequest();
const createEnvironment = useCreateEnvironment();
const dialog = useDialog();
const workspaceId = useActiveWorkspaceId();
const activeEnvironment = useActiveEnvironment();
const workspace = useActiveWorkspace();
const sendRequest = useSendAnyHttpRequest();
const renameRequest = useRenameRequest(activeRequest?.id ?? null);
const deleteRequest = useDeleteRequest(activeRequest?.id ?? null);
@@ -86,9 +87,9 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
label: 'Open Settings',
action: 'settings.show',
onSelect: async () => {
if (workspaceId == null) return;
if (workspace == null) return;
await invokeCmd('cmd_new_nested_window', {
url: routes.paths.workspaceSettings({ workspaceId }),
url: routes.paths.workspaceSettings({ workspaceId: workspace.id }),
label: 'settings',
title: 'Yaak Settings',
});
@@ -155,6 +156,13 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
label: 'Send Request',
onSelect: () => sendRequest.mutateAsync(activeRequest.id),
});
for (const a of httpRequestActions) {
commands.push({
key: a.key,
label: a.label,
onSelect: () => a.call(activeRequest),
});
}
}
if (activeRequest != null) {
@@ -186,11 +194,12 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
createWorkspace.mutate,
deleteRequest.mutate,
dialog,
httpRequestActions,
renameRequest.mutate,
routes.paths,
sendRequest,
setSidebarHidden,
workspaceId,
workspace,
]);
const sortedRequests = useMemo(() => {
@@ -263,7 +272,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
searchText: fallbackRequestName(r),
label: (
<HStack space={2}>
<HttpMethodTag className="text-fg-subtler" request={r} />
<HttpMethodTag className="text-text-subtlest" request={r} />
<div className="truncate">{fallbackRequestName(r)}</div>
</HStack>
),
@@ -271,7 +280,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
return routes.navigate('request', {
workspaceId: r.workspaceId,
requestId: r.id,
environmentId: activeEnvironmentId ?? undefined,
environmentId: activeEnvironment?.id,
});
},
});
@@ -290,7 +299,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
environmentGroup.items.push({
key: `switch-environment-${e.id}`,
label: e.name,
onSelect: () => routes.setEnvironment(e),
onSelect: () => setActiveEnvironmentId(e.id),
});
}
@@ -313,9 +322,9 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
workspaceCommands,
sortedRequests,
routes,
activeEnvironmentId,
activeEnvironment,
sortedEnvironments,
activeEnvironment?.id,
setActiveEnvironmentId,
sortedWorkspaces,
openWorkspace,
]);
@@ -383,13 +392,13 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
);
return (
<div className="h-full w-[400px] grid grid-rows-[auto_minmax(0,1fr)] overflow-hidden">
<div className="px-2 py-2 w-full">
<div className="h-full w-[400px] grid grid-rows-[auto_minmax(0,1fr)] overflow-hidden py-2">
<div className="px-2 w-full">
<PlainInput
hideLabel
leftSlot={
<div className="h-md w-10 flex justify-center items-center">
<Icon icon="search" className="text-fg-subtle" />
<Icon icon="search" className="text-text-subtle" />
</div>
}
name="command"
@@ -401,7 +410,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
onKeyDownCapture={handleKeyDown}
/>
</div>
<div className="h-full px-1.5 overflow-y-auto pb-1">
<div className="h-full px-1.5 overflow-y-auto pt-2 pb-1">
{filteredGroups.map((g) => (
<div key={g.key} className="mb-1.5 w-full">
<Heading size={2} className="!text-xs uppercase px-1.5 h-sm flex items-center">
@@ -437,8 +446,12 @@ function CommandPaletteItem({
onClick: () => void;
rightSlot?: ReactNode;
}) {
const ref = useRef<HTMLButtonElement | null>(null);
useScrollIntoView(ref.current, active);
return (
<Button
ref={ref}
onClick={onClick}
tabIndex={active ? undefined : -1}
rightSlot={rightSlot}
@@ -446,9 +459,9 @@ function CommandPaletteItem({
justify="start"
className={classNames(
'w-full h-sm flex items-center rounded px-1.5',
'hover:text-fg',
active && 'bg-background-highlight-secondary text-fg',
!active && 'text-fg-subtle',
'hover:text-text',
active && 'bg-surface-highlight',
!active && 'text-text-subtle',
)}
>
<span className="truncate">{children}</span>

View File

@@ -1,6 +1,6 @@
import type { Cookie } from '@yaakapp/api';
import { useCookieJars } from '../hooks/useCookieJars';
import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar';
import type { Cookie } from '../lib/models/Cookie';
import { cookieDomain } from '../lib/models';
import { Banner } from './core/Banner';
import { IconButton } from './core/IconButton';
@@ -12,7 +12,7 @@ interface Props {
export const CookieDialog = function ({ cookieJarId }: Props) {
const updateCookieJar = useUpdateCookieJar(cookieJarId ?? null);
const cookieJars = useCookieJars();
const cookieJars = useCookieJars().data ?? [];
const cookieJar = cookieJars.find((c) => c.id === cookieJarId);
if (cookieJar == null) {
@@ -29,7 +29,7 @@ export const CookieDialog = function ({ cookieJarId }: Props) {
return (
<div className="pb-2">
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-background-highlight">
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
<thead>
<tr>
<th className="py-2 text-left">Domain</th>
@@ -37,13 +37,13 @@ export const CookieDialog = function ({ cookieJarId }: Props) {
<th className="py-2 pl-4"></th>
</tr>
</thead>
<tbody className="divide-y divide-background-highlight-secondary">
<tbody className="divide-y divide-surface-highlight">
{cookieJar?.cookies.map((c: Cookie) => (
<tr key={c.domain + c.raw_cookie}>
<td className="py-2 select-text cursor-text font-mono font-semibold max-w-0">
{cookieDomain(c)}
</td>
<td className="py-2 pl-4 select-text cursor-text font-mono text-fg-subtle whitespace-nowrap overflow-x-auto max-w-[200px] hide-scrollbars">
<td className="py-2 pl-4 select-text cursor-text font-mono text-text-subtle whitespace-nowrap overflow-x-auto max-w-[200px] hide-scrollbars">
{c.raw_cookie}
</td>
<td className="max-w-0 w-10">

View File

@@ -12,8 +12,8 @@ import { InlineCode } from './core/InlineCode';
import { useDialog } from './DialogContext';
export function CookieDropdown() {
const cookieJars = useCookieJars();
const { activeCookieJar, setActiveCookieJarId } = useActiveCookieJar();
const cookieJars = useCookieJars().data ?? [];
const [activeCookieJar, setActiveCookieJarId] = useActiveCookieJar();
const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null);
const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null);
const createCookieJar = useCreateCookieJar();

View File

@@ -1,4 +1,4 @@
import { useClipboardText } from '../hooks/useClipboardText';
import { useCopy } from '../hooks/useCopy';
import { useTimedBoolean } from '../hooks/useTimedBoolean';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
@@ -8,7 +8,7 @@ interface Props extends ButtonProps {
}
export function CopyButton({ text, ...props }: Props) {
const [, copy] = useClipboardText({ disableToast: true });
const copy = useCopy({ disableToast: true });
const [copied, setCopied] = useTimedBoolean();
return (
<Button

View File

@@ -22,7 +22,7 @@ export function DefaultLayout() {
transition={{ duration: 0.1, delay: 0.1 }}
className={classNames(
'w-full h-full',
osInfo?.osType === 'linux' && 'border border-background-highlight-secondary',
osInfo?.osType === 'linux' && 'border border-border-subtle',
)}
>
<Outlet />

View File

@@ -14,7 +14,7 @@ export const DropMarker = memo(
'relative w-full h-0 overflow-visible pointer-events-none',
)}
>
<div className="absolute z-50 left-2 right-2 -bottom-[0.1rem] h-[0.2rem] bg-fg-primary rounded-full" />
<div className="absolute z-50 left-2 right-2 -bottom-[0.1rem] h-[0.2rem] bg-primary rounded-full" />
</div>
);
},

View File

@@ -12,8 +12,8 @@ export function EmptyStateText({ children, className }: Props) {
<div
className={classNames(
className,
'rounded-lg border border-dashed border-background-highlight',
'h-full py-2 text-fg-subtler flex items-center justify-center italic',
'rounded-lg border border-dashed border-border-subtle',
'h-full py-2 text-text-subtlest flex items-center justify-center italic',
)}
>
{children}

View File

@@ -1,7 +1,6 @@
import classNames from 'classnames';
import { memo, useCallback, useMemo } from 'react';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useEnvironments } from '../hooks/useEnvironments';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
@@ -20,9 +19,8 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
...buttonProps
}: Props) {
const environments = useEnvironments();
const activeEnvironment = useActiveEnvironment();
const [activeEnvironment, setActiveEnvironmentId] = useActiveEnvironment();
const dialog = useDialog();
const routes = useAppRoutes();
const showEnvironmentDialog = useCallback(() => {
dialog.toggle({
@@ -43,9 +41,9 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
leftSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: async () => {
if (e.id !== activeEnvironment?.id) {
routes.setEnvironment(e);
setActiveEnvironmentId(e.id);
} else {
routes.setEnvironment(null);
setActiveEnvironmentId(null);
}
},
}),
@@ -62,7 +60,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
onSelect: showEnvironmentDialog,
},
],
[activeEnvironment?.id, environments, routes, showEnvironmentDialog],
[activeEnvironment?.id, environments, setActiveEnvironmentId, showEnvironmentDialog],
);
return (
@@ -71,8 +69,8 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
size="sm"
className={classNames(
className,
'text-fg !px-2 truncate',
activeEnvironment == null && 'text-fg-subtler italic',
'text !px-2 truncate',
activeEnvironment == null && 'text-text-subtlest italic',
)}
{...buttonProps}
>

View File

@@ -68,7 +68,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
color="custom"
title="Add sub environment"
icon="plusCircle"
iconClassName="text-fg-subtler group-hover:text-fg-subtle"
iconClassName="text-text-subtlest group-hover:text-text-subtle"
className="group"
onClick={handleCreateEnvironment}
/>
@@ -97,7 +97,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
secondSlot={() =>
activeWorkspace != null && (
<EnvironmentEditor
className="pt-2 border-l border-background-highlight-secondary"
className="pt-2 border-l border-border-subtle"
environment={selectedEnvironment}
workspace={activeWorkspace}
/>
@@ -175,7 +175,7 @@ const EnvironmentEditor = function ({
<Heading className="w-full flex items-center gap-1">
<div>{environment?.name ?? 'Global Variables'}</div>
<IconButton
iconClassName="text-fg-subtler"
iconClassName="text-text-subtlest"
size="sm"
icon={valueVisibility.value ? 'eye' : 'eyeClosed'}
title={valueVisibility.value ? 'Hide Values' : 'Reveal Values'}
@@ -238,7 +238,7 @@ function SidebarButton({
className={classNames(
className,
'w-full grid grid-cols-[minmax(0,1fr)_auto] items-center gap-0.5',
'px-2', // Padding to show focus border
'px-2', // Padding to show the focus border
)}
>
<Button
@@ -246,7 +246,7 @@ function SidebarButton({
size="xs"
className={classNames(
'w-full',
active ? 'text-fg bg-background-active' : 'text-fg-subtle hover:text-fg',
active ? 'text bg-surface-active' : 'text-text-subtle hover:text',
)}
justify="start"
onClick={onClick}

View File

@@ -59,7 +59,7 @@ export function ExportDataDialog({
const noneSelected = numSelected === 0;
return (
<VStack space={3} className="w-full mb-3 px-4">
<table className="w-full mb-auto min-w-full max-w-full divide-y divide-background-highlight-secondary">
<table className="w-full mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
<thead>
<tr>
<th className="w-6 min-w-0 py-2 text-left pl-1">
@@ -76,7 +76,7 @@ export function ExportDataDialog({
</th>
</tr>
</thead>
<tbody className="divide-y divide-background-highlight-secondary">
<tbody className="divide-y divide-surface-highlight">
{workspaces.map((w) => (
<tr key={w.id}>
<td className="min-w-0 py-1 pl-1">
@@ -90,7 +90,7 @@ export function ExportDataDialog({
/>
</td>
<td
className="py-1 pl-4 text-fg whitespace-nowrap overflow-x-auto hide-scrollbars"
className="py-1 pl-4 text whitespace-nowrap overflow-x-auto hide-scrollbars"
onClick={() => setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] }))}
>
{w.name} {w.id === activeWorkspace.id ? '(current workspace)' : ''}

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