Compare commits

...

14 Commits

Author SHA1 Message Date
Gregory Schier
c29b3c6509 Change version 2024-06-17 15:09:44 -07:00
Gregory Schier
7faa423aba Recursive environments 2024-06-17 12:24:06 -07:00
Gregory Schier
5b2162e48d Fix flash loading response viewer 2024-06-17 11:43:45 -07:00
Gregory Schier
ee776143b2 Fix parsing notification timestamp 2024-06-17 11:43:20 -07:00
Gregory Schier
7a18fb29e4 More dynamic plugin access 2024-06-15 22:13:01 -07:00
Gregory Schier
4485cad9e8 Workspace dropdown to RadioDropdown 2024-06-14 17:07:35 -07:00
Gregory Schier
33763b6d2f Fix plugins 2024-06-13 07:03:50 -07:00
Gregory Schier
641fe86cf7 Support nested functions 2024-06-12 23:13:36 -07:00
Gregory Schier
65e7c804d7 Parse window title and theme better 2024-06-12 10:15:14 -07:00
Gregory Schier
23ec8bee8f Move binary detection to TextViewer 2024-06-12 09:47:34 -07:00
Gregory Schier
8aeeaa2e09 Bump version 2024-06-12 00:23:50 -07:00
Gregory Schier
57f01d249e Entitlement for v8/Deno 2024-06-12 00:23:32 -07:00
Gregory Schier
6c5a914db6 Variables in pair editor 2024-06-11 12:33:43 -07:00
Gregory Schier
155e51aa74 Put toast on top 2024-06-11 12:04:46 -07:00
53 changed files with 1358 additions and 405 deletions

Binary file not shown.

View File

@@ -1,5 +1,5 @@
{
"name": "yaak-app",
"name": "yaak",
"private": true,
"version": "0.0.0",
"type": "module",

View File

@@ -10,6 +10,6 @@ export default defineConfig({
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
outDir: resolve(__dirname, '../../src-tauri/plugins/exporter-curl'),
},
});

View File

@@ -10,6 +10,6 @@ export default defineConfig({
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
outDir: resolve(__dirname, '../../src-tauri/plugins/filter-jsonpath'),
},
});

View File

@@ -10,6 +10,6 @@ export default defineConfig({
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
outDir: resolve(__dirname, '../../src-tauri/plugins/filter-xpath'),
},
});

View File

@@ -10,6 +10,6 @@ export default defineConfig({
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
outDir: resolve(__dirname, '../../src-tauri/plugins/importer-curl'),
},
});

View File

@@ -10,6 +10,6 @@ export default defineConfig({
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
outDir: resolve(__dirname, '../../src-tauri/plugins/importer-insomnia'),
},
});

View File

@@ -10,6 +10,6 @@ export default defineConfig({
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
outDir: resolve(__dirname, '../../src-tauri/plugins/importer-postman'),
},
});

View File

@@ -10,6 +10,6 @@ export default defineConfig({
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, 'build'),
outDir: resolve(__dirname, '../../src-tauri/plugins/importer-yaak'),
},
});

1
src-tauri/Cargo.lock generated
View File

@@ -8172,6 +8172,7 @@ dependencies = [
"tauri-plugin-updater",
"tauri-plugin-window-state",
"templates",
"thiserror",
"tokio",
"tokio-stream",
"uuid",

View File

@@ -56,3 +56,4 @@ tauri-plugin-window-state = "2.0.0-beta"
tokio = { version = "1.36.0", features = ["sync"] }
tokio-stream = "0.1.15"
uuid = "1.7.0"
thiserror = "1.0.61"

View File

@@ -1,10 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Re-enable for sandboxing. Currently disabled because auto-updater doesn't work with sandboxing.-->
<!-- <key>com.apple.security.app-sandbox</key> <true/>-->
<!-- <key>com.apple.security.files.user-selected.read-write</key> <true/>-->
<!-- <key>com.apple.security.network.client</key> <true/>-->
</dict>
<dict>
<!-- Enable for v8 execution -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<!-- Re-enable for sandboxing. Currently disabled because auto-updater doesn't work with sandboxing.-->
<!-- <key>com.apple.security.app-sandbox</key> <true/>-->
<!-- <key>com.apple.security.files.user-selected.read-write</key> <true/>-->
<!-- <key>com.apple.security.network.client</key> <true/>-->
</dict>
</plist>

View File

@@ -1,36 +1,37 @@
const o = `\\
`;
function d(n) {
var h, f, r, u, l, s;
const t = ["curl"];
n.method && t.push("-X", n.method), n.url && t.push(i(n.url)), t.push(o);
for (const a of (n.urlParameters ?? []).filter(p))
t.push("--url-query", i(`${a.name}=${a.value}`)), t.push(o);
for (const a of (n.headers ?? []).filter(p))
t.push("--header", i(`${a.name}: ${a.value}`)), t.push(o);
if (Array.isArray((h = n.body) == null ? void 0 : h.form)) {
const a = n.bodyType === "multipart/form-data" ? "--form" : "--data";
for (const e of (((f = n.body) == null ? void 0 : f.form) ?? []).filter(p)) {
function y(p, t) {
var f, r, u, l, s, c;
const n = ["curl"];
t.method && n.push("-X", t.method), t.url && n.push(i(t.url)), n.push(o);
for (const a of (t.urlParameters ?? []).filter(h))
n.push("--url-query", i(`${a.name}=${a.value}`)), n.push(o);
for (const a of (t.headers ?? []).filter(h))
n.push("--header", i(`${a.name}: ${a.value}`)), n.push(o);
if (Array.isArray((f = t.body) == null ? void 0 : f.form)) {
const a = t.bodyType === "multipart/form-data" ? "--form" : "--data";
for (const e of (((r = t.body) == null ? void 0 : r.form) ?? []).filter(h)) {
if (e.file) {
let c = `${e.name}=@${e.file}`;
c += e.contentType ? `;type=${e.contentType}` : "", t.push(a, c);
let d = `${e.name}=@${e.file}`;
d += e.contentType ? `;type=${e.contentType}` : "", n.push(a, d);
} else
t.push(a, i(`${e.name}=${e.value}`));
t.push(o);
n.push(a, i(`${e.name}=${e.value}`));
n.push(o);
}
} else
typeof ((r = n.body) == null ? void 0 : r.text) == "string" && (t.push("--data-raw", `$${i(n.body.text)}`), t.push(o));
return (n.authenticationType === "basic" || n.authenticationType === "digest") && (n.authenticationType === "digest" && t.push("--digest"), t.push(
typeof ((u = t.body) == null ? void 0 : u.text) == "string" && (n.push("--data-raw", `$${i(t.body.text)}`), n.push(o));
return (t.authenticationType === "basic" || t.authenticationType === "digest") && (t.authenticationType === "digest" && n.push("--digest"), n.push(
"--user",
i(`${((u = n.authentication) == null ? void 0 : u.username) ?? ""}:${((l = n.authentication) == null ? void 0 : l.password) ?? ""}`)
), t.push(o)), n.authenticationType === "bearer" && (t.push("--header", i(`Authorization: Bearer ${((s = n.authentication) == null ? void 0 : s.token) ?? ""}`)), t.push(o)), t[t.length - 1] === o && t.splice(t.length - 1, 1), t.join(" ");
i(`${((l = t.authentication) == null ? void 0 : l.username) ?? ""}:${((s = t.authentication) == null ? void 0 : s.password) ?? ""}`)
), n.push(o)), t.authenticationType === "bearer" && (n.push("--header", i(`Authorization: Bearer ${((c = t.authentication) == null ? void 0 : c.token) ?? ""}`)), n.push(o)), n[n.length - 1] === o && n.splice(n.length - 1, 1), n.join(" ");
}
function i(n) {
return `'${n.replace(/'/g, "\\'")}'`;
function i(p) {
return `'${p.replace(/'/g, "\\'")}'`;
}
function p(n) {
return n.enabled !== !1 && !!n.name;
function h(p) {
return p.enabled !== !1 && !!p.name;
}
export {
d as pluginHookExport
y as pluginHookExport
};
//# sourceMappingURL=index.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","sources":["../../../plugins/exporter-curl/src/index.ts"],"sourcesContent":["import { HttpRequest } from '../../../src-web/lib/models';\n\nconst NEWLINE = '\\\\\\n ';\n\nexport function pluginHookExport(_: any, request: Partial<HttpRequest>) {\n const xs = ['curl'];\n\n // Add method and URL all on first line\n if (request.method) xs.push('-X', request.method);\n if (request.url) xs.push(quote(request.url));\n\n xs.push(NEWLINE);\n\n // Add URL params\n for (const p of (request.urlParameters ?? []).filter(onlyEnabled)) {\n xs.push('--url-query', quote(`${p.name}=${p.value}`));\n xs.push(NEWLINE);\n }\n\n // Add headers\n for (const h of (request.headers ?? []).filter(onlyEnabled)) {\n xs.push('--header', quote(`${h.name}: ${h.value}`));\n xs.push(NEWLINE);\n }\n\n // Add form params\n if (Array.isArray(request.body?.form)) {\n const flag = request.bodyType === 'multipart/form-data' ? '--form' : '--data';\n for (const p of (request.body?.form ?? []).filter(onlyEnabled)) {\n if (p.file) {\n let v = `${p.name}=@${p.file}`;\n v += p.contentType ? `;type=${p.contentType}` : '';\n xs.push(flag, v);\n } else {\n xs.push(flag, quote(`${p.name}=${p.value}`));\n }\n xs.push(NEWLINE);\n }\n } else if (typeof request.body?.text === 'string') {\n // --data-raw $'...' to do special ANSI C quoting\n xs.push('--data-raw', `$${quote(request.body.text)}`);\n xs.push(NEWLINE);\n }\n\n // Add basic/digest authentication\n if (request.authenticationType === 'basic' || request.authenticationType === 'digest') {\n if (request.authenticationType === 'digest') xs.push('--digest');\n xs.push(\n '--user',\n quote(`${request.authentication?.username ?? ''}:${request.authentication?.password ?? ''}`),\n );\n xs.push(NEWLINE);\n }\n\n // Add bearer authentication\n if (request.authenticationType === 'bearer') {\n xs.push('--header', quote(`Authorization: Bearer ${request.authentication?.token ?? ''}`));\n xs.push(NEWLINE);\n }\n\n // Remove trailing newline\n if (xs[xs.length - 1] === NEWLINE) {\n xs.splice(xs.length - 1, 1);\n }\n\n return xs.join(' ');\n}\n\nfunction quote(arg: string): string {\n const escaped = arg.replace(/'/g, \"\\\\'\");\n return `'${escaped}'`;\n}\n\nfunction onlyEnabled(v: { name?: string; enabled?: boolean }): boolean {\n return v.enabled !== false && !!v.name;\n}\n"],"names":["NEWLINE","pluginHookExport","_","request","_a","_b","_c","_d","_e","_f","xs","quote","p","onlyEnabled","h","flag","v","arg"],"mappings":"AAEA,MAAMA,IAAU;AAAA;AAEA,SAAAC,EAAiBC,GAAQC,GAA+B;AAFxE,MAAAC,GAAAC,GAAAC,GAAAC,GAAAC,GAAAC;AAGQ,QAAAC,IAAK,CAAC,MAAM;AAGlB,EAAIP,EAAQ,UAAWO,EAAA,KAAK,MAAMP,EAAQ,MAAM,GAC5CA,EAAQ,OAAKO,EAAG,KAAKC,EAAMR,EAAQ,GAAG,CAAC,GAE3CO,EAAG,KAAKV,CAAO;AAGf,aAAWY,MAAMT,EAAQ,iBAAiB,IAAI,OAAOU,CAAW;AAC3D,IAAAH,EAAA,KAAK,eAAeC,EAAM,GAAGC,EAAE,IAAI,IAAIA,EAAE,KAAK,EAAE,CAAC,GACpDF,EAAG,KAAKV,CAAO;AAIjB,aAAWc,MAAMX,EAAQ,WAAW,IAAI,OAAOU,CAAW;AACrD,IAAAH,EAAA,KAAK,YAAYC,EAAM,GAAGG,EAAE,IAAI,KAAKA,EAAE,KAAK,EAAE,CAAC,GAClDJ,EAAG,KAAKV,CAAO;AAIjB,MAAI,MAAM,SAAQI,IAAAD,EAAQ,SAAR,gBAAAC,EAAc,IAAI,GAAG;AACrC,UAAMW,IAAOZ,EAAQ,aAAa,wBAAwB,WAAW;AAC1D,eAAAS,QAAMP,IAAAF,EAAQ,SAAR,gBAAAE,EAAc,SAAQ,CAAI,GAAA,OAAOQ,CAAW,GAAG;AAC9D,UAAID,EAAE,MAAM;AACV,YAAII,IAAI,GAAGJ,EAAE,IAAI,KAAKA,EAAE,IAAI;AAC5B,QAAAI,KAAKJ,EAAE,cAAc,SAASA,EAAE,WAAW,KAAK,IAC7CF,EAAA,KAAKK,GAAMC,CAAC;AAAA,MAAA;AAEZ,QAAAN,EAAA,KAAKK,GAAMJ,EAAM,GAAGC,EAAE,IAAI,IAAIA,EAAE,KAAK,EAAE,CAAC;AAE7C,MAAAF,EAAG,KAAKV,CAAO;AAAA,IACjB;AAAA,EACS;AAAA,IAAA,SAAOM,IAAAH,EAAQ,SAAR,gBAAAG,EAAc,SAAS,aAEpCI,EAAA,KAAK,cAAc,IAAIC,EAAMR,EAAQ,KAAK,IAAI,CAAC,EAAE,GACpDO,EAAG,KAAKV,CAAO;AAIjB,UAAIG,EAAQ,uBAAuB,WAAWA,EAAQ,uBAAuB,cACvEA,EAAQ,uBAAuB,YAAUO,EAAG,KAAK,UAAU,GAC5DA,EAAA;AAAA,IACD;AAAA,IACAC,EAAM,KAAGJ,IAAAJ,EAAQ,mBAAR,gBAAAI,EAAwB,aAAY,EAAE,MAAIC,IAAAL,EAAQ,mBAAR,gBAAAK,EAAwB,aAAY,EAAE,EAAE;AAAA,EAAA,GAE7FE,EAAG,KAAKV,CAAO,IAIbG,EAAQ,uBAAuB,aAC9BO,EAAA,KAAK,YAAYC,EAAM,2BAAyBF,IAAAN,EAAQ,mBAAR,gBAAAM,EAAwB,UAAS,EAAE,EAAE,CAAC,GACzFC,EAAG,KAAKV,CAAO,IAIbU,EAAGA,EAAG,SAAS,CAAC,MAAMV,KACxBU,EAAG,OAAOA,EAAG,SAAS,GAAG,CAAC,GAGrBA,EAAG,KAAK,GAAG;AACpB;AAEA,SAASC,EAAMM,GAAqB;AAElC,SAAO,IADSA,EAAI,QAAQ,MAAM,KAAK,CACrB;AACpB;AAEA,SAASJ,EAAYG,GAAkD;AACrE,SAAOA,EAAE,YAAY,MAAS,CAAC,CAACA,EAAE;AACpC;"}

View File

@@ -1,15 +1,15 @@
var Xe = typeof globalThis < "u" ? globalThis : typeof window < "u" ? window : typeof global < "u" ? global : typeof self < "u" ? self : {};
function Nt(le) {
return le && le.__esModule && Object.prototype.hasOwnProperty.call(le, "default") ? le.default : le;
function Nt(ce) {
return ce && ce.__esModule && Object.prototype.hasOwnProperty.call(ce, "default") ? ce.default : ce;
}
function Be(le) {
throw new Error('Could not dynamically require "' + le + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.');
function Be(ce) {
throw new Error('Could not dynamically require "' + ce + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.');
}
var Ve = { exports: {} };
/*! jsonpath 1.1.1 */
(function(le, Ne) {
(function(ce, Ne) {
(function(T) {
le.exports = T();
ce.exports = T();
})(function() {
return function T(L, B, v) {
function y(E, d) {
@@ -1131,7 +1131,7 @@ var Ve = { exports: {} };
function Ze() {
var e = [], t;
for (t = i, O("["); !_("]"); )
_(",") ? (N(), e.push(null)) : (e.push(ce()), _("]") || O(","));
_(",") ? (N(), e.push(null)) : (e.push(le()), _("]") || O(","));
return N(), n.markEnd(n.createArrayExpression(e), t);
}
function _e(e, t) {
@@ -1145,11 +1145,11 @@ var Ve = { exports: {} };
function et() {
var e, t, u, l, C, x;
if (e = i, x = i, e.type === y.Identifier)
return u = De(), e.value === "get" && !_(":") ? (t = De(), O("("), O(")"), l = _e([]), n.markEnd(n.createProperty("get", t, l), x)) : e.value === "set" && !_(":") ? (t = De(), O("("), e = i, e.type !== y.Identifier ? (O(")"), G(e, a.UnexpectedToken, e.value), l = _e([])) : (C = [ye()], O(")"), l = _e(C, e)), n.markEnd(n.createProperty("set", t, l), x)) : (O(":"), l = ce(), n.markEnd(n.createProperty("init", u, l), x));
return u = De(), e.value === "get" && !_(":") ? (t = De(), O("("), O(")"), l = _e([]), n.markEnd(n.createProperty("get", t, l), x)) : e.value === "set" && !_(":") ? (t = De(), O("("), e = i, e.type !== y.Identifier ? (O(")"), G(e, a.UnexpectedToken, e.value), l = _e([])) : (C = [ye()], O(")"), l = _e(C, e)), n.markEnd(n.createProperty("set", t, l), x)) : (O(":"), l = le(), n.markEnd(n.createProperty("init", u, l), x));
if (e.type === y.EOF || e.type === y.Punctuator)
pe(e);
else
return t = De(), O(":"), l = ce(), n.markEnd(n.createProperty("init", t, l), x);
return t = De(), O(":"), l = le(), n.markEnd(n.createProperty("init", t, l), x);
}
function tt() {
var e = [], t, u, l, C, x = {}, j = String, W;
@@ -1184,7 +1184,7 @@ var Ve = { exports: {} };
function je() {
var e = [];
if (O("("), !_(")"))
for (; r < h && (e.push(ce()), !_(")")); )
for (; r < h && (e.push(le()), !_(")")); )
O(",");
return O(")"), e;
}
@@ -1300,17 +1300,17 @@ var Ve = { exports: {} };
}
function st() {
var e, t, u, l, C;
return C = i, e = at(), _("?") && (N(), t = p.allowIn, p.allowIn = !0, u = ce(), p.allowIn = t, O(":"), l = ce(), e = n.createConditionalExpression(e, u, l), n.markEnd(e, C)), e;
return C = i, e = at(), _("?") && (N(), t = p.allowIn, p.allowIn = !0, u = le(), p.allowIn = t, O(":"), l = le(), e = n.createConditionalExpression(e, u, l), n.markEnd(e, C)), e;
}
function ce() {
function le() {
var e, t, u, l, C;
return e = i, C = i, l = t = st(), Ye() && (be(t) || G({}, a.InvalidLHSInAssignment), c && t.type === E.Identifier && J(t.name) && G(e, a.StrictLHSAssignment), e = N(), u = ce(), l = n.markEnd(n.createAssignmentExpression(e.value, t, u), C)), l;
return e = i, C = i, l = t = st(), Ye() && (be(t) || G({}, a.InvalidLHSInAssignment), c && t.type === E.Identifier && J(t.name) && G(e, a.StrictLHSAssignment), e = N(), u = le(), l = n.markEnd(n.createAssignmentExpression(e.value, t, u), C)), l;
}
function te() {
var e, t = i;
if (e = ce(), _(",")) {
if (e = le(), _(",")) {
for (e = n.createSequenceExpression([e]); r < h && _(","); )
N(), e.expressions.push(ce());
N(), e.expressions.push(le());
n.markEnd(e, t);
}
return e;
@@ -1330,7 +1330,7 @@ var Ve = { exports: {} };
}
function lt(e) {
var t = null, u, l;
return l = i, u = ye(), c && J(u.name) && G({}, a.StrictVarName), e === "const" ? (O("="), t = ce()) : _("=") && (N(), t = ce()), n.markEnd(n.createVariableDeclarator(u, t), l);
return l = i, u = ye(), c && J(u.name) && G({}, a.StrictVarName), e === "const" ? (O("="), t = le()) : _("=") && (N(), t = le()), n.markEnd(n.createVariableDeclarator(u, t), l);
}
function Te(e) {
var t = [];
@@ -3176,16 +3176,17 @@ Expecting ` + se.join(", ") + ", got '" + (this.terminals_[w] || w) + "'" : ie =
})(Ve);
var Pt = Ve.exports;
const Lt = /* @__PURE__ */ Nt(Pt);
function Rt(le, Ne) {
let T;
function Rt(ce, Ne, T) {
let L;
try {
T = JSON.parse(Ne);
L = JSON.parse(T);
} catch {
return;
}
const L = Lt.query(T, le);
return { filtered: JSON.stringify(L, null, 2) };
const B = Lt.query(L, Ne);
return { filtered: JSON.stringify(B, null, 2) };
}
export {
Rt as pluginHookResponseFilter
};
//# sourceMappingURL=index.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -6116,10 +6116,11 @@ Tr.__DOMHandler = Dr;
Tr.normalizeLineEndings = Rt;
Tr.DOMParser = _t;
var Cu = Tr.DOMParser;
function yu(t, u) {
const n = new Cu().parseFromString(u, "text/xml");
return { filtered: `${Qt.select(t, n)}` };
function yu(t, u, n) {
const s = new Cu().parseFromString(n, "text/xml");
return { filtered: `${Qt.select(u, s)}` };
}
export {
yu as pluginHookResponseFilter
};
//# sourceMappingURL=index.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,298 @@
var j = "(?:" + [
"\\|\\|",
"\\&\\&",
";;",
"\\|\\&",
"\\<\\(",
"\\<\\<\\<",
">>",
">\\&",
"<\\&",
"[&;()|<>]"
].join("|") + ")", D = new RegExp("^" + j + "$"), _ = "|&;()<> \\t", M = '"((\\\\"|[^"])*?)"', Q = "'((\\\\'|[^'])*?)'", V = /^#$/, q = "'", G = '"', U = "$", R = "", z = 4294967296;
for (var L = 0; L < 4; L++)
R += (z * Math.random()).toString(16);
var J = new RegExp("^" + R);
function X(n, s) {
for (var t = s.lastIndex, r = [], l; l = s.exec(n); )
r.push(l), s.lastIndex === l.index && (s.lastIndex += 1);
return s.lastIndex = t, r;
}
function F(n, s, t) {
var r = typeof n == "function" ? n(t) : n[t];
return typeof r > "u" && t != "" ? r = "" : typeof r > "u" && (r = "$"), typeof r == "object" ? s + R + JSON.stringify(r) + R : s + r;
}
function K(n, s, t) {
t || (t = {});
var r = t.escape || "\\", l = "(\\" + r + `['"` + _ + `]|[^\\s'"` + _ + "])+", h = new RegExp([
"(" + j + ")",
// control chars
"(" + l + "|" + M + "|" + Q + ")+"
].join("|"), "g"), p = X(n, h);
if (p.length === 0)
return [];
s || (s = {});
var g = !1;
return p.map(function(d) {
var e = d[0];
if (!e || g)
return;
if (D.test(e))
return { op: e };
var c = !1, T = !1, m = "", O = !1, a;
function $() {
a += 1;
var x, f, C = e.charAt(a);
if (C === "{") {
if (a += 1, e.charAt(a) === "}")
throw new Error("Bad substitution: " + e.slice(a - 2, a + 1));
if (x = e.indexOf("}", a), x < 0)
throw new Error("Bad substitution: " + e.slice(a));
f = e.slice(a, x), a = x;
} else if (/[*@#?$!_-]/.test(C))
f = C, a += 1;
else {
var w = e.slice(a);
x = w.match(/[^\w\d_]/), x ? (f = w.slice(0, x.index), a += x.index - 1) : (f = w, a = e.length);
}
return F(s, "", f);
}
for (a = 0; a < e.length; a++) {
var u = e.charAt(a);
if (O = O || !c && (u === "*" || u === "?"), T)
m += u, T = !1;
else if (c)
u === c ? c = !1 : c == q ? m += u : u === r ? (a += 1, u = e.charAt(a), u === G || u === r || u === U ? m += u : m += r + u) : u === U ? m += $() : m += u;
else if (u === G || u === q)
c = u;
else {
if (D.test(u))
return { op: e };
if (V.test(u)) {
g = !0;
var b = { comment: n.slice(d.index + a + 1) };
return m.length ? [m, b] : [b];
} else
u === r ? T = !0 : u === U ? m += $() : m += u;
}
}
return O ? { op: "glob", pattern: m } : m;
}).reduce(function(d, e) {
return typeof e > "u" ? d : d.concat(e);
}, []);
}
var Y = function(s, t, r) {
var l = K(s, t, r);
return typeof t != "function" ? l : l.reduce(function(h, p) {
if (typeof p == "object")
return h.concat(p);
var g = p.split(RegExp("(" + R + ".*?" + R + ")", "g"));
return g.length === 1 ? h.concat(g[0]) : h.concat(g.filter(Boolean).map(function(d) {
return J.test(d) ? JSON.parse(d.split(R)[1]) : d;
}));
}, []);
}, Z = Y;
const ae = "curl", se = "cURL", ie = "cURL command line tool", H = ["d", "data", "data-raw", "data-urlencode", "data-binary", "data-ascii"], ee = [
["url"],
// Specify the URL explicitly
["user", "u"],
// Authentication
["digest"],
// Apply auth as digest
["header", "H"],
["cookie", "b"],
["get", "G"],
// Put the post data in the URL
["d", "data"],
// Add url encoded data
["data-raw"],
["data-urlencode"],
["data-binary"],
["data-ascii"],
["form", "F"],
// Add multipart data
["request", "X"],
// Request method
H
].flatMap((n) => n);
function oe(n, s) {
if (!s.match(/^\s*curl /))
return null;
const t = [], r = s.replace(/\ncurl/g, "; curl");
let l = [];
const p = Z(r).flatMap((e) => typeof e == "string" && e.startsWith("-") && !e.startsWith("--") && e.length > 2 ? [e.slice(0, 2), e.slice(2)] : e);
for (const e of p) {
if (typeof e == "string") {
e.startsWith("$") ? l.push(e.slice(1)) : l.push(e);
continue;
}
if ("comment" in e)
continue;
const { op: c } = e;
if (c === ";") {
t.push(l), l = [];
continue;
}
if (c != null && c.startsWith("$")) {
const T = c.slice(2, c.length - 1).replace(/\\'/g, "'");
l.push(T);
continue;
}
c === "glob" && l.push(e.pattern);
}
t.push(l);
const g = {
model: "workspace",
id: N("workspace"),
name: "Curl Import"
};
return {
resources: {
httpRequests: t.filter((e) => e[0] === "curl").map((e) => te(e, g.id)),
workspaces: [g]
}
};
}
function te(n, s) {
const t = {}, r = [];
for (let i = 1; i < n.length; i++) {
let o = n[i];
if (typeof o == "string" && (o = o.trim()), typeof o == "string" && o.match(/^-{1,2}[\w-]+/)) {
const E = o[0] === "-" && o[1] !== "-";
let v = o.replace(/^-{1,2}/, "");
if (!ee.includes(v))
continue;
let y;
const S = n[i + 1];
E && v.length > 1 ? (y = v.slice(1), v = v.slice(0, 1)) : typeof S == "string" && !S.startsWith("-") ? (y = S, i++) : y = !0, t[v] = t[v] || [], t[v].push(y);
} else
o && r.push(o);
}
let l, h;
const p = A(t, r[0] || "", ["url"]), [g, d] = W(p, "?");
l = (d == null ? void 0 : d.split("&").map((i) => {
const o = W(i, "=");
return { name: o[0] ?? "", value: o[1] ?? "", enabled: !0 };
})) ?? [], h = g ?? p;
const [e, c] = A(t, "", ["u", "user"]).split(/:(.*)$/), T = A(t, !1, ["digest"]), m = e ? T ? "digest" : "basic" : null, O = e ? {
username: e.trim(),
password: (c ?? "").trim()
} : {}, a = [
...t.header || [],
...t.H || []
].map((i) => {
const [o, E] = i.split(/:(.*)$/);
return E ? {
name: (o ?? "").trim(),
value: E.trim(),
enabled: !0
} : {
name: (o ?? "").trim().replace(/;$/, ""),
value: "",
enabled: !0
};
}), $ = [
...t.cookie || [],
...t.b || []
].map((i) => {
const o = i.split("=", 1)[0], E = i.replace(`${o}=`, "");
return `${o}=${E}`;
}).join("; "), u = a.find((i) => i.name.toLowerCase() === "cookie");
$ && u ? u.value += `; ${$}` : $ && a.push({
name: "Cookie",
value: $,
enabled: !0
});
const b = ne(t), x = a.find((i) => i.name.toLowerCase() === "content-type"), f = x ? x.value.split(";")[0] : null, C = [
...t.form || [],
...t.F || []
].map((i) => {
const o = i.split("="), E = o[0] ?? "", v = o[1] ?? "", y = {
name: E,
enabled: !0
};
return v.indexOf("@") === 0 ? y.file = v.slice(1) : y.value = v, y;
});
let w = {}, I = null;
const B = A(t, !1, ["G", "get"]);
b.length > 0 && B ? l.push(...b) : b.length > 0 && (f == null || f === "application/x-www-form-urlencoded") ? (I = f ?? "application/x-www-form-urlencoded", w = {
form: b.map((i) => ({
...i,
name: decodeURIComponent(i.name || ""),
value: decodeURIComponent(i.value || "")
}))
}, a.push({
name: "Content-Type",
value: "application/x-www-form-urlencoded",
enabled: !0
})) : b.length > 0 ? (I = f === "application/json" || f === "text/xml" || f === "text/plain" ? f : "other", w = {
text: b.map(({ name: i, value: o }) => i && o ? `${i}=${o}` : i || o).join("&")
}) : C.length && (I = f ?? "multipart/form-data", w = {
form: C
}, f == null && a.push({
name: "Content-Type",
value: "multipart/form-data",
enabled: !0
}));
let P = A(t, "", ["X", "request"]).toUpperCase();
return P === "" && w && (P = "text" in w || "form" in w ? "POST" : "GET"), {
id: N("http_request"),
model: "http_request",
workspaceId: s,
name: "",
urlParameters: l,
url: h,
method: P,
headers: a,
authentication: O,
authenticationType: m,
body: w,
bodyType: I,
folderId: null,
sortPriority: 0
};
}
const ne = (n) => {
let s = [];
for (const t of H) {
const r = n[t];
if (!(!r || r.length === 0))
for (const l of r) {
if (typeof l != "string")
continue;
const [h, p] = l.split("=");
l.startsWith("@") ? s.push({
name: h ?? "",
value: "",
filePath: l.slice(1),
enabled: !0
}) : s.push({
name: h ?? "",
value: t === "data-urlencode" ? encodeURIComponent(p ?? "") : p ?? "",
enabled: !0
});
}
}
return s;
}, A = (n, s, t) => {
for (const r of t)
if (n[r] && n[r].length)
return n[r][0];
return s;
};
function W(n, s) {
const t = n.indexOf(s);
return t > -1 ? [n.slice(0, t), n.slice(t + 1)] : [n];
}
const k = {};
function N(n) {
return k[n] = (k[n] ?? -1) + 1, `GENERATE_ID::${n.toUpperCase()}_${k[n]}`;
}
export {
ie as description,
ae as id,
te as importCommand,
se as name,
oe as pluginHookImport
};
//# sourceMappingURL=index.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,184 @@
function O(e, o) {
let n;
try {
n = JSON.parse(o);
} catch {
}
try {
n = n ?? YAML.parse(o);
} catch (a) {
console.log("FAILED", a);
}
if (!f(n) || !Array.isArray(n.resources))
return;
const t = {
workspaces: [],
httpRequests: [],
grpcRequests: [],
environments: [],
folders: []
}, i = n.resources.filter(_);
for (const a of i) {
const s = n.resources.find(
(l) => y(l) && l.parentId === a._id
);
t.workspaces.push({
id: p(a._id),
createdAt: new Date(i.created ?? Date.now()).toISOString().replace("Z", ""),
updatedAt: new Date(i.updated ?? Date.now()).toISOString().replace("Z", ""),
model: "workspace",
name: a.name,
variables: s ? h(s.data) : []
});
const d = n.resources.filter(
(l) => y(l) && l.parentId === (s == null ? void 0 : s._id)
);
t.environments.push(
...d.map((l) => b(l, a._id))
);
const c = (l) => {
const w = n.resources.filter((u) => u.parentId === l);
let r = 0;
for (const u of w)
D(u) ? (t.folders.push(I(u, a._id)), c(u._id)) : v(u) ? t.httpRequests.push(
g(u, a._id, r++)
) : q(u) && t.grpcRequests.push(
S(u, a._id, r++)
);
};
c(a._id);
}
return t.httpRequests = t.httpRequests.filter(Boolean), t.grpcRequests = t.grpcRequests.filter(Boolean), t.environments = t.environments.filter(Boolean), t.workspaces = t.workspaces.filter(Boolean), { resources: t };
}
function b(e, o) {
return {
id: p(e._id),
createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""),
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""),
workspaceId: p(o),
model: "environment",
name: e.name,
variables: Object.entries(e.data).map(([n, t]) => ({
enabled: !0,
name: n,
value: `${t}`
}))
};
}
function I(e, o) {
return {
id: p(e._id),
createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""),
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""),
folderId: e.parentId === o ? null : p(e.parentId),
workspaceId: p(o),
model: "folder",
name: e.name
};
}
function S(e, o, n = 0) {
var s;
const t = e.protoMethodName.split("/").filter((d) => d !== ""), i = t[0] ?? null, a = t[1] ?? null;
return {
id: p(e._id),
createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""),
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""),
workspaceId: p(o),
folderId: e.parentId === o ? null : p(e.parentId),
model: "grpc_request",
sortPriority: n,
name: e.name,
url: m(e.url),
service: i,
method: a,
message: ((s = e.body) == null ? void 0 : s.text) ?? "",
metadata: (e.metadata ?? []).map((d) => ({
enabled: !d.disabled,
name: d.name ?? "",
value: d.value ?? ""
})).filter(({ name: d, value: c }) => d !== "" || c !== "")
};
}
function g(e, o, n = 0) {
var d, c, l, w;
let t = null, i = {};
e.body.mimeType === "application/octet-stream" ? (t = "binary", i = { filePath: e.body.fileName ?? "" }) : ((d = e.body) == null ? void 0 : d.mimeType) === "application/x-www-form-urlencoded" ? (t = "application/x-www-form-urlencoded", i = {
form: (e.body.params ?? []).map((r) => ({
enabled: !r.disabled,
name: r.name ?? "",
value: r.value ?? ""
}))
}) : ((c = e.body) == null ? void 0 : c.mimeType) === "multipart/form-data" ? (t = "multipart/form-data", i = {
form: (e.body.params ?? []).map((r) => ({
enabled: !r.disabled,
name: r.name ?? "",
value: r.value ?? "",
file: r.fileName ?? null
}))
}) : ((l = e.body) == null ? void 0 : l.mimeType) === "application/graphql" ? (t = "graphql", i = { text: m(e.body.text ?? "") }) : ((w = e.body) == null ? void 0 : w.mimeType) === "application/json" && (t = "application/json", i = { text: m(e.body.text ?? "") });
let a = null, s = {};
return e.authentication.type === "bearer" ? (a = "bearer", s = {
token: m(e.authentication.token)
}) : e.authentication.type === "basic" && (a = "basic", s = {
username: m(e.authentication.username),
password: m(e.authentication.password)
}), {
id: p(e._id),
createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""),
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""),
workspaceId: p(o),
folderId: e.parentId === o ? null : p(e.parentId),
model: "http_request",
sortPriority: n,
name: e.name,
url: m(e.url),
body: i,
bodyType: t,
authentication: s,
authenticationType: a,
method: e.method,
headers: (e.headers ?? []).map((r) => ({
enabled: !r.disabled,
name: r.name ?? "",
value: r.value ?? ""
})).filter(({ name: r, value: u }) => r !== "" || u !== "")
};
}
function h(e) {
return Object.entries(e).map(([o, n]) => ({
enabled: !0,
name: o,
value: `${n}`
}));
}
function m(e) {
return A(e) ? e.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : e;
}
function _(e) {
return f(e) && e._type === "workspace";
}
function D(e) {
return f(e) && e._type === "request_group";
}
function v(e) {
return f(e) && e._type === "request";
}
function q(e) {
return f(e) && e._type === "grpc_request";
}
function y(e) {
return f(e) && e._type === "environment";
}
function f(e) {
return Object.prototype.toString.call(e) === "[object Object]";
}
function A(e) {
return Object.prototype.toString.call(e) === "[object String]";
}
function p(e) {
return e.startsWith("GENERATE_ID::") ? e : `GENERATE_ID::${e}`;
}
export {
O as pluginHookImport
};
//# sourceMappingURL=index.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,184 @@
const _ = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", C = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", O = [C, _];
function v(e, t) {
var w;
console.log("CTX", e);
const a = q(t);
if (a == null)
return;
const s = i(a.info);
if (!O.includes(s.schema) || !Array.isArray(a.item))
return;
const u = k(a.auth), n = {
workspaces: [],
environments: [],
httpRequests: [],
folders: []
}, p = {
model: "workspace",
id: b("workspace"),
name: s.name || "Postman Import",
description: s.description || "",
variables: ((w = a.variable) == null ? void 0 : w.map((r) => ({
name: r.key,
value: r.value
}))) ?? []
};
n.workspaces.push(p);
const g = (r, d = null) => {
if (typeof r.name == "string" && Array.isArray(r.item)) {
const o = {
model: "folder",
workspaceId: p.id,
id: b("folder"),
name: r.name,
folderId: d
};
n.folders.push(o);
for (const l of r.item)
g(l, o.id);
} else if (typeof r.name == "string" && "request" in r) {
const o = i(r.request), l = j(o.body), A = k(o.auth), m = A.authenticationType == null ? u : A, S = {
model: "http_request",
id: b("http_request"),
workspaceId: p.id,
folderId: d,
name: r.name,
method: o.method || "GET",
url: typeof o.url == "string" ? o.url : i(o.url).raw,
body: l.body,
bodyType: l.bodyType,
authentication: m.authentication,
authenticationType: m.authenticationType,
headers: [
...l.headers,
...m.headers,
...f(o.header).map((y) => ({
name: y.key,
value: y.value,
enabled: !y.disabled
}))
]
};
n.httpRequests.push(S);
} else
console.log("Unknown item", r, d);
};
for (const r of a.item)
g(r);
return { resources: T(n) };
}
function k(e) {
const t = i(e);
return "basic" in t ? {
headers: [],
authenticationType: "basic",
authentication: {
username: t.basic.username || "",
password: t.basic.password || ""
}
} : "bearer" in t ? {
headers: [],
authenticationType: "bearer",
authentication: {
token: t.bearer.token || ""
}
} : { headers: [], authenticationType: null, authentication: {} };
}
function j(e) {
var a, s, c, u;
const t = i(e);
return "graphql" in t ? {
headers: [
{
name: "Content-Type",
value: "application/json",
enabled: !0
}
],
bodyType: "graphql",
body: {
text: JSON.stringify(
{ query: t.graphql.query, variables: q(t.graphql.variables) },
null,
2
)
}
} : "urlencoded" in t ? {
headers: [
{
name: "Content-Type",
value: "application/x-www-form-urlencoded",
enabled: !0
}
],
bodyType: "application/x-www-form-urlencoded",
body: {
form: f(t.urlencoded).map((n) => ({
enabled: !n.disabled,
name: n.key ?? "",
value: n.value ?? ""
}))
}
} : "formdata" in t ? {
headers: [
{
name: "Content-Type",
value: "multipart/form-data",
enabled: !0
}
],
bodyType: "multipart/form-data",
body: {
form: f(t.formdata).map(
(n) => n.src != null ? {
enabled: !n.disabled,
contentType: n.contentType ?? null,
name: n.key ?? "",
file: n.src ?? ""
} : {
enabled: !n.disabled,
name: n.key ?? "",
value: n.value ?? ""
}
)
}
} : "raw" in t ? {
headers: [
{
name: "Content-Type",
value: ((s = (a = t.options) == null ? void 0 : a.raw) == null ? void 0 : s.language) === "json" ? "application/json" : "",
enabled: !0
}
],
bodyType: ((u = (c = t.options) == null ? void 0 : c.raw) == null ? void 0 : u.language) === "json" ? "application/json" : "other",
body: {
text: t.raw ?? ""
}
} : { headers: [], bodyType: null, body: {} };
}
function q(e) {
try {
return i(JSON.parse(e));
} catch {
}
return null;
}
function i(e) {
return Object.prototype.toString.call(e) === "[object Object]" ? e : {};
}
function f(e) {
return Object.prototype.toString.call(e) === "[object Array]" ? e : [];
}
function T(e) {
return typeof e == "string" ? e.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : Array.isArray(e) && e != null ? e.map(T) : typeof e == "object" && e != null ? Object.fromEntries(
Object.entries(e).map(([t, a]) => [t, T(a)])
) : e;
}
const h = {};
function b(e) {
return h[e] = (h[e] ?? -1) + 1, `GENERATE_ID::${e.toUpperCase()}_${h[e]}`;
}
export {
v as pluginHookImport
};
//# sourceMappingURL=index.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
function u(r, t) {
let e;
try {
e = JSON.parse(t);
} catch {
return;
}
if (!(!s(e) || !("yaakSchema" in e)))
return "requests" in e.resources && (e.resources.httpRequests = e.resources.requests, delete e.resources.requests), { resources: e.resources };
}
function s(r) {
return Object.prototype.toString.call(r) === "[object Object]";
}
export {
s as isJSObject,
u as pluginHookImport
};
//# sourceMappingURL=index.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","sources":["../../../plugins/importer-yaak/src/index.ts"],"sourcesContent":["export function pluginHookImport(ctx: any, contents: string) {\n let parsed;\n try {\n parsed = JSON.parse(contents);\n } catch (err) {\n return undefined;\n }\n\n if (!isJSObject(parsed)) {\n return undefined;\n }\n\n const isYaakExport = 'yaakSchema' in parsed;\n if (!isYaakExport) {\n return;\n }\n\n // Migrate v1 to v2 -- changes requests to httpRequests\n if ('requests' in parsed.resources) {\n parsed.resources.httpRequests = parsed.resources.requests;\n delete parsed.resources['requests'];\n }\n\n return { resources: parsed.resources }; // Should already be in the correct format\n}\n\nexport function isJSObject(obj: any) {\n return Object.prototype.toString.call(obj) === '[object Object]';\n}\n"],"names":["pluginHookImport","ctx","contents","parsed","isJSObject","obj"],"mappings":"AAAgB,SAAAA,EAAiBC,GAAUC,GAAkB;AACvD,MAAAC;AACA,MAAA;AACO,IAAAA,IAAA,KAAK,MAAMD,CAAQ;AAAA,UAChB;AACL;AAAA,EACT;AAOA,MALI,GAACE,EAAWD,CAAM,KAKlB,EADiB,gBAAgBA;AAMjC,WAAA,cAAcA,EAAO,cAChBA,EAAA,UAAU,eAAeA,EAAO,UAAU,UAC1C,OAAAA,EAAO,UAAU,WAGnB,EAAE,WAAWA,EAAO;AAC7B;AAEO,SAASC,EAAWC,GAAU;AACnC,SAAO,OAAO,UAAU,SAAS,KAAKA,CAAG,MAAM;AACjD;"}

View File

@@ -9,7 +9,6 @@ use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use crate::deno_ops::op_yaml_parse;
use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
@@ -32,6 +31,9 @@ use deno_core::SourceMapGetter;
use deno_core::{resolve_import, v8};
use tokio::task::block_in_place;
use crate::deno_ops::op_yaml_parse;
use crate::plugin::PluginCapability;
#[derive(Clone)]
struct SourceMapStore(Rc<RefCell<HashMap<String, Vec<u8>>>>);
@@ -134,13 +136,13 @@ impl ModuleLoader for TypescriptModuleLoader {
}
}
pub fn run_plugin_deno_block(
pub fn run_plugin_block(
plugin_index_file: &str,
fn_name: &str,
fn_args: Vec<serde_json::Value>,
) -> Result<serde_json::Value, Error> {
block_in_place(|| {
tauri::async_runtime::block_on(run_plugin_deno_2(plugin_index_file, fn_name, fn_args))
tauri::async_runtime::block_on(run_plugin(plugin_index_file, fn_name, fn_args))
})
}
@@ -151,39 +153,13 @@ deno_core::extension!(
esm = [dir "src/plugin-runtime", "yaml.js"]
);
async fn run_plugin_deno_2(
async fn run_plugin(
plugin_index_file: &str,
fn_name: &str,
fn_args: Vec<serde_json::Value>,
) -> Result<serde_json::Value, Error> {
let source_map_store = SourceMapStore(Rc::new(RefCell::new(HashMap::new())));
let mut ext_console = deno_console::deno_console::init_ops_and_esm();
ext_console.esm_entry_point = Some("ext:deno_console/01_console.js");
let ext_yaak = yaak_runtime::init_ops_and_esm();
let mut js_runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(Rc::new(TypescriptModuleLoader {
source_maps: source_map_store.clone(),
})),
source_map_getter: Some(Rc::new(source_map_store)),
extensions: vec![ext_console, ext_yaak],
..Default::default()
});
let main_module = resolve_path(
plugin_index_file,
&std::env::current_dir().context("Unable to get CWD")?,
)?;
// Load the main module so we can do stuff with it
let mod_id = js_runtime.load_main_es_module(&main_module).await?;
let result = js_runtime.mod_evaluate(mod_id);
js_runtime.run_event_loop(Default::default()).await?;
result.await?;
let module_namespace = js_runtime.get_module_namespace(mod_id).unwrap();
let mut js_runtime = load_js_runtime()?;
let module_namespace = load_main_module(&mut js_runtime, plugin_index_file).await?;
let scope = &mut js_runtime.handle_scope();
let module_namespace = v8::Local::<v8::Object>::new(scope, module_namespace);
@@ -235,3 +211,95 @@ async fn run_plugin_deno_2(
}
}
}
pub fn get_plugin_capabilities_block(plugin_index_file: &str) -> Result<Vec<PluginCapability>, Error> {
block_in_place(|| tauri::async_runtime::block_on(get_plugin_capabilities(plugin_index_file)))
}
pub async fn get_plugin_capabilities(
plugin_index_file: &str,
) -> Result<Vec<PluginCapability>, Error> {
let mut js_runtime = load_js_runtime()?;
let module_namespace = load_main_module(&mut js_runtime, plugin_index_file).await?;
let scope = &mut js_runtime.handle_scope();
let module_namespace = v8::Local::<v8::Object>::new(scope, module_namespace);
let property_names =
match module_namespace.get_own_property_names(scope, v8::GetPropertyNamesArgs::default()) {
None => return Ok(Vec::new()),
Some(names) => names,
};
let mut capabilities: Vec<PluginCapability> = Vec::new();
for i in 0..property_names.length() {
let name = property_names.get_index(scope, i);
let name = match name {
Some(name) => name,
None => return Ok(Vec::new()),
};
match name.to_rust_string_lossy(scope).as_str() {
"pluginHookImport" => _ = capabilities.push(PluginCapability::Import),
"pluginHookExport" => _ = capabilities.push(PluginCapability::Export),
"pluginHookResponseFilter" => _ = capabilities.push(PluginCapability::Filter),
_ => {}
};
}
Ok(capabilities)
}
async fn load_main_module(
js_runtime: &mut JsRuntime,
plugin_index_file: &str,
) -> Result<v8::Global<v8::Object>, Error> {
let main_module = resolve_path(
plugin_index_file,
&std::env::current_dir().context("Unable to get CWD")?,
)?;
// Load the main module so we can do stuff with it
let mod_id = js_runtime.load_main_es_module(&main_module).await?;
let result = js_runtime.mod_evaluate(mod_id);
js_runtime.run_event_loop(Default::default()).await?;
result.await?;
let module_namespace = js_runtime.get_module_namespace(mod_id).unwrap();
Ok(module_namespace)
}
fn load_js_runtime<'s>() -> Result<JsRuntime, Error> {
let source_map_store = SourceMapStore(Rc::new(RefCell::new(HashMap::new())));
let mut ext_console = deno_console::deno_console::init_ops_and_esm();
ext_console.esm_entry_point = Some("ext:deno_console/01_console.js");
let ext_yaak = yaak_runtime::init_ops_and_esm();
let js_runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(Rc::new(TypescriptModuleLoader {
source_maps: source_map_store.clone(),
})),
source_map_getter: Some(Rc::new(source_map_store)),
extensions: vec![ext_console, ext_yaak],
..Default::default()
});
// let main_module = resolve_path(
// plugin_index_file.to_str().unwrap(),
// &std::env::current_dir().context("Unable to get CWD")?,
// )?;
//
// // Load the main module so we can do stuff with it
// let mod_id = js_runtime.load_main_es_module(&main_module).await?;
// let result = js_runtime.mod_evaluate(mod_id);
// js_runtime.run_event_loop(Default::default()).await?;
// result.await?;
//
// let module_namespace = js_runtime.get_module_namespace(mod_id).unwrap();
// let scope = &mut js_runtime.handle_scope();
// let module_namespace = v8::Local::<v8::Object>::new(scope, module_namespace);
Ok(js_runtime)
}

View File

@@ -18,6 +18,7 @@ use tauri::{Manager, WebviewWindow};
use tokio::sync::oneshot;
use tokio::sync::watch::Receiver;
use crate::render::variables_from_environment;
use crate::{models, render, response_err};
pub async fn send_http_request(
@@ -33,8 +34,9 @@ pub async fn send_http_request(
let workspace = models::get_workspace(window, &request.workspace_id)
.await
.expect("Failed to get Workspace");
let vars = variables_from_environment(&workspace, environment_ref);
let mut url_string = render::render(&request.url, &workspace, environment.as_ref());
let mut url_string = render::render(&request.url, &vars);
url_string = ensure_proto(&url_string);
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
@@ -144,8 +146,8 @@ pub async fn send_http_request(
continue;
}
let name = render::render(&h.name, &workspace, environment_ref);
let value = render::render(&h.value, &workspace, environment_ref);
let name = render::render(&h.name, &vars);
let value = render::render(&h.value, &vars);
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
Ok(n) => n,
@@ -180,8 +182,8 @@ pub async fn send_http_request(
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let username = render::render(raw_username, &workspace, environment_ref);
let password = render::render(raw_password, &workspace, environment_ref);
let username = render::render(raw_username, &vars);
let password = render::render(raw_password, &vars);
let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
@@ -191,7 +193,7 @@ pub async fn send_http_request(
);
} else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
let token = render::render(raw_token, &workspace, environment_ref);
let token = render::render(raw_token, &vars);
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
@@ -205,8 +207,8 @@ pub async fn send_http_request(
continue;
}
query_params.push((
render::render(&p.name, &workspace, environment_ref),
render::render(&p.value, &workspace, environment_ref),
render::render(&p.name, &vars),
render::render(&p.value, &vars),
));
}
request_builder = request_builder.query(&query_params);
@@ -222,7 +224,7 @@ pub async fn send_http_request(
.unwrap_or(empty_string)
.as_str()
.unwrap_or("");
let body = render::render(raw_text, &workspace, environment_ref);
let body = render::render(raw_text, &vars);
request_builder = request_builder.body(body);
} else if body_type == "application/x-www-form-urlencoded"
&& request_body.contains_key("form")
@@ -249,10 +251,7 @@ pub async fn send_http_request(
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
form_params.push((
render::render(name, &workspace, environment_ref),
render::render(value, &workspace, environment_ref),
));
form_params.push((render::render(name, &vars), render::render(value, &vars)));
}
}
request_builder = request_builder.form(&form_params);
@@ -301,13 +300,9 @@ pub async fn send_http_request(
.as_str()
.unwrap_or_default();
let name = render::render(name_raw, &workspace, environment_ref);
let name = render::render(name_raw, &vars);
let mut part = if file_path.is_empty() {
multipart::Part::text(render::render(
value_raw,
&workspace,
environment_ref,
))
multipart::Part::text(render::render(value_raw, &vars))
} else {
match fs::read(file_path) {
Ok(f) => multipart::Part::bytes(f),
@@ -324,7 +319,7 @@ pub async fn send_http_request(
.unwrap_or_default();
if !ct_raw.is_empty() {
let content_type = render::render(ct_raw, &workspace, environment_ref);
let content_type = render::render(ct_raw, &vars);
part = part
.mime_str(content_type.as_str())
.map_err(|e| e.to_string())?;

View File

@@ -6,7 +6,7 @@ extern crate objc;
use std::collections::HashMap;
use std::env::current_dir;
use std::fs;
use std::fs::{create_dir_all, read_to_string, File};
use std::fs::{create_dir_all, File, read_to_string};
use std::path::PathBuf;
use std::process::exit;
use std::str::FromStr;
@@ -17,44 +17,47 @@ use fern::colors::ColoredLevelConfig;
use log::{debug, error, info, warn};
use rand::random;
use serde_json::{json, Value};
use sqlx::{Pool, Sqlite, SqlitePool};
use sqlx::migrate::Migrator;
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::types::Json;
use sqlx::{Pool, Sqlite, SqlitePool};
use tauri::{AppHandle, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{Manager, WindowEvent};
use tauri::path::BaseDirectory;
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::{AppHandle, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{Manager, WindowEvent};
use tauri_plugin_log::{fern, Target, TargetKind};
use tauri_plugin_shell::ShellExt;
use tokio::sync::Mutex;
use ::grpc::{Code, deserialize_message, serialize_message, ServiceDefinition};
use ::grpc::manager::{DynamicMessage, GrpcHandle};
use ::grpc::{deserialize_message, serialize_message, Code, ServiceDefinition};
use crate::analytics::{AnalyticsAction, AnalyticsResource};
use crate::grpc::metadata_to_map;
use crate::http_request::send_http_request;
use crate::models::{
cancel_pending_grpc_connections, cancel_pending_responses, create_http_response,
delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment,
delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request,
delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request,
generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection,
cancel_pending_grpc_connections, cancel_pending_responses, CookieJar,
create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar,
delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request,
delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request,
duplicate_http_request, Environment, EnvironmentVariable, Folder, generate_model_id,
get_cookie_jar, get_environment, get_folder, get_grpc_connection,
get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
get_or_create_settings, get_workspace, get_workspace_export_resources, list_cookie_jars,
list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests,
list_http_requests, list_responses, list_workspaces, set_key_value_raw, update_response_if_id,
update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar,
Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType,
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Settings, Workspace,
get_or_create_settings, get_workspace, get_workspace_export_resources, GrpcConnection, GrpcEvent,
GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, KeyValue,
list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events,
list_grpc_requests, list_http_requests, list_responses, list_workspaces, ModelType,
set_key_value_raw, Settings, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment,
upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, Workspace,
WorkspaceExportResources,
};
use crate::notifications::YaakNotifier;
use crate::plugin::{run_plugin_export_curl, run_plugin_import, ImportResult};
use crate::render::render_request;
use crate::plugin::{
find_plugins, get_plugin, ImportResult, PluginCapability,
run_plugin_export_curl, run_plugin_filter, run_plugin_import,
};
use crate::render::{render_request, variables_from_environment};
use crate::updates::{UpdateMode, YaakUpdater};
use crate::window_menu::app_menu;
@@ -173,6 +176,7 @@ async fn cmd_grpc_go(
.await
.map_err(|e| e.to_string())?;
let mut metadata = HashMap::new();
let vars = variables_from_environment(&workspace, environment.as_ref());
// Add rest of metadata
for h in req.clone().metadata.0 {
@@ -184,15 +188,14 @@ async fn cmd_grpc_go(
continue;
}
let name = render::render(&h.name, &workspace, environment.as_ref());
let value = render::render(&h.value, &workspace, environment.as_ref());
let name = render::render(&h.name, &vars);
let value = render::render(&h.value, &vars);
metadata.insert(name, value);
}
if let Some(b) = &req.authentication_type {
let req = req.clone();
let environment_ref = environment.as_ref();
let empty_value = &serde_json::to_value("").unwrap();
let a = req.authentication.0;
@@ -207,15 +210,15 @@ async fn cmd_grpc_go(
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let username = render::render(raw_username, &workspace, environment_ref);
let password = render::render(raw_password, &workspace, environment_ref);
let username = render::render(raw_username, &vars);
let password = render::render(raw_password, &vars);
let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
metadata.insert("Authorization".to_string(), format!("Basic {}", encoded));
} else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
let token = render::render(raw_token, &workspace, environment_ref);
let token = render::render(raw_token, &vars);
metadata.insert("Authorization".to_string(), format!("Bearer {token}"));
}
}
@@ -287,11 +290,10 @@ async fn cmd_grpc_go(
let cb = {
let cancelled_rx = cancelled_rx.clone();
let environment = environment.clone();
let workspace = workspace.clone();
let w = w.clone();
let base_msg = base_msg.clone();
let method_desc = method_desc.clone();
let vars = vars.clone();
move |ev: tauri::Event| {
if *cancelled_rx.borrow() {
@@ -314,9 +316,8 @@ async fn cmd_grpc_go(
Ok(IncomingMsg::Message(raw_msg)) => {
let w = w.clone();
let base_msg = base_msg.clone();
let environment_ref = environment.as_ref();
let method_desc = method_desc.clone();
let msg = render::render(raw_msg.as_str(), &workspace, environment_ref);
let msg = render::render(raw_msg.as_str(), &vars);
let d_msg: DynamicMessage = match deserialize_message(msg.as_str(), method_desc)
{
Ok(d_msg) => d_msg,
@@ -368,14 +369,13 @@ async fn cmd_grpc_go(
let w = w.clone();
let base_event = base_msg.clone();
let req = req.clone();
let workspace = workspace.clone();
let environment = environment.clone();
let vars = vars.clone();
let raw_msg = if req.message.is_empty() {
"{}".to_string()
} else {
req.message
};
let msg = render::render(&raw_msg, &workspace, environment.as_ref());
let msg = render::render(&raw_msg, &vars);
upsert_grpc_event(
&w,
@@ -735,7 +735,11 @@ async fn cmd_filter_response(
};
let body = read_to_string(response.body_path.unwrap()).unwrap();
let filter_result = plugin::run_plugin_filter(plugin_name, filter, &body)
let plugin = match get_plugin(&w.app_handle(), plugin_name).map_err(|e| e.to_string())? {
None => return Err("Failed to get plugin".into()),
Some(p) => p,
};
let filter_result = run_plugin_filter(&plugin, filter, &body)
.await
.expect("Failed to run filter");
Ok(filter_result.filtered)
@@ -748,26 +752,23 @@ async fn cmd_import_data(
_workspace_id: &str,
) -> Result<WorkspaceExportResources, String> {
let mut result: Option<ImportResult> = None;
let plugins = vec![
"importer-postman",
"importer-insomnia",
"importer-yaak",
"importer-curl",
];
let file =
read_to_string(file_path).unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
let file_contents = file.as_str();
for plugin_name in plugins {
let v = run_plugin_import(plugin_name, file_contents)
let plugins = find_plugins(w.app_handle(), &PluginCapability::Import)
.await
.map_err(|e| e.to_string())?;
for plugin in plugins {
let v = run_plugin_import(&plugin, file_contents)
.await
.map_err(|e| e.to_string())?;
if let Some(r) = v {
info!("Imported data using {}", plugin_name);
info!("Imported data using {}", plugin.name);
analytics::track_event(
&w.app_handle(),
AnalyticsResource::App,
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
Some(json!({ "plugin": plugin.name })),
)
.await;
result = Some(r);
@@ -903,14 +904,21 @@ async fn cmd_request_to_curl(
.await
.map_err(|e| e.to_string())?;
let rendered = render_request(&request, &workspace, environment.as_ref());
Ok(run_plugin_export_curl(&rendered)?)
Ok(run_plugin_export_curl(&app, &rendered)?)
}
#[tauri::command]
async fn cmd_curl_to_request(command: &str, workspace_id: &str) -> Result<HttpRequest, String> {
let v = run_plugin_import("importer-curl", command)
.await
.map_err(|e| e.to_string());
async fn cmd_curl_to_request(
app_handle: AppHandle,
command: &str,
workspace_id: &str,
) -> Result<HttpRequest, String> {
let plugin = match get_plugin(&app_handle, "importer-curl").map_err(|e| e.to_string())? {
None => return Err("Failed to find plugin".into()),
Some(p) => p,
};
let v = run_plugin_import(&plugin, command).await;
match v {
Ok(Some(r)) => r
.resources

View File

@@ -1,6 +1,6 @@
use std::time::SystemTime;
use chrono::{Duration, NaiveDateTime, Utc};
use chrono::{DateTime, Duration, Utc};
use log::debug;
use reqwest::Method;
use serde::{Deserialize, Serialize};
@@ -23,7 +23,7 @@ pub struct YaakNotifier {
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct YaakNotification {
timestamp: NaiveDateTime,
timestamp: DateTime<Utc>,
id: String,
message: String,
action: Option<YaakNotificationAction>,
@@ -77,7 +77,7 @@ impl YaakNotifier {
let age = notification
.timestamp
.signed_duration_since(Utc::now().naive_utc());
.signed_duration_since(Utc::now());
let seen = get_kv(app).await?;
if seen.contains(&notification.id) || (age > Duration::days(1)) {
debug!("Already seen notification {}", notification.id);

View File

@@ -1,11 +1,24 @@
use std::path;
use std::{fs, io};
use log::error;
use serde::{Deserialize, Serialize};
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager};
use thiserror::Error;
use crate::deno::run_plugin_deno_block;
use crate::deno::{get_plugin_capabilities_block, run_plugin_block};
use crate::models::{HttpRequest, WorkspaceExportResources};
#[derive(Error, Debug)]
pub enum PluginError {
#[error("directory not found")]
DirectoryNotFound(#[from] io::Error),
#[error("anyhow error")]
V8(#[from] anyhow::Error),
// #[error("unknown data store error")]
// Unknown,
}
#[derive(Default, Debug, Deserialize, Serialize)]
pub struct FilterResult {
pub filtered: String,
@@ -16,16 +29,71 @@ pub struct ImportResult {
pub resources: WorkspaceExportResources,
}
#[derive(Eq, PartialEq, Hash, Clone)]
pub enum PluginCapability {
Export,
Import,
Filter,
}
pub struct PluginDef {
pub name: String,
pub path: String,
pub capabilities: Vec<PluginCapability>,
}
pub fn scan_plugins(app_handle: &AppHandle) -> Result<Vec<PluginDef>, PluginError> {
let plugins_dir = app_handle
.path()
.resolve("plugins", BaseDirectory::Resource)
.expect("failed to resolve plugin directory resource");
let plugin_entries = fs::read_dir(plugins_dir)?;
let mut plugins = Vec::new();
for entry in plugin_entries {
let plugin_dir_entry = match entry {
Err(_) => continue,
Ok(entry) => entry,
};
let plugin_index_file = plugin_dir_entry.path().join("index.mjs");
let capabilities = get_plugin_capabilities_block(&plugin_index_file.to_str().unwrap())?;
plugins.push(PluginDef {
name: plugin_dir_entry.file_name().to_string_lossy().to_string(),
path: plugin_index_file.to_string_lossy().to_string(),
capabilities,
});
}
Ok(plugins)
}
pub async fn find_plugins(
app_handle: &AppHandle,
capability: &PluginCapability,
) -> Result<Vec<PluginDef>, PluginError> {
let plugins = scan_plugins(app_handle)?
.into_iter()
.filter(|p| p.capabilities.contains(capability))
.collect();
Ok(plugins)
}
pub fn get_plugin(app_handle: &AppHandle, name: &str) -> Result<Option<PluginDef>, PluginError> {
Ok(scan_plugins(app_handle)?
.into_iter()
.find(|p| p.name == name))
}
pub async fn run_plugin_filter(
plugin_name: &str,
plugin: &PluginDef,
response_body: &str,
filter: &str,
) -> Option<FilterResult> {
let plugin_dir = path::Path::new("/Users/gschier/Workspace/yaak/plugins");
let plugin_index_file = plugin_dir.join(plugin_name).join("build/index.mjs");
let result = run_plugin_deno_block(
plugin_index_file.to_str().unwrap(),
let result = run_plugin_block(
&plugin.path,
"pluginHookResponseFilter",
vec![
serde_json::to_value(response_body).unwrap(),
@@ -36,7 +104,7 @@ pub async fn run_plugin_filter(
.expect("Failed to run plugin");
if result.is_null() {
error!("Plugin {} failed to run", plugin_name);
error!("Plugin {} failed to run", plugin.name);
return None;
}
@@ -45,31 +113,29 @@ pub async fn run_plugin_filter(
Some(resources)
}
pub fn run_plugin_export_curl(request: &HttpRequest) -> Result<String, String> {
let plugin_dir = path::Path::new("/Users/gschier/Workspace/yaak/plugins");
let plugin_index_file = plugin_dir.join("exporter-curl").join("build/index.mjs");
pub fn run_plugin_export_curl(
app_handle: &AppHandle,
request: &HttpRequest,
) -> Result<String, String> {
let plugin = match get_plugin(app_handle, "exporter-curl").map_err(|e| e.to_string())? {
None => return Err("Failed to get plugin".into()),
Some(p) => p,
};
let request_json = serde_json::to_value(request).map_err(|e| e.to_string())?;
let result = run_plugin_deno_block(
plugin_index_file.to_str().unwrap(),
"pluginHookExport",
vec![request_json],
)
.map_err(|e| e.to_string())?;
let result = run_plugin_block(&plugin.path, "pluginHookExport", vec![request_json])
.map_err(|e| e.to_string())?;
let export_str: String = serde_json::from_value(result).map_err(|e| e.to_string())?;
Ok(export_str)
}
pub async fn run_plugin_import(
plugin_name: &str,
plugin: &PluginDef,
file_contents: &str,
) -> Result<Option<ImportResult>, String> {
let plugin_dir = path::Path::new("/Users/gschier/Workspace/yaak/plugins");
let plugin_index_file = plugin_dir.join(plugin_name).join("build/index.mjs");
let result = run_plugin_deno_block(
plugin_index_file.to_str().unwrap(),
let result = run_plugin_block(
&plugin.path,
"pluginHookImport",
vec![serde_json::to_value(file_contents).map_err(|e| e.to_string())?],
)

View File

@@ -9,16 +9,18 @@ use templates::parse_and_render;
pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -> HttpRequest {
let r = r.clone();
let vars = &variables_from_environment(w, e);
HttpRequest {
url: render(r.url.as_str(), w, e),
url: render(r.url.as_str(), vars),
url_parameters: Json(
r.url_parameters
.0
.iter()
.map(|p| HttpUrlParameter {
enabled: p.enabled,
name: render(p.name.as_str(), w, e),
value: render(p.value.as_str(), w, e),
name: render(p.name.as_str(), vars),
value: render(p.value.as_str(), vars),
})
.collect::<Vec<HttpUrlParameter>>(),
),
@@ -28,8 +30,8 @@ pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -
.iter()
.map(|p| HttpRequestHeader {
enabled: p.enabled,
name: render(p.name.as_str(), w, e),
value: render(p.value.as_str(), w, e),
name: render(p.name.as_str(), vars),
value: render(p.value.as_str(), vars),
})
.collect::<Vec<HttpRequestHeader>>(),
),
@@ -39,11 +41,11 @@ pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -
.iter()
.map(|(k, v)| {
let v = if v.is_string() {
render(v.as_str().unwrap(), w, e)
render(v.as_str().unwrap(), vars)
} else {
v.to_string()
};
(render(k, w, e), JsonValue::from(v))
(render(k, vars), JsonValue::from(v))
})
.collect::<HashMap<String, JsonValue>>(),
),
@@ -53,11 +55,11 @@ pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -
.iter()
.map(|(k, v)| {
let v = if v.is_string() {
render(v.as_str().unwrap(), w, e)
render(v.as_str().unwrap(), vars)
} else {
v.to_string()
};
(render(k, w, e), JsonValue::from(v))
(render(k, vars), JsonValue::from(v))
})
.collect::<HashMap<String, JsonValue>>(),
),
@@ -65,7 +67,31 @@ pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -
}
}
pub fn render(template: &str, workspace: &Workspace, environment: Option<&Environment>) -> String {
pub fn recursively_render_variables<'s>(
m: &HashMap<String, String>,
render_count: usize,
) -> HashMap<String, String> {
let mut did_render = false;
let mut new_map = m.clone();
for (k, v) in m.clone() {
let rendered = render(v.as_str(), m);
if rendered != v {
did_render = true
}
new_map.insert(k, rendered);
}
if did_render && render_count <= 3 {
new_map = recursively_render_variables(&new_map, render_count + 1);
}
new_map
}
pub fn variables_from_environment(
workspace: &Workspace,
environment: Option<&Environment>,
) -> HashMap<String, String> {
let mut variables = HashMap::new();
variables = add_variable_to_map(variables, &workspace.variables.0);
@@ -73,13 +99,17 @@ pub fn render(template: &str, workspace: &Workspace, environment: Option<&Enviro
variables = add_variable_to_map(variables, &e.variables.0);
}
parse_and_render(template, variables, None)
recursively_render_variables(&variables, 0)
}
fn add_variable_to_map<'a>(
m: HashMap<&'a str, &'a str>,
variables: &'a Vec<EnvironmentVariable>,
) -> HashMap<&'a str, &'a str> {
pub fn render(template: &str, vars: &HashMap<String, String>) -> String {
parse_and_render(template, vars, None)
}
fn add_variable_to_map(
m: HashMap<String, String>,
variables: &Vec<EnvironmentVariable>,
) -> HashMap<String, String> {
let mut map = m.clone();
for variable in variables {
if !variable.enabled || variable.value.is_empty() {
@@ -87,7 +117,7 @@ fn add_variable_to_map<'a>(
}
let name = variable.name.as_str();
let value = variable.value.as_str();
map.insert(name, value);
map.insert(name.into(), value.into());
}
map

View File

@@ -1,4 +1,5 @@
use hex_color::HexColor;
use log::warn;
use objc::{msg_send, sel, sel_impl};
use rand::{distributions::Alphanumeric, Rng};
use tauri::{
@@ -25,15 +26,35 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
let window_for_theme = window.clone();
let id1 = h.listen("yaak_bg_changed", move |ev| {
let payload = serde_json::from_str::<&str>(ev.payload()).unwrap().trim();
let color = HexColor::parse_rgb(payload).unwrap();
update_window_theme(window_for_theme.clone(), color);
let color_str: String = match serde_json::from_str(ev.payload()) {
Ok(color) => color,
Err(err) => {
warn!("Failed to JSON parse color '{}': {}", ev.payload(), err);
return;
}
};
match HexColor::parse_rgb(color_str.trim()) {
Ok(color) => {
update_window_theme(window_for_theme.clone(), color);
}
Err(err) => {
warn!("Failed to parse background color '{}': {}", color_str, err)
}
}
});
let window_for_title = window.clone();
let id2 = h.listen("yaak_title_changed", move |ev| {
let payload = serde_json::from_str::<&str>(ev.payload()).unwrap().trim();
update_window_title(window_for_title.clone(), payload.to_string());
let title: String = match serde_json::from_str(ev.payload()) {
Ok(title) => title,
Err(err) => {
warn!("Failed to parse window title \"{}\": {}", ev.payload(), err);
return;
}
};
update_window_title(window_for_title.clone(), title);
});
let h = h.clone();

View File

@@ -1,6 +1,6 @@
{
"productName": "yaak",
"version": "2024.6.0",
"version": "2024.6.5",
"identifier": "app.yaak.desktop",
"build": {
"beforeBuildCommand": "npm run build",

View File

@@ -1,14 +1,14 @@
#[derive(Clone, PartialEq, Debug)]
pub enum Val {
Str(String),
Ident(String),
Var(String),
Fn { name: String, args: Vec<Val> },
}
#[derive(Clone, PartialEq, Debug)]
pub enum Token {
Raw(String),
Var { name: String },
Fn { name: String, args: Vec<Val> },
Tag(Val),
Eof,
}
@@ -66,17 +66,10 @@ impl Parser {
// Parse up to first identifier
// ${[ my_var...
self.skip_whitespace();
let name = match self.parse_ident() {
None => return None,
Some(v) => v,
};
// Parse fn args if they exist
// ${[ my_var(a, b, c)
let args = if self.match_str("(") {
self.parse_fn_args()
} else {
None
let val = match self.parse_value() {
Some(v) => v,
None => return None,
};
// Parse to closing tag
@@ -86,29 +79,65 @@ impl Parser {
return None;
}
Some(match args {
Some(a) => Token::Fn { args: a, name },
None => Token::Var { name },
})
Some(Token::Tag(val))
}
#[allow(dead_code)]
fn debug_pos(&self, x: &str) {
println!(
r#"Position: {x} -- [{}] = {} --> "{}"#,
r#"Position: {x} -- [{}] = {} --> "{}" --> {:?}"#,
self.pos,
self.chars[self.pos],
self.chars.iter().collect::<String>()
self.chars.iter().collect::<String>(),
self.tokens,
);
}
fn parse_value(&mut self) -> Option<Val> {
if let Some((name, args)) = self.parse_fn() {
Some(Val::Fn { name, args })
} else if let Some(v) = self.parse_ident() {
Some(Val::Var(v))
} else if let Some(v) = self.parse_string() {
Some(Val::Str(v))
} else {
None
}
}
fn parse_fn(&mut self) -> Option<(String, Vec<Val>)> {
let start_pos = self.pos;
let name = match self.parse_ident() {
Some(v) => v,
None => {
self.pos = start_pos;
return None;
}
};
let args = match self.parse_fn_args() {
Some(args) => args,
None => {
self.pos = start_pos;
return None;
}
};
Some((name, args))
}
fn parse_fn_args(&mut self) -> Option<Vec<Val>> {
if !self.match_str("(") {
return None;
}
let start_pos = self.pos;
let mut args: Vec<Val> = Vec::new();
while self.pos < self.chars.len() {
self.skip_whitespace();
if let Some(v) = self.parse_ident_or_string() {
if let Some(v) = self.parse_value() {
args.push(v);
}
@@ -121,6 +150,7 @@ impl Parser {
// If we don't find a comma, that's bad
if !args.is_empty() && !self.match_str(",") {
self.pos = start_pos;
return None;
}
@@ -132,16 +162,6 @@ impl Parser {
return Some(args);
}
fn parse_ident_or_string(&mut self) -> Option<Val> {
if let Some(i) = self.parse_ident() {
Some(Val::Ident(i))
} else if let Some(s) = self.parse_string() {
Some(Val::Str(s))
} else {
None
}
}
fn parse_ident(&mut self) -> Option<String> {
let start_pos = self.pos;
@@ -161,6 +181,7 @@ impl Parser {
}
if text.is_empty() {
self.pos = start_pos;
return None;
}
@@ -169,6 +190,7 @@ impl Parser {
fn parse_string(&mut self) -> Option<String> {
let start_pos = self.pos;
let mut text = String::new();
if !self.match_str("\"") {
return None;
@@ -264,7 +286,7 @@ mod tests {
let mut p = Parser::new("${[ foo ]}");
assert_eq!(
p.parse(),
vec![Token::Var { name: "foo".into() }, Token::Eof]
vec![Token::Tag(Val::Var("foo".into())), Token::Eof]
);
}
@@ -282,7 +304,7 @@ mod tests {
let mut p = Parser::new(r#"${[ "foo \"bar\" baz" ]}"#);
assert_eq!(
p.parse(),
vec![Token::Raw(r#"${[ "foo \"bar\" baz" ]}"#.into()), Token::Eof]
vec![Token::Tag(Val::Str(r#"foo "bar" baz"#.into())), Token::Eof]
);
}
@@ -293,7 +315,7 @@ mod tests {
p.parse(),
vec![
Token::Raw("Hello ".to_string()),
Token::Var { name: "foo".into() },
Token::Tag(Val::Var("foo".into())),
Token::Raw("!".to_string()),
Token::Eof,
]
@@ -306,10 +328,10 @@ mod tests {
assert_eq!(
p.parse(),
vec![
Token::Fn {
Token::Tag(Val::Fn {
name: "foo".into(),
args: Vec::new(),
},
}),
Token::Eof
]
);
@@ -321,10 +343,10 @@ mod tests {
assert_eq!(
p.parse(),
vec![
Token::Fn {
Token::Tag(Val::Fn {
name: "foo".into(),
args: vec![Val::Ident("bar".into())],
},
args: vec![Val::Var("bar".into())],
}),
Token::Eof
]
);
@@ -336,14 +358,14 @@ mod tests {
assert_eq!(
p.parse(),
vec![
Token::Fn {
Token::Tag(Val::Fn {
name: "foo".into(),
args: vec![
Val::Ident("bar".into()),
Val::Ident("baz".into()),
Val::Ident("qux".into()),
Val::Var("bar".into()),
Val::Var("baz".into()),
Val::Var("qux".into()),
],
},
}),
Token::Eof
]
);
@@ -355,14 +377,35 @@ mod tests {
assert_eq!(
p.parse(),
vec![
Token::Fn {
Token::Tag(Val::Fn {
name: "foo".into(),
args: vec![
Val::Ident("bar".into()),
Val::Var("bar".into()),
Val::Str(r#"baz "hi""#.into()),
Val::Ident("qux".into()),
Val::Var("qux".into()),
],
},
}),
Token::Eof
]
);
}
#[test]
fn fn_nested() {
let mut p = Parser::new(r#"${[ outer(inner(foo, "i"), "o") ]}"#);
assert_eq!(
p.parse(),
vec![
Token::Tag(Val::Fn {
name: "outer".into(),
args: vec![
Val::Fn {
name: "inner".into(),
args: vec![Val::Var("foo".into()), Val::Str("i".into()),],
},
Val::Str("o".into())
],
}),
Token::Eof
]
);

View File

@@ -1,11 +1,12 @@
use crate::{Parser, Token, Val};
use std::collections::HashMap;
type TemplateCallback = fn(name: &str, args: Vec<&str>) -> String;
use crate::{Parser, Token, Val};
type TemplateCallback = fn(name: &str, args: Vec<String>) -> String;
pub fn parse_and_render(
template: &str,
vars: HashMap<&str, &str>,
vars: &HashMap<String, String>,
cb: Option<TemplateCallback>,
) -> String {
let mut p = Parser::new(template);
@@ -15,7 +16,7 @@ pub fn parse_and_render(
pub fn render(
tokens: Vec<Token>,
vars: HashMap<&str, &str>,
vars: &HashMap<String, String>,
cb: Option<TemplateCallback>,
) -> String {
let mut doc_str: Vec<String> = Vec::new();
@@ -23,26 +24,7 @@ pub fn render(
for t in tokens {
match t {
Token::Raw(s) => doc_str.push(s),
Token::Var { name } => {
if let Some(v) = vars.get(name.as_str()) {
doc_str.push(v.to_string());
}
}
Token::Fn { name, args } => {
let empty = &"";
let resolved_args = args
.iter()
.map(|a| match a {
Val::Str(s) => s.as_str(),
Val::Ident(i) => vars.get(i.as_str()).unwrap_or(empty),
})
.collect();
let val = match cb {
Some(cb) => cb(name.as_str(), resolved_args),
None => "".into(),
};
doc_str.push(val);
}
Token::Tag(val) => doc_str.push(render_tag(val, &vars, cb)),
Token::Eof => {}
}
}
@@ -50,17 +32,47 @@ pub fn render(
return doc_str.join("");
}
fn render_tag(
val: Val,
vars: &HashMap<String, String>,
cb: Option<TemplateCallback>,
) -> String {
match val {
Val::Str(s) => s.into(),
Val::Var(name) => match vars.get(name.as_str()) {
Some(v) => v.to_string(),
None => "".into(),
},
Val::Fn { name, args } => {
let empty = "".to_string();
let resolved_args = args
.iter()
.map(|a| match a {
Val::Str(s) => s.to_string(),
Val::Var(i) => vars.get(i.as_str()).unwrap_or(&empty).to_string(),
val => render_tag(val.clone(), vars, cb),
})
.collect::<Vec<String>>();
match cb {
Some(cb) => cb(name.as_str(), resolved_args),
None => "".into(),
}
}
}
}
#[cfg(test)]
mod tests {
use crate::*;
use std::collections::HashMap;
use crate::*;
#[test]
fn render_empty() {
let template = "";
let vars = HashMap::new();
let result = "";
assert_eq!(parse_and_render(template, vars, None), result.to_string());
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
}
#[test]
@@ -68,23 +80,23 @@ mod tests {
let template = "Hello World!";
let vars = HashMap::new();
let result = "Hello World!";
assert_eq!(parse_and_render(template, vars, None), result.to_string());
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
}
#[test]
fn render_simple() {
let template = "${[ foo ]}";
let vars = HashMap::from([("foo", "bar")]);
let vars = HashMap::from([("foo".to_string(), "bar".to_string())]);
let result = "bar";
assert_eq!(parse_and_render(template, vars, None), result.to_string());
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
}
#[test]
fn render_surrounded() {
let template = "hello ${[ word ]} world!";
let vars = HashMap::from([("word", "cruel")]);
let vars = HashMap::from([("word".to_string(), "cruel".to_string())]);
let result = "hello cruel world!";
assert_eq!(parse_and_render(template, vars, None), result.to_string());
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
}
#[test]
@@ -92,10 +104,28 @@ mod tests {
let vars = HashMap::new();
let template = r#"${[ say_hello("John", "Kate") ]}"#;
let result = r#"say_hello: ["John", "Kate"]"#;
let cb: fn(&str, Vec<&str>) -> String =
|name: &str, args: Vec<&str>| format!("{name}: {:?}", args);
fn cb(name: &str, args: Vec<String>) -> String {
format!("{name}: {:?}", args)
}
assert_eq!(parse_and_render(template, &vars, Some(cb)), result);
}
#[test]
fn render_nested_fn() {
let vars = HashMap::new();
let template = r#"${[ upper(secret()) ]}"#;
let result = r#"ABC"#;
fn cb(name: &str, args: Vec<String>) -> String {
match name {
"secret" => "abc".to_string(),
"upper" => args[0].to_string().to_uppercase(),
_ => "".to_string(),
}
}
assert_eq!(
parse_and_render(template, vars, Some(cb)),
parse_and_render(template, &vars, Some(cb)),
result.to_string()
);
}

View File

@@ -278,7 +278,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
workspaceGroup.items.push({
key: `switch-workspace-${w.id}`,
label: w.name,
onSelect: () => openWorkspace.mutate({ workspace: w, inNewWindow: false }),
onSelect: () => openWorkspace.mutate({ workspaceId: w.id, inNewWindow: false }),
});
}

View File

@@ -193,7 +193,7 @@ const EnvironmentEditor = function ({
namePlaceholder="VAR_NAME"
nameValidate={validateName}
valueType={valueVisibility.value ? 'text' : 'password'}
valueAutocompleteVariables={false}
valueAutocompleteVariables={true}
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
pairs={variables}
onChange={handleChange}

View File

@@ -59,7 +59,7 @@ export function ExportDataDialog({
const noneSelected = numSelected === 0;
return (
<VStack space={3} className="w-full mb-3 px-4">
<table className="w-full mb-auto min-w-full max-w-full divide-y">
<table className="w-full mb-auto min-w-full max-w-full divide-y divide-background-highlight-secondary">
<thead>
<tr>
<th className="w-6 min-w-0 py-2 text-left pl-1">
@@ -76,7 +76,7 @@ export function ExportDataDialog({
</th>
</tr>
</thead>
<tbody className="divide-y">
<tbody className="divide-y divide-background-highlight-secondary">
{workspaces.map((w) => (
<tr key={w.id}>
<td className="min-w-0 py-1 pl-1">

View File

@@ -31,7 +31,7 @@ export function OpenWorkspaceDialog({ hide, workspace }: Props) {
color="primary"
onClick={() => {
hide();
openWorkspace.mutate({ workspace, inNewWindow: false });
openWorkspace.mutate({ workspaceId: workspace.id, inNewWindow: false });
if (remember) {
updateSettings.mutate({ openWorkspaceNewWindow: false });
}
@@ -45,7 +45,7 @@ export function OpenWorkspaceDialog({ hide, workspace }: Props) {
rightSlot={<Icon icon="externalLink" />}
onClick={() => {
hide();
openWorkspace.mutate({ workspace, inNewWindow: true });
openWorkspace.mutate({ workspaceId: workspace.id, inNewWindow: true });
if (remember) {
updateSettings.mutate({ openWorkspaceNewWindow: true });
}

View File

@@ -5,7 +5,6 @@ import { createGlobalState } from 'react-use';
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useResponseViewMode } from '../hooks/useResponseViewMode';
import { isBinaryContentType } from '../lib/data/mimetypes';
import type { HttpRequest } from '../lib/models';
import { isResponseLoading } from '../lib/models';
import { Banner } from './core/Banner';
@@ -22,7 +21,6 @@ import { EmptyStateText } from './EmptyStateText';
import { RecentResponsesDropdown } from './RecentResponsesDropdown';
import { ResponseHeaders } from './ResponseHeaders';
import { AudioViewer } from './responseViewers/AudioViewer';
import { BinaryViewer } from './responseViewers/BinaryViewer';
import { CsvViewer } from './responseViewers/CsvViewer';
import { ImageViewer } from './responseViewers/ImageViewer';
import { PdfViewer } from './responseViewers/PdfViewer';
@@ -163,12 +161,8 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
<VideoViewer response={activeResponse} />
) : contentType?.match(/pdf/) ? (
<PdfViewer response={activeResponse} />
) : isBinaryContentType(contentType) ? (
<BinaryViewer response={activeResponse} />
) : contentType?.match(/csv|tab-separated/) ? (
<CsvViewer className="pb-2" response={activeResponse} />
) : activeResponse.contentLength > 2 * 1000 * 1000 ? (
<EmptyStateText>Cannot preview text responses larger than 2MB</EmptyStateText>
) : viewMode === 'pretty' && contentType?.includes('html') ? (
<WebPageViewer response={activeResponse} />
) : (

View File

@@ -67,7 +67,7 @@ export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
<ToastContext.Provider value={state}>
{children}
<Portal name="toasts">
<div className="absolute right-0 bottom-0">
<div className="absolute right-0 bottom-0 z-10">
<AnimatePresence>
{toasts.map((props: PrivateToastEntry) => (
<ToastInstance key={props.id} {...props} />

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import { memo, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
@@ -8,12 +8,14 @@ import { usePrompt } from '../hooks/usePrompt';
import { useSettings } from '../hooks/useSettings';
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { getWorkspace } from '../lib/store';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
import type { RadioDropdownItem } from './core/RadioDropdown';
import { RadioDropdown } from './core/RadioDropdown';
import { useDialog } from './DialogContext';
import { OpenWorkspaceDialog } from './OpenWorkspaceDialog';
@@ -35,39 +37,18 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const openWorkspace = useOpenWorkspace();
const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null;
const items: DropdownItem[] = useMemo(() => {
const workspaceItems: DropdownItem[] = workspaces.map((w) => ({
const { workspaceItems, extraItems } = useMemo<{
workspaceItems: RadioDropdownItem[];
extraItems: DropdownItem[];
}>(() => {
const workspaceItems: RadioDropdownItem[] = workspaces.map((w) => ({
key: w.id,
label: w.name,
value: w.id,
leftSlot: w.id === activeWorkspaceId ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: async () => {
if (typeof openWorkspaceNewWindow === 'boolean') {
openWorkspace.mutate({ workspace: w, inNewWindow: openWorkspaceNewWindow });
return;
}
dialog.show({
id: 'open-workspace',
size: 'sm',
title: 'Open Workspace',
render: ({ hide }) => <OpenWorkspaceDialog workspace={w} hide={hide} />,
});
},
}));
const activeWorkspaceItems: DropdownItem[] =
workspaces.length <= 1
? []
: [
...workspaceItems,
{
type: 'separator',
label: activeWorkspace?.name,
},
];
return [
...activeWorkspaceItems,
const extraItems: DropdownItem[] = [
{
key: 'rename',
label: 'Rename',
@@ -104,21 +85,47 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
onSelect: createWorkspace.mutate,
},
];
return { workspaceItems, extraItems };
}, [
activeWorkspace?.name,
activeWorkspaceId,
createWorkspace,
deleteWorkspace.mutate,
dialog,
openWorkspace,
prompt,
openWorkspaceNewWindow,
updateWorkspace,
workspaces,
]);
const handleChange = useCallback(
async (workspaceId: string | null) => {
if (workspaceId == null) return;
if (typeof openWorkspaceNewWindow === 'boolean') {
openWorkspace.mutate({ workspaceId, inNewWindow: openWorkspaceNewWindow });
return;
}
const workspace = await getWorkspace(workspaceId);
if (workspace == null) return;
dialog.show({
id: 'open-workspace',
size: 'sm',
title: 'Open Workspace',
render: ({ hide }) => <OpenWorkspaceDialog workspace={workspace} hide={hide} />,
});
},
[dialog, openWorkspace, openWorkspaceNewWindow],
);
return (
<Dropdown items={items}>
<RadioDropdown
items={workspaceItems}
extraItems={extraItems}
onChange={handleChange}
value={activeWorkspaceId}
>
<Button
size="sm"
className={classNames(
@@ -130,6 +137,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
>
{activeWorkspace?.name ?? 'Workspace'}
</Button>
</Dropdown>
</RadioDropdown>
);
});

View File

@@ -2,12 +2,7 @@ import { useCallback, useMemo } from 'react';
import { Editor } from './Editor';
import type { PairEditorProps } from './PairEditor';
type Props = Pick<
PairEditorProps,
'onChange' | 'pairs' | 'namePlaceholder' | 'valuePlaceholder'
> & {
foo?: string;
};
type Props = PairEditorProps;
export function BulkPairEditor({ pairs, onChange, namePlaceholder, valuePlaceholder }: Props) {
const pairsText = useMemo(() => {
@@ -30,6 +25,8 @@ export function BulkPairEditor({ pairs, onChange, namePlaceholder, valuePlacehol
return (
<Editor
useTemplating
autocompleteVariables
placeholder={`${namePlaceholder ?? 'name'}: ${valuePlaceholder ?? 'value'}`}
defaultValue={pairsText}
contentType="pairs"

View File

@@ -13,8 +13,8 @@ export function CsvViewer({ response, className }: Props) {
const body = useResponseBodyText(response);
const parsed = useMemo(() => {
if (body === null) return null;
return Papa.parse<string[]>(body);
if (body.data == null) return null;
return Papa.parse<string[]>(body.data);
}, [body]);
if (parsed === null) return null;

View File

@@ -9,12 +9,15 @@ interface Props {
}
export function JsonViewer({ response, className }: Props) {
const rawBody = useResponseBodyText(response) ?? '';
const rawBody = useResponseBodyText(response);
if (rawBody.isLoading || rawBody.data == null) return null;
let parsed = {};
try {
parsed = JSON.parse(rawBody);
parsed = JSON.parse(rawBody.data);
} catch (e) {
// foo
// Nothing yet
}
return (

View File

@@ -12,6 +12,8 @@ import { Editor } from '../core/Editor';
import { hyperlink } from '../core/Editor/hyperlink/extension';
import { IconButton } from '../core/IconButton';
import { Input } from '../core/Input';
import { EmptyStateText } from '../EmptyStateText';
import { BinaryViewer } from './BinaryViewer';
const extraExtensions = [hyperlink];
@@ -35,7 +37,7 @@ export function TextViewer({ response, pretty, className }: Props) {
);
const contentType = useContentTypeFromHeaders(response.headers);
const rawBody = useResponseBodyText(response) ?? null;
const rawBody = useResponseBodyText(response);
const isSearching = filterText != null;
const filteredResponse = useFilterResponse({
@@ -97,16 +99,25 @@ export function TextViewer({ response, pretty, className }: Props) {
return result;
}, [canFilter, filterText, isJson, isSearching, response.id, setFilterText, toggleSearch]);
if (rawBody == null) {
return 'bad';
if (rawBody.isLoading) {
return null;
}
if (rawBody.data == null) {
return <BinaryViewer response={response} />;
}
if ((response.contentLength ?? 0) > 2 * 1000 * 1000) {
return <EmptyStateText>Cannot preview text responses larger than 2MB</EmptyStateText>;
}
const formattedBody =
pretty && contentType?.includes('json')
? tryFormatJson(rawBody)
? tryFormatJson(rawBody.data)
: pretty && contentType?.includes('xml')
? tryFormatXml(rawBody)
: rawBody;
? tryFormatXml(rawBody.data)
: rawBody.data;
const body = isSearching && filterText?.length > 0 ? filteredResponse : formattedBody;
return (

View File

@@ -8,7 +8,7 @@ interface Props {
export function WebPageViewer({ response }: Props) {
const { url } = response;
const body = useResponseBodyText(response) ?? '';
const body = useResponseBodyText(response).data ?? '';
const contentForIframe: string | undefined = useMemo(() => {
if (body.includes('<head>')) {

View File

@@ -1,18 +1,25 @@
import { useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
import { InlineCode } from '../components/core/InlineCode';
import { minPromiseMillis } from '../lib/minPromiseMillis';
import { useAlert } from './useAlert';
import { useAppInfo } from './useAppInfo';
export function useCheckForUpdates() {
const alert = useAlert();
const appInfo = useAppInfo();
return useMutation({
mutationFn: async () => {
const hasUpdate: boolean = await minPromiseMillis(invoke('cmd_check_for_updates'), 500);
if (!hasUpdate) {
alert({
id: 'no-updates',
title: 'No Updates',
body: 'You are currently up to date',
title: 'No Update Available',
body: (
<>
You are currently on the latest version <InlineCode>{appInfo?.version}</InlineCode>
</>
),
});
}
},

View File

@@ -18,11 +18,13 @@ export function useClipboardText() {
const setText = useCallback(
(text: string) => {
writeText(text).catch(console.error);
toast.show({
id: 'copied',
variant: 'copied',
message: 'Copied to clipboard',
});
if (text != '') {
toast.show({
id: 'copied',
variant: 'copied',
message: 'Copied to clipboard',
});
}
setValue(text);
},
[setValue, toast],

View File

@@ -1,6 +1,5 @@
import { useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
import type { Workspace } from '../lib/models';
import { useAppRoutes } from './useAppRoutes';
import { getRecentEnvironments } from './useRecentEnvironments';
import { getRecentRequests } from './useRecentRequests';
@@ -10,37 +9,35 @@ export function useOpenWorkspace() {
return useMutation({
mutationFn: async ({
workspace,
workspaceId,
inNewWindow,
}: {
workspace: Workspace;
workspaceId: string;
inNewWindow: boolean;
}) => {
if (workspace == null) return;
if (inNewWindow) {
const environmentId = (await getRecentEnvironments(workspace.id))[0];
const requestId = (await getRecentRequests(workspace.id))[0];
const environmentId = (await getRecentEnvironments(workspaceId))[0];
const requestId = (await getRecentRequests(workspaceId))[0];
const path =
requestId != null
? routes.paths.request({
workspaceId: workspace.id,
workspaceId,
environmentId,
requestId,
})
: routes.paths.workspace({ workspaceId: workspace.id, environmentId });
: routes.paths.workspace({ workspaceId, environmentId });
await invoke('cmd_new_window', { url: path });
} else {
const environmentId = (await getRecentEnvironments(workspace.id))[0];
const requestId = (await getRecentRequests(workspace.id))[0];
const environmentId = (await getRecentEnvironments(workspaceId))[0];
const requestId = (await getRecentRequests(workspaceId))[0];
if (requestId != null) {
routes.navigate('request', {
workspaceId: workspace.id,
workspaceId: workspaceId,
environmentId,
requestId,
});
} else {
routes.navigate('workspace', { workspaceId: workspace.id, environmentId });
routes.navigate('workspace', { workspaceId, environmentId });
}
}
},

View File

@@ -5,7 +5,6 @@ import { getResponseBodyText } from '../lib/responseBody';
export function useResponseBodyText(response: HttpResponse) {
return useQuery<string | null>({
queryKey: ['response-body-text', response?.updatedAt],
initialData: null,
queryFn: () => getResponseBodyText(response),
}).data;
});
}

View File

@@ -206,28 +206,3 @@ export const mimeTypes = [
'video/x-flv',
'video/x-m4v',
];
export function isBinaryContentType(contentType: string | null) {
const mimeType = contentType?.split(';')[0];
if (mimeType == null) return false;
const [first, second] = mimeType.split('/').map((s) => s.trim().toLowerCase());
if (first == 'text' || second == null) {
return false;
}
if (first != 'application') {
return true;
}
const isTextSubtype =
second === 'json' ||
second === 'ld+json' ||
second === 'x-httpd-php' ||
second === 'x-sh' ||
second === 'x-csh' ||
second === 'xhtml+xml' ||
second === 'xml';
return !isTextSubtype;
}