mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-18 08:37:48 +01:00
Compare commits
17 Commits
v2025.5.0-
...
v2025.5.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5b7b1638d | ||
|
|
9d6ac8a107 | ||
|
|
6440df492e | ||
|
|
2cdd97cabb | ||
|
|
20681e5be3 | ||
|
|
a258a80fbd | ||
|
|
1b90842d30 | ||
|
|
f1acb3c925 | ||
|
|
28630bbb6c | ||
|
|
86a09642e7 | ||
|
|
0b38948826 | ||
|
|
c09083ddec | ||
|
|
44ee020383 | ||
|
|
c609d0ff0c | ||
|
|
7eb3f123c6 | ||
|
|
2bd8a50df4 | ||
|
|
178cc88efb |
19
package-lock.json
generated
19
package-lock.json
generated
@@ -26,6 +26,7 @@
|
||||
"plugins/importer-postman",
|
||||
"plugins/importer-yaak",
|
||||
"plugins/template-function-cookie",
|
||||
"plugins/template-function-timestamp",
|
||||
"plugins/template-function-encode",
|
||||
"plugins/template-function-fs",
|
||||
"plugins/template-function-hash",
|
||||
@@ -4174,6 +4175,10 @@
|
||||
"resolved": "plugins/template-function-response",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@yaak/template-function-timestamp": {
|
||||
"resolved": "plugins/template-function-timestamp",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@yaak/template-function-uuid": {
|
||||
"resolved": "plugins/template-function-uuid",
|
||||
"link": true
|
||||
@@ -18585,6 +18590,13 @@
|
||||
"name": "@yaak/template-function-cookie",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"plugins/template-function-datetime": {
|
||||
"version": "0.1.0",
|
||||
"extraneous": true,
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"plugins/template-function-encode": {
|
||||
"name": "@yaak/template-function-encode",
|
||||
"version": "0.1.0"
|
||||
@@ -18631,6 +18643,13 @@
|
||||
"@types/jsonpath": "^0.2.4"
|
||||
}
|
||||
},
|
||||
"plugins/template-function-timestamp": {
|
||||
"name": "@yaak/template-function-timestamp",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"plugins/template-function-uuid": {
|
||||
"name": "@yaak/template-function-uuid",
|
||||
"version": "0.1.0",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"plugins/importer-postman",
|
||||
"plugins/importer-yaak",
|
||||
"plugins/template-function-cookie",
|
||||
"plugins/template-function-timestamp",
|
||||
"plugins/template-function-encode",
|
||||
"plugins/template-function-fs",
|
||||
"plugins/template-function-hash",
|
||||
@@ -56,6 +57,9 @@
|
||||
"migration": "node scripts/create-migration.cjs",
|
||||
"build": "npm run --workspaces --if-present build",
|
||||
"build-plugins": "npm run --workspaces --if-present build",
|
||||
"icons": "run-p icons:*",
|
||||
"icons:dev": "tauri icon src-tauri/icons/icon.png --output src-tauri/icons/release",
|
||||
"icons:release": "tauri icon src-tauri/icons/icon-dev.png --output src-tauri/icons/dev",
|
||||
"bootstrap": "run-p bootstrap:* && npm run --workspaces --if-present bootstrap",
|
||||
"bootstrap:vendor-node": "node scripts/vendor-node.cjs",
|
||||
"bootstrap:vendor-plugins": "node scripts/vendor-plugins.cjs",
|
||||
|
||||
@@ -36,7 +36,7 @@ export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key
|
||||
|
||||
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
|
||||
|
||||
export type CallTemplateFunctionResponse = { value: string | null, };
|
||||
export type CallTemplateFunctionResponse = { value: string | null, error?: string, };
|
||||
|
||||
export type CloseWindowRequest = { label: string, };
|
||||
|
||||
|
||||
@@ -308,15 +308,27 @@ export class PluginInstance {
|
||||
const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name);
|
||||
if (typeof fn?.onRender === 'function') {
|
||||
applyFormInputDefaults(fn.args, payload.args.values);
|
||||
const result = await fn.onRender(ctx, payload.args);
|
||||
this.#sendPayload(
|
||||
windowContext,
|
||||
{
|
||||
type: 'call_template_function_response',
|
||||
value: result ?? null,
|
||||
},
|
||||
replyId,
|
||||
);
|
||||
try {
|
||||
const result = await fn.onRender(ctx, payload.args);
|
||||
this.#sendPayload(
|
||||
windowContext,
|
||||
{
|
||||
type: 'call_template_function_response',
|
||||
value: result ?? null,
|
||||
},
|
||||
replyId,
|
||||
);
|
||||
} catch (err) {
|
||||
this.#sendPayload(
|
||||
windowContext,
|
||||
{
|
||||
type: 'call_template_function_response',
|
||||
value: null,
|
||||
error: `${err}`.replace(/^Error:\s*/g, ''),
|
||||
},
|
||||
replyId,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import type { AccessToken } from './store';
|
||||
import { getToken } from './store';
|
||||
|
||||
export async function getAccessTokenIfNotExpired(
|
||||
ctx: Context,
|
||||
contextId: string,
|
||||
): Promise<AccessToken | null> {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token == null || isTokenExpired(token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
export function isTokenExpired(token: AccessToken) {
|
||||
return token.expiresAt && Date.now() > token.expiresAt;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Context, HttpRequest } from '@yaakapp/api';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { isTokenExpired } from './getAccessTokenIfNotExpired';
|
||||
import type { AccessToken, AccessTokenRawResponse } from './store';
|
||||
import type { AccessToken, AccessTokenRawResponse, TokenStoreArgs } from './store';
|
||||
import { deleteToken, getToken, storeToken } from './store';
|
||||
import { isTokenExpired } from './util';
|
||||
|
||||
export async function getOrRefreshAccessToken(
|
||||
ctx: Context,
|
||||
contextId: string,
|
||||
tokenArgs: TokenStoreArgs,
|
||||
{
|
||||
scope,
|
||||
accessTokenUrl,
|
||||
@@ -23,7 +23,7 @@ export async function getOrRefreshAccessToken(
|
||||
forceRefresh?: boolean;
|
||||
},
|
||||
): Promise<AccessToken | null> {
|
||||
const token = await getToken(ctx, contextId);
|
||||
const token = await getToken(ctx, tokenArgs);
|
||||
if (token == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export async function getOrRefreshAccessToken(
|
||||
// Bad refresh token, so we'll force it to fetch a fresh access token by deleting
|
||||
// and returning null;
|
||||
console.log('[oauth2] Unauthorized refresh_token request');
|
||||
await deleteToken(ctx, contextId);
|
||||
await deleteToken(ctx, tokenArgs);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -108,5 +108,5 @@ export async function getOrRefreshAccessToken(
|
||||
refresh_token: response.refresh_token ?? token.response.refresh_token,
|
||||
};
|
||||
|
||||
return storeToken(ctx, contextId, newResponse);
|
||||
return storeToken(ctx, tokenArgs, newResponse);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Context } from '@yaakapp/api';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { fetchAccessToken } from '../fetchAccessToken';
|
||||
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
|
||||
import type { AccessToken } from '../store';
|
||||
import type { AccessToken, TokenStoreArgs } from '../store';
|
||||
import { getDataDirKey, storeToken } from '../store';
|
||||
|
||||
export const PKCE_SHA256 = 'S256';
|
||||
@@ -41,7 +41,14 @@ export async function getAuthorizationCode(
|
||||
tokenName: 'access_token' | 'id_token';
|
||||
},
|
||||
): Promise<AccessToken> {
|
||||
const token = await getOrRefreshAccessToken(ctx, contextId, {
|
||||
const tokenArgs: TokenStoreArgs = {
|
||||
contextId,
|
||||
clientId,
|
||||
accessTokenUrl,
|
||||
authorizationUrl: authorizationUrlRaw,
|
||||
};
|
||||
|
||||
const token = await getOrRefreshAccessToken(ctx, tokenArgs, {
|
||||
accessTokenUrl,
|
||||
scope,
|
||||
clientId,
|
||||
@@ -128,7 +135,7 @@ export async function getAuthorizationCode(
|
||||
],
|
||||
});
|
||||
|
||||
return storeToken(ctx, contextId, response, tokenName);
|
||||
return storeToken(ctx, tokenArgs, response, tokenName);
|
||||
}
|
||||
|
||||
export function genPkceCodeVerifier() {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { fetchAccessToken } from '../fetchAccessToken';
|
||||
import { isTokenExpired } from '../getAccessTokenIfNotExpired';
|
||||
import type { TokenStoreArgs } from '../store';
|
||||
import { getToken, storeToken } from '../store';
|
||||
import { isTokenExpired } from '../util';
|
||||
|
||||
export async function getClientCredentials(
|
||||
ctx: Context,
|
||||
@@ -22,7 +23,13 @@ export async function getClientCredentials(
|
||||
credentialsInBody: boolean;
|
||||
},
|
||||
) {
|
||||
const token = await getToken(ctx, contextId);
|
||||
const tokenArgs: TokenStoreArgs = {
|
||||
contextId,
|
||||
clientId,
|
||||
accessTokenUrl,
|
||||
authorizationUrl: null,
|
||||
};
|
||||
const token = await getToken(ctx, tokenArgs);
|
||||
if (token && !isTokenExpired(token)) {
|
||||
return token;
|
||||
}
|
||||
@@ -38,5 +45,5 @@ export async function getClientCredentials(
|
||||
params: [],
|
||||
});
|
||||
|
||||
return storeToken(ctx, contextId, response);
|
||||
return storeToken(ctx, tokenArgs, response);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { isTokenExpired } from '../getAccessTokenIfNotExpired';
|
||||
import type { AccessToken, AccessTokenRawResponse} from '../store';
|
||||
import type { AccessToken, AccessTokenRawResponse } from '../store';
|
||||
import { getToken, storeToken } from '../store';
|
||||
import { isTokenExpired } from '../util';
|
||||
|
||||
export async function getImplicit(
|
||||
ctx: Context,
|
||||
@@ -26,7 +26,13 @@ export async function getImplicit(
|
||||
tokenName: 'access_token' | 'id_token';
|
||||
},
|
||||
): Promise<AccessToken> {
|
||||
const token = await getToken(ctx, contextId);
|
||||
const tokenArgs = {
|
||||
contextId,
|
||||
clientId,
|
||||
accessTokenUrl: null,
|
||||
authorizationUrl: authorizationUrlRaw,
|
||||
};
|
||||
const token = await getToken(ctx, tokenArgs);
|
||||
if (token != null && !isTokenExpired(token)) {
|
||||
return token;
|
||||
}
|
||||
@@ -82,7 +88,7 @@ export async function getImplicit(
|
||||
|
||||
const response = Object.fromEntries(params) as unknown as AccessTokenRawResponse;
|
||||
try {
|
||||
resolve(storeToken(ctx, contextId, response));
|
||||
resolve(storeToken(ctx, tokenArgs, response));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { fetchAccessToken } from '../fetchAccessToken';
|
||||
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
|
||||
import type { AccessToken} from '../store';
|
||||
import type { AccessToken, TokenStoreArgs } from '../store';
|
||||
import { storeToken } from '../store';
|
||||
|
||||
export async function getPassword(
|
||||
@@ -27,7 +27,13 @@ export async function getPassword(
|
||||
credentialsInBody: boolean;
|
||||
},
|
||||
): Promise<AccessToken> {
|
||||
const token = await getOrRefreshAccessToken(ctx, contextId, {
|
||||
const tokenArgs: TokenStoreArgs = {
|
||||
contextId,
|
||||
clientId,
|
||||
accessTokenUrl,
|
||||
authorizationUrl: null,
|
||||
};
|
||||
const token = await getOrRefreshAccessToken(ctx, tokenArgs, {
|
||||
accessTokenUrl,
|
||||
scope,
|
||||
clientId,
|
||||
@@ -52,5 +58,5 @@ export async function getPassword(
|
||||
],
|
||||
});
|
||||
|
||||
return storeToken(ctx, contextId, response);
|
||||
return storeToken(ctx, tokenArgs, response);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { getClientCredentials } from './grants/clientCredentials';
|
||||
import { getImplicit } from './grants/implicit';
|
||||
import { getPassword } from './grants/password';
|
||||
import type { AccessToken } from './store';
|
||||
import type { AccessToken, TokenStoreArgs } from './store';
|
||||
import { deleteToken, getToken, resetDataDirKey } from './store';
|
||||
|
||||
type GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials';
|
||||
@@ -83,8 +83,14 @@ export const plugin: PluginDefinition = {
|
||||
actions: [
|
||||
{
|
||||
label: 'Copy Current Token',
|
||||
async onSelect(ctx, { contextId }) {
|
||||
const token = await getToken(ctx, contextId);
|
||||
async onSelect(ctx, { contextId, values }) {
|
||||
const tokenArgs: TokenStoreArgs = {
|
||||
contextId,
|
||||
authorizationUrl: stringArg(values, 'authorizationUrl'),
|
||||
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
};
|
||||
const token = await getToken(ctx, tokenArgs);
|
||||
if (token == null) {
|
||||
await ctx.toast.show({ message: 'No token to copy', color: 'warning' });
|
||||
} else {
|
||||
@@ -99,8 +105,14 @@ export const plugin: PluginDefinition = {
|
||||
},
|
||||
{
|
||||
label: 'Delete Token',
|
||||
async onSelect(ctx, { contextId }) {
|
||||
if (await deleteToken(ctx, contextId)) {
|
||||
async onSelect(ctx, { contextId, values }) {
|
||||
const tokenArgs: TokenStoreArgs = {
|
||||
contextId,
|
||||
authorizationUrl: stringArg(values, 'authorizationUrl'),
|
||||
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
};
|
||||
if (await deleteToken(ctx, tokenArgs)) {
|
||||
await ctx.toast.show({ message: 'Token deleted', color: 'success' });
|
||||
} else {
|
||||
await ctx.toast.show({ message: 'No token to delete', color: 'warning' });
|
||||
@@ -281,8 +293,14 @@ export const plugin: PluginDefinition = {
|
||||
{
|
||||
type: 'accordion',
|
||||
label: 'Access Token Response',
|
||||
async dynamic(ctx, { contextId }) {
|
||||
const token = await getToken(ctx, contextId);
|
||||
async dynamic(ctx, { contextId, values }) {
|
||||
const tokenArgs: TokenStoreArgs = {
|
||||
contextId,
|
||||
authorizationUrl: stringArg(values, 'authorizationUrl'),
|
||||
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
};
|
||||
const token = await getToken(ctx, tokenArgs);
|
||||
if (token == null) {
|
||||
return { hidden: true };
|
||||
}
|
||||
@@ -316,9 +334,10 @@ export const plugin: PluginDefinition = {
|
||||
accessTokenUrl === '' || accessTokenUrl.match(/^https?:\/\//)
|
||||
? accessTokenUrl
|
||||
: `https://${accessTokenUrl}`,
|
||||
authorizationUrl: authorizationUrl === '' || authorizationUrl.match(/^https?:\/\//)
|
||||
? authorizationUrl
|
||||
: `https://${authorizationUrl}`,
|
||||
authorizationUrl:
|
||||
authorizationUrl === '' || authorizationUrl.match(/^https?:\/\//)
|
||||
? authorizationUrl
|
||||
: `https://${authorizationUrl}`,
|
||||
clientId: stringArg(values, 'clientId'),
|
||||
clientSecret: stringArg(values, 'clientSecret'),
|
||||
redirectUri: stringArgOrNull(values, 'redirectUri'),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
export async function storeToken(
|
||||
ctx: Context,
|
||||
contextId: string,
|
||||
args: TokenStoreArgs,
|
||||
response: AccessTokenRawResponse,
|
||||
tokenName: 'access_token' | 'id_token' = 'access_token',
|
||||
) {
|
||||
@@ -15,16 +16,16 @@ export async function storeToken(
|
||||
response,
|
||||
expiresAt,
|
||||
};
|
||||
await ctx.store.set<AccessToken>(tokenStoreKey(contextId), token);
|
||||
await ctx.store.set<AccessToken>(tokenStoreKey(args), token);
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function getToken(ctx: Context, contextId: string) {
|
||||
return ctx.store.get<AccessToken>(tokenStoreKey(contextId));
|
||||
export async function getToken(ctx: Context, args: TokenStoreArgs) {
|
||||
return ctx.store.get<AccessToken>(tokenStoreKey(args));
|
||||
}
|
||||
|
||||
export async function deleteToken(ctx: Context, contextId: string) {
|
||||
return ctx.store.delete(tokenStoreKey(contextId));
|
||||
export async function deleteToken(ctx: Context, args: TokenStoreArgs) {
|
||||
return ctx.store.delete(tokenStoreKey(args));
|
||||
}
|
||||
|
||||
export async function resetDataDirKey(ctx: Context, contextId: string) {
|
||||
@@ -37,8 +38,25 @@ export async function getDataDirKey(ctx: Context, contextId: string) {
|
||||
return `${contextId}::${key}`;
|
||||
}
|
||||
|
||||
function tokenStoreKey(contextId: string) {
|
||||
return ['token', contextId].join('::');
|
||||
export interface TokenStoreArgs {
|
||||
contextId: string;
|
||||
clientId: string;
|
||||
accessTokenUrl: string | null;
|
||||
authorizationUrl: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a store key to use based on some arguments. The arguments will be normalized a bit to
|
||||
* account for slight variations (like domains with and without a protocol scheme).
|
||||
*/
|
||||
function tokenStoreKey(args: TokenStoreArgs) {
|
||||
const hash = createHash('md5');
|
||||
if (args.contextId) hash.update(args.contextId.trim());
|
||||
if (args.clientId) hash.update(args.clientId.trim());
|
||||
if (args.accessTokenUrl) hash.update(args.accessTokenUrl.trim().replace(/^https?:\/\//, ''));
|
||||
if (args.authorizationUrl) hash.update(args.authorizationUrl.trim().replace(/^https?:\/\//, ''));
|
||||
const key = hash.digest('hex');
|
||||
return ['token', key].join('::');
|
||||
}
|
||||
|
||||
function dataDirStoreKey(contextId: string) {
|
||||
|
||||
5
plugins/auth-oauth2/src/util.ts
Normal file
5
plugins/auth-oauth2/src/util.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { AccessToken } from './store';
|
||||
|
||||
export function isTokenExpired(token: AccessToken) {
|
||||
return token.expiresAt && Date.now() > token.expiresAt;
|
||||
}
|
||||
@@ -1,32 +1,74 @@
|
||||
import type { TemplateFunctionArg } from '@yaakapp-internal/plugins';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
|
||||
const inputArg: TemplateFunctionArg = {
|
||||
type: 'text',
|
||||
name: 'input',
|
||||
label: 'Input Text',
|
||||
multiLine: true,
|
||||
};
|
||||
|
||||
const regexArg: TemplateFunctionArg = {
|
||||
type: 'text',
|
||||
name: 'regex',
|
||||
label: 'Regular Expression',
|
||||
placeholder: '\\w+',
|
||||
defaultValue: '.*',
|
||||
description:
|
||||
'A JavaScript regular expression. Use a capture group to reference parts of the match in the replacement.',
|
||||
};
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'regex.match',
|
||||
description: 'Extract',
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'regex',
|
||||
label: 'Regular Expression',
|
||||
placeholder: '^\\w+=(?<value>\\w*)$',
|
||||
defaultValue: '^(.*)$',
|
||||
description:
|
||||
'A JavaScript regular expression, evaluated using the Node.js RegExp engine. Capture groups or named groups can be used to extract values.',
|
||||
},
|
||||
{ type: 'text', name: 'input', label: 'Input Text', multiLine: true },
|
||||
],
|
||||
description: 'Extract text using a regular expression',
|
||||
args: [inputArg, regexArg],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
if (!args.values.regex || !args.values.input) return '';
|
||||
const input = String(args.values.input ?? '');
|
||||
const regex = new RegExp(String(args.values.regex ?? ''));
|
||||
|
||||
const input = String(args.values.input);
|
||||
const regex = new RegExp(String(args.values.regex));
|
||||
const match = input.match(regex);
|
||||
return match?.groups
|
||||
? (Object.values(match.groups)[0] ?? '')
|
||||
: (match?.[1] ?? match?.[0] ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'regex.replace',
|
||||
description: 'Replace text using a regular expression',
|
||||
args: [
|
||||
inputArg,
|
||||
regexArg,
|
||||
{
|
||||
type: 'text',
|
||||
name: 'replacement',
|
||||
label: 'Replacement Text',
|
||||
placeholder: 'hello $1',
|
||||
description:
|
||||
'The replacement text. Use $1, $2, ... to reference capture groups or $& to reference the entire match.',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'flags',
|
||||
label: 'Flags',
|
||||
placeholder: 'g',
|
||||
defaultValue: 'g',
|
||||
optional: true,
|
||||
description:
|
||||
'Regular expression flags (g for global, i for case-insensitive, m for multiline, etc.)',
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const input = String(args.values.input ?? '');
|
||||
const replacement = String(args.values.replacement ?? '');
|
||||
const flags = String(args.values.flags || '');
|
||||
const regex = String(args.values.regex);
|
||||
|
||||
if (!regex) return '';
|
||||
|
||||
return input.replace(new RegExp(String(args.values.regex), flags), replacement);
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
194
plugins/template-function-regex/tests/regex.test.ts
Normal file
194
plugins/template-function-regex/tests/regex.test.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { Context } from '@yaakapp/api';
|
||||
import { plugin } from '../src';
|
||||
|
||||
describe('regex.match', () => {
|
||||
const matchFunction = plugin.templateFunctions!.find(f => f.name === 'regex.match');
|
||||
|
||||
it('should exist', () => {
|
||||
expect(matchFunction).toBeDefined();
|
||||
});
|
||||
|
||||
it('should extract first capture group', async () => {
|
||||
const result = await matchFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Hello (\\w+)',
|
||||
input: 'Hello World',
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('World');
|
||||
});
|
||||
|
||||
it('should extract named capture group', async () => {
|
||||
const result = await matchFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Hello (?<name>\\w+)',
|
||||
input: 'Hello World',
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('World');
|
||||
});
|
||||
|
||||
it('should return full match when no capture groups', async () => {
|
||||
const result = await matchFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Hello \\w+',
|
||||
input: 'Hello World'
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should return empty string when no match', async () => {
|
||||
const result = await matchFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Goodbye',
|
||||
input: 'Hello World'
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string when regex is empty', async () => {
|
||||
const result = await matchFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: '',
|
||||
input: 'Hello World'
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string when input is empty', async () => {
|
||||
const result = await matchFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Hello',
|
||||
input: ''
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('regex.replace', () => {
|
||||
const replaceFunction = plugin.templateFunctions!.find(f => f.name === 'regex.replace');
|
||||
|
||||
it('should exist', () => {
|
||||
expect(replaceFunction).toBeDefined();
|
||||
});
|
||||
|
||||
it('should replace one occurrence by default', async () => {
|
||||
const result = await replaceFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'o',
|
||||
input: 'Hello World',
|
||||
replacement: 'a'
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('Hella World');
|
||||
});
|
||||
|
||||
it('should replace with capture groups', async () => {
|
||||
const result = await replaceFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: '(\\w+) (\\w+)',
|
||||
input: 'Hello World',
|
||||
replacement: '$2 $1'
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('World Hello');
|
||||
});
|
||||
|
||||
it('should replace with full match reference', async () => {
|
||||
const result = await replaceFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'World',
|
||||
input: 'Hello World',
|
||||
replacement: '[$&]'
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('Hello [World]');
|
||||
});
|
||||
|
||||
it('should respect flags parameter', async () => {
|
||||
const result = await replaceFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'hello',
|
||||
input: 'Hello World',
|
||||
replacement: 'Hi',
|
||||
flags: 'i'
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('Hi World');
|
||||
});
|
||||
|
||||
it('should handle empty replacement', async () => {
|
||||
const result = await replaceFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'World',
|
||||
input: 'Hello World',
|
||||
replacement: ''
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('Hello ');
|
||||
});
|
||||
|
||||
it('should return original input when no match', async () => {
|
||||
const result = await replaceFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Goodbye',
|
||||
input: 'Hello World',
|
||||
replacement: 'Hi'
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should return empty string when regex is empty', async () => {
|
||||
const result = await replaceFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: '',
|
||||
input: 'Hello World',
|
||||
replacement: 'Hi'
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string when input is empty', async () => {
|
||||
const result = await replaceFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: 'Hello',
|
||||
input: '',
|
||||
replacement: 'Hi'
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should throw on invalid regex', async () => {
|
||||
const fn = replaceFunction!.onRender({} as Context, {
|
||||
values: {
|
||||
regex: '[',
|
||||
input: 'Hello World',
|
||||
replacement: 'Hi'
|
||||
},
|
||||
purpose: 'send',
|
||||
});
|
||||
await expect(fn).rejects.toThrow('Invalid regular expression: /[/: Unterminated character class');
|
||||
});
|
||||
});
|
||||
13
plugins/template-function-timestamp/package.json
Executable file
13
plugins/template-function-timestamp/package.json
Executable file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@yaak/template-function-timestamp",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
"lint": "eslint . --ext .ts,.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0"
|
||||
}
|
||||
}
|
||||
155
plugins/template-function-timestamp/src/index.ts
Executable file
155
plugins/template-function-timestamp/src/index.ts
Executable file
@@ -0,0 +1,155 @@
|
||||
import type { TemplateFunctionArg } from '@yaakapp-internal/plugins';
|
||||
import type { PluginDefinition } from '@yaakapp/api';
|
||||
|
||||
import {
|
||||
addDays,
|
||||
addHours,
|
||||
addMinutes,
|
||||
addMonths,
|
||||
addSeconds,
|
||||
addYears,
|
||||
format as formatDate,
|
||||
isValid,
|
||||
parseISO,
|
||||
subDays,
|
||||
subHours,
|
||||
subMinutes,
|
||||
subMonths,
|
||||
subSeconds,
|
||||
subYears,
|
||||
} from 'date-fns';
|
||||
|
||||
const dateArg: TemplateFunctionArg = {
|
||||
type: 'text',
|
||||
name: 'date',
|
||||
label: 'Timestamp',
|
||||
optional: true,
|
||||
description: 'Can be a timestamp in milliseconds, ISO string, or anything parseable by JS `new Date()`',
|
||||
placeholder: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const expressionArg: TemplateFunctionArg = {
|
||||
type: 'text',
|
||||
name: 'expression',
|
||||
label: 'Expression',
|
||||
description: "Modification expression (eg. '-5d +2h 3m'). Available units: y, M, d, h, m, s",
|
||||
optional: true,
|
||||
placeholder: '-5d +2h 3m',
|
||||
};
|
||||
|
||||
const formatArg: TemplateFunctionArg = {
|
||||
name: 'format',
|
||||
label: 'Format String',
|
||||
description: "Format string to describe the output (eg. 'yyyy-MM-dd at HH:mm:ss')",
|
||||
optional: true,
|
||||
placeholder: 'yyyy-MM-dd HH:mm:ss',
|
||||
type: 'text',
|
||||
};
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: 'timestamp.unix',
|
||||
description: 'Get the current timestamp in seconds',
|
||||
args: [],
|
||||
onRender: async () => String(Math.floor(Date.now() / 1000)),
|
||||
},
|
||||
{
|
||||
name: 'timestamp.unixMillis',
|
||||
description: 'Get the current timestamp in milliseconds',
|
||||
args: [],
|
||||
onRender: async () => String(Date.now()),
|
||||
},
|
||||
{
|
||||
name: 'timestamp.iso8601',
|
||||
description: 'Get the current date in ISO8601 format',
|
||||
args: [],
|
||||
onRender: async () => new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
name: 'timestamp.format',
|
||||
description: 'Format a date using a dayjs-compatible format string',
|
||||
args: [dateArg, formatArg],
|
||||
onRender: async (_ctx, args) => formatDatetime(args.values),
|
||||
},
|
||||
{
|
||||
name: 'timestamp.offset',
|
||||
description: 'Get the offset of a date based on an expression',
|
||||
args: [dateArg, expressionArg],
|
||||
onRender: async (_ctx, args) => calculateDatetime(args.values),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function applyDateOp(d: Date, sign: string, amount: number, unit: string): Date {
|
||||
switch (unit) {
|
||||
case 'y':
|
||||
return sign === '-' ? subYears(d, amount) : addYears(d, amount);
|
||||
case 'M':
|
||||
return sign === '-' ? subMonths(d, amount) : addMonths(d, amount);
|
||||
case 'd':
|
||||
return sign === '-' ? subDays(d, amount) : addDays(d, amount);
|
||||
case 'h':
|
||||
return sign === '-' ? subHours(d, amount) : addHours(d, amount);
|
||||
case 'm':
|
||||
return sign === '-' ? subMinutes(d, amount) : addMinutes(d, amount);
|
||||
case 's':
|
||||
return sign === '-' ? subSeconds(d, amount) : addSeconds(d, amount);
|
||||
default:
|
||||
throw new Error(`Invalid data calculation unit: ${unit}`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseOp(op: string): { sign: string; amount: number; unit: string } | null {
|
||||
const match = op.match(/^([+-]?)(\d+)([yMdhms])$/);
|
||||
if (!match) {
|
||||
throw new Error(`Invalid date expression: ${op}`);
|
||||
}
|
||||
const [, sign, amount, unit] = match;
|
||||
if (!unit) return null;
|
||||
return { sign: sign ?? '+', amount: Number(amount ?? 0), unit };
|
||||
}
|
||||
|
||||
function parseDateString(date: string): Date {
|
||||
if (!date.trim()) {
|
||||
return new Date();
|
||||
}
|
||||
|
||||
const isoDate = parseISO(date);
|
||||
if (isValid(isoDate)) {
|
||||
return isoDate;
|
||||
}
|
||||
|
||||
const jsDate = /^\d+(\.\d+)?$/.test(date) ? new Date(Number(date)) : new Date(date);
|
||||
if (isValid(jsDate)) {
|
||||
return jsDate;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid date: ${date}`);
|
||||
}
|
||||
|
||||
export function calculateDatetime(args: { date?: string; expression?: string }): string {
|
||||
const { date, expression } = args;
|
||||
let jsDate = parseDateString(date ?? '');
|
||||
|
||||
if (expression) {
|
||||
const ops = String(expression)
|
||||
.split(' ')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
for (const op of ops) {
|
||||
const parsed = parseOp(op);
|
||||
if (parsed) {
|
||||
jsDate = applyDateOp(jsDate, parsed.sign, parsed.amount, parsed.unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return jsDate.toISOString();
|
||||
}
|
||||
|
||||
export function formatDatetime(args: { date?: string; format?: string }): string {
|
||||
const { date, format = 'yyyy-MM-dd HH:mm:ss' } = args;
|
||||
const d = parseDateString(date ?? '');
|
||||
return formatDate(d, String(format));
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { calculateDatetime, formatDatetime } from '../src';
|
||||
|
||||
describe('formatDatetime', () => {
|
||||
it('returns formatted current date', () => {
|
||||
const result = formatDatetime({});
|
||||
expect(result).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
it('returns formatted specific date', () => {
|
||||
const result = formatDatetime({ date: '2025-07-13T12:34:56' });
|
||||
expect(result).toBe('2025-07-13 12:34:56');
|
||||
});
|
||||
|
||||
it('returns formatted specific timestamp', () => {
|
||||
const result = formatDatetime({ date: '1752435296000' });
|
||||
expect(result).toBe('2025-07-13 12:34:56');
|
||||
});
|
||||
|
||||
it('returns formatted specific timestamp with decimals', () => {
|
||||
const result = formatDatetime({ date: '1752435296000.19' });
|
||||
expect(result).toBe('2025-07-13 12:34:56');
|
||||
});
|
||||
|
||||
it('returns formatted date with custom output', () => {
|
||||
const result = formatDatetime({ date: '2025-07-13T12:34:56', format: 'dd/MM/yyyy' });
|
||||
expect(result).toBe('13/07/2025');
|
||||
});
|
||||
|
||||
it('handles invalid date gracefully', () => {
|
||||
expect(() => formatDatetime({ date: 'invalid-date' })).toThrow('Invalid date: invalid-date');
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateDatetime', () => {
|
||||
it('returns ISO string for current date', () => {
|
||||
const result = calculateDatetime({});
|
||||
expect(result).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
it('returns ISO string for specific date', () => {
|
||||
const result = calculateDatetime({ date: '2025-07-13T12:34:56Z' });
|
||||
expect(result).toBe('2025-07-13T12:34:56.000Z');
|
||||
});
|
||||
|
||||
it('applies calc operations', () => {
|
||||
const result = calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: '+1d 2h' });
|
||||
expect(result).toBe('2025-07-14T14:00:00.000Z');
|
||||
});
|
||||
|
||||
it('applies negative calc operations', () => {
|
||||
const result = calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: '-1d -2h 1m' });
|
||||
expect(result).toBe('2025-07-12T10:01:00.000Z');
|
||||
});
|
||||
|
||||
it('throws error for invalid unit', () => {
|
||||
expect(() => calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: '+1x' })).toThrow(
|
||||
'Invalid date expression: +1x',
|
||||
);
|
||||
});
|
||||
it('throws error for invalid unit weird', () => {
|
||||
expect(() => calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: '+1&#^%' })).toThrow(
|
||||
'Invalid date expression: +1&#^%',
|
||||
);
|
||||
});
|
||||
it('throws error for bad expression', () => {
|
||||
expect(() =>
|
||||
calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: 'bad expr' }),
|
||||
).toThrow('Invalid date expression: bad');
|
||||
});
|
||||
});
|
||||
3
plugins/template-function-timestamp/tsconfig.json
Normal file
3
plugins/template-function-timestamp/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -8103,6 +8103,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"md5 0.7.0",
|
||||
"reqwest_cookie_store",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
|
||||
@@ -50,7 +50,7 @@ md5 = "0.8.0"
|
||||
mime_guess = "2.0.5"
|
||||
rand = "0.9.0"
|
||||
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider", "socks"] }
|
||||
reqwest_cookie_store = "0.8.0"
|
||||
reqwest_cookie_store = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true, features = ["raw_value"] }
|
||||
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
|
||||
@@ -97,6 +97,7 @@ tauri-plugin-shell = "2.3.0"
|
||||
tokio = "1.45.1"
|
||||
thiserror = "2.0.12"
|
||||
ts-rs = "11.0.1"
|
||||
reqwest_cookie_store = "0.8.0"
|
||||
rustls = { version = "0.23.27", default-features = false }
|
||||
rustls-platform-verifier = "0.6.0"
|
||||
sha2 = "0.10.9"
|
||||
|
||||
BIN
src-tauri/icons/dev/64x64.png
Normal file
BIN
src-tauri/icons/dev/64x64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
BIN
src-tauri/icons/release/64x64.png
Normal file
BIN
src-tauri/icons/release/64x64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 51 KiB |
@@ -55,11 +55,6 @@ pub async fn send_http_request<R: Runtime>(
|
||||
let response_id = og_response.id.clone();
|
||||
let response = Arc::new(Mutex::new(og_response.clone()));
|
||||
|
||||
let cb = PluginTemplateCallback::new(
|
||||
window.app_handle(),
|
||||
&PluginWindowContext::new(window),
|
||||
RenderPurpose::Send,
|
||||
);
|
||||
let update_source = UpdateSource::from_window(window);
|
||||
|
||||
let (resolved_request, auth_context_id) = match resolve_http_request(window, unrendered_request)
|
||||
@@ -75,6 +70,12 @@ pub async fn send_http_request<R: Runtime>(
|
||||
}
|
||||
};
|
||||
|
||||
let cb = PluginTemplateCallback::new(
|
||||
window.app_handle(),
|
||||
&PluginWindowContext::new(window),
|
||||
RenderPurpose::Send,
|
||||
);
|
||||
|
||||
let request =
|
||||
match render_http_request(&resolved_request, &base_environment, environment.as_ref(), &cb)
|
||||
.await
|
||||
@@ -488,10 +489,11 @@ pub async fn send_http_request<R: Runtime>(
|
||||
};
|
||||
}
|
||||
|
||||
let mut query_pairs = sendable_req.url_mut().query_pairs_mut();
|
||||
for p in plugin_result.set_query_parameters.unwrap_or_default() {
|
||||
println!("Adding query parameter: {:?}", p);
|
||||
query_pairs.append_pair(&p.name, &p.value);
|
||||
if let Some(params) = plugin_result.set_query_parameters {
|
||||
let mut query_pairs = sendable_req.url_mut().query_pairs_mut();
|
||||
for p in params {
|
||||
query_pairs.append_pair(&p.name, &p.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,13 @@ use yaak_models::models::{
|
||||
};
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
|
||||
use yaak_plugins::events::{CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest};
|
||||
use yaak_plugins::events::{
|
||||
CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs,
|
||||
CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse,
|
||||
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
|
||||
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent,
|
||||
InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest,
|
||||
};
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_plugins::plugin_meta::PluginMetadata;
|
||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||
@@ -818,9 +824,29 @@ async fn cmd_get_http_authentication_config<R: Runtime>(
|
||||
auth_name: &str,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
request_id: &str,
|
||||
environment_id: Option<&str>,
|
||||
workspace_id: &str,
|
||||
) -> YaakResult<GetHttpAuthenticationConfigResponse> {
|
||||
let base_environment = window.db().get_base_environment(&workspace_id)?;
|
||||
let environment = match environment_id {
|
||||
Some(id) => match window.db().get_environment(id) {
|
||||
Ok(env) => Some(env),
|
||||
Err(e) => {
|
||||
warn!("Failed to find environment by id {id} {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
Ok(plugin_manager
|
||||
.get_http_authentication_config(&window, auth_name, values, request_id)
|
||||
.get_http_authentication_config(
|
||||
&window,
|
||||
&base_environment,
|
||||
environment.as_ref(),
|
||||
auth_name,
|
||||
values,
|
||||
request_id,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
@@ -872,9 +898,30 @@ async fn cmd_call_http_authentication_action<R: Runtime>(
|
||||
action_index: i32,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model_id: &str,
|
||||
workspace_id: &str,
|
||||
environment_id: Option<&str>,
|
||||
) -> YaakResult<()> {
|
||||
let base_environment = window.db().get_base_environment(&workspace_id)?;
|
||||
let environment = match environment_id {
|
||||
Some(id) => match window.db().get_environment(id) {
|
||||
Ok(env) => Some(env),
|
||||
Err(e) => {
|
||||
warn!("Failed to find environment by id {id} {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
Ok(plugin_manager
|
||||
.call_http_authentication_action(&window, auth_name, action_index, values, model_id)
|
||||
.call_http_authentication_action(
|
||||
&window,
|
||||
&base_environment,
|
||||
environment.as_ref(),
|
||||
auth_name,
|
||||
action_index,
|
||||
values,
|
||||
model_id,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
@@ -1238,7 +1285,10 @@ pub fn run() {
|
||||
let _ = app_handle.emit(
|
||||
"show_toast",
|
||||
ShowToastRequest {
|
||||
message: format!("Error handling deep link: {}", e.to_string()),
|
||||
message: format!(
|
||||
"Error handling deep link: {}",
|
||||
e.to_string()
|
||||
),
|
||||
color: Some(Color::Danger),
|
||||
icon: None,
|
||||
},
|
||||
|
||||
@@ -53,6 +53,7 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
|
||||
"import-data" => {
|
||||
let mut file_path = query_map.get("path").map(|s| s.to_owned());
|
||||
let name = query_map.get("name").map(|s| s.to_owned()).unwrap_or("data".to_string());
|
||||
_ = window.set_focus();
|
||||
|
||||
if let Some(file_url) = query_map.get("url") {
|
||||
let confirmed_import = app_handle
|
||||
@@ -96,7 +97,6 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
|
||||
};
|
||||
|
||||
let results = import_data(window, &file_path).await?;
|
||||
_ = window.set_focus();
|
||||
window.emit(
|
||||
"show_toast",
|
||||
ShowToastRequest {
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
{
|
||||
"productName": "yaak"
|
||||
"productName": "yaak",
|
||||
"bundle": {
|
||||
"linux": {
|
||||
"deb": {
|
||||
"desktopTemplate": "./template.desktop"
|
||||
},
|
||||
"rpm": {
|
||||
"desktopTemplate": "./template.desktop"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
src-tauri/template.desktop
Normal file
9
src-tauri/template.desktop
Normal file
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Categories={{categories}}
|
||||
Comment={{comment}}
|
||||
Exec={{exec}}
|
||||
Icon={{icon}}
|
||||
Name={{name}}
|
||||
StartupWMClass={{exec}}
|
||||
Terminal=false
|
||||
Type=Application
|
||||
@@ -36,7 +36,7 @@ export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key
|
||||
|
||||
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
|
||||
|
||||
export type CallTemplateFunctionResponse = { value: string | null, };
|
||||
export type CallTemplateFunctionResponse = { value: string | null, error?: string, };
|
||||
|
||||
export type CloseWindowRequest = { label: string, };
|
||||
|
||||
|
||||
@@ -974,6 +974,8 @@ pub struct CallTemplateFunctionRequest {
|
||||
#[ts(export, export_to = "gen_events.ts")]
|
||||
pub struct CallTemplateFunctionResponse {
|
||||
pub value: Option<String>,
|
||||
#[ts(optional)]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
|
||||
@@ -6,18 +6,21 @@ use crate::events::{
|
||||
BootRequest, CallGrpcRequestActionRequest, CallHttpAuthenticationActionArgs,
|
||||
CallHttpAuthenticationActionRequest, CallHttpAuthenticationRequest,
|
||||
CallHttpAuthenticationResponse, CallHttpRequestActionRequest, CallTemplateFunctionArgs,
|
||||
CallTemplateFunctionRequest, CallTemplateFunctionResponse, EmptyPayload, FilterRequest,
|
||||
FilterResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigRequest,
|
||||
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
|
||||
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, GetThemesRequest,
|
||||
GetThemesResponse, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload,
|
||||
JsonPrimitive, PluginWindowContext, RenderPurpose,
|
||||
CallTemplateFunctionRequest, CallTemplateFunctionResponse, EmptyPayload, ErrorResponse,
|
||||
FilterRequest, FilterResponse, GetGrpcRequestActionsResponse,
|
||||
GetHttpAuthenticationConfigRequest, GetHttpAuthenticationConfigResponse,
|
||||
GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse,
|
||||
GetTemplateFunctionsResponse, GetThemesRequest, GetThemesResponse, ImportRequest,
|
||||
ImportResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext,
|
||||
RenderPurpose,
|
||||
};
|
||||
use crate::native_template_functions::template_function_secure;
|
||||
use crate::nodejs::start_nodejs_plugin_runtime;
|
||||
use crate::plugin_handle::PluginHandle;
|
||||
use crate::server_ws::PluginRuntimeServerWebsocket;
|
||||
use crate::template_callback::PluginTemplateCallback;
|
||||
use log::{error, info, warn};
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -29,10 +32,13 @@ use tokio::fs::read_dir;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
use tokio::time::{Instant, timeout};
|
||||
use yaak_models::models::Environment;
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_models::render::make_vars_hashmap;
|
||||
use yaak_models::util::generate_id;
|
||||
use yaak_templates::error::Error::RenderError;
|
||||
use yaak_templates::error::Result as TemplateResult;
|
||||
use yaak_templates::render_json_value_raw;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginManager {
|
||||
@@ -568,6 +574,8 @@ impl PluginManager {
|
||||
pub async fn get_http_authentication_config<R: Runtime>(
|
||||
&self,
|
||||
window: &WebviewWindow<R>,
|
||||
base_environment: &Environment,
|
||||
environment: Option<&Environment>,
|
||||
auth_name: &str,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
request_id: &str,
|
||||
@@ -578,13 +586,23 @@ impl PluginManager {
|
||||
.find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None })
|
||||
.ok_or(PluginNotFoundErr(auth_name.into()))?;
|
||||
|
||||
let vars = &make_vars_hashmap(&base_environment, environment);
|
||||
let cb = PluginTemplateCallback::new(
|
||||
window.app_handle(),
|
||||
&PluginWindowContext::new(&window),
|
||||
RenderPurpose::Preview,
|
||||
);
|
||||
let rendered_values = render_json_value_raw(json!(values), vars, &cb).await?;
|
||||
let context_id = format!("{:x}", md5::compute(request_id.to_string()));
|
||||
let event = self
|
||||
.send_to_plugin_and_wait(
|
||||
&PluginWindowContext::new(window),
|
||||
&plugin,
|
||||
&InternalEventPayload::GetHttpAuthenticationConfigRequest(
|
||||
GetHttpAuthenticationConfigRequest { values, context_id },
|
||||
GetHttpAuthenticationConfigRequest {
|
||||
values: serde_json::from_value(rendered_values)?,
|
||||
context_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
@@ -601,11 +619,24 @@ impl PluginManager {
|
||||
pub async fn call_http_authentication_action<R: Runtime>(
|
||||
&self,
|
||||
window: &WebviewWindow<R>,
|
||||
base_environment: &Environment,
|
||||
environment: Option<&Environment>,
|
||||
auth_name: &str,
|
||||
action_index: i32,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model_id: &str,
|
||||
) -> Result<()> {
|
||||
let vars = &make_vars_hashmap(&base_environment, environment);
|
||||
let rendered_values = render_json_value_raw(
|
||||
json!(values),
|
||||
vars,
|
||||
&PluginTemplateCallback::new(
|
||||
window.app_handle(),
|
||||
&PluginWindowContext::new(&window),
|
||||
RenderPurpose::Preview,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
let results = self.get_http_authentication_summaries(window).await?;
|
||||
let plugin = results
|
||||
.iter()
|
||||
@@ -620,7 +651,10 @@ impl PluginManager {
|
||||
CallHttpAuthenticationActionRequest {
|
||||
index: action_index,
|
||||
plugin_ref_id: plugin.clone().ref_id,
|
||||
args: CallHttpAuthenticationActionArgs { context_id, values },
|
||||
args: CallHttpAuthenticationActionArgs {
|
||||
context_id,
|
||||
values: serde_json::from_value(rendered_values)?,
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -644,7 +678,7 @@ impl PluginManager {
|
||||
info!("Not applying disabled auth {:?}", auth_name);
|
||||
return Ok(CallHttpAuthenticationResponse {
|
||||
set_headers: None,
|
||||
set_query_parameters: None
|
||||
set_query_parameters: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -689,16 +723,25 @@ impl PluginManager {
|
||||
.map_err(|e| RenderError(format!("Failed to call template function {e:}")))?;
|
||||
|
||||
let value = events.into_iter().find_map(|e| match e.payload {
|
||||
// Error returned
|
||||
InternalEventPayload::CallTemplateFunctionResponse(CallTemplateFunctionResponse {
|
||||
error: Some(error),
|
||||
..
|
||||
}) => Some(Err(error)),
|
||||
// Value or null returned
|
||||
InternalEventPayload::CallTemplateFunctionResponse(CallTemplateFunctionResponse {
|
||||
value,
|
||||
}) => Some(value),
|
||||
..
|
||||
}) => Some(Ok(value.unwrap_or_default())),
|
||||
// Generic error returned
|
||||
InternalEventPayload::ErrorResponse(ErrorResponse { error }) => Some(Err(error)),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
match value {
|
||||
None => Err(RenderError(format!("Template function {fn_name}(…) not found "))),
|
||||
Some(Some(v)) => Ok(v), // Plugin returned string
|
||||
Some(None) => Ok("".to_string()), // Plugin returned null
|
||||
Some(Ok(v)) => Ok(v),
|
||||
Some(Err(e)) => Err(RenderError(e)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ publish = false
|
||||
futures-util = "0.3.31"
|
||||
log = "0.4.20"
|
||||
md5 = "0.7.0"
|
||||
reqwest_cookie_store = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tauri = { workspace = true }
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::error::Result;
|
||||
use crate::manager::WebsocketManager;
|
||||
use crate::render::render_websocket_request;
|
||||
use crate::resolve::resolve_websocket_request;
|
||||
use log::debug;
|
||||
use log::{info, warn};
|
||||
use std::str::FromStr;
|
||||
use tauri::http::{HeaderMap, HeaderName};
|
||||
@@ -293,18 +294,53 @@ pub(crate) async fn connect<R: Runtime>(
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
let mut query_pairs = url.query_pairs_mut();
|
||||
for p in plugin_result.set_query_parameters.unwrap_or_default() {
|
||||
query_pairs.append_pair(&p.name, &p.value);
|
||||
if let Some(params) = plugin_result.set_query_parameters {
|
||||
let mut query_pairs = url.query_pairs_mut();
|
||||
for p in params {
|
||||
query_pairs.append_pair(&p.name, &p.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle cookies
|
||||
let _cookie_jar = match cookie_jar_id {
|
||||
Some(id) => Some(app_handle.db().get_cookie_jar(id)?),
|
||||
None => None,
|
||||
};
|
||||
// Add cookies to WS HTTP Upgrade
|
||||
if let Some(id) = cookie_jar_id {
|
||||
let cookie_jar = app_handle.db().get_cookie_jar(id)?;
|
||||
|
||||
let cookies = cookie_jar
|
||||
.cookies
|
||||
.iter()
|
||||
.filter_map(|cookie| {
|
||||
// HACK: same as in src-tauri/src/http_request.rs
|
||||
let json_cookie = serde_json::to_value(cookie).ok()?;
|
||||
match serde_json::from_value(json_cookie) {
|
||||
Ok(cookie) => Some(Ok(cookie)),
|
||||
Err(_e) => None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<Result<_>>>();
|
||||
|
||||
let store = reqwest_cookie_store::CookieStore::from_cookies(cookies, true)?;
|
||||
|
||||
// Convert WS URL -> HTTP URL bc reqwest_cookie_store's `get_request_values`
|
||||
// strictly matches based on Path/HttpOnly/Secure attributes even though WS upgrades are HTTP requests
|
||||
let http_url = convert_ws_url_to_http(&url);
|
||||
let pairs: Vec<_> = store.get_request_values(&http_url).collect();
|
||||
debug!("Inserting {} cookies into WS upgrade to {}", pairs.len(), url);
|
||||
|
||||
let cookie_header_value = pairs
|
||||
.into_iter()
|
||||
.map(|(name, value)| format!("{}={}", name, value))
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ");
|
||||
|
||||
if !cookie_header_value.is_empty() {
|
||||
headers.insert(
|
||||
HeaderName::from_static("cookie"),
|
||||
HeaderValue::from_str(&cookie_header_value).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let (receive_tx, mut receive_rx) = mpsc::channel::<Message>(128);
|
||||
let mut ws_manager = ws_manager.lock().await;
|
||||
@@ -337,7 +373,7 @@ pub(crate) async fn connect<R: Runtime>(
|
||||
Err(e) => {
|
||||
return Ok(app_handle.db().upsert_websocket_connection(
|
||||
&WebsocketConnection {
|
||||
error: Some(format!("{e:?}")),
|
||||
error: Some(e.to_string()),
|
||||
state: WebsocketConnectionState::Closed,
|
||||
..connection
|
||||
},
|
||||
@@ -448,3 +484,23 @@ pub(crate) async fn connect<R: Runtime>(
|
||||
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
/// Convert WS URL to HTTP URL for cookie filtering
|
||||
/// WebSocket upgrade requests are HTTP requests initially, so HttpOnly cookies should apply
|
||||
fn convert_ws_url_to_http(ws_url: &Url) -> Url {
|
||||
let mut http_url = ws_url.clone();
|
||||
|
||||
match ws_url.scheme() {
|
||||
"ws" => {
|
||||
http_url.set_scheme("http").expect("Failed to set http scheme");
|
||||
}
|
||||
"wss" => {
|
||||
http_url.set_scheme("https").expect("Failed to set https scheme");
|
||||
}
|
||||
_ => {
|
||||
// Already HTTP/HTTPS, no conversion needed
|
||||
}
|
||||
}
|
||||
|
||||
http_url
|
||||
}
|
||||
|
||||
@@ -182,23 +182,24 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
);
|
||||
case 'accordion':
|
||||
return (
|
||||
<DetailsBanner
|
||||
key={i}
|
||||
summary={input.label}
|
||||
className={classNames(disabled && 'opacity-disabled')}
|
||||
>
|
||||
<div className="mb-3 px-3">
|
||||
<FormInputs
|
||||
data={data}
|
||||
disabled={disabled}
|
||||
inputs={input.inputs}
|
||||
setDataAttr={setDataAttr}
|
||||
stateKey={stateKey}
|
||||
autocompleteFunctions={autocompleteFunctions || false}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
/>
|
||||
</div>
|
||||
</DetailsBanner>
|
||||
<div key={i}>
|
||||
<DetailsBanner
|
||||
summary={input.label}
|
||||
className={classNames('!mb-auto', disabled && 'opacity-disabled')}
|
||||
>
|
||||
<div className="mb-3 px-3">
|
||||
<FormInputs
|
||||
data={data}
|
||||
disabled={disabled}
|
||||
inputs={input.inputs}
|
||||
setDataAttr={setDataAttr}
|
||||
stateKey={stateKey}
|
||||
autocompleteFunctions={autocompleteFunctions || false}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
/>
|
||||
</div>
|
||||
</DetailsBanner>
|
||||
</div>
|
||||
);
|
||||
case 'banner':
|
||||
return (
|
||||
@@ -309,6 +310,7 @@ function EditorArg({
|
||||
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
|
||||
disabled={arg.disabled}
|
||||
language={arg.language}
|
||||
readOnly={arg.readOnly}
|
||||
onChange={onChange}
|
||||
heightMode="auto"
|
||||
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
|
||||
@@ -329,9 +331,9 @@ function EditorArg({
|
||||
showDialog({
|
||||
id: 'id',
|
||||
size: 'full',
|
||||
title: 'Edit Value',
|
||||
title: arg.readOnly ? 'View Value' : 'Edit Value',
|
||||
className: '!max-w-[50rem] !max-h-[60rem]',
|
||||
description: (
|
||||
description: arg.label && (
|
||||
<Label
|
||||
htmlFor={id}
|
||||
required={!arg.optional}
|
||||
@@ -355,6 +357,7 @@ function EditorArg({
|
||||
}
|
||||
disabled={arg.disabled}
|
||||
language={arg.language}
|
||||
readOnly={arg.readOnly}
|
||||
onChange={onChange}
|
||||
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
|
||||
placeholder={arg.placeholder ?? undefined}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { invokeCmd } from '../lib/tauri';
|
||||
import { Button } from './core/Button';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { DetailsBanner } from './core/DetailsBanner';
|
||||
import { Link } from './core/Link';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
@@ -83,69 +84,81 @@ function ExportDataDialogContent({
|
||||
const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length;
|
||||
const noneSelected = numSelected === 0;
|
||||
return (
|
||||
<VStack space={3} className="w-full mb-3 px-4">
|
||||
<table className="w-full mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-6 min-w-0 py-2 text-left pl-1">
|
||||
<Checkbox
|
||||
checked={!allSelected && !noneSelected ? 'indeterminate' : allSelected}
|
||||
hideLabel
|
||||
title="All workspaces"
|
||||
onChange={handleToggleAll}
|
||||
/>
|
||||
</th>
|
||||
<th className="py-2 text-left pl-4" onClick={handleToggleAll}>
|
||||
Workspace
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-surface-highlight">
|
||||
{workspaces.map((w) => (
|
||||
<tr key={w.id}>
|
||||
<td className="min-w-0 py-1 pl-1">
|
||||
<div className="w-full grid grid-rows-[minmax(0,1fr)_auto]">
|
||||
<VStack space={3} className="overflow-auto px-5 pb-6">
|
||||
<table className="w-full mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-6 min-w-0 py-2 text-left pl-1">
|
||||
<Checkbox
|
||||
checked={selectedWorkspaces[w.id] ?? false}
|
||||
title={w.name}
|
||||
checked={!allSelected && !noneSelected ? 'indeterminate' : allSelected}
|
||||
hideLabel
|
||||
onChange={() =>
|
||||
title="All workspaces"
|
||||
onChange={handleToggleAll}
|
||||
/>
|
||||
</th>
|
||||
<th className="py-2 text-left pl-4" onClick={handleToggleAll}>
|
||||
Workspace
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-surface-highlight">
|
||||
{workspaces.map((w) => (
|
||||
<tr key={w.id}>
|
||||
<td className="min-w-0 py-1 pl-1">
|
||||
<Checkbox
|
||||
checked={selectedWorkspaces[w.id] ?? false}
|
||||
title={w.name}
|
||||
hideLabel
|
||||
onChange={() =>
|
||||
setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] }))
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className="py-1 pl-4 text whitespace-nowrap overflow-x-auto hide-scrollbars"
|
||||
onClick={() =>
|
||||
setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] }))
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className="py-1 pl-4 text whitespace-nowrap overflow-x-auto hide-scrollbars"
|
||||
onClick={() => setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] }))}
|
||||
>
|
||||
{w.name} {w.id === activeWorkspace.id ? '(current workspace)' : ''}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<DetailsBanner color="secondary" open summary="Extra Settings">
|
||||
<Checkbox
|
||||
checked={includePrivateEnvironments}
|
||||
onChange={setIncludePrivateEnvironments}
|
||||
title="Include private environments"
|
||||
help='Environments marked as "sharable" will be exported by default'
|
||||
/>
|
||||
</DetailsBanner>
|
||||
<HStack space={2} justifyContent="end">
|
||||
<Button className="focus" variant="border" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="focus"
|
||||
color="primary"
|
||||
disabled={noneSelected}
|
||||
onClick={() => handleExport()}
|
||||
>
|
||||
Export{' '}
|
||||
{pluralizeCount('Workspace', numSelected, { omitSingle: true, noneWord: 'Nothing' })}
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
>
|
||||
{w.name} {w.id === activeWorkspace.id ? '(current workspace)' : ''}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<DetailsBanner color="secondary" open summary="Extra Settings">
|
||||
<Checkbox
|
||||
checked={includePrivateEnvironments}
|
||||
onChange={setIncludePrivateEnvironments}
|
||||
title="Include private environments"
|
||||
help='Environments marked as "sharable" will be exported by default'
|
||||
/>
|
||||
</DetailsBanner>
|
||||
</VStack>
|
||||
<footer className="px-5 grid grid-cols-[1fr_auto] items-center bg-surface-highlight py-2 border-t border-border-subtle">
|
||||
<div>
|
||||
<Link href="https://yaak.app/button/new" noUnderline className="text-text-subtle">
|
||||
Create Run Button
|
||||
</Link>
|
||||
</div>
|
||||
<HStack space={2} justifyContent="end">
|
||||
<Button size="sm" className="focus" variant="border" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
type="submit"
|
||||
className="focus"
|
||||
color="primary"
|
||||
disabled={noneSelected}
|
||||
onClick={() => handleExport()}
|
||||
>
|
||||
Export{' '}
|
||||
{pluralizeCount('Workspace', numSelected, { omitSingle: true, noneWord: 'Nothing' })}
|
||||
</Button>
|
||||
</HStack>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import { settingsAtom } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useStoplightsVisible } from '../hooks/useStoplightsVisible';
|
||||
import { useIsFullscreen } from '../hooks/useIsFullscreen';
|
||||
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from '../lib/constants';
|
||||
import { WindowControls } from './WindowControls';
|
||||
|
||||
@@ -23,7 +24,7 @@ export function HeaderSize({
|
||||
children,
|
||||
}: HeaderSizeProps) {
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const stoplightsVisible = useStoplightsVisible();
|
||||
const isFullscreen = useIsFullscreen();
|
||||
const finalStyle = useMemo<CSSProperties>(() => {
|
||||
const s = { ...style };
|
||||
|
||||
@@ -31,20 +32,22 @@ export function HeaderSize({
|
||||
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
|
||||
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
|
||||
|
||||
// Add large padding for window controls
|
||||
if (stoplightsVisible && !ignoreControlsSpacing) {
|
||||
s.paddingLeft = 72 / settings.interfaceScale;
|
||||
} else if (!stoplightsVisible && !ignoreControlsSpacing && !settings.hideWindowControls) {
|
||||
if (type() === 'macos') {
|
||||
if (!isFullscreen) {
|
||||
// Add large padding for window controls
|
||||
s.paddingLeft = 72 / settings.interfaceScale;
|
||||
}
|
||||
} else if (!ignoreControlsSpacing && !settings.hideWindowControls) {
|
||||
s.paddingRight = WINDOW_CONTROLS_WIDTH;
|
||||
}
|
||||
|
||||
return s;
|
||||
}, [
|
||||
ignoreControlsSpacing,
|
||||
isFullscreen,
|
||||
settings.hideWindowControls,
|
||||
settings.interfaceScale,
|
||||
size,
|
||||
stoplightsVisible,
|
||||
style,
|
||||
]);
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function Settings({ hide }: Props) {
|
||||
layout="horizontal"
|
||||
value={tab}
|
||||
addBorders
|
||||
tabListClassName="min-w-[10rem] bg-surface x-theme-sidebar border-r border-border"
|
||||
tabListClassName="min-w-[10rem] bg-surface x-theme-sidebar border-r border-border pl-3"
|
||||
label="Settings"
|
||||
onChangeValue={setTab}
|
||||
tabs={tabs.map((value) => ({ value, label: capitalize(value) }))}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function SettingsPlugins() {
|
||||
addBorders
|
||||
tabListClassName="!-ml-3"
|
||||
tabs={[
|
||||
{ label: 'Marketplace', value: 'search' },
|
||||
{ label: 'Discover', value: 'search' },
|
||||
{
|
||||
label: 'Installed',
|
||||
value: 'installed',
|
||||
|
||||
@@ -42,6 +42,12 @@ export function SettingsDropdown() {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Plugins',
|
||||
leftSlot: <Icon icon="puzzle" />,
|
||||
onSelect: () => openSettings.mutate('plugins'),
|
||||
},
|
||||
{ type: 'separator', label: 'Share Workspace(s)' },
|
||||
{
|
||||
label: 'Import Data',
|
||||
leftSlot: <Icon icon="folder_input" />,
|
||||
@@ -52,6 +58,11 @@ export function SettingsDropdown() {
|
||||
leftSlot: <Icon icon="folder_output" />,
|
||||
onSelect: () => exportData.mutate(),
|
||||
},
|
||||
{
|
||||
label: 'Create Run Button',
|
||||
leftSlot: <Icon icon="rocket" />,
|
||||
onSelect: () => openUrl('https://yaak.app/button/new'),
|
||||
},
|
||||
{ type: 'separator', label: `Yaak v${appInfo.version}` },
|
||||
{
|
||||
label: 'Purchase License',
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
import { useToggle } from '../hooks/useToggle';
|
||||
import { convertTemplateToInsecure } from '../lib/encryption';
|
||||
import { setupOrConfigureEncryption } from '../lib/setupOrConfigureEncryption';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { LoadingIcon } from './core/LoadingIcon';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { DYNAMIC_FORM_NULL_ARG, DynamicForm } from './DynamicForm';
|
||||
@@ -178,7 +178,10 @@ function InitializedTemplateFunctionDialog({
|
||||
{enablePreview && (
|
||||
<VStack className="w-full" space={1}>
|
||||
<HStack space={0.5}>
|
||||
<div className="text-sm text-text-subtle">Rendered Preview</div>
|
||||
<HStack className="text-sm text-text-subtle" space={1.5}>
|
||||
Rendered Preview
|
||||
{rendered.isPending && <LoadingIcon size="xs" />}
|
||||
</HStack>
|
||||
<IconButton
|
||||
size="xs"
|
||||
iconSize="sm"
|
||||
@@ -191,26 +194,24 @@ function InitializedTemplateFunctionDialog({
|
||||
)}
|
||||
/>
|
||||
</HStack>
|
||||
{rendered.error || tagText.error ? (
|
||||
<Banner color="danger">{`${rendered.error || tagText.error}`}</Banner>
|
||||
) : (
|
||||
<InlineCode
|
||||
className={classNames(
|
||||
'whitespace-pre select-text cursor-text max-h-[10rem] overflow-y-auto hide-scrollbars',
|
||||
tooLarge && 'italic text-danger',
|
||||
)}
|
||||
>
|
||||
{dataContainsSecrets && !showSecretsInPreview ? (
|
||||
<span className="italic text-text-subtle">
|
||||
------ sensitive values hidden ------
|
||||
</span>
|
||||
) : tooLarge ? (
|
||||
'too large to preview'
|
||||
) : (
|
||||
rendered.data || <> </>
|
||||
)}
|
||||
</InlineCode>
|
||||
)}
|
||||
<InlineCode
|
||||
className={classNames(
|
||||
'whitespace-pre-wrap select-text cursor-text max-h-[10rem] overflow-y-auto hide-scrollbars',
|
||||
tooLarge && 'italic text-danger',
|
||||
)}
|
||||
>
|
||||
{rendered.error || tagText.error ? (
|
||||
<em className="text-danger">
|
||||
{`${rendered.error || tagText.error}`.replace(/^Render Error: /, '')}
|
||||
</em>
|
||||
) : dataContainsSecrets && !showSecretsInPreview ? (
|
||||
<span className="italic text-text-subtle">------ sensitive values hidden ------</span>
|
||||
) : tooLarge ? (
|
||||
'too large to preview'
|
||||
) : (
|
||||
rendered.data || <> </>
|
||||
)}
|
||||
</InlineCode>
|
||||
</VStack>
|
||||
)}
|
||||
<div className="flex justify-stretch w-full flex-grow gap-2 [&>*]:flex-1">
|
||||
|
||||
@@ -21,10 +21,12 @@ export function DetailsBanner({ className, color, summary, children, ...extraPro
|
||||
'w-0 h-0 border-t-[0.3em] border-b-[0.3em] border-l-[0.5em] border-r-0',
|
||||
'border-t-transparent border-b-transparent border-l-text-subtle',
|
||||
)}
|
||||
></div>
|
||||
/>
|
||||
{summary}
|
||||
</summary>
|
||||
<div className="mt-1.5">
|
||||
{children}
|
||||
</div>
|
||||
</details>
|
||||
</Banner>
|
||||
);
|
||||
|
||||
@@ -12,9 +12,10 @@ import { settingsAtom } from '@yaakapp-internal/models';
|
||||
import type { EditorLanguage, TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import { parseTemplate } from '@yaakapp-internal/templates';
|
||||
import classNames from 'classnames';
|
||||
import type { GraphQLSchema } from 'graphql';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { md5 } from 'js-md5';
|
||||
import type { MutableRefObject, ReactNode } from 'react';
|
||||
import type { ReactNode, RefObject } from 'react';
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
@@ -77,6 +78,7 @@ export interface EditorProps {
|
||||
hideGutter?: boolean;
|
||||
id?: string;
|
||||
language?: EditorLanguage | 'pairs' | 'url';
|
||||
graphQLSchema?: GraphQLSchema | null;
|
||||
onBlur?: () => void;
|
||||
onChange?: (value: string) => void;
|
||||
onFocus?: () => void;
|
||||
@@ -115,6 +117,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
format,
|
||||
heightMode,
|
||||
hideGutter,
|
||||
graphQLSchema,
|
||||
language,
|
||||
onBlur,
|
||||
onChange,
|
||||
@@ -276,7 +279,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
const show = () =>
|
||||
showDialog({
|
||||
id: 'template-function-' + Math.random(), // Allow multiple at once
|
||||
size: 'sm',
|
||||
size: 'md',
|
||||
title: <InlineCode>{fn.name}(…)</InlineCode>,
|
||||
description: fn.description,
|
||||
render: ({ hide }) => (
|
||||
@@ -374,6 +377,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
onClickVariable,
|
||||
onClickMissingVariable,
|
||||
onClickPathParameter,
|
||||
graphQLSchema: graphQLSchema ?? null,
|
||||
});
|
||||
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
||||
}, [
|
||||
@@ -386,6 +390,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
onClickPathParameter,
|
||||
completionOptions,
|
||||
useTemplating,
|
||||
graphQLSchema,
|
||||
]);
|
||||
|
||||
// Initialize the editor when ref mounts
|
||||
@@ -408,6 +413,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
onClickVariable,
|
||||
onClickMissingVariable,
|
||||
onClickPathParameter,
|
||||
graphQLSchema: graphQLSchema ?? null,
|
||||
});
|
||||
const extensions = [
|
||||
languageCompartment.of(langExt),
|
||||
@@ -595,12 +601,12 @@ function getExtensions({
|
||||
}: Pick<EditorProps, 'singleLine' | 'readOnly' | 'hideGutter'> & {
|
||||
stateKey: EditorProps['stateKey'];
|
||||
container: HTMLDivElement | null;
|
||||
onChange: MutableRefObject<EditorProps['onChange']>;
|
||||
onPaste: MutableRefObject<EditorProps['onPaste']>;
|
||||
onPasteOverwrite: MutableRefObject<EditorProps['onPasteOverwrite']>;
|
||||
onFocus: MutableRefObject<EditorProps['onFocus']>;
|
||||
onBlur: MutableRefObject<EditorProps['onBlur']>;
|
||||
onKeyDown: MutableRefObject<EditorProps['onKeyDown']>;
|
||||
onChange: RefObject<EditorProps['onChange']>;
|
||||
onPaste: RefObject<EditorProps['onPaste']>;
|
||||
onPasteOverwrite: RefObject<EditorProps['onPasteOverwrite']>;
|
||||
onFocus: RefObject<EditorProps['onFocus']>;
|
||||
onBlur: RefObject<EditorProps['onBlur']>;
|
||||
onKeyDown: RefObject<EditorProps['onKeyDown']>;
|
||||
}) {
|
||||
// TODO: Ensure tooltips render inside the dialog if we are in one.
|
||||
const parent =
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import type { EnvironmentVariable } from '@yaakapp-internal/models';
|
||||
import { graphql } from 'cm6-graphql';
|
||||
import type { GraphQLSchema } from 'graphql';
|
||||
import { activeRequestIdAtom } from '../../../hooks/useActiveRequestId';
|
||||
import { jotaiStore } from '../../../lib/jotai';
|
||||
import { renderMarkdown } from '../../../lib/markdown';
|
||||
@@ -106,6 +107,7 @@ export function getLanguageExtension({
|
||||
onClickMissingVariable,
|
||||
onClickPathParameter,
|
||||
completionOptions,
|
||||
graphQLSchema,
|
||||
}: {
|
||||
useTemplating: boolean;
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
@@ -113,6 +115,7 @@ export function getLanguageExtension({
|
||||
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
|
||||
onClickPathParameter: (name: string) => void;
|
||||
completionOptions: TwigCompletionOption[];
|
||||
graphQLSchema: GraphQLSchema | null;
|
||||
} & Pick<EditorProps, 'language' | 'autocomplete'>) {
|
||||
const extraExtensions: Extension[] = [];
|
||||
|
||||
@@ -128,7 +131,7 @@ export function getLanguageExtension({
|
||||
// GraphQL is a special exception
|
||||
if (language === 'graphql') {
|
||||
return [
|
||||
graphql(undefined, {
|
||||
graphql(graphQLSchema ?? undefined, {
|
||||
async onCompletionInfoRender(gqlCompletionItem): Promise<Node | null> {
|
||||
if (!gqlCompletionItem.documentation) return null;
|
||||
const innerHTML = await renderMarkdown(gqlCompletionItem.documentation);
|
||||
|
||||
@@ -92,7 +92,9 @@ const icons = {
|
||||
plug: lucide.Plug,
|
||||
plus: lucide.PlusIcon,
|
||||
plus_circle: lucide.PlusCircleIcon,
|
||||
puzzle: lucide.PuzzleIcon,
|
||||
refresh: lucide.RefreshCwIcon,
|
||||
rocket: lucide.RocketIcon,
|
||||
save: lucide.SaveIcon,
|
||||
search: lucide.SearchIcon,
|
||||
send_horizontal: lucide.SendHorizonalIcon,
|
||||
@@ -140,7 +142,7 @@ export const Icon = memo(function Icon({
|
||||
title={title}
|
||||
className={classNames(
|
||||
className,
|
||||
!spin && 'transform-cpu',
|
||||
!spin && 'transform-gpu',
|
||||
spin && 'animate-spin',
|
||||
'flex-shrink-0',
|
||||
size === 'xl' && 'h-6 w-6',
|
||||
|
||||
@@ -9,6 +9,7 @@ type Props = Omit<TooltipProps, 'children'> & {
|
||||
iconSize?: IconProps['size'];
|
||||
iconColor?: IconProps['color'];
|
||||
className?: string;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export function IconTooltip({
|
||||
|
||||
@@ -519,7 +519,7 @@ function EncryptionInput({
|
||||
color="danger"
|
||||
size={props.size}
|
||||
className="text-sm"
|
||||
rightSlot={<IconTooltip content={state.error} icon="alert_triangle" />}
|
||||
rightSlot={<IconTooltip tabIndex={-1} content={state.error} icon="alert_triangle" />}
|
||||
onClick={() => {
|
||||
setupOrConfigureEncryption();
|
||||
}}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function Label({
|
||||
({tag})
|
||||
</span>
|
||||
))}
|
||||
{help && <IconTooltip content={help} />}
|
||||
{help && <IconTooltip tabIndex={-1} content={help} />}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function Tabs({
|
||||
tabListClassName,
|
||||
addBorders && '!-ml-1',
|
||||
'flex items-center hide-scrollbars mb-2',
|
||||
layout === 'horizontal' && 'h-full overflow-auto p-2',
|
||||
layout === 'horizontal' && 'h-full overflow-auto p-2 -mr-2',
|
||||
layout === 'vertical' && 'overflow-x-auto overflow-y-visible ',
|
||||
// Give space for button focus states within overflow boundary.
|
||||
layout === 'vertical' && 'py-1 -ml-5 pl-3 pr-1',
|
||||
@@ -108,7 +108,7 @@ export function Tabs({
|
||||
: layout === 'vertical'
|
||||
? 'border-border-subtle'
|
||||
: 'border-transparent',
|
||||
layout === 'horizontal' && 'flex justify-between',
|
||||
layout === 'horizontal' && 'flex justify-between min-w-[10rem]',
|
||||
);
|
||||
|
||||
if ('options' in t) {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { updateSchema } from 'cm6-graphql';
|
||||
|
||||
import { formatSdl } from 'format-graphql';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { useIntrospectGraphQL } from '../../hooks/useIntrospectGraphQL';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
@@ -63,12 +62,6 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
|
||||
onChange(newBody);
|
||||
};
|
||||
|
||||
// Refetch the schema when the URL changes
|
||||
useEffect(() => {
|
||||
if (editorViewRef.current == null) return;
|
||||
updateSchema(editorViewRef.current, schema ?? undefined);
|
||||
}, [schema]);
|
||||
|
||||
const actions = useMemo<EditorProps['actions']>(
|
||||
() => [
|
||||
<div key="actions" className="flex flex-row !opacity-100 !shadow">
|
||||
@@ -201,6 +194,7 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
|
||||
<Editor
|
||||
language="graphql"
|
||||
heightMode="auto"
|
||||
graphQLSchema={schema}
|
||||
format={formatSdl}
|
||||
defaultValue={currentBody.query}
|
||||
onChange={handleChangeQuery}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function useAuthTab<T extends string>(tabValue: T, model: AuthenticatedMo
|
||||
<IconTooltip
|
||||
icon="magic_wand"
|
||||
iconSize="xs"
|
||||
content="Authenticatin was inherited from an ancestor"
|
||||
content="Authentication was inherited from an ancestor"
|
||||
/>
|
||||
</HStack>
|
||||
) : (
|
||||
|
||||
@@ -21,6 +21,10 @@ export function useFilterResponse({
|
||||
filter,
|
||||
})) as FilterResponse;
|
||||
|
||||
if (result.error) {
|
||||
console.log("Failed to filter response:", result.error);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -26,8 +26,8 @@ export type HotkeyAction =
|
||||
| 'workspace_settings.show';
|
||||
|
||||
const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'app.zoom_in': ['CmdCtrl+='],
|
||||
'app.zoom_out': ['CmdCtrl+-'],
|
||||
'app.zoom_in': ['CmdCtrl+Equal'],
|
||||
'app.zoom_out': ['CmdCtrl+Minus'],
|
||||
'app.zoom_reset': ['CmdCtrl+0'],
|
||||
'command_palette.toggle': ['CmdCtrl+k'],
|
||||
'environmentEditor.toggle': ['CmdCtrl+Shift+E', 'CmdCtrl+Shift+e'],
|
||||
@@ -67,6 +67,8 @@ const hotkeyLabels: Record<HotkeyAction, string> = {
|
||||
'workspace_settings.show': 'Open Workspace Settings',
|
||||
};
|
||||
|
||||
const layoutInsensitiveKeys = ['Equal', 'Minus', 'BracketLeft', 'BracketRight', 'Backquote'];
|
||||
|
||||
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
|
||||
|
||||
interface Options {
|
||||
@@ -106,7 +108,8 @@ export function useHotKey(
|
||||
return;
|
||||
}
|
||||
|
||||
currentKeys.current.add(e.key);
|
||||
const keyToAdd = layoutInsensitiveKeys.includes(e.code) ? e.code : e.key;
|
||||
currentKeys.current.add(keyToAdd);
|
||||
|
||||
const currentKeysWithModifiers = new Set(currentKeys.current);
|
||||
if (e.altKey) currentKeysWithModifiers.add('Alt');
|
||||
@@ -150,7 +153,9 @@ export function useHotKey(
|
||||
if (options.enable === false) {
|
||||
return;
|
||||
}
|
||||
currentKeys.current.delete(e.key);
|
||||
|
||||
const keyToRemove = layoutInsensitiveKeys.includes(e.code) ? e.code : e.key;
|
||||
currentKeys.current.delete(keyToRemove);
|
||||
|
||||
// Clear all keys if no longer holding modifier
|
||||
// HACK: This is to get around the case of DOWN SHIFT -> DOWN : -> UP SHIFT -> UP ;
|
||||
|
||||
@@ -12,12 +12,16 @@ import { useAtomValue } from 'jotai';
|
||||
import { md5 } from 'js-md5';
|
||||
import { useState } from 'react';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { activeEnvironmentIdAtom } from './useActiveEnvironment';
|
||||
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
|
||||
|
||||
export function useHttpAuthenticationConfig(
|
||||
authName: string | null,
|
||||
values: Record<string, JsonPrimitive>,
|
||||
requestId: string,
|
||||
) {
|
||||
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
|
||||
const environmentId = useAtomValue(activeEnvironmentIdAtom);
|
||||
const responses = useAtomValue(httpResponsesAtom);
|
||||
const [forceRefreshCounter, setForceRefreshCounter] = useState<number>(0);
|
||||
|
||||
@@ -38,6 +42,8 @@ export function useHttpAuthenticationConfig(
|
||||
values,
|
||||
responseKey,
|
||||
forceRefreshCounter,
|
||||
workspaceId,
|
||||
environmentId,
|
||||
],
|
||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||
queryFn: async () => {
|
||||
@@ -48,6 +54,8 @@ export function useHttpAuthenticationConfig(
|
||||
authName,
|
||||
values,
|
||||
requestId,
|
||||
workspaceId,
|
||||
environmentId,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -64,6 +72,8 @@ export function useHttpAuthenticationConfig(
|
||||
authName,
|
||||
values,
|
||||
modelId,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
// Ensure the config is refreshed after the action is done
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { minPromiseMillis } from '../lib/minPromiseMillis';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
|
||||
@@ -8,10 +9,9 @@ export function useRenderTemplate(template: string) {
|
||||
const workspaceId = useAtomValue(activeWorkspaceIdAtom) ?? 'n/a';
|
||||
const environmentId = useActiveEnvironment()?.id ?? null;
|
||||
return useQuery<string>({
|
||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||
refetchOnWindowFocus: false,
|
||||
queryKey: ['render_template', template, workspaceId, environmentId],
|
||||
queryFn: () => renderTemplate({ template, workspaceId, environmentId }),
|
||||
queryFn: () => minPromiseMillis(renderTemplate({ template, workspaceId, environmentId }), 200),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
export function useTemplateTokensToString(tokens: Tokens) {
|
||||
return useQuery<string>({
|
||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||
refetchOnWindowFocus: false,
|
||||
queryKey: ['template_tokens_to_string', tokens],
|
||||
queryFn: () => templateTokensToString(tokens),
|
||||
|
||||
@@ -33,7 +33,7 @@ export function resolvedModelName(r: AnyModel | null): string {
|
||||
}
|
||||
|
||||
// Strip unnecessary protocol
|
||||
const withoutProto = withoutVariables.replace(/^https?:\/\//, '');
|
||||
const withoutProto = withoutVariables.replace(/^(http|https|ws|wss):\/\//, '');
|
||||
|
||||
return withoutProto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user