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