Dynamic plugins (#68)

This commit is contained in:
Gregory Schier
2024-09-06 10:43:25 -07:00
committed by GitHub
parent e4e888c47a
commit c02aa4f2d0
37 changed files with 533 additions and 184 deletions

View File

@@ -38,10 +38,6 @@ jobs:
with:
node-version: 20
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
@@ -81,7 +77,7 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install yaak CLI
run: go install github.com/yaakapp/yaakcli@latest
run: npm install -g @yaakapp/cli
- name: Run lint
run: npm run lint

96
package-lock.json generated
View File

@@ -27,7 +27,7 @@
"@tauri-apps/plugin-log": "^2.0.0-rc.1",
"@tauri-apps/plugin-os": "^2.0.0-rc.1",
"@tauri-apps/plugin-shell": "^2.0.0-rc.1",
"@yaakapp/api": "^0.1.15",
"@yaakapp/api": "^0.1.16",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",
@@ -59,7 +59,7 @@
"devDependencies": {
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tanstack/react-query-devtools": "^5.45.1",
"@tauri-apps/cli": "^2.0.0-rc.2",
"@tauri-apps/cli": "^2.0.0-rc.12",
"@types/node": "^18.7.10",
"@types/papaparse": "^5.3.7",
"@types/parse-color": "^1.0.1",
@@ -2354,9 +2354,9 @@
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-rc.3.tgz",
"integrity": "sha512-iNF95pieBmverl1EmQyqh+fhcIClS544fN5Ex5lAbYLTiHZ/gm3lOfVBhF6NPaKd/sfLuy7K1tfDXlHztBfANw==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-rc.12.tgz",
"integrity": "sha512-rNcVSyGHGz8vNk542isYKPk5fEMAsgmzER+1s9YYbGZCH7m4e0rH89p/P9W40I/Z4AZk4ZqjpEeajeS5izDI4g==",
"dev": true,
"bin": {
"tauri": "tauri.js"
@@ -2369,22 +2369,22 @@
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.0.0-rc.3",
"@tauri-apps/cli-darwin-x64": "2.0.0-rc.3",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-rc.3",
"@tauri-apps/cli-linux-arm64-gnu": "2.0.0-rc.3",
"@tauri-apps/cli-linux-arm64-musl": "2.0.0-rc.3",
"@tauri-apps/cli-linux-x64-gnu": "2.0.0-rc.3",
"@tauri-apps/cli-linux-x64-musl": "2.0.0-rc.3",
"@tauri-apps/cli-win32-arm64-msvc": "2.0.0-rc.3",
"@tauri-apps/cli-win32-ia32-msvc": "2.0.0-rc.3",
"@tauri-apps/cli-win32-x64-msvc": "2.0.0-rc.3"
"@tauri-apps/cli-darwin-arm64": "2.0.0-rc.12",
"@tauri-apps/cli-darwin-x64": "2.0.0-rc.12",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-rc.12",
"@tauri-apps/cli-linux-arm64-gnu": "2.0.0-rc.12",
"@tauri-apps/cli-linux-arm64-musl": "2.0.0-rc.12",
"@tauri-apps/cli-linux-x64-gnu": "2.0.0-rc.12",
"@tauri-apps/cli-linux-x64-musl": "2.0.0-rc.12",
"@tauri-apps/cli-win32-arm64-msvc": "2.0.0-rc.12",
"@tauri-apps/cli-win32-ia32-msvc": "2.0.0-rc.12",
"@tauri-apps/cli-win32-x64-msvc": "2.0.0-rc.12"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-rc.3.tgz",
"integrity": "sha512-szYCSr/ChbCF+S6Wnr15TYpI2cZR07d+AQOiFGuScP0preM8Pbsk/sb0hfLwqzepjVFFNVWQba9sG7FEW2Y2XA==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-rc.12.tgz",
"integrity": "sha512-zYxcAH4reyqKkqCAybggszFWkBvC+ZyZPTWFKXXVQ20MZc1q+e/0UJYC8UKsaumrbi1uptgamvnM8yql56x5QQ==",
"cpu": [
"arm64"
],
@@ -2398,9 +2398,9 @@
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-rc.3.tgz",
"integrity": "sha512-BJv6EJOY1DJbRzVtfg8CcBAlnS5OjhBAc5YKjh4BT7EyOcop8HStBSxhL6yjWrUP7eLR1iIsW/uSehVJwzYIdQ==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-rc.12.tgz",
"integrity": "sha512-eme7pQzEzeGCk13V3uxUNRnkVZJukqwHotqEb2RdovXqJWSyESrighBy4PBG5Xn6wNYTOyoquY9+In4TOfJAzw==",
"cpu": [
"x64"
],
@@ -2414,9 +2414,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-rc.3.tgz",
"integrity": "sha512-fwx805/xL4sF/EdMYqcUHQHzMYwo+OVTBTz5x/JWK8D57rnmLHAP+ZhnfFsZQLRo2QRT2l1Ye3bDyU+QRA1JFA==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-rc.12.tgz",
"integrity": "sha512-113T2NsLeoy6GXsqc0yjMoozt+KXzkAtUB7DL9Kcvx9IMfA87cUVaTNjnb2GFsoQqpCWGfHei3nr9n1PGEbwMg==",
"cpu": [
"arm"
],
@@ -2430,9 +2430,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-rc.3.tgz",
"integrity": "sha512-3KauzO1Ls4kuY0nr82S4X8XFxlQAMN+Mqp8LLqvQ+PPMp92XQAkPH7osQdoHIEoW5gsE69U2JaiQ5tHSqNM9og==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-rc.12.tgz",
"integrity": "sha512-9TrUyNg0vmsYF7IbG+/sybEeiz6ikA1Kjd6JjC4iwfXjRff8fuTR7CIOb06imabxbLzGP79qSAnGAeTXz8E7qA==",
"cpu": [
"arm64"
],
@@ -2446,9 +2446,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-rc.3.tgz",
"integrity": "sha512-ngHS0foffm1xO5gqnDKGeYMKj8ceGmrFP5dDldoaaMQubw1SyFa0pRUjb7fZSYiO7F4SOSa8NYeMqlF9peZmnQ==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-rc.12.tgz",
"integrity": "sha512-YvE40+wdkNcXhwUAJNPyhNzJ8YS3saJoxGj7mtNQeNeNrKhxyj6hA5T6gw9KtMkwBOp+HGtqn+eDXiu0X7BBHQ==",
"cpu": [
"arm64"
],
@@ -2462,9 +2462,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-rc.3.tgz",
"integrity": "sha512-0/am9pVvuUHGmz32M8ffz1fpLnc08j3nzcRe5wUdL2AxfT+wKMII+Dn99GtCVgcdDW4jSXDMRUwrBkGocGC2OA==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-rc.12.tgz",
"integrity": "sha512-q+MJp/lSA5WINs78dCFMlU0/jQeUkGr9GHbKeppcVcpkcY/1vog70b4KhneyvbuklKBn/V8kd0FtIKCn8VP+KQ==",
"cpu": [
"x64"
],
@@ -2478,9 +2478,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-rc.3.tgz",
"integrity": "sha512-r7mRi8q8TqTFVjb9kAsU7IgwUgno2s8Ip4xwq9psQhlRE3JGEZQmSEcy1jqTjfl6KFh6lJcDR7l+9/EMhL/D3Q==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-rc.12.tgz",
"integrity": "sha512-5zodtleH2GFsB9lszDYrzPTLcr+MMqtpQpJWHATC1K03bLEA8ia8zSdBqRwm7u8NraMLl8TE7hc7hwq0uxGEcg==",
"cpu": [
"x64"
],
@@ -2494,9 +2494,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-rc.3.tgz",
"integrity": "sha512-2J6KjmDIQCw6HF1X6/yPcd+JLl7pxrH2zVMGmNllaoWhHeByvRobqFWnT7gcdHaA3dGTo432CwWvOgTgrINQpQ==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-rc.12.tgz",
"integrity": "sha512-nSu6VHpuq61DYM2YowLDLDwkK8im7dzYxIHXs+h8/rhkmadTujGhbyUhHPI1STA6hNyITUtSFpo6P2mEbfpAIg==",
"cpu": [
"arm64"
],
@@ -2510,9 +2510,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-rc.3.tgz",
"integrity": "sha512-8q75CsHDSEDdgi6xPwim+BaQZFCswK2Dn/qL38V3Mh9kmVvC8oGJMPC66bC20dF+v3KWeFm2FNNGQqOSXCveHg==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-rc.12.tgz",
"integrity": "sha512-d/4y57OisMuB+MUkTpZsryQRi9ZQXQ8SsMhrvEgu8sbX8/WRm0iZyGuIZ01RlZZHLIasXbKTkPX+hPQC5Juk8Q==",
"cpu": [
"ia32"
],
@@ -2526,9 +2526,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-rc.3.tgz",
"integrity": "sha512-qeBRJYalahxEXolekcpZJ/HBrIJacG2NWJBGhhi797mIwnbmlpbHMc8blIJtNNNwVUb2BjXuxKQVfojQ5YYrcg==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-rc.12.tgz",
"integrity": "sha512-RsPUvsbFza03ysh0nU2nM3P2CVWz9cu7CRHwOEdtXjWWNREHUYCaVpqQKz0tn2sG19yXiNIB40iqrIBUmb/IoA==",
"cpu": [
"x64"
],
@@ -2986,9 +2986,9 @@
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="
},
"node_modules/@yaakapp/api": {
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.15.tgz",
"integrity": "sha512-4nrImM9r4Afih0CcG6PWtGA6Luap/Ki5ZVl56WejWA8WPpy8AhEpC0KErpJChNzzqRjgK9ZEWdQNsBHppzAs8A==",
"version": "0.1.16",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.16.tgz",
"integrity": "sha512-rWXXb0iUiUk6jtLhZFcbdMRIf6dYXY9wyIvFG9rVpyMxB0LWjim8VpvwF4IK4Y/paxvZ9bjvnaD5PAB4pH6prg==",
"dependencies": {
"@types/node": "^22.0.0"
}

View File

@@ -42,7 +42,7 @@
"@tauri-apps/plugin-log": "^2.0.0-rc.1",
"@tauri-apps/plugin-os": "^2.0.0-rc.1",
"@tauri-apps/plugin-shell": "^2.0.0-rc.1",
"@yaakapp/api": "^0.1.15",
"@yaakapp/api": "^0.1.16",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",
@@ -74,7 +74,7 @@
"devDependencies": {
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tanstack/react-query-devtools": "^5.45.1",
"@tauri-apps/cli": "^2.0.0-rc.2",
"@tauri-apps/cli": "^2.0.0-rc.12",
"@types/node": "^18.7.10",
"@types/papaparse": "^5.3.7",
"@types/parse-color": "^1.0.1",

View File

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

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GrpcEventType } from "./GrpcEventType";
export type GrpcEvent = { id: string, model: "grpc_event", workspaceId: string, requestId: string, connectionId: string, createdAt: string, content: string, eventType: GrpcEventType, metadata: { [key: string]: string }, status: number | null, error: string | null, };
export type GrpcEvent = { id: string, model: "grpc_event", workspaceId: string, requestId: string, connectionId: string, createdAt: string, updatedAt: string, content: string, eventType: GrpcEventType, metadata: { [key: string]: string }, status: number | null, error: string | null, };

View File

@@ -1,6 +1,4 @@
// 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";
@@ -19,10 +17,12 @@ import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse";
import type { GetTemplateFunctionsResponse } from "./GetTemplateFunctionsResponse";
import type { ImportRequest } from "./ImportRequest";
import type { ImportResponse } from "./ImportResponse";
import type { PluginBootRequest } from "./PluginBootRequest";
import type { PluginBootResponse } from "./PluginBootResponse";
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": "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;
export type InternalEventPayload = { "type": "boot_request" } & PluginBootRequest | { "type": "boot_response" } & PluginBootResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & GetHttpRequestActionsRequest | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyResponse;

View File

@@ -8,7 +8,8 @@ import type { GrpcRequest } from "./GrpcRequest";
import type { HttpRequest } from "./HttpRequest";
import type { HttpResponse } from "./HttpResponse";
import type { KeyValue } from "./KeyValue";
import type { Plugin } from "./Plugin";
import type { Settings } from "./Settings";
import type { Workspace } from "./Workspace";
export type Model = Environment | Folder | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | KeyValue | Workspace | CookieJar | Settings;
export type Model = Environment | Folder | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | KeyValue | Workspace | CookieJar | Settings | Plugin;

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Plugin = { id: string, model: "plugin", createdAt: string, updatedAt: string, checkedAt: string | null, name: string, version: string, uri: string, enabled: boolean, };

View File

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

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type BootResponse = { name: string, version: string, capabilities: Array<string>, };
export type PluginBootResponse = { name: string, version: string, capabilities: Array<string>, };

View File

@@ -2,8 +2,6 @@ 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';
@@ -63,3 +61,4 @@ export * from './gen/TemplateFunctionSelectOption';
export * from './gen/TemplateFunctionTextArg';
export * from './gen/ToastVariant';
export * from './gen/Workspace';
export * from './gen/Plugin';

View File

@@ -9,7 +9,7 @@ export type { Context } from './Context';
/**
* The global structure of a Yaak plugin
*/
export type Plugin = {
export type PluginDefinition = {
importer?: ImporterPlugin;
theme?: ThemePlugin;
filter?: FilterPlugin;

View File

@@ -0,0 +1,12 @@
CREATE TABLE plugins
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'plugin' NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
checked_at DATETIME NULL,
enabled BOOLEAN NOT NULL,
directory TEXT NULL NOT NULL,
url TEXT NULL
);

View File

@@ -28,6 +28,7 @@ pub enum AnalyticsResource {
HttpRequest,
HttpResponse,
KeyValue,
Plugin,
Setting,
Sidebar,
Theme,

View File

@@ -40,7 +40,7 @@ use crate::updates::{UpdateMode, YaakUpdater};
use crate::window_menu::app_menu;
use yaak_models::models::{
CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType,
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Settings, Workspace,
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Plugin, Settings, Workspace,
};
use yaak_models::queries::{
cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response,
@@ -49,16 +49,17 @@ use yaak_models::queries::{
delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request,
generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection,
get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
get_or_create_settings, get_workspace, list_cookie_jars, list_environments, list_folders,
list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests,
list_http_responses, list_workspaces, set_key_value_raw, update_response_if_id,
get_or_create_settings, get_plugin, get_workspace, list_cookie_jars, list_environments,
list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests,
list_http_responses, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id,
update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
};
use yaak_plugin_runtime::events::{
CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse,
InternalEvent, InternalEventPayload, RenderHttpRequestResponse, SendHttpRequestResponse,
InternalEvent, InternalEventPayload, PluginBootResponse, RenderHttpRequestResponse,
SendHttpRequestResponse,
};
use yaak_templates::{Parser, Tokens};
@@ -77,6 +78,9 @@ mod window_menu;
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
const MIN_WINDOW_WIDTH: f64 = 300.0;
const MIN_WINDOW_HEIGHT: f64 = 300.0;
#[derive(serde::Serialize)]
#[serde(default, rename_all = "camelCase")]
struct AppMetaData {
@@ -1165,6 +1169,20 @@ async fn cmd_create_workspace(name: &str, w: WebviewWindow) -> Result<Workspace,
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_create_plugin(directory: &str, url: Option<String>, w: WebviewWindow) -> Result<Plugin, String> {
upsert_plugin(
&w,
Plugin {
directory: directory.into(),
url,
..Default::default()
},
)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_update_cookie_jar(
cookie_jar: CookieJar,
@@ -1408,10 +1426,9 @@ async fn cmd_list_grpc_requests(
workspace_id: &str,
w: WebviewWindow,
) -> Result<Vec<GrpcRequest>, String> {
let requests = list_grpc_requests(&w, workspace_id)
list_grpc_requests(&w, workspace_id)
.await
.map_err(|e| e.to_string())?;
Ok(requests)
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1419,11 +1436,9 @@ async fn cmd_list_http_requests(
workspace_id: &str,
w: WebviewWindow,
) -> Result<Vec<HttpRequest>, String> {
let requests = list_http_requests(&w, workspace_id)
list_http_requests(&w, workspace_id)
.await
.expect("Failed to find requests");
// .map_err(|e| e.to_string())
Ok(requests)
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1431,11 +1446,27 @@ async fn cmd_list_environments(
workspace_id: &str,
w: WebviewWindow,
) -> Result<Vec<Environment>, String> {
let environments = list_environments(&w, workspace_id)
list_environments(&w, workspace_id)
.await
.expect("Failed to find environments");
.map_err(|e| e.to_string())
}
Ok(environments)
#[tauri::command]
async fn cmd_list_plugins(w: WebviewWindow) -> Result<Vec<Plugin>, String> {
list_plugins(&w).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_plugin_info(
id: &str,
w: WebviewWindow,
plugin_manager: State<'_, PluginManager>,
) -> Result<PluginBootResponse, String> {
let plugin = get_plugin(&w, id).await.map_err(|e| e.to_string())?;
plugin_manager
.get_plugin_info(plugin.directory.as_str())
.await
.ok_or("Failed to find plugin info".to_string())
}
#[tauri::command]
@@ -1689,6 +1720,7 @@ pub fn run() {
cmd_create_folder,
cmd_create_grpc_request,
cmd_create_http_request,
cmd_create_plugin,
cmd_create_workspace,
cmd_curl_to_request,
cmd_delete_all_grpc_connections,
@@ -1730,6 +1762,8 @@ pub fn run() {
cmd_list_grpc_requests,
cmd_list_http_requests,
cmd_list_http_responses,
cmd_list_plugins,
cmd_plugin_info,
cmd_list_workspaces,
cmd_metadata,
cmd_new_nested_window,
@@ -1831,6 +1865,7 @@ fn create_nested_window(
.title(title)
.parent(&window)
.unwrap()
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
.inner_size(DEFAULT_WINDOW_WIDTH * 0.7, DEFAULT_WINDOW_HEIGHT * 0.9);
// Add macOS-only things
@@ -1841,7 +1876,7 @@ fn create_nested_window(
.title_bar_style(TitleBarStyle::Overlay);
}
// Add non-MacOS things
// Add non-macOS things
#[cfg(not(target_os = "macos"))]
{
win_builder = win_builder.decorations(false);
@@ -1874,7 +1909,7 @@ fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow {
100.0 + random::<f64>() * 30.0,
100.0 + random::<f64>() * 30.0,
)
.min_inner_size(300.0, 300.0)
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
.title(handle.package_info().name.to_string());
// Add macOS-only things
@@ -1900,7 +1935,8 @@ fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow {
return;
}
match event.id().0.as_str() {
let event_id = event.id().0.as_str();
match event_id {
"quit" => exit(0),
"close" => w.close().unwrap(),
"zoom_reset" => w.emit("zoom_reset", true).unwrap(),
@@ -1971,7 +2007,7 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
}
async fn handle_plugin_event<R: Runtime>(app_handle: &AppHandle<R>, event: &InternalEvent) {
info!("Got event to app {}", event.id);
// info!("Got event to app {}", event.id);
let response_event: Option<InternalEventPayload> = match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => {
app_handle

View File

@@ -655,6 +655,7 @@ pub struct GrpcEvent {
pub request_id: String,
pub connection_id: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub content: String,
pub event_type: GrpcEventType,
pub metadata: HashMap<String, String>,
@@ -693,6 +694,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcEvent {
request_id: r.get("request_id")?,
connection_id: r.get("connection_id")?,
created_at: r.get("created_at")?,
updated_at: r.get("updated_at")?,
content: r.get("content")?,
event_type: serde_json::from_str(event_type.as_str()).unwrap_or_default(),
metadata: serde_json::from_str(metadata.as_str()).unwrap_or_default(),
@@ -702,6 +704,51 @@ impl<'s> TryFrom<&Row<'s>> for GrpcEvent {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
pub struct Plugin {
pub id: String,
#[ts(type = "\"plugin\"")]
pub model: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub checked_at: Option<NaiveDateTime>,
pub directory: String,
pub url: Option<String>,
pub enabled: bool,
}
#[derive(Iden)]
pub enum PluginIden {
#[iden = "plugins"]
Table,
Id,
Model,
CreatedAt,
UpdatedAt,
CheckedAt,
Directory,
Url,
Enabled,
}
impl<'s> TryFrom<&Row<'s>> for Plugin {
type Error = rusqlite::Error;
fn try_from(r: &Row<'s>) -> Result<Self, Self::Error> {
Ok(Plugin {
id: r.get("id")?,
model: r.get("model")?,
created_at: r.get("created_at")?,
updated_at: r.get("updated_at")?,
checked_at: r.get("checked_at")?,
url: r.get("url")?,
directory: r.get("directory")?,
enabled: r.get("enabled")?,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
pub struct KeyValue {
@@ -758,6 +805,7 @@ pub enum ModelType {
TypeGrpcRequest,
TypeHttpRequest,
TypeHttpResponse,
TypePlugin,
TypeWorkspace,
}
@@ -772,6 +820,7 @@ impl ModelType {
ModelType::TypeGrpcRequest => "gr",
ModelType::TypeHttpRequest => "rq",
ModelType::TypeHttpResponse => "rs",
ModelType::TypePlugin => "pg",
ModelType::TypeWorkspace => "wk",
}
.to_string()

View File

@@ -1,12 +1,7 @@
use std::fs;
use crate::error::Result;
use crate::models::{
CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection,
GrpcConnectionIden, GrpcEvent, GrpcEventIden, GrpcRequest, GrpcRequestIden, HttpRequest,
HttpRequestIden, HttpResponse, HttpResponseHeader, HttpResponseIden, KeyValue, KeyValueIden,
ModelType, Settings, SettingsIden, Workspace, WorkspaceIden,
};
use crate::models::{CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection, GrpcConnectionIden, GrpcEvent, GrpcEventIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestIden, HttpResponse, HttpResponseHeader, HttpResponseIden, KeyValue, KeyValueIden, ModelType, Plugin, PluginIden, Settings, SettingsIden, Workspace, WorkspaceIden};
use crate::plugin::SqliteConnection;
use log::{debug, error};
use rand::distributions::{Alphanumeric, DistString};
@@ -848,7 +843,7 @@ pub async fn upsert_environment<R: Runtime>(
serde_json::to_string(&environment.variables)?.into(),
])
.on_conflict(
OnConflict::column(GrpcEventIden::Id)
OnConflict::column(EnvironmentIden::Id)
.update_columns([
EnvironmentIden::UpdatedAt,
EnvironmentIden::Name,
@@ -877,6 +872,88 @@ pub async fn get_environment<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Res
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn get_plugin<R: Runtime>(
mgr: &impl Manager<R>,
id: &str
) -> Result<Plugin> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
.from(PluginIden::Table)
.column(Asterisk)
.cond_where(Expr::col(EnvironmentIden::Id).eq(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 list_plugins<R: Runtime>(
mgr: &impl Manager<R>,
) -> Result<Vec<Plugin>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
.from(PluginIden::Table)
.column(Asterisk)
.order_by(PluginIden::CreatedAt, Order::Desc)
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn upsert_plugin<R: Runtime>(
window: &WebviewWindow<R>,
plugin: Plugin,
) -> Result<Plugin> {
let id = match plugin.id.as_str() {
"" => generate_model_id(ModelType::TypePlugin),
_ => plugin.id.to_string(),
};
let dbm = &*window.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::insert()
.into_table(PluginIden::Table)
.columns([
PluginIden::Id,
PluginIden::CreatedAt,
PluginIden::UpdatedAt,
PluginIden::CheckedAt,
PluginIden::Directory,
PluginIden::Url,
PluginIden::Enabled,
])
.values_panic([
id.as_str().into(),
CurrentTimestamp.into(),
CurrentTimestamp.into(),
plugin.checked_at.into(),
plugin.directory.into(),
plugin.url.into(),
plugin.enabled.into(),
])
.on_conflict(
OnConflict::column(PluginIden::Id)
.update_columns([
PluginIden::UpdatedAt,
PluginIden::CheckedAt,
PluginIden::Directory,
PluginIden::Url,
PluginIden::Enabled,
])
.to_owned(),
)
.returning_all()
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
let m = stmt.query_row(&*params.as_params(), |row| row.try_into())?;
Ok(emit_upserted_model(window, m))
}
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();

View File

@@ -2,10 +2,7 @@ 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, Plugin, Settings, Workspace};
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -21,8 +18,8 @@ pub struct InternalEvent {
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export)]
pub enum InternalEventPayload {
BootRequest(BootRequest),
BootResponse(BootResponse),
BootRequest(PluginBootRequest),
BootResponse(PluginBootResponse),
ImportRequest(ImportRequest),
ImportResponse(ImportResponse),
@@ -71,14 +68,14 @@ pub struct EmptyResponse {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct BootRequest {
pub struct PluginBootRequest {
pub dir: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct BootResponse {
pub struct PluginBootResponse {
pub name: String,
pub version: String,
pub capabilities: Vec<String>,
@@ -401,4 +398,5 @@ pub enum Model {
Workspace(Workspace),
CookieJar(CookieJar),
Settings(Settings),
Plugin(Plugin),
}

View File

@@ -1,9 +1,9 @@
use crate::error::Result;
use crate::events::{
CallHttpRequestActionRequest, CallTemplateFunctionArgs, RenderPurpose,
PluginBootResponse, CallHttpRequestActionRequest, CallTemplateFunctionArgs,
CallTemplateFunctionRequest, CallTemplateFunctionResponse, FilterRequest, FilterResponse,
GetHttpRequestActionsRequest, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse,
ImportRequest, ImportResponse, InternalEvent, InternalEventPayload,
ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, RenderPurpose,
};
use std::collections::HashMap;
@@ -64,6 +64,10 @@ impl PluginManager {
.await
}
pub async fn get_plugin_info(&self, dir: &str) -> Option<PluginBootResponse> {
self.server.plugin_by_dir(dir).await.ok()?.info().await
}
pub async fn get_http_request_actions(&self) -> Result<Vec<GetHttpRequestActionsResponse>> {
let reply_events = self
.server
@@ -155,7 +159,9 @@ impl PluginManager {
});
match result {
None => Err(PluginErr("No importers found for file contents".to_string())),
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;

View File

@@ -15,21 +15,30 @@ use tokio::fs::read_dir;
use tokio::net::TcpListener;
use tonic::codegen::tokio_stream;
use tonic::transport::Server;
use yaak_models::queries::list_plugins;
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak_plugin_runtime")
.setup(|app, _| {
let plugins_dir = app
.setup(|app_handle, _| {
let plugins_dir = app_handle
.path()
.resolve("plugins", BaseDirectory::Resource)
.expect("failed to resolve plugin directory resource");
tauri::async_runtime::block_on(async move {
let plugin_dirs = read_plugins_dir(&plugins_dir)
let bundled_plugin_dirs = read_plugins_dir(&plugins_dir)
.await
.expect(format!("Failed to read plugins dir: {:?}", plugins_dir).as_str());
let manager = PluginManager::new(&app, plugin_dirs).await;
app.manage(manager);
let plugins = list_plugins(app_handle).await.unwrap_or_default();
let installed_plugin_dirs = plugins
.iter()
.map(|p| p.directory.to_owned())
.collect::<Vec<String>>();
let plugin_dirs = [installed_plugin_dirs, bundled_plugin_dirs].concat();
let manager = PluginManager::new(&app_handle, plugin_dirs).await;
app_handle.manage(manager);
Ok(())
})
})

View File

@@ -1,4 +1,3 @@
use log::info;
use rand::distributions::{Alphanumeric, DistString};
use std::collections::HashMap;
use std::pin::Pin;
@@ -11,7 +10,7 @@ use tonic::{Request, Response, Status, Streaming};
use crate::error::Error::PluginNotFoundErr;
use crate::error::Result;
use crate::events::{BootRequest, BootResponse, InternalEvent, InternalEventPayload};
use crate::events::{PluginBootRequest, PluginBootResponse, InternalEvent, InternalEventPayload};
use crate::server::plugin_runtime::plugin_runtime_server::PluginRuntime;
use plugin_runtime::EventStreamEvent;
use yaak_models::queries::generate_id;
@@ -28,7 +27,7 @@ pub struct PluginHandle {
dir: String,
to_plugin_tx: Arc<Mutex<mpsc::Sender<tonic::Result<EventStreamEvent>>>>,
ref_id: String,
boot_resp: Arc<Mutex<Option<BootResponse>>>,
boot_resp: Arc<Mutex<Option<PluginBootResponse>>>,
}
impl PluginHandle {
@@ -38,6 +37,11 @@ impl PluginHandle {
Some(r) => r.name.to_owned(),
}
}
pub async fn info(&self) -> Option<PluginBootResponse> {
let resp = &*self.boot_resp.lock().await;
resp.clone()
}
pub fn build_event_to_send(
&self,
@@ -53,11 +57,11 @@ impl PluginHandle {
}
pub async fn send(&self, event: &InternalEvent) -> Result<()> {
info!(
"Sending event to plugin {} {:?}",
event.id,
self.name().await
);
// info!(
// "Sending event to plugin {} {:?}",
// event.id,
// self.name().await
// );
self.to_plugin_tx
.lock()
.await
@@ -68,7 +72,7 @@ impl PluginHandle {
Ok(())
}
pub async fn boot(&self, resp: &BootResponse) {
pub async fn boot(&self, resp: &PluginBootResponse) {
let mut boot_resp = self.boot_resp.lock().await;
*boot_resp = Some(resp.clone());
}
@@ -120,7 +124,7 @@ impl PluginRuntimeGrpcServer {
};
}
pub async fn boot_plugin(&self, id: &str, resp: &BootResponse) {
pub async fn boot_plugin(&self, id: &str, resp: &PluginBootResponse) {
match self.plugin_ref_to_plugin.lock().await.get(id) {
None => {
println!("Tried booting non-existing plugin {}", id);
@@ -151,61 +155,27 @@ impl PluginRuntimeGrpcServer {
plugin_handle
}
// pub async fn callback(
// &self,
// source_event: InternalEvent,
// payload: InternalEventPayload,
// ) -> Result<InternalEvent> {
// let reply_id = match source_event.clone().reply_id {
// None => {
// let msg = format!("Source event missing reply Id {:?}", source_event.clone());
// return Err(MissingCallbackIdErr(msg));
// }
// Some(id) => id,
// };
//
// let callbacks = self.callbacks.lock().await;
// let plugin_name = match callbacks.get(reply_id.as_str()) {
// None => {
// let msg = format!("Callback not found {:?}", source_event);
// return Err(MissingCallbackErr(msg));
// }
// Some(n) => n,
// };
//
// let plugins = self.plugins.lock().await;
// let plugin = match plugins.get(plugin_name) {
// None => {
// let msg = format!(
// "Plugin not found {plugin_name}. Choices were {:?}",
// plugins.keys()
// );
// return Err(UnknownPluginErr(msg));
// }
// Some(n) => n,
// };
//
// plugin.send(&payload, Some(reply_id)).await
// }
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(PluginNotFoundErr(ref_id.into()));
}
match plugins.get(ref_id) {
None => Err(PluginNotFoundErr(ref_id.into())),
Some(p) => Ok(p.to_owned()),
}
}
pub async fn plugin_by_dir(&self, dir: &str) -> Result<PluginHandle> {
let plugins = self.plugin_ref_to_plugin.lock().await;
for p in plugins.values() {
if p.dir == dir {
return Ok(p.to_owned());
}
}
Err(PluginNotFoundErr(dir.into()))
}
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(PluginNotFoundErr(plugin_name.into()));
}
for p in plugins.values() {
if p.name().await == plugin_name {
return Ok(p.to_owned());
@@ -341,7 +311,7 @@ impl PluginRuntimeGrpcServer {
plugin_ids.push(plugin.clone().ref_id);
let event = plugin.build_event_to_send(
&InternalEventPayload::BootRequest(BootRequest {
&InternalEventPayload::BootRequest(PluginBootRequest {
dir: dir.to_string(),
}),
None,

View File

@@ -18,6 +18,7 @@ import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
import { keyValueQueryKey } from '../hooks/useKeyValue';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useNotificationToast } from '../hooks/useNotificationToast';
import { pluginsAtom } from '../hooks/usePlugins';
import { useRecentCookieJars } from '../hooks/useRecentCookieJars';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
@@ -67,6 +68,7 @@ export function GlobalHooks() {
const setSettings = useSetAtom(settingsAtom);
const setWorkspaces = useSetAtom(workspacesAtom);
const setPlugins = useSetAtom(pluginsAtom);
const setHttpRequests = useSetAtom(httpRequestsAtom);
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
const setEnvironments = useSetAtom(environmentsAtom);
@@ -100,6 +102,8 @@ export function GlobalHooks() {
if (model.model === 'workspace') {
setWorkspaces(updateModelList(model, pushToFront));
} else if (model.model === 'plugin') {
setPlugins(updateModelList(model, pushToFront));
} else if (model.model === 'http_request') {
setHttpRequests(updateModelList(model, pushToFront));
} else if (model.model === 'grpc_request') {
@@ -129,6 +133,8 @@ export function GlobalHooks() {
if (model.model === 'workspace') {
setWorkspaces(removeById(model));
} else if (model.model === 'plugin') {
setPlugins(removeById(model));
} else if (model.model === 'http_request') {
setHttpRequests(removeById(model));
} else if (model.model === 'http_response') {

View File

@@ -9,20 +9,31 @@ import { HStack } from './core/Stacks';
type Props = ButtonProps & {
onChange: (value: { filePath: string | null; contentType: string | null }) => void;
filePath: string | null;
directory?: boolean;
inline?: boolean;
noun?: string;
};
// Special character to insert ltr text in rtl element
const rtlEscapeChar = <>&#x200E;</>;
export function SelectFile({ onChange, filePath, inline, className }: Props) {
export function SelectFile({
onChange,
filePath,
inline,
className,
directory,
noun,
size = 'sm',
...props
}: Props) {
const handleClick = async () => {
const filePath = await open({
title: 'Select File',
multiple: false,
directory,
});
if (filePath == null) return;
const contentType = filePath ? mime.getType(filePath) : null;
onChange({ filePath, contentType });
};
@@ -31,31 +42,41 @@ export function SelectFile({ onChange, filePath, inline, className }: Props) {
onChange({ filePath: null, contentType: null });
};
const itemLabel = noun ?? (directory ? 'Folder' : 'File');
const selectOrChange = (filePath ? 'Change ' : 'Select ') + itemLabel;
return (
<HStack space={1.5} className="group relative justify-stretch">
<HStack space={1.5} className="group relative justify-stretch overflow-hidden">
<Button
className={classNames(className, 'font-mono text-xs rtl', inline && 'w-full')}
color="secondary"
size="sm"
onClick={handleClick}
size={size}
{...props}
>
{rtlEscapeChar}
{inline ? <>{filePath || 'Select File'}</> : <>Select File</>}
{inline ? filePath || selectOrChange : selectOrChange}
</Button>
{!inline && (
<>
{filePath && (
<IconButton
size="sm"
size={size}
variant="border"
icon="x"
title="Unset File"
title={'Unset ' + itemLabel}
onClick={handleClear}
/>
)}
<div className="text-sm font-mono truncate rtl pr-3 text">
<div
className={classNames(
'font-mono truncate rtl pl-1.5 pr-3 text-text',
size === 'xs' && 'text-xs',
size === 'sm' && 'text-sm',
)}
>
{rtlEscapeChar}
{filePath ?? 'No file selected'}
{filePath ?? `No ${itemLabel.toLowerCase()} selected`}
</div>
</>
)}

View File

@@ -10,13 +10,15 @@ import { HeaderSize } from '../HeaderSize';
import { WindowControls } from '../WindowControls';
import { SettingsAppearance } from './SettingsAppearance';
import { SettingsGeneral } from './SettingsGeneral';
import { SettingsPlugins } from './SettingsPlugins';
enum Tab {
General = 'general',
Appearance = 'appearance',
Plugins = 'plugins',
}
const tabs = [Tab.General, Tab.Appearance];
const tabs = [Tab.General, Tab.Appearance, Tab.Plugins];
export const Settings = () => {
const osInfo = useOsInfo();
@@ -58,6 +60,9 @@ export const Settings = () => {
<TabContent value={Tab.Appearance} className="pt-3 overflow-y-auto h-full px-4">
<SettingsAppearance />
</TabContent>
<TabContent value={Tab.Plugins} className="pt-3 overflow-y-auto h-full px-4">
<SettingsPlugins />
</TabContent>
</Tabs>
</div>
);

View File

@@ -0,0 +1,95 @@
import type { Plugin } from '@yaakapp/api';
import React from 'react';
import { useCreatePlugin } from '../../hooks/useCreatePlugin';
import { usePluginInfo } from '../../hooks/usePluginInfo';
import { usePlugins, useRefreshPlugins } from '../../hooks/usePlugins';
import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { IconButton } from '../core/IconButton';
import { InlineCode } from '../core/InlineCode';
import { HStack } from '../core/Stacks';
import { SelectFile } from '../SelectFile';
export function SettingsPlugins() {
const [directory, setDirectory] = React.useState<string | null>(null);
const plugins = usePlugins();
const createPlugin = useCreatePlugin();
const refreshPlugins = useRefreshPlugins();
return (
<div className="grid grid-rows-[minmax(0,1fr)_auto] h-full">
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
<thead>
<tr>
<th></th>
<th className="py-2 text-left">Plugin</th>
<th className="py-2 text-right">Version</th>
<th></th>
</tr>
</thead>
<tbody className="divide-y divide-surface-highlight">
{plugins.map((p) => (
<PluginInfo key={p.id} plugin={p} />
))}
</tbody>
</table>
<form
onSubmit={(e) => {
e.preventDefault();
if (directory == null) return;
createPlugin.mutate(directory);
setDirectory(null);
}}
>
<footer className="grid grid-cols-[minmax(0,1fr)_auto] -mx-4 py-2 px-4 border-t bg-surface-highlight border-border-subtle min-w-0">
<SelectFile
size="xs"
noun="Plugin"
directory
onChange={({ filePath }) => setDirectory(filePath)}
filePath={directory}
/>
<HStack>
{directory && (
<Button size="xs" type="submit" color="primary" className="ml-auto">
Add Plugin
</Button>
)}
<IconButton
size="sm"
icon="refresh"
title="Reload plugins"
spin={refreshPlugins.isPending}
onClick={() => refreshPlugins.mutate()}
/>
</HStack>
</footer>
</form>
</div>
);
}
function PluginInfo({ plugin }: { plugin: Plugin }) {
const pluginInfo = usePluginInfo(plugin.id);
if (pluginInfo.data == null) return null;
return (
<tr className="group">
<td className="pr-2">
<Checkbox hideLabel checked={true} title="foo" onChange={() => null} />
</td>
<td className="py-2 select-text cursor-text w-full">
<InlineCode>{pluginInfo.data?.name}</InlineCode>
</td>
<td className="py-2 select-text cursor-text text-right">
<InlineCode>{pluginInfo.data?.version}</InlineCode>
</td>
<td className="py-2 select-text cursor-text pl-2">
<IconButton
size="sm"
icon="trash"
title="Uninstall plugin"
className="text-text-subtlest"
/>
</td>
</tr>
);
}

View File

@@ -73,7 +73,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
size === 'md' && 'h-md px-3 rounded-md',
size === 'sm' && 'h-sm px-2.5 rounded-md',
size === 'xs' && 'h-xs px-2 text-sm rounded-md',
size === '2xs' && 'h-5 px-1 text-xs rounded',
size === '2xs' && 'h-2xs px-1 text-xs rounded',
// Solids
variant === 'solid' && 'border-transparent',

View File

@@ -58,9 +58,9 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
'!px-0',
color === 'custom' && 'text-text-subtle',
color === 'default' && 'text-text-subtle',
size === 'md' && 'w-9',
size === 'sm' && 'w-8',
size === 'xs' && 'w-6',
size === 'md' && 'w-md',
size === 'sm' && 'w-sm',
size === 'xs' && 'w-xs',
size === '2xs' && 'w-5',
)}
{...props}

View File

@@ -6,6 +6,7 @@ export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanEleme
<code
className={classNames(
className,
'select-text cursor-text',
'font-mono text-shrink bg-surface-highlight border border-border-subtle',
'px-1.5 py-0.5 rounded text shadow-inner break-words',
)}

View File

@@ -14,9 +14,11 @@ export function useActiveWorkspaceChangedToast() {
setId(activeWorkspace?.id ?? null);
// Don't notify on first load
// Don't notify on the first load
if (id === null) return;
console.log('HELLO?', activeWorkspace?.id, id, window.location);
toast.show({
id: 'workspace-changed',
timeout: 3000,

View File

@@ -0,0 +1,13 @@
import { useMutation } from '@tanstack/react-query';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
export function useCreatePlugin() {
return useMutation<void, unknown, string>({
mutationKey: ['create_plugin'],
mutationFn: async (directory: string) => {
await invokeCmd('cmd_create_plugin', { directory });
},
onSettled: () => trackEvent('plugin', 'create'),
});
}

View File

@@ -0,0 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import type { BootResponse } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
export function usePluginInfo(id: string) {
return useQuery({
queryKey: ['plugin_info', id],
queryFn: async () => {
const info = (await invokeCmd('cmd_plugin_info', { id })) as BootResponse;
return info;
},
});
}

View File

@@ -0,0 +1,23 @@
import { useMutation } from '@tanstack/react-query';
import type { Plugin } from '@yaakapp/api';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import { minPromiseMillis } from '../lib/minPromiseMillis';
import { listPlugins } from '../lib/store';
const plugins = await listPlugins();
export const pluginsAtom = atom<Plugin[]>(plugins);
export function usePlugins() {
return useAtomValue(pluginsAtom);
}
export function useRefreshPlugins() {
const setPlugins = useSetAtom(pluginsAtom);
return useMutation({
mutationKey: ['refresh_plugins'],
mutationFn: async () => {
const plugins = await minPromiseMillis(listPlugins());
setPlugins(plugins);
},
});
}

View File

@@ -13,6 +13,7 @@ export type TrackResource =
| 'http_request'
| 'http_response'
| 'key_value'
| 'plugin'
| 'setting'
| 'sidebar'
| 'theme'

View File

@@ -1,7 +1,7 @@
import { sleep } from './sleep';
/** Ensures a promise takes at least a certain number of milliseconds to resolve */
export async function minPromiseMillis<T>(promise: Promise<T>, millis: number) {
export async function minPromiseMillis<T>(promise: Promise<T>, millis = 300) {
const start = Date.now();
let result;
let err;

View File

@@ -4,6 +4,7 @@ import type {
Folder,
GrpcRequest,
HttpRequest,
Plugin,
Settings,
Workspace,
} from '@yaakapp/api';
@@ -63,6 +64,11 @@ export async function listWorkspaces(): Promise<Workspace[]> {
return workspaces;
}
export async function listPlugins(): Promise<Plugin[]> {
const plugins: Plugin[] = (await invokeCmd('cmd_list_plugins')) ?? [];
return plugins;
}
export async function getCookieJar(id: string | null): Promise<CookieJar | null> {
if (id === null) return null;
const cookieJar: CookieJar = (await invokeCmd('cmd_get_cookie_jar', { id })) ?? null;

View File

@@ -6,6 +6,7 @@ type TauriCmd =
| 'cmd_check_for_updates'
| 'cmd_create_cookie_jar'
| 'cmd_create_environment'
| 'cmd_create_plugin'
| 'cmd_template_tokens_to_string'
| 'cmd_create_folder'
| 'cmd_create_grpc_request'
@@ -47,11 +48,13 @@ type TauriCmd =
| 'cmd_list_grpc_requests'
| 'cmd_list_http_requests'
| 'cmd_list_http_responses'
| 'cmd_list_plugins'
| 'cmd_list_workspaces'
| 'cmd_metadata'
| 'cmd_new_nested_window'
| 'cmd_new_window'
| 'cmd_parse_template'
| 'cmd_plugin_info'
| 'cmd_render_template'
| 'cmd_save_response'
| 'cmd_send_ephemeral_request'

View File

@@ -1,6 +1,7 @@
const plugin = require('tailwindcss/plugin');
const height = {
const sizes = {
'2xs': '1.25rem',
xs: '1.8rem',
sm: '2.0rem',
md: '2.5rem',
@@ -18,8 +19,10 @@ module.exports = {
fontSize: {
xs: '0.8rem',
},
height,
minHeight: height,
height: sizes,
width: sizes,
minHeight: sizes,
minWidth: sizes,
lineHeight: {
// HACK: Minus 2 to account for borders inside inputs
xs: 'calc(1.75rem - 2px)',