(feat) Add ability to disable plugins and show bundled plugins (#337)

This commit is contained in:
Gregory Schier
2026-01-01 09:32:48 -08:00
committed by GitHub
parent 07ea1ea7dc
commit 92a8da03af
41 changed files with 515 additions and 1183 deletions
+1
View File
@@ -36,3 +36,4 @@ out
tmp tmp
.zed .zed
codebook.toml codebook.toml
target
+4 -4
View File
@@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.3.7/schema.json", "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
@@ -39,13 +39,13 @@
"!**/dist", "!**/dist",
"!**/build", "!**/build",
"!scripts", "!scripts",
"!packages/plugin-runtime",
"!packages/plugin-runtime-types",
"!src-tauri", "!src-tauri",
"!src-web/tailwind.config.cjs", "!src-web/tailwind.config.cjs",
"!src-web/postcss.config.cjs", "!src-web/postcss.config.cjs",
"!src-web/vite.config.ts", "!src-web/vite.config.ts",
"!src-web/routeTree.gen.ts" "!src-web/routeTree.gen.ts",
"!packages/plugin-runtime-types/lib",
"!**/bindings"
] ]
} }
} }
+34 -50
View File
@@ -15,6 +15,7 @@
"plugins-external/template-function-faker", "plugins-external/template-function-faker",
"plugins/action-copy-curl", "plugins/action-copy-curl",
"plugins/action-copy-grpcurl", "plugins/action-copy-grpcurl",
"plugins/action-send-folder",
"plugins/auth-apikey", "plugins/auth-apikey",
"plugins/auth-aws", "plugins/auth-aws",
"plugins/auth-basic", "plugins/auth-basic",
@@ -4047,6 +4048,10 @@
"resolved": "plugins/action-copy-grpcurl", "resolved": "plugins/action-copy-grpcurl",
"link": true "link": true
}, },
"node_modules/@yaak/action-send-folder": {
"resolved": "plugins/action-send-folder",
"link": true
},
"node_modules/@yaak/auth-apikey": { "node_modules/@yaak/auth-apikey": {
"resolved": "plugins/auth-apikey", "resolved": "plugins/auth-apikey",
"link": true "link": true
@@ -4690,9 +4695,9 @@
} }
}, },
"node_modules/async": { "node_modules/async": {
"version": "3.2.4", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/async-each": { "node_modules/async-each": {
@@ -8719,9 +8724,9 @@
} }
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "10.4.5", "version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
@@ -10773,12 +10778,12 @@
} }
}, },
"node_modules/jws": { "node_modules/jws": {
"version": "3.2.2", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"jwa": "^1.4.1", "jwa": "^1.4.2",
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
@@ -11591,9 +11596,9 @@
} }
}, },
"node_modules/mdast-util-to-hast": { "node_modules/mdast-util-to-hast": {
"version": "13.2.0", "version": "13.2.1",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/hast": "^3.0.0", "@types/hast": "^3.0.0",
@@ -13559,15 +13564,15 @@
} }
}, },
"node_modules/openapi-to-postmanv2": { "node_modules/openapi-to-postmanv2": {
"version": "5.0.0", "version": "5.7.0",
"resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-5.0.0.tgz", "resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-5.7.0.tgz",
"integrity": "sha512-ousMf9rXKen9tscJQ0H8BE+hfgOvFRb2SspYwGnQTmnjzkPNejHUQpNmRUIK/gZ6ZwiNWAnQCKWNogaZXUlFew==", "integrity": "sha512-fhwjpL+CF1kOKX5G1m5E/tnZWc9KuSXZA4oOGZv9c4NzKtC3ecUHxtp4KTvwzFh8/b+ssSZo+blC/3OK4/9ySA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"ajv": "8.11.0", "ajv": "^8.11.0",
"ajv-draft-04": "1.0.0", "ajv-draft-04": "1.0.0",
"ajv-formats": "2.1.1", "ajv-formats": "2.1.1",
"async": "3.2.4", "async": "3.2.6",
"commander": "2.20.3", "commander": "2.20.3",
"graphlib": "2.1.8", "graphlib": "2.1.8",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
@@ -13589,22 +13594,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/openapi-to-postmanv2/node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/openapi-to-postmanv2/node_modules/ajv-draft-04": { "node_modules/openapi-to-postmanv2/node_modules/ajv-draft-04": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
@@ -14554,9 +14543,9 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.14.0", "version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.1.0" "side-channel": "^1.1.0"
@@ -14786,16 +14775,16 @@
} }
}, },
"node_modules/react-syntax-highlighter": { "node_modules/react-syntax-highlighter": {
"version": "15.6.1", "version": "15.6.6",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz",
"integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.3.1", "@babel/runtime": "^7.3.1",
"highlight.js": "^10.4.1", "highlight.js": "^10.4.1",
"highlightjs-vue": "^1.0.0", "highlightjs-vue": "^1.0.0",
"lowlight": "^1.17.0", "lowlight": "^1.17.0",
"prismjs": "^1.27.0", "prismjs": "^1.30.0",
"refractor": "^3.6.0" "refractor": "^3.6.0"
}, },
"peerDependencies": { "peerDependencies": {
@@ -18029,15 +18018,6 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/urix": { "node_modules/urix": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
@@ -19110,6 +19090,10 @@
"name": "@yaak/action-copy-grpcurl", "name": "@yaak/action-copy-grpcurl",
"version": "0.1.0" "version": "0.1.0"
}, },
"plugins/action-send-folder": {
"name": "@yaak/action-send-folder",
"version": "0.1.0"
},
"plugins/auth-apikey": { "plugins/auth-apikey": {
"name": "@yaak/auth-apikey", "name": "@yaak/auth-apikey",
"version": "0.1.0" "version": "0.1.0"
+1
View File
@@ -14,6 +14,7 @@
"plugins-external/template-function-faker", "plugins-external/template-function-faker",
"plugins/action-copy-curl", "plugins/action-copy-curl",
"plugins/action-copy-grpcurl", "plugins/action-copy-grpcurl",
"plugins/action-send-folder",
"plugins/auth-apikey", "plugins/auth-apikey",
"plugins/auth-aws", "plugins/auth-aws",
"plugins/auth-basic", "plugins/auth-basic",
+1 -1
View File
@@ -423,7 +423,7 @@ export type ListCookieNamesRequest = {};
export type ListCookieNamesResponse = { names: Array<string>, }; export type ListCookieNamesResponse = { names: Array<string>, };
export type ListFoldersRequest = Record<string, never>; export type ListFoldersRequest = {};
export type ListFoldersResponse = { folders: Array<Folder>, }; export type ListFoldersResponse = { folders: Array<Folder>, };
@@ -1,4 +1,4 @@
import { import type {
CallHttpAuthenticationActionArgs, CallHttpAuthenticationActionArgs,
CallHttpAuthenticationRequest, CallHttpAuthenticationRequest,
CallHttpAuthenticationResponse, CallHttpAuthenticationResponse,
@@ -6,8 +6,8 @@ import {
GetHttpAuthenticationSummaryResponse, GetHttpAuthenticationSummaryResponse,
HttpAuthenticationAction, HttpAuthenticationAction,
} from '../bindings/gen_events'; } from '../bindings/gen_events';
import { MaybePromise } from '../helpers'; import type { MaybePromise } from '../helpers';
import { Context } from './Context'; import type { Context } from './Context';
type AddDynamicMethod<T> = { type AddDynamicMethod<T> = {
dynamic?: ( dynamic?: (
@@ -16,6 +16,7 @@ type AddDynamicMethod<T> = {
) => MaybePromise<Partial<T> | null | undefined>; ) => MaybePromise<Partial<T> | null | undefined>;
}; };
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern
type AddDynamic<T> = T extends any type AddDynamic<T> = T extends any
? T extends { inputs?: FormInput[] } ? T extends { inputs?: FormInput[] }
? Omit<T, 'inputs'> & { ? Omit<T, 'inputs'> & {
@@ -1,4 +1,4 @@
import { FilterResponse } from '../bindings/gen_events'; import type { FilterResponse } from '../bindings/gen_events';
import type { Context } from './Context'; import type { Context } from './Context';
export type FilterPlugin = { export type FilterPlugin = {
@@ -1,4 +1,4 @@
import { CallGrpcRequestActionArgs, GrpcRequestAction } from '../bindings/gen_events'; import type { CallGrpcRequestActionArgs, GrpcRequestAction } from '../bindings/gen_events';
import type { Context } from './Context'; import type { Context } from './Context';
export type GrpcRequestActionPlugin = GrpcRequestAction & { export type GrpcRequestActionPlugin = GrpcRequestAction & {
@@ -1,5 +1,5 @@
import { ImportResources } from '../bindings/gen_events'; import type { ImportResources } from '../bindings/gen_events';
import { AtLeast, MaybePromise } from '../helpers'; import type { AtLeast, MaybePromise } from '../helpers';
import type { Context } from './Context'; import type { Context } from './Context';
type RootFields = 'name' | 'id' | 'model'; type RootFields = 'name' | 'id' | 'model';
@@ -1,6 +1,6 @@
import { CallTemplateFunctionArgs, FormInput, TemplateFunction } from '../bindings/gen_events'; import type { CallTemplateFunctionArgs, FormInput, TemplateFunction } from '../bindings/gen_events';
import { MaybePromise } from '../helpers'; import type { MaybePromise } from '../helpers';
import { Context } from './Context'; import type { Context } from './Context';
type AddDynamicMethod<T> = { type AddDynamicMethod<T> = {
dynamic?: ( dynamic?: (
@@ -9,6 +9,7 @@ type AddDynamicMethod<T> = {
) => MaybePromise<Partial<T> | null | undefined>; ) => MaybePromise<Partial<T> | null | undefined>;
}; };
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern
type AddDynamic<T> = T extends any type AddDynamic<T> = T extends any
? T extends { inputs?: FormInput[] } ? T extends { inputs?: FormInput[] }
? Omit<T, 'inputs'> & { ? Omit<T, 'inputs'> & {
@@ -1,3 +1,3 @@
import { Theme } from '../bindings/gen_events'; import type { Theme } from '../bindings/gen_events';
export type ThemePlugin = Theme; export type ThemePlugin = Theme;
@@ -1,8 +1,8 @@
import { AuthenticationPlugin } from './AuthenticationPlugin'; import type { AuthenticationPlugin } from './AuthenticationPlugin';
import type { Context } from './Context'; import type { Context } from './Context';
import type { FilterPlugin } from './FilterPlugin'; import type { FilterPlugin } from './FilterPlugin';
import { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin'; import type { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin'; import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
import type { WebsocketRequestActionPlugin } from './WebsocketRequestActionPlugin'; import type { WebsocketRequestActionPlugin } from './WebsocketRequestActionPlugin';
import type { WorkspaceActionPlugin } from './WorkspaceActionPlugin'; import type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';
+2 -2
View File
@@ -1,7 +1,7 @@
import { PluginContext } from '@yaakapp-internal/plugins'; import type { PluginContext } from '@yaakapp-internal/plugins';
import type { BootRequest, InternalEvent } from '@yaakapp/api'; import type { BootRequest, InternalEvent } from '@yaakapp/api';
import type { EventChannel } from './EventChannel'; import type { EventChannel } from './EventChannel';
import { PluginInstance, PluginWorkerData } from './PluginInstance'; import { PluginInstance, type PluginWorkerData } from './PluginInstance';
export class PluginHandle { export class PluginHandle {
#instance: PluginInstance; #instance: PluginInstance;
+37 -27
View File
@@ -1,5 +1,8 @@
import { applyFormInputDefaults, validateTemplateFunctionArgs } from '@yaakapp-internal/lib/templateFunction';
import { import {
applyFormInputDefaults,
validateTemplateFunctionArgs,
} from '@yaakapp-internal/lib/templateFunction';
import type {
BootRequest, BootRequest,
DeleteKeyValueResponse, DeleteKeyValueResponse,
DeleteModelResponse, DeleteModelResponse,
@@ -12,9 +15,13 @@ import {
HttpAuthenticationAction, HttpAuthenticationAction,
HttpRequest, HttpRequest,
HttpRequestAction, HttpRequestAction,
ImportResources,
InternalEvent, InternalEvent,
InternalEventPayload, InternalEventPayload,
ListCookieNamesResponse, ListCookieNamesResponse,
ListFoldersResponse,
ListHttpRequestsRequest,
ListHttpRequestsResponse,
ListWorkspacesResponse, ListWorkspacesResponse,
PluginContext, PluginContext,
PromptTextResponse, PromptTextResponse,
@@ -22,11 +29,12 @@ import {
RenderHttpRequestResponse, RenderHttpRequestResponse,
SendHttpRequestResponse, SendHttpRequestResponse,
TemplateFunction, TemplateFunction,
TemplateRenderRequest,
TemplateRenderResponse, TemplateRenderResponse,
UpsertModelResponse, UpsertModelResponse,
WindowInfoResponse, WindowInfoResponse,
} from '@yaakapp-internal/plugins'; } from '@yaakapp-internal/plugins';
import { Context, PluginDefinition } from '@yaakapp/api'; import type { Context, PluginDefinition } from '@yaakapp/api';
import console from 'node:console'; import console from 'node:console';
import { type Stats, statSync, watch } from 'node:fs'; import { type Stats, statSync, watch } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -56,7 +64,7 @@ export class PluginInstance {
await this.#onMessage(event); await this.#onMessage(event);
}); });
this.#mod = {} as any; this.#mod = {};
const fileChangeCallback = async () => { const fileChangeCallback = async () => {
await this.#mod?.dispose?.(); await this.#mod?.dispose?.();
@@ -120,8 +128,7 @@ export class PluginInstance {
if (reply != null) { if (reply != null) {
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'import_response', type: 'import_response',
// deno-lint-ignore no-explicit-any resources: reply.resources as ImportResources,
resources: reply.resources as any,
}; };
this.#sendPayload(context, replyPayload, replyId); this.#sendPayload(context, replyPayload, replyId);
return; return;
@@ -262,7 +269,7 @@ export class PluginInstance {
payload.type === 'get_template_function_config_request' && payload.type === 'get_template_function_config_request' &&
Array.isArray(this.#mod?.templateFunctions) Array.isArray(this.#mod?.templateFunctions)
) { ) {
let templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name); const templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name);
if (templateFunction == null) { if (templateFunction == null) {
this.#sendEmpty(context, replyId); this.#sendEmpty(context, replyId);
return; return;
@@ -381,10 +388,7 @@ export class PluginInstance {
} }
} }
if ( if (payload.type === 'call_folder_action_request' && Array.isArray(this.#mod.folderActions)) {
payload.type === 'call_folder_action_request' &&
Array.isArray(this.#mod.folderActions)
) {
const action = this.#mod.folderActions[payload.index]; const action = this.#mod.folderActions[payload.index];
if (typeof action?.onSelect === 'function') { if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args); await action.onSelect(ctx, payload.args);
@@ -703,12 +707,15 @@ export class PluginInstance {
return httpRequest; return httpRequest;
}, },
list: async (args?: { folderId?: string }) => { list: async (args?: { folderId?: string }) => {
const payload = { const payload: InternalEventPayload = {
type: 'list_http_requests_request', type: 'list_http_requests_request',
folderId: args?.folderId, folderId: args?.folderId,
} as any; } satisfies ListHttpRequestsRequest & { type: 'list_http_requests_request' };
const { httpRequests } = await this.#sendForReply<any>(context, payload); const { httpRequests } = await this.#sendForReply<ListHttpRequestsResponse>(
return httpRequests as any[]; context,
payload,
);
return httpRequests;
}, },
create: async (args) => { create: async (args) => {
const payload = { const payload = {
@@ -747,11 +754,9 @@ export class PluginInstance {
}, },
folder: { folder: {
list: async () => { list: async () => {
const payload = { const payload = { type: 'list_folders_request' } as const;
type: 'list_folders_request', const { folders } = await this.#sendForReply<ListFoldersResponse>(context, payload);
} as any; return folders;
const { folders } = await this.#sendForReply<any>(context, payload);
return folders as any[];
}, },
}, },
cookies: { cookies: {
@@ -774,9 +779,10 @@ export class PluginInstance {
* Invoke Yaak's template engine to render a value. If the value is a nested type * Invoke Yaak's template engine to render a value. If the value is a nested type
* (eg. object), it will be recursively rendered. * (eg. object), it will be recursively rendered.
*/ */
render: async (args) => { render: async (args: TemplateRenderRequest) => {
const payload = { type: 'template_render_request', ...args } as const; const payload = { type: 'template_render_request', ...args } as const;
const result = await this.#sendForReply<TemplateRenderResponse>(context, payload); const result = await this.#sendForReply<TemplateRenderResponse>(context, payload);
// biome-ignore lint/suspicious/noExplicitAny: That's okay
return result.data as any; return result.data as any;
}, },
}, },
@@ -809,15 +815,19 @@ export class PluginInstance {
workspace: { workspace: {
list: async () => { list: async () => {
const payload = { const payload = {
type: 'list_workspaces_request' type: 'list_workspaces_request',
} as InternalEventPayload; } as InternalEventPayload;
const response = await this.#sendForReply<ListWorkspacesResponse>(context, payload); const response = await this.#sendForReply<ListWorkspacesResponse>(context, payload);
return response.workspaces.map((w) => ({ return response.workspaces.map((w) => {
id: w.id, // Internal workspace info includes label field not in public API
name: w.name, type WorkspaceInfoInternal = typeof w & { label?: string };
// Hide label from plugin authors, but keep it for internal routing return {
_label: (w as any).label as string, id: w.id,
})); name: w.name,
// Hide label from plugin authors, but keep it for internal routing
_label: (w as WorkspaceInfoInternal).label as string,
};
});
}, },
withContext: (workspaceHandle: { id: string; name: string; _label?: string }) => { withContext: (workspaceHandle: { id: string; name: string; _label?: string }) => {
// Create a new context with the workspace's window label // Create a new context with the workspace's window label
+23 -7
View File
@@ -1,5 +1,8 @@
import { CallHttpAuthenticationActionArgs, CallTemplateFunctionArgs } from '@yaakapp-internal/plugins'; import type { Context, DynamicAuthenticationArg, DynamicTemplateFunctionArg } from '@yaakapp/api';
import { Context, DynamicAuthenticationArg, DynamicTemplateFunctionArg } from '@yaakapp/api'; import type {
CallHttpAuthenticationActionArgs,
CallTemplateFunctionArgs,
} from '@yaakapp-internal/plugins';
export async function applyDynamicFormInput( export async function applyDynamicFormInput(
ctx: Context, ctx: Context,
@@ -18,15 +21,28 @@ export async function applyDynamicFormInput(
args: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[], args: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[],
callArgs: CallTemplateFunctionArgs | CallHttpAuthenticationActionArgs, callArgs: CallTemplateFunctionArgs | CallHttpAuthenticationActionArgs,
): Promise<(DynamicTemplateFunctionArg | DynamicAuthenticationArg)[]> { ): Promise<(DynamicTemplateFunctionArg | DynamicAuthenticationArg)[]> {
const resolvedArgs: any[] = []; const resolvedArgs: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[] = [];
for (const { dynamic, ...arg } of args) { for (const { dynamic, ...arg } of args) {
const newArg: any = { const dynamicResult =
typeof dynamic === 'function'
? await dynamic(
ctx,
callArgs as CallTemplateFunctionArgs & CallHttpAuthenticationActionArgs,
)
: undefined;
const newArg = {
...arg, ...arg,
...(typeof dynamic === 'function' ? await dynamic(ctx, callArgs as any) : undefined), ...dynamicResult,
}; } as DynamicTemplateFunctionArg | DynamicAuthenticationArg;
if ('inputs' in newArg && Array.isArray(newArg.inputs)) { if ('inputs' in newArg && Array.isArray(newArg.inputs)) {
try { try {
newArg.inputs = await applyDynamicFormInput(ctx, newArg.inputs, callArgs as any); newArg.inputs = await applyDynamicFormInput(
ctx,
newArg.inputs as DynamicTemplateFunctionArg[],
callArgs as CallTemplateFunctionArgs & CallHttpAuthenticationActionArgs,
);
} catch (e) { } catch (e) {
console.error('Failed to apply dynamic form input', e); console.error('Failed to apply dynamic form input', e);
} }
+9 -4
View File
@@ -5,12 +5,12 @@ import WebSocket from 'ws';
const port = process.env.PORT; const port = process.env.PORT;
if (!port) { if (!port) {
throw new Error('Plugin runtime missing PORT') throw new Error('Plugin runtime missing PORT');
} }
const host = process.env.HOST; const host = process.env.HOST;
if (!host) { if (!host) {
throw new Error('Plugin runtime missing HOST') throw new Error('Plugin runtime missing HOST');
} }
const pluginToAppEvents = new EventChannel(); const pluginToAppEvents = new EventChannel();
@@ -26,7 +26,7 @@ ws.on('message', async (e: Buffer) => {
} }
}); });
ws.on('open', () => console.log('Plugin runtime connected to websocket')); ws.on('open', () => console.log('Plugin runtime connected to websocket'));
ws.on('error', (err: any) => console.error('Plugin runtime websocket error', err)); ws.on('error', (err: unknown) => console.error('Plugin runtime websocket error', err));
ws.on('close', (code: number) => console.log('Plugin runtime websocket closed', code)); ws.on('close', (code: number) => console.log('Plugin runtime websocket closed', code));
// Listen for incoming events from plugins // Listen for incoming events from plugins
@@ -39,7 +39,12 @@ async function handleIncoming(msg: string) {
const pluginEvent: InternalEvent = JSON.parse(msg); const pluginEvent: InternalEvent = JSON.parse(msg);
// Handle special event to bootstrap plugin // Handle special event to bootstrap plugin
if (pluginEvent.payload.type === 'boot_request') { if (pluginEvent.payload.type === 'boot_request') {
const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.context, pluginEvent.payload, pluginToAppEvents); const plugin = new PluginHandle(
pluginEvent.pluginRefId,
pluginEvent.context,
pluginEvent.payload,
pluginToAppEvents,
);
plugins[pluginEvent.pluginRefId] = plugin; plugins[pluginEvent.pluginRefId] = plugin;
} }
+11 -20
View File
@@ -1,28 +1,20 @@
import process from "node:process"; import process from 'node:process';
export function interceptStdout( export function interceptStdout(intercept: (text: string) => string) {
intercept: (text: string) => string,
) {
const old_stdout_write = process.stdout.write; const old_stdout_write = process.stdout.write;
const old_stderr_write = process.stderr.write; const old_stderr_write = process.stderr.write;
process.stdout.write = (function (write) { process.stdout.write = ((write) =>
return function (text: string) { ((text: string, ...args: never[]) => {
arguments[0] = interceptor(text, intercept); write.call(process.stdout, interceptor(text, intercept), ...args);
// deno-lint-ignore no-explicit-any
write.apply(process.stdout, arguments as any);
return true; return true;
}; }) as typeof process.stdout.write)(process.stdout.write);
})(process.stdout.write);
process.stderr.write = (function (write) { process.stderr.write = ((write) =>
return function (text: string) { ((text: string, ...args: never[]) => {
arguments[0] = interceptor(text, intercept); write.call(process.stderr, interceptor(text, intercept), ...args);
// deno-lint-ignore no-explicit-any
write.apply(process.stderr, arguments as any);
return true; return true;
}; }) as typeof process.stderr.write)(process.stderr.write);
})(process.stderr.write);
// puts back to original // puts back to original
return function unhook() { return function unhook() {
@@ -32,6 +24,5 @@ export function interceptStdout(
} }
function interceptor(text: string, fn: (text: string) => string) { function interceptor(text: string, fn: (text: string) => string) {
return fn(text).replace(/\n$/, "") + return fn(text).replace(/\n$/, '') + (fn(text) && /\n$/.test(text) ? '\n' : '');
(fn(text) && /\n$/.test(text) ? "\n" : "");
} }
+9 -4
View File
@@ -5,10 +5,15 @@ export function migrateTemplateFunctionSelectOptions(
): TemplateFunctionPlugin { ): TemplateFunctionPlugin {
const migratedArgs = f.args.map((a) => { const migratedArgs = f.args.map((a) => {
if (a.type === 'select') { if (a.type === 'select') {
a.options = a.options.map((o) => ({ // Migrate old options that had 'name' instead of 'label'
...o, type LegacyOption = { label?: string; value: string; name?: string };
label: o.label || (o as any).name, a.options = a.options.map((o) => {
})); const legacy = o as LegacyOption;
return {
label: legacy.label ?? legacy.name ?? '',
value: legacy.value,
};
});
} }
return a; return a;
}); });
+2 -2
View File
@@ -1,6 +1,6 @@
import { applyFormInputDefaults } from '@yaakapp-internal/lib/templateFunction'; import { applyFormInputDefaults } from '@yaakapp-internal/lib/templateFunction';
import { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins'; import type { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins';
import { Context, DynamicTemplateFunctionArg } from '@yaakapp/api'; import type { Context, DynamicTemplateFunctionArg } from '@yaakapp/api';
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from 'vitest';
import { applyDynamicFormInput } from '../src/common'; import { applyDynamicFormInput } from '../src/common';
+16
View File
@@ -0,0 +1,16 @@
{
"name": "@yaak/action-send-folder",
"displayName": "Send All",
"description": "Send all HTTP requests in a folder sequentially",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/action-send-folder"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev"
}
}
+74
View File
@@ -0,0 +1,74 @@
import type { PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
folderActions: [
{
label: 'Send All',
icon: 'send_horizontal',
async onSelect(ctx, args) {
const targetFolder = args.folder;
// Get all folders and HTTP requests
const [allFolders, allRequests] = await Promise.all([
ctx.folder.list(),
ctx.httpRequest.list(),
]);
// Build a set of all folder IDs that are descendants of the target folder
const folderIds = new Set<string>([targetFolder.id]);
const addDescendants = (parentId: string) => {
for (const folder of allFolders) {
if (folder.folderId === parentId && !folderIds.has(folder.id)) {
folderIds.add(folder.id);
addDescendants(folder.id);
}
}
};
addDescendants(targetFolder.id);
// Filter HTTP requests to those in the target folder or its descendants
const requestsToSend = allRequests.filter(
(req) => req.folderId != null && folderIds.has(req.folderId),
);
if (requestsToSend.length === 0) {
await ctx.toast.show({
message: 'No requests in folder',
icon: 'info',
color: 'info',
});
return;
}
// Send each request sequentially
let successCount = 0;
let errorCount = 0;
for (const request of requestsToSend) {
try {
await ctx.httpRequest.send({ httpRequest: request });
successCount++;
} catch (error) {
errorCount++;
console.error(`Failed to send request ${request.id}:`, error);
}
}
// Show summary toast
if (errorCount === 0) {
await ctx.toast.show({
message: `Sent ${successCount} request${successCount !== 1 ? 's' : ''}`,
icon: 'send_horizontal',
color: 'success',
});
} else {
await ctx.toast.show({
message: `Sent ${successCount}, failed ${errorCount}`,
icon: 'alert_triangle',
color: 'warning',
});
}
},
},
],
};
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}
+1 -1
View File
@@ -1276,7 +1276,7 @@ async fn cmd_install_plugin<R: Runtime>(
app_handle: AppHandle<R>, app_handle: AppHandle<R>,
window: WebviewWindow<R>, window: WebviewWindow<R>,
) -> YaakResult<Plugin> { ) -> YaakResult<Plugin> {
plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &directory).await?; plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &directory, true).await?;
Ok(app_handle.db().upsert_plugin( Ok(app_handle.db().upsert_plugin(
&Plugin { directory: directory.into(), url, ..Default::default() }, &Plugin { directory: directory.into(), url, ..Default::default() },
+10 -18
View File
@@ -21,10 +21,10 @@ use yaak_plugins::error::Error::PluginErr;
use yaak_plugins::events::{ use yaak_plugins::events::{
Color, DeleteKeyValueResponse, EmptyPayload, ErrorResponse, FindHttpResponsesResponse, Color, DeleteKeyValueResponse, EmptyPayload, ErrorResponse, FindHttpResponsesResponse,
GetCookieValueResponse, GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, GetCookieValueResponse, GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent,
InternalEventPayload, ListCookieNamesResponse, ListHttpRequestsResponse, ListWorkspacesResponse, InternalEventPayload, ListCookieNamesResponse, ListHttpRequestsResponse,
RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse, ListWorkspacesResponse, RenderGrpcRequestResponse, RenderHttpRequestResponse,
SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse, WindowInfoResponse, SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse,
WindowNavigateEvent, WorkspaceInfo, WindowInfoResponse, WindowNavigateEvent, WorkspaceInfo,
}; };
use yaak_plugins::plugin_handle::PluginHandle; use yaak_plugins::plugin_handle::PluginHandle;
use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_plugins::template_callback::PluginTemplateCallback;
@@ -107,7 +107,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
Workspace(app_handle.db().upsert_workspace(m, &UpdateSource::Plugin)?) Workspace(app_handle.db().upsert_workspace(m, &UpdateSource::Plugin)?)
} }
_ => { _ => {
return Err(PluginErr("Upsert not supported for this model type".into()).into()) return Err(PluginErr("Upsert not supported for this model type".into()).into());
} }
}; };
@@ -118,14 +118,10 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
InternalEventPayload::DeleteModelRequest(req) => { InternalEventPayload::DeleteModelRequest(req) => {
let model = match req.model.as_str() { let model = match req.model.as_str() {
"http_request" => AnyModel::HttpRequest( "http_request" => AnyModel::HttpRequest(
app_handle app_handle.db().delete_http_request_by_id(&req.id, &UpdateSource::Plugin)?,
.db()
.delete_http_request_by_id(&req.id, &UpdateSource::Plugin)?,
), ),
"grpc_request" => AnyModel::GrpcRequest( "grpc_request" => AnyModel::GrpcRequest(
app_handle app_handle.db().delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)?,
.db()
.delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)?,
), ),
"websocket_request" => AnyModel::WebsocketRequest( "websocket_request" => AnyModel::WebsocketRequest(
app_handle app_handle
@@ -133,17 +129,13 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
.delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin)?, .delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin)?,
), ),
"folder" => AnyModel::Folder( "folder" => AnyModel::Folder(
app_handle app_handle.db().delete_folder_by_id(&req.id, &UpdateSource::Plugin)?,
.db()
.delete_folder_by_id(&req.id, &UpdateSource::Plugin)?,
), ),
"environment" => AnyModel::Environment( "environment" => AnyModel::Environment(
app_handle app_handle.db().delete_environment_by_id(&req.id, &UpdateSource::Plugin)?,
.db()
.delete_environment_by_id(&req.id, &UpdateSource::Plugin)?,
), ),
_ => { _ => {
return Err(PluginErr("Delete not supported for this model type".into()).into()) return Err(PluginErr("Delete not supported for this model type".into()).into());
} }
}; };
+3
View File
@@ -5,6 +5,9 @@ version = "0.1.0"
edition = "2024" edition = "2024"
publish = false publish = false
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("cargo-clippy"))'] }
[build-dependencies] [build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] } tauri-plugin = { workspace = true, features = ["build"] }
@@ -8,6 +8,10 @@ impl<'a> DbContext<'a> {
self.find_one(PluginIden::Id, id) self.find_one(PluginIden::Id, id)
} }
pub fn get_plugin_by_directory(&self, directory: &str) -> Option<Plugin> {
self.find_optional(PluginIden::Directory, directory)
}
pub fn list_plugins(&self) -> Result<Vec<Plugin>> { pub fn list_plugins(&self) -> Result<Vec<Plugin>> {
self.find_all() self.find_all()
} }
+1 -1
View File
@@ -423,7 +423,7 @@ export type ListCookieNamesRequest = {};
export type ListCookieNamesResponse = { names: Array<string>, }; export type ListCookieNamesResponse = { names: Array<string>, };
export type ListFoldersRequest = Record<string, never>; export type ListFoldersRequest = {};
export type ListFoldersResponse = { folders: Array<Folder>, }; export type ListFoldersResponse = { folders: Array<Folder>, };
+2 -2
View File
@@ -1351,8 +1351,8 @@ pub struct ListHttpRequestsResponse {
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default)]
#[ts(export, export_to = "gen_events.ts")] #[ts(export, type = "{}", export_to = "gen_events.ts")]
pub struct ListFoldersRequest {} pub struct ListFoldersRequest {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
+1 -1
View File
@@ -55,7 +55,7 @@ pub async fn download_and_install<R: Runtime>(
zip_extract::extract(Cursor::new(&bytes), &plugin_dir, true)?; zip_extract::extract(Cursor::new(&bytes), &plugin_dir, true)?;
info!("Extracted plugin {} to {}", plugin_version.id, plugin_dir_str); info!("Extracted plugin {} to {}", plugin_version.id, plugin_dir_str);
plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &plugin_dir_str).await?; plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &plugin_dir_str, true).await?;
window.db().upsert_plugin( window.db().upsert_plugin(
&Plugin { &Plugin {
+113 -79
View File
@@ -1,5 +1,6 @@
use crate::error::Error::{ use crate::error::Error::{
AuthPluginNotFound, ClientNotInitializedErr, PluginErr, PluginNotFoundErr, UnknownEventErr, self, AuthPluginNotFound, ClientNotInitializedErr, PluginErr, PluginNotFoundErr,
UnknownEventErr,
}; };
use crate::error::Result; use crate::error::Result;
use crate::events::{ use crate::events::{
@@ -35,10 +36,10 @@ use tokio::net::TcpListener;
use tokio::sync::mpsc::error::TrySendError; use tokio::sync::mpsc::error::TrySendError;
use tokio::sync::{Mutex, mpsc}; use tokio::sync::{Mutex, mpsc};
use tokio::time::{Instant, timeout}; use tokio::time::{Instant, timeout};
use yaak_models::models::Environment; use yaak_models::models::{Environment, Plugin};
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::render::make_vars_hashmap; use yaak_models::render::make_vars_hashmap;
use yaak_models::util::generate_id; use yaak_models::util::{UpdateSource, generate_id};
use yaak_templates::error::Error::RenderError; use yaak_templates::error::Error::RenderError;
use yaak_templates::error::Result as TemplateResult; use yaak_templates::error::Result as TemplateResult;
use yaak_templates::{RenderErrorBehavior, RenderOptions, render_json_value_raw}; use yaak_templates::{RenderErrorBehavior, RenderOptions, render_json_value_raw};
@@ -46,18 +47,13 @@ use yaak_templates::{RenderErrorBehavior, RenderOptions, render_json_value_raw};
#[derive(Clone)] #[derive(Clone)]
pub struct PluginManager { pub struct PluginManager {
subscribers: Arc<Mutex<HashMap<String, mpsc::Sender<InternalEvent>>>>, subscribers: Arc<Mutex<HashMap<String, mpsc::Sender<InternalEvent>>>>,
plugins: Arc<Mutex<Vec<PluginHandle>>>, plugin_handles: Arc<Mutex<Vec<PluginHandle>>>,
kill_tx: tokio::sync::watch::Sender<bool>, kill_tx: tokio::sync::watch::Sender<bool>,
ws_service: Arc<PluginRuntimeServerWebsocket>, ws_service: Arc<PluginRuntimeServerWebsocket>,
vendored_plugin_dir: PathBuf, vendored_plugin_dir: PathBuf,
pub(crate) installed_plugin_dir: PathBuf, pub(crate) installed_plugin_dir: PathBuf,
} }
#[derive(Clone)]
struct PluginCandidate {
dir: String,
}
impl PluginManager { impl PluginManager {
pub fn new<R: Runtime>(app_handle: AppHandle<R>) -> PluginManager { pub fn new<R: Runtime>(app_handle: AppHandle<R>) -> PluginManager {
let (events_tx, mut events_rx) = mpsc::channel(128); let (events_tx, mut events_rx) = mpsc::channel(128);
@@ -80,7 +76,7 @@ impl PluginManager {
.join("installed-plugins"); .join("installed-plugins");
let plugin_manager = PluginManager { let plugin_manager = PluginManager {
plugins: Default::default(), plugin_handles: Default::default(),
subscribers: Default::default(), subscribers: Default::default(),
ws_service: Arc::new(ws_service.clone()), ws_service: Arc::new(ws_service.clone()),
kill_tx: kill_server_tx, kill_tx: kill_server_tx,
@@ -109,7 +105,7 @@ impl PluginManager {
// Handle when client plugin runtime disconnects // Handle when client plugin runtime disconnects
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
while let Some(_) = client_disconnect_rx.recv().await { while (client_disconnect_rx.recv().await).is_some() {
// Happens when the app is closed // Happens when the app is closed
info!("Plugin runtime client disconnected"); info!("Plugin runtime client disconnected");
} }
@@ -163,10 +159,10 @@ impl PluginManager {
plugin_manager plugin_manager
} }
async fn list_plugin_dirs<R: Runtime>( async fn list_available_plugins<R: Runtime>(
&self, &self,
app_handle: &AppHandle<R>, app_handle: &AppHandle<R>,
) -> Vec<PluginCandidate> { ) -> Result<Vec<Plugin>> {
let plugins_dir = if is_dev() { let plugins_dir = if is_dev() {
// Use plugins directly for easy development // Use plugins directly for easy development
env::current_dir() env::current_dir()
@@ -178,18 +174,27 @@ impl PluginManager {
info!("Loading bundled plugins from {plugins_dir:?}"); info!("Loading bundled plugins from {plugins_dir:?}");
let bundled_plugin_dirs: Vec<PluginCandidate> = read_plugins_dir(&plugins_dir) // Read bundled plugin directories from disk
let bundled_plugin_dirs: Vec<String> = read_plugins_dir(&plugins_dir)
.await .await
.expect(format!("Failed to read plugins dir: {:?}", plugins_dir).as_str()) .expect(&format!("Failed to read plugins dir: {:?}", plugins_dir));
.iter()
.map(|d| PluginCandidate { dir: d.into() })
.collect();
let plugins = app_handle.db().list_plugins().unwrap_or_default(); // Ensure all bundled plugins make it into the database
let installed_plugin_dirs: Vec<PluginCandidate> = for dir in &bundled_plugin_dirs {
plugins.iter().map(|p| PluginCandidate { dir: p.directory.to_owned() }).collect(); if app_handle.db().get_plugin_by_directory(dir).is_none() {
app_handle.db().upsert_plugin(
&Plugin {
directory: dir.clone(),
enabled: true,
url: None,
..Default::default()
},
&UpdateSource::Background,
)?;
}
}
[bundled_plugin_dirs, installed_plugin_dirs].concat() Ok(app_handle.db().list_plugins()?)
} }
pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> { pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> {
@@ -202,16 +207,18 @@ impl PluginManager {
plugin_context: &PluginContext, plugin_context: &PluginContext,
plugin: &PluginHandle, plugin: &PluginHandle,
) -> Result<()> { ) -> Result<()> {
// Terminate the plugin // Terminate the plugin if it's enabled
self.send_to_plugin_and_wait( if plugin.enabled {
plugin_context, self.send_to_plugin_and_wait(
plugin, plugin_context,
&InternalEventPayload::TerminateRequest, plugin,
) &InternalEventPayload::TerminateRequest,
.await?; )
.await?;
}
// Remove the plugin from the list // Remove the plugin from the list
let mut plugins = self.plugins.lock().await; let mut plugins = self.plugin_handles.lock().await;
let pos = plugins.iter().position(|p| p.ref_id == plugin.ref_id); let pos = plugins.iter().position(|p| p.ref_id == plugin.ref_id);
if let Some(pos) = pos { if let Some(pos) = pos {
plugins.remove(pos); plugins.remove(pos);
@@ -220,7 +227,12 @@ impl PluginManager {
Ok(()) Ok(())
} }
pub async fn add_plugin_by_dir(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> { pub async fn add_plugin_by_dir(
&self,
plugin_context: &PluginContext,
dir: &str,
enabled: bool,
) -> Result<()> {
info!("Adding plugin by dir {dir}"); info!("Adding plugin by dir {dir}");
let maybe_tx = self.ws_service.app_to_plugin_events_tx.lock().await; let maybe_tx = self.ws_service.app_to_plugin_events_tx.lock().await;
@@ -228,32 +240,32 @@ impl PluginManager {
None => return Err(ClientNotInitializedErr), None => return Err(ClientNotInitializedErr),
Some(tx) => tx, Some(tx) => tx,
}; };
let plugin_handle = PluginHandle::new(dir, tx.clone())?; let plugin_handle = PluginHandle::new(dir, enabled, tx.clone())?;
let dir_path = Path::new(dir); let dir_path = Path::new(dir);
let is_vendored = dir_path.starts_with(self.vendored_plugin_dir.as_path()); let is_vendored = dir_path.starts_with(self.vendored_plugin_dir.as_path());
let is_installed = dir_path.starts_with(self.installed_plugin_dir.as_path()); let is_installed = dir_path.starts_with(self.installed_plugin_dir.as_path());
// Boot the plugin // Boot the plugin if it's enabled
let event = timeout( if enabled {
Duration::from_secs(5), let event = self
self.send_to_plugin_and_wait( .send_to_plugin_and_wait(
plugin_context, plugin_context,
&plugin_handle, &plugin_handle,
&InternalEventPayload::BootRequest(BootRequest { &InternalEventPayload::BootRequest(BootRequest {
dir: dir.to_string(), dir: dir.to_string(),
watch: !is_vendored && !is_installed, watch: !is_vendored && !is_installed,
}), }),
), )
) .await?;
.await??;
if !matches!(event.payload, InternalEventPayload::BootResponse) { if !matches!(event.payload, InternalEventPayload::BootResponse) {
return Err(UnknownEventErr); return Err(UnknownEventErr);
}
} }
let mut plugins = self.plugins.lock().await; let mut plugin_handles = self.plugin_handles.lock().await;
plugins.retain(|p| p.dir != dir); plugin_handles.retain(|p| p.dir != dir);
plugins.push(plugin_handle.clone()); plugin_handles.push(plugin_handle.clone());
Ok(()) Ok(())
} }
@@ -263,22 +275,24 @@ impl PluginManager {
app_handle: &AppHandle<R>, app_handle: &AppHandle<R>,
plugin_context: &PluginContext, plugin_context: &PluginContext,
) -> Result<()> { ) -> Result<()> {
info!("Initializing all plugins");
let start = Instant::now(); let start = Instant::now();
let candidates = self.list_plugin_dirs(app_handle).await; for plugin in self.list_available_plugins(app_handle).await?.clone() {
for candidate in candidates.clone() { // First remove the plugin if it exists and is enabled
// First remove the plugin if it exists if let Some(plugin_handle) = self.get_plugin_by_dir(&plugin.directory).await {
if let Some(plugin) = self.get_plugin_by_dir(candidate.dir.as_str()).await { if let Err(e) = self.remove_plugin(plugin_context, &plugin_handle).await {
if let Err(e) = self.remove_plugin(plugin_context, &plugin).await { error!("Failed to remove plugin {} {e:?}", plugin.directory);
error!("Failed to remove plugin {} {e:?}", candidate.dir);
continue; continue;
} }
} }
if let Err(e) = self.add_plugin_by_dir(plugin_context, candidate.dir.as_str()).await { if let Err(e) =
warn!("Failed to add plugin {} {e:?}", candidate.dir); self.add_plugin_by_dir(plugin_context, &plugin.directory, plugin.enabled).await
{
warn!("Failed to add plugin {} {e:?}", plugin.directory);
} }
} }
let plugins = self.plugins.lock().await; let plugins = self.plugin_handles.lock().await;
let names = plugins.iter().map(|p| p.dir.to_string()).collect::<Vec<String>>(); let names = plugins.iter().map(|p| p.dir.to_string()).collect::<Vec<String>>();
info!( info!(
"Initialized {} plugins in {:?}:\n - {}", "Initialized {} plugins in {:?}:\n - {}",
@@ -324,15 +338,15 @@ impl PluginManager {
} }
pub async fn get_plugin_by_ref_id(&self, ref_id: &str) -> Option<PluginHandle> { pub async fn get_plugin_by_ref_id(&self, ref_id: &str) -> Option<PluginHandle> {
self.plugins.lock().await.iter().find(|p| p.ref_id == ref_id).cloned() self.plugin_handles.lock().await.iter().find(|p| p.ref_id == ref_id).cloned()
} }
pub async fn get_plugin_by_dir(&self, dir: &str) -> Option<PluginHandle> { pub async fn get_plugin_by_dir(&self, dir: &str) -> Option<PluginHandle> {
self.plugins.lock().await.iter().find(|p| p.dir == dir).cloned() self.plugin_handles.lock().await.iter().find(|p| p.dir == dir).cloned()
} }
pub async fn get_plugin_by_name(&self, name: &str) -> Option<PluginHandle> { pub async fn get_plugin_by_name(&self, name: &str) -> Option<PluginHandle> {
for plugin in self.plugins.lock().await.iter().cloned() { for plugin in self.plugin_handles.lock().await.iter().cloned() {
let info = plugin.info(); let info = plugin.info();
if info.name == name { if info.name == name {
return Some(plugin); return Some(plugin);
@@ -347,9 +361,19 @@ impl PluginManager {
plugin: &PluginHandle, plugin: &PluginHandle,
payload: &InternalEventPayload, payload: &InternalEventPayload,
) -> Result<InternalEvent> { ) -> Result<InternalEvent> {
if !plugin.enabled {
return Err(Error::PluginErr(format!("Plugin {} is disabled", plugin.metadata.name)));
}
let events = let events =
self.send_to_plugins_and_wait(plugin_context, payload, vec![plugin.to_owned()]).await?; self.send_to_plugins_and_wait(plugin_context, payload, vec![plugin.to_owned()]).await?;
Ok(events.first().unwrap().to_owned()) Ok(events
.first()
.ok_or(Error::PluginErr(format!(
"No plugin events returned for: {}",
plugin.metadata.name
)))?
.to_owned())
} }
async fn send_and_wait( async fn send_and_wait(
@@ -357,7 +381,7 @@ impl PluginManager {
plugin_context: &PluginContext, plugin_context: &PluginContext,
payload: &InternalEventPayload, payload: &InternalEventPayload,
) -> Result<Vec<InternalEvent>> { ) -> Result<Vec<InternalEvent>> {
let plugins = { self.plugins.lock().await.clone() }; let plugins = { self.plugin_handles.lock().await.clone() };
self.send_to_plugins_and_wait(plugin_context, payload, plugins).await self.send_to_plugins_and_wait(plugin_context, payload, plugins).await
} }
@@ -373,6 +397,7 @@ impl PluginManager {
// 1. Build the events with IDs and everything // 1. Build the events with IDs and everything
let events_to_send = plugins let events_to_send = plugins
.iter() .iter()
.filter(|p| p.enabled)
.map(|p| p.build_event_to_send(plugin_context, payload, None)) .map(|p| p.build_event_to_send(plugin_context, payload, None))
.collect::<Vec<InternalEvent>>(); .collect::<Vec<InternalEvent>>();
@@ -383,19 +408,28 @@ impl PluginManager {
tokio::spawn(async move { tokio::spawn(async move {
let mut found_events = Vec::new(); let mut found_events = Vec::new();
while let Some(event) = rx.recv().await { let collect_events = async {
let matched_sent_event = events_to_send while let Some(event) = rx.recv().await {
.iter() let matched_sent_event =
.find(|e| Some(e.id.to_owned()) == event.reply_id) events_to_send.iter().any(|e| Some(e.id.to_owned()) == event.reply_id);
.is_some(); if matched_sent_event {
if matched_sent_event { found_events.push(event.clone());
found_events.push(event.clone()); };
};
let found_them_all = found_events.len() == events_to_send.len(); let found_them_all = found_events.len() == events_to_send.len();
if found_them_all { if found_them_all {
break; break;
}
} }
};
// Timeout after 10 seconds to prevent hanging forever if plugin doesn't respond
if timeout(Duration::from_secs(5), collect_events).await.is_err() {
warn!(
"Timeout waiting for plugin responses. Got {}/{} responses",
found_events.len(),
events_to_send.len()
);
} }
found_events found_events
@@ -586,7 +620,7 @@ impl PluginManager {
// We don't want to fail for this op because the UI will not be able to list any auth types then // We don't want to fail for this op because the UI will not be able to list any auth types then
let render_opt = RenderOptions { error_behavior: RenderErrorBehavior::ReturnEmpty }; let render_opt = RenderOptions { error_behavior: RenderErrorBehavior::ReturnEmpty };
let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?; let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?;
let context_id = format!("{:x}", md5::compute(model_id.to_string())); let context_id = format!("{:x}", md5::compute(model_id));
let event = self let event = self
.send_to_plugin_and_wait( .send_to_plugin_and_wait(
@@ -754,7 +788,7 @@ impl PluginManager {
// We don't want to fail for this op because the UI will not be able to list any auth types then // We don't want to fail for this op because the UI will not be able to list any auth types then
let render_opt = RenderOptions { error_behavior: RenderErrorBehavior::ReturnEmpty }; let render_opt = RenderOptions { error_behavior: RenderErrorBehavior::ReturnEmpty };
let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?; let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?;
let context_id = format!("{:x}", md5::compute(model_id.to_string())); let context_id = format!("{:x}", md5::compute(model_id));
let event = self let event = self
.send_to_plugin_and_wait( .send_to_plugin_and_wait(
&PluginContext::new(window), &PluginContext::new(window),
@@ -804,7 +838,7 @@ impl PluginManager {
.find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None }) .find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None })
.ok_or(PluginNotFoundErr(auth_name.into()))?; .ok_or(PluginNotFoundErr(auth_name.into()))?;
let context_id = format!("{:x}", md5::compute(model_id.to_string())); let context_id = format!("{:x}", md5::compute(model_id));
self.send_to_plugin_and_wait( self.send_to_plugin_and_wait(
&PluginContext::new(window), &PluginContext::new(window),
&plugin, &plugin,
@@ -831,7 +865,7 @@ impl PluginManager {
plugin_context: &PluginContext, plugin_context: &PluginContext,
) -> Result<CallHttpAuthenticationResponse> { ) -> Result<CallHttpAuthenticationResponse> {
let disabled = match req.values.get("disabled") { let disabled = match req.values.get("disabled") {
Some(JsonPrimitive::Boolean(v)) => v.clone(), Some(JsonPrimitive::Boolean(v)) => *v,
_ => false, _ => false,
}; };
+3 -1
View File
@@ -10,12 +10,13 @@ use tokio::sync::{Mutex, mpsc};
pub struct PluginHandle { pub struct PluginHandle {
pub ref_id: String, pub ref_id: String,
pub dir: String, pub dir: String,
pub enabled: bool,
pub(crate) to_plugin_tx: Arc<Mutex<mpsc::Sender<InternalEvent>>>, pub(crate) to_plugin_tx: Arc<Mutex<mpsc::Sender<InternalEvent>>>,
pub(crate) metadata: PluginMetadata, pub(crate) metadata: PluginMetadata,
} }
impl PluginHandle { impl PluginHandle {
pub fn new(dir: &str, tx: mpsc::Sender<InternalEvent>) -> Result<Self> { pub fn new(dir: &str, enabled: bool, tx: mpsc::Sender<InternalEvent>) -> Result<Self> {
let ref_id = gen_id(); let ref_id = gen_id();
let metadata = get_plugin_meta(&Path::new(dir))?; let metadata = get_plugin_meta(&Path::new(dir))?;
@@ -23,6 +24,7 @@ impl PluginHandle {
ref_id: ref_id.clone(), ref_id: ref_id.clone(),
dir: dir.to_string(), dir: dir.to_string(),
to_plugin_tx: Arc::new(Mutex::new(tx)), to_plugin_tx: Arc::new(Mutex::new(tx)),
enabled,
metadata, metadata,
}) })
} }
+20 -4
View File
@@ -73,10 +73,17 @@ impl PluginRuntimeServerWebsocket {
// Skip non-text messages // Skip non-text messages
if !msg.is_text() { if !msg.is_text() {
return; warn!("Received non-text message from plugin runtime");
continue;
} }
let msg_text = msg.into_text().unwrap(); let msg_text = match msg.into_text() {
Ok(text) => text,
Err(e) => {
error!("Failed to convert message to text: {e:?}");
continue;
}
};
let event = match serde_json::from_str::<InternalEventRawPayload>(&msg_text) { let event = match serde_json::from_str::<InternalEventRawPayload>(&msg_text) {
Ok(e) => e, Ok(e) => e,
Err(e) => { Err(e) => {
@@ -117,9 +124,18 @@ impl PluginRuntimeServerWebsocket {
return; return;
}, },
Some(event) => { Some(event) => {
let event_bytes = serde_json::to_string(&event).unwrap(); let event_bytes = match serde_json::to_string(&event) {
Ok(bytes) => bytes,
Err(e) => {
error!("Failed to serialize event: {:?}", e);
continue;
}
};
let msg = Message::text(event_bytes); let msg = Message::text(event_bytes);
ws_sender.send(msg).await.unwrap(); if let Err(e) = ws_sender.send(msg).await {
error!("Failed to send message to plugin runtime: {:?}", e);
break;
}
} }
} }
} }
+2 -2
View File
@@ -1,5 +1,5 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export function unescape_template(template: string): any;
export function escape_template(template: string): any;
export function parse_template(template: string): any; export function parse_template(template: string): any;
export function escape_template(template: string): any;
export function unescape_template(template: string): any;
+4 -4
View File
@@ -165,10 +165,10 @@ function takeFromExternrefTable0(idx) {
* @param {string} template * @param {string} template
* @returns {any} * @returns {any}
*/ */
export function unescape_template(template) { export function parse_template(template) {
const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN; const len0 = WASM_VECTOR_LEN;
const ret = wasm.unescape_template(ptr0, len0); const ret = wasm.parse_template(ptr0, len0);
if (ret[2]) { if (ret[2]) {
throw takeFromExternrefTable0(ret[1]); throw takeFromExternrefTable0(ret[1]);
} }
@@ -193,10 +193,10 @@ export function escape_template(template) {
* @param {string} template * @param {string} template
* @returns {any} * @returns {any}
*/ */
export function parse_template(template) { export function unescape_template(template) {
const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN; const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_template(ptr0, len0); const ret = wasm.unescape_template(ptr0, len0);
if (ret[2]) { if (ret[2]) {
throw takeFromExternrefTable0(ret[1]); throw takeFromExternrefTable0(ret[1]);
} }
Binary file not shown.
+13
View File
@@ -5,6 +5,7 @@ import { useAtomValue } from 'jotai';
import type { CSSProperties, ReactNode } from 'react'; import type { CSSProperties, ReactNode } from 'react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { allRequestsAtom } from '../hooks/useAllRequests'; import { allRequestsAtom } from '../hooks/useAllRequests';
import { useFolderActions } from '../hooks/useFolderActions';
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse'; import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
import { sendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest'; import { sendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { showDialog } from '../lib/dialog'; import { showDialog } from '../lib/dialog';
@@ -30,6 +31,12 @@ interface Props {
export function FolderLayout({ folder, style }: Props) { export function FolderLayout({ folder, style }: Props) {
const folders = useAtomValue(foldersAtom); const folders = useAtomValue(foldersAtom);
const requests = useAtomValue(allRequestsAtom); const requests = useAtomValue(allRequestsAtom);
const folderActions = useFolderActions();
const sendAllAction = useMemo(
() => folderActions.find((a) => a.label === 'Send All'),
[folderActions],
);
const children = useMemo(() => { const children = useMemo(() => {
return [ return [
...folders.filter((f) => f.folderId === folder.id), ...folders.filter((f) => f.folderId === folder.id),
@@ -37,6 +44,10 @@ export function FolderLayout({ folder, style }: Props) {
]; ];
}, [folder.id, folders, requests]); }, [folder.id, folders, requests]);
const handleSendAll = useCallback(() => {
sendAllAction?.call(folder);
}, [sendAllAction, folder]);
return ( return (
<div style={style} className="p-6 pt-4 overflow-y-auto @container"> <div style={style} className="p-6 pt-4 overflow-y-auto @container">
<HStack space={2} alignItems="center"> <HStack space={2} alignItems="center">
@@ -48,6 +59,8 @@ export function FolderLayout({ folder, style }: Props) {
color="secondary" color="secondary"
size="sm" size="sm"
variant="border" variant="border"
onClick={handleSendAll}
disabled={sendAllAction == null}
> >
Send All Send All
</Button> </Button>
@@ -1,7 +1,7 @@
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { openUrl } from '@tauri-apps/plugin-opener'; import { openUrl } from '@tauri-apps/plugin-opener';
import type { Plugin } from '@yaakapp-internal/models'; import type { Plugin } from '@yaakapp-internal/models';
import { pluginsAtom } from '@yaakapp-internal/models'; import { patchModel, pluginsAtom } from '@yaakapp-internal/models';
import type { PluginVersion } from '@yaakapp-internal/plugins'; import type { PluginVersion } from '@yaakapp-internal/plugins';
import { import {
checkPluginUpdates, checkPluginUpdates,
@@ -18,6 +18,7 @@ import { usePluginsKey, useRefreshPlugins } from '../../hooks/usePlugins';
import { showConfirmDelete } from '../../lib/confirm'; import { showConfirmDelete } from '../../lib/confirm';
import { minPromiseMillis } from '../../lib/minPromiseMillis'; import { minPromiseMillis } from '../../lib/minPromiseMillis';
import { Button } from '../core/Button'; import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { CountBadge } from '../core/CountBadge'; import { CountBadge } from '../core/CountBadge';
import { Icon } from '../core/Icon'; import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton'; import { IconButton } from '../core/IconButton';
@@ -34,6 +35,8 @@ import { SelectFile } from '../SelectFile';
export function SettingsPlugins() { export function SettingsPlugins() {
const [directory, setDirectory] = useState<string | null>(null); const [directory, setDirectory] = useState<string | null>(null);
const plugins = useAtomValue(pluginsAtom); const plugins = useAtomValue(pluginsAtom);
const bundledPlugins = plugins.filter((p) => p.url == null);
const installedPlugins = plugins.filter((p) => p.url != null);
const createPlugin = useInstallPlugin(); const createPlugin = useInstallPlugin();
const refreshPlugins = useRefreshPlugins(); const refreshPlugins = useRefreshPlugins();
const [tab, setTab] = useState<string>(); const [tab, setTab] = useState<string>();
@@ -49,7 +52,12 @@ export function SettingsPlugins() {
{ {
label: 'Installed', label: 'Installed',
value: 'installed', value: 'installed',
rightSlot: <CountBadge count={plugins.length} />, rightSlot: <CountBadge count={installedPlugins.length} />,
},
{
label: 'Bundled',
value: 'bundled',
rightSlot: <CountBadge count={bundledPlugins.length} />,
}, },
]} ]}
> >
@@ -101,6 +109,9 @@ export function SettingsPlugins() {
</footer> </footer>
</div> </div>
</TabContent> </TabContent>
<TabContent value="bundled" className="pb-0">
<BundledPlugins />
</TabContent>
</Tabs> </Tabs>
</div> </div>
); );
@@ -119,6 +130,27 @@ function PluginTableRowForInstalledPlugin({ plugin }: { plugin: Plugin }) {
name={info.name} name={info.name}
displayName={info.displayName} displayName={info.displayName}
url={plugin.url} url={plugin.url}
showCheckbox={true}
showUninstall={true}
/>
);
}
function PluginTableRowForBundledPlugin({ plugin }: { plugin: Plugin }) {
const info = usePluginInfo(plugin.id).data;
if (info == null) {
return null;
}
return (
<PluginTableRow
plugin={plugin}
version={info.version}
name={info.name}
displayName={info.displayName}
url={plugin.url}
showCheckbox={true}
showUninstall={false}
/> />
); );
} }
@@ -134,6 +166,7 @@ function PluginTableRowForRemotePluginVersion({ pluginVersion }: { pluginVersion
name={pluginVersion.name} name={pluginVersion.name}
displayName={pluginVersion.displayName} displayName={pluginVersion.displayName}
url={pluginVersion.url} url={pluginVersion.url}
showCheckbox={false}
/> />
); );
} }
@@ -144,12 +177,16 @@ function PluginTableRow({
version, version,
displayName, displayName,
url, url,
showCheckbox = true,
showUninstall = true,
}: { }: {
plugin: Plugin | null; plugin: Plugin | null;
name: string; name: string;
version: string; version: string;
displayName: string; displayName: string;
url: string | null; url: string | null;
showCheckbox?: boolean;
showUninstall?: boolean;
}) { }) {
const updates = usePluginUpdates(); const updates = usePluginUpdates();
const latestVersion = updates.data?.plugins.find((u) => u.name === name)?.version; const latestVersion = updates.data?.plugins.find((u) => u.name === name)?.version;
@@ -158,9 +195,26 @@ function PluginTableRow({
mutationFn: (name: string) => installPlugin(name, null), mutationFn: (name: string) => installPlugin(name, null),
}); });
const uninstall = usePromptUninstall(plugin?.id ?? null, displayName); const uninstall = usePromptUninstall(plugin?.id ?? null, displayName);
const refreshPlugins = useRefreshPlugins();
return ( return (
<TableRow> <TableRow>
{showCheckbox && (
<TableCell className="!py-0">
<Checkbox
hideLabel
title={plugin?.enabled ? 'Disable plugin' : 'Enable plugin'}
checked={plugin?.enabled ?? false}
disabled={plugin == null}
onChange={async (enabled) => {
if (plugin) {
await patchModel(plugin, { enabled });
refreshPlugins.mutate();
}
}}
/>
</TableCell>
)}
<TableCell className="font-semibold"> <TableCell className="font-semibold">
{url ? ( {url ? (
<Link noUnderline href={url}> <Link noUnderline href={url}>
@@ -170,6 +224,9 @@ function PluginTableRow({
displayName displayName
)} )}
</TableCell> </TableCell>
<TableCell>
<InlineCode>{name}</InlineCode>
</TableCell>
<TableCell> <TableCell>
<HStack space={1.5}> <HStack space={1.5}>
<InlineCode>{version}</InlineCode> <InlineCode>{version}</InlineCode>
@@ -206,7 +263,7 @@ function PluginTableRow({
Install Install
</Button> </Button>
) : null} ) : null}
{uninstall != null && ( {showUninstall && uninstall != null && (
<Button <Button
size="xs" size="xs"
title="Uninstall plugin" title="Uninstall plugin"
@@ -253,6 +310,7 @@ function PluginSearch() {
<Table scrollable> <Table scrollable>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableHeaderCell>Display Name</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell> <TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell> <TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell /> <TableHeaderCell />
@@ -271,7 +329,7 @@ function PluginSearch() {
} }
function InstalledPlugins() { function InstalledPlugins() {
const plugins = useAtomValue(pluginsAtom); const plugins = useAtomValue(pluginsAtom).filter((p) => p.url != null);
return plugins.length === 0 ? ( return plugins.length === 0 ? (
<div className="pb-4"> <div className="pb-4">
@@ -285,6 +343,8 @@ function InstalledPlugins() {
<Table scrollable> <Table scrollable>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableHeaderCell className="w-0" />
<TableHeaderCell>Display Name</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell> <TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell> <TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell /> <TableHeaderCell />
@@ -299,6 +359,33 @@ function InstalledPlugins() {
); );
} }
function BundledPlugins() {
const plugins = useAtomValue(pluginsAtom).filter((p) => p.url == null);
return plugins.length === 0 ? (
<div className="pb-4">
<EmptyStateText className="text-center">No bundled plugins found.</EmptyStateText>
</div>
) : (
<Table scrollable>
<TableHead>
<TableRow>
<TableHeaderCell className="w-0" />
<TableHeaderCell>Display Name</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell />
</TableRow>
</TableHead>
<tbody className="divide-y divide-surface-highlight">
{plugins.map((p) => (
<PluginTableRowForBundledPlugin key={p.id} plugin={p} />
))}
</tbody>
</Table>
);
}
function usePromptUninstall(pluginId: string | null, name: string) { function usePromptUninstall(pluginId: string | null, name: string) {
const mut = useMutation({ const mut = useMutation({
mutationKey: ['uninstall_plugin', pluginId], mutationKey: ['uninstall_plugin', pluginId],
-17
View File
@@ -27,8 +27,6 @@ import { selectAtom } from 'jotai/utils';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { moveToWorkspace } from '../commands/moveToWorkspace'; import { moveToWorkspace } from '../commands/moveToWorkspace';
import { openFolderSettings } from '../commands/openFolderSettings'; import { openFolderSettings } from '../commands/openFolderSettings';
import { activeCookieJarAtom } from '../hooks/useActiveCookieJar';
import { activeEnvironmentAtom } from '../hooks/useActiveEnvironment';
import { activeFolderIdAtom } from '../hooks/useActiveFolderId'; import { activeFolderIdAtom } from '../hooks/useActiveFolderId';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId'; import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
@@ -49,7 +47,6 @@ import { jotaiStore } from '../lib/jotai';
import { resolvedModelName } from '../lib/resolvedModelName'; import { resolvedModelName } from '../lib/resolvedModelName';
import { isSidebarFocused } from '../lib/scopes'; import { isSidebarFocused } from '../lib/scopes';
import { navigateToRequestOrFolderOrWorkspace } from '../lib/setWorkspaceSearchParams'; import { navigateToRequestOrFolderOrWorkspace } from '../lib/setWorkspaceSearchParams';
import { invokeCmd } from '../lib/tauri';
import type { ContextMenuProps, DropdownItem } from './core/Dropdown'; import type { ContextMenuProps, DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown'; import { Dropdown } from './core/Dropdown';
import type { FieldDef } from './core/Editor/filter/extension'; import type { FieldDef } from './core/Editor/filter/extension';
@@ -331,20 +328,6 @@ function Sidebar({ className }: { className?: string }) {
leftSlot: <Icon icon="folder_cog" />, leftSlot: <Icon icon="folder_cog" />,
onSelect: () => openFolderSettings(child.id), onSelect: () => openFolderSettings(child.id),
}, },
{
label: 'Send All',
hidden: !(items.length === 1 && child.model === 'folder'),
leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => {
const environment = jotaiStore.get(activeEnvironmentAtom);
const cookieJar = jotaiStore.get(activeCookieJarAtom);
invokeCmd('cmd_send_folder', {
folderId: child.id,
environmentId: environment?.id,
cookieJarId: cookieJar?.id,
});
},
},
{ {
label: 'Send', label: 'Send',
hotKeyAction: 'request.send', hotKeyAction: 'request.send',
+5 -1
View File
@@ -5,12 +5,16 @@ import { jotaiStore } from '../lib/jotai';
import { minPromiseMillis } from '../lib/minPromiseMillis'; import { minPromiseMillis } from '../lib/minPromiseMillis';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { activeWorkspaceIdAtom } from './useActiveWorkspace'; import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useDebouncedValue } from './useDebouncedValue';
import { invalidateAllPluginInfo } from './usePluginInfo'; import { invalidateAllPluginInfo } from './usePluginInfo';
export function usePluginsKey() { export function usePluginsKey() {
return useAtomValue(pluginsAtom) const pluginKey = useAtomValue(pluginsAtom)
.map((p) => p.id + p.updatedAt) .map((p) => p.id + p.updatedAt)
.join(','); .join(',');
// Debounce plugins both for efficiency and to give plugins a chance to reload after the DB updates
return useDebouncedValue(pluginKey, 1000);
} }
/** /**
-1
View File
@@ -1 +0,0 @@
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
File diff suppressed because one or more lines are too long