mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-17 16:17:45 +01:00
Compare commits
37 Commits
v2024.8.0-
...
v2024.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
badcbc7aef | ||
|
|
4b91601b98 | ||
|
|
93e0202b86 | ||
|
|
e75d6abe33 | ||
|
|
24a4e3494e | ||
|
|
124fb35dcd | ||
|
|
1aa2839c51 | ||
|
|
8d3260f394 | ||
|
|
7e194b9148 | ||
|
|
f4abc1c7cb | ||
|
|
6766bc8f59 | ||
|
|
adda44e861 | ||
|
|
6aab017d3b | ||
|
|
68c1dca9d1 | ||
|
|
066b7ea4f4 | ||
|
|
0763c1b9b8 | ||
|
|
323e27a047 | ||
|
|
bd02206df2 | ||
|
|
3411575ecc | ||
|
|
1193e1d7aa | ||
|
|
1b63f33d43 | ||
|
|
9f04cfb673 | ||
|
|
1fd1a901f8 | ||
|
|
06517417b7 | ||
|
|
d924709b0c | ||
|
|
6d274cea56 | ||
|
|
b95fa25898 | ||
|
|
12f4c2c668 | ||
|
|
e47a2c5fab | ||
|
|
fc279f67a1 | ||
|
|
6ed6e3e0e0 | ||
|
|
b5242b9a3f | ||
|
|
a0950ce5b8 | ||
|
|
1dd8034cf9 | ||
|
|
7e73b680e6 | ||
|
|
352ffe9415 | ||
|
|
e461851f6f |
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs>
|
||||
<env name="RUST_BACKTRACE" value="1" />
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
</component>
|
||||
@@ -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
29
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
4
plugin-runtime-types/package-lock.json
generated
4
plugin-runtime-types/package-lock.json
generated
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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, };
|
||||
@@ -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, };
|
||||
4
plugin-runtime-types/src/gen/CallTemplateFunctionArgs.ts
Normal file
4
plugin-runtime-types/src/gen/CallTemplateFunctionArgs.ts
Normal 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 }, };
|
||||
@@ -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, };
|
||||
@@ -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, };
|
||||
@@ -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, };
|
||||
3
plugin-runtime-types/src/gen/FindHttpResponsesRequest.ts
Normal file
3
plugin-runtime-types/src/gen/FindHttpResponsesRequest.ts
Normal 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, };
|
||||
@@ -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>, };
|
||||
@@ -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>;
|
||||
@@ -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, };
|
||||
@@ -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, };
|
||||
@@ -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, };
|
||||
@@ -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, };
|
||||
3
plugin-runtime-types/src/gen/HttpRequestAction.ts
Normal file
3
plugin-runtime-types/src/gen/HttpRequestAction.ts
Normal 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, };
|
||||
@@ -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;
|
||||
|
||||
5
plugin-runtime-types/src/gen/RenderHttpRequestRequest.ts
Normal file
5
plugin-runtime-types/src/gen/RenderHttpRequestRequest.ts
Normal 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, };
|
||||
@@ -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, };
|
||||
3
plugin-runtime-types/src/gen/RenderPurpose.ts
Normal file
3
plugin-runtime-types/src/gen/RenderPurpose.ts
Normal 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";
|
||||
3
plugin-runtime-types/src/gen/RenderRequest.ts
Normal file
3
plugin-runtime-types/src/gen/RenderRequest.ts
Normal 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, };
|
||||
3
plugin-runtime-types/src/gen/RenderResponse.ts
Normal file
3
plugin-runtime-types/src/gen/RenderResponse.ts
Normal 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, };
|
||||
4
plugin-runtime-types/src/gen/SendHttpRequestRequest.ts
Normal file
4
plugin-runtime-types/src/gen/SendHttpRequestRequest.ts
Normal 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, };
|
||||
4
plugin-runtime-types/src/gen/SendHttpRequestResponse.ts
Normal file
4
plugin-runtime-types/src/gen/SendHttpRequestResponse.ts
Normal 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, };
|
||||
@@ -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, };
|
||||
|
||||
4
plugin-runtime-types/src/gen/ShowToastRequest.ts
Normal file
4
plugin-runtime-types/src/gen/ShowToastRequest.ts
Normal 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, };
|
||||
4
plugin-runtime-types/src/gen/TemplateFunction.ts
Normal file
4
plugin-runtime-types/src/gen/TemplateFunction.ts
Normal 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>, };
|
||||
7
plugin-runtime-types/src/gen/TemplateFunctionArg.ts
Normal file
7
plugin-runtime-types/src/gen/TemplateFunctionArg.ts
Normal 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;
|
||||
3
plugin-runtime-types/src/gen/TemplateFunctionBaseArg.ts
Normal file
3
plugin-runtime-types/src/gen/TemplateFunctionBaseArg.ts
Normal 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, };
|
||||
@@ -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, };
|
||||
@@ -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, };
|
||||
@@ -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, };
|
||||
@@ -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, };
|
||||
3
plugin-runtime-types/src/gen/TemplateFunctionTextArg.ts
Normal file
3
plugin-runtime-types/src/gen/TemplateFunctionTextArg.ts
Normal 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, };
|
||||
3
plugin-runtime-types/src/gen/ToastVariant.ts
Normal file
3
plugin-runtime-types/src/gen/ToastVariant.ts
Normal 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";
|
||||
@@ -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';
|
||||
|
||||
26
plugin-runtime-types/src/plugins/Context.ts
Normal file
26
plugin-runtime-types/src/plugins/Context.ts
Normal 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']>;
|
||||
};
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
8
plugin-runtime-types/src/plugins/ThemePlugin.ts
Normal file
8
plugin-runtime-types/src/plugins/ThemePlugin.ts
Normal 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>;
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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: "" };
|
||||
}
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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
90
src-tauri/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]}}
|
||||
2
src-tauri/migrations/20240814013812_fix-env-model.sql
Normal file
2
src-tauri/migrations/20240814013812_fix-env-model.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE environments DROP COLUMN model;
|
||||
ALTER TABLE environments ADD COLUMN model TEXT DEFAULT 'environment';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE settings ADD COLUMN telemetry BOOLEAN DEFAULT TRUE;
|
||||
@@ -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(¶ms);
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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(¬ification.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()),
|
||||
}
|
||||
|
||||
@@ -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"]}
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
69
src-tauri/src/template_callback.rs
Normal file
69
src-tauri/src/template_callback.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
[package]
|
||||
name = "templates"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.22"
|
||||
@@ -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
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "grpc"
|
||||
name = "yaak_grpc"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
@@ -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")?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
src-tauri/yaak_templates/Cargo.toml
Normal file
11
src-tauri/yaak_templates/Cargo.toml
Normal 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"] }
|
||||
6
src-tauri/yaak_templates/build.rs
Normal file
6
src-tauri/yaak_templates/build.rs
Normal 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(())
|
||||
}
|
||||
@@ -2,6 +2,4 @@ pub mod parser;
|
||||
pub mod renderer;
|
||||
|
||||
pub use parser::*;
|
||||
pub use renderer::*;
|
||||
|
||||
pub fn template_foo() {}
|
||||
pub use renderer::*;
|
||||
733
src-tauri/yaak_templates/src/parser.rs
Normal file
733
src-tauri/yaak_templates/src/parser.rs
Normal 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' ]}"#
|
||||
);
|
||||
}
|
||||
}
|
||||
230
src-tauri/yaak_templates/src/renderer.rs
Normal file
230
src-tauri/yaak_templates/src/renderer.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user