mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-21 17:09:09 +01:00
Initial "plugin" system with importer (#7)
This commit is contained in:
4
Makefile
4
Makefile
@@ -5,3 +5,7 @@ sqlx-prepare:
|
||||
|
||||
dev:
|
||||
npm run tauri-dev
|
||||
|
||||
|
||||
build:
|
||||
./node_modules/.bin/tauri build
|
||||
|
||||
Binary file not shown.
4
plugins/hello-world/greet.js
Normal file
4
plugins/hello-world/greet.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export function greet() {
|
||||
// Call Rust-provided fn!
|
||||
sayHello('Plugin');
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { hello } from './hello.js';
|
||||
import { greet } from './greet.js';
|
||||
|
||||
export function entrypoint() {
|
||||
hello();
|
||||
export function hello() {
|
||||
greet();
|
||||
console.log('Try JSON parse', JSON.parse(`{ "hello": 123 }`).hello);
|
||||
console.log('Try RegExp', '123'.match(/[\d]+/));
|
||||
}
|
||||
100
plugins/insomnia-importer/Insomnia_hello-world.json
Normal file
100
plugins/insomnia-importer/Insomnia_hello-world.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"_type": "export",
|
||||
"__export_format": 4,
|
||||
"__export_date": "2023-11-01T23:41:02.844Z",
|
||||
"__export_source": "insomnia.desktop.app:v8.3.0",
|
||||
"resources": [
|
||||
{
|
||||
"_id": "req_c8ae891b0fe549a4a530a75da59b6e34",
|
||||
"parentId": "wrk_ea69a78d6a0540f583d2ec80666a1724",
|
||||
"modified": 1698767088880,
|
||||
"created": 1698767077168,
|
||||
"url": "https://schier.co",
|
||||
"name": "My Request",
|
||||
"description": "",
|
||||
"method": "GET",
|
||||
"body": {},
|
||||
"parameters": [],
|
||||
"headers": [{ "name": "User-Agent", "value": "insomnia/8.3.0" }],
|
||||
"authentication": {},
|
||||
"metaSortKey": -1698767077168,
|
||||
"isPrivate": false,
|
||||
"settingStoreCookies": true,
|
||||
"settingSendCookies": true,
|
||||
"settingDisableRenderRequestBody": false,
|
||||
"settingEncodeUrl": true,
|
||||
"settingRebuildPath": true,
|
||||
"settingFollowRedirects": "global",
|
||||
"_type": "request"
|
||||
},
|
||||
{
|
||||
"_id": "wrk_ea69a78d6a0540f583d2ec80666a1724",
|
||||
"parentId": null,
|
||||
"modified": 1698767073768,
|
||||
"created": 1698767068649,
|
||||
"name": "Hello World",
|
||||
"description": "",
|
||||
"scope": "collection",
|
||||
"_type": "workspace"
|
||||
},
|
||||
{
|
||||
"_id": "env_90b3abd7ed857fd535396167018da33932100672",
|
||||
"parentId": "wrk_ea69a78d6a0540f583d2ec80666a1724",
|
||||
"modified": 1698881852559,
|
||||
"created": 1698767068650,
|
||||
"name": "Base Environment",
|
||||
"data": { "base": true },
|
||||
"dataPropertyOrder": { "&": ["base"] },
|
||||
"color": null,
|
||||
"isPrivate": false,
|
||||
"metaSortKey": 1698767068650,
|
||||
"_type": "environment"
|
||||
},
|
||||
{
|
||||
"_id": "jar_90b3abd7ed857fd535396167018da33932100672",
|
||||
"parentId": "wrk_ea69a78d6a0540f583d2ec80666a1724",
|
||||
"modified": 1698767090390,
|
||||
"created": 1698767068651,
|
||||
"name": "Default Jar",
|
||||
"cookies": [
|
||||
{
|
||||
"key": "_gorilla_csrf",
|
||||
"value": "MTY5ODc2NzA5MHxJa1Z1U0RCVVMzcDJhbEJFWkd0Q09WVkllbXBMVlhSd1VtaGFkVlpsVVhobVNVNDVTV2hDWmpFd1JtTTlJZ289fPkab2rsnQwWmJi-pCbg5Wz4O_6csc29ZcYOdB0tOLtD",
|
||||
"expires": "2023-11-07T15:44:50.000Z",
|
||||
"maxAge": 604800,
|
||||
"domain": "schier.co",
|
||||
"path": "/",
|
||||
"httpOnly": true,
|
||||
"hostOnly": true,
|
||||
"creation": "2023-10-31T15:44:50.390Z",
|
||||
"lastAccessed": "2023-10-31T15:44:50.390Z",
|
||||
"sameSite": "lax",
|
||||
"id": "672286917061701"
|
||||
}
|
||||
],
|
||||
"_type": "cookie_jar"
|
||||
},
|
||||
{
|
||||
"_id": "env_d04deba50c2f44b0b9bd01c53efebff4",
|
||||
"parentId": "env_90b3abd7ed857fd535396167018da33932100672",
|
||||
"modified": 1698882026143,
|
||||
"created": 1698881855600,
|
||||
"name": "Sub Environment",
|
||||
"data": {
|
||||
"string": "string",
|
||||
"bool": true,
|
||||
"number": 123,
|
||||
"object": { "foo": "bar" },
|
||||
"array": [1, 2, 3]
|
||||
},
|
||||
"dataPropertyOrder": {
|
||||
"&": ["string", "bool", "number", "object", "array"],
|
||||
"&~|object": ["foo"]
|
||||
},
|
||||
"color": null,
|
||||
"isPrivate": false,
|
||||
"metaSortKey": 1698881855600,
|
||||
"_type": "environment"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
plugins/insomnia-importer/importers/environment.js
Normal file
23
plugins/insomnia-importer/importers/environment.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Import an Insomnia environment object.
|
||||
* @param {Object} e - The environment object to import.
|
||||
*/
|
||||
export function importEnvironment(e) {
|
||||
if (e.parentId.startsWith('env_')) {
|
||||
return null;
|
||||
}
|
||||
console.log('IMPORTING Environment', e._id, e.name, JSON.stringify(e, null, 2));
|
||||
return {
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
workspaceId: e.parentId,
|
||||
model: 'environment',
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data).map(([name, value]) => ({
|
||||
enabled: true,
|
||||
name,
|
||||
value: `${value}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
28
plugins/insomnia-importer/importers/request.js
Normal file
28
plugins/insomnia-importer/importers/request.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Import an Insomnia request object.
|
||||
* @param {Object} r - The request object to import.
|
||||
* @param {number} sortPriority - The sort priority to use for the request.
|
||||
*/
|
||||
export function importRequest(r, sortPriority = 0) {
|
||||
console.log('IMPORTING REQUEST', r._id, r.name, JSON.stringify(r, null, 2));
|
||||
return {
|
||||
id: r._id,
|
||||
createdAt: new Date(r.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(r.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
workspaceId: r.parentId,
|
||||
model: 'http_request',
|
||||
sortPriority,
|
||||
name: r.name,
|
||||
url: r.url,
|
||||
body: null, // TODO: Import body
|
||||
bodyType: null,
|
||||
authentication: {}, // TODO: Import authentication
|
||||
authenticationType: null,
|
||||
method: r.method,
|
||||
headers: (r.headers ?? []).map(({ name, value, disabled }) => ({
|
||||
enabled: !disabled,
|
||||
name,
|
||||
value,
|
||||
})),
|
||||
};
|
||||
}
|
||||
14
plugins/insomnia-importer/importers/workspace.js
Normal file
14
plugins/insomnia-importer/importers/workspace.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Import an Insomnia workspace object.
|
||||
* @param {Object} w - The workspace object to import.
|
||||
*/
|
||||
export function importWorkspace(w) {
|
||||
console.log('IMPORTING Workpace', w._id, w.name, JSON.stringify(w, null, 2));
|
||||
return {
|
||||
id: w._id,
|
||||
createdAt: new Date(w.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(w.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
model: 'workspace',
|
||||
name: w.name,
|
||||
};
|
||||
}
|
||||
50
plugins/insomnia-importer/index.js
Normal file
50
plugins/insomnia-importer/index.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { importEnvironment } from './importers/environment.js';
|
||||
import { importRequest } from './importers/request.js';
|
||||
import { importWorkspace } from './importers/workspace.js';
|
||||
|
||||
const TYPES = {
|
||||
workspace: 'workspace',
|
||||
request: 'request',
|
||||
environment: 'environment',
|
||||
};
|
||||
|
||||
export function pluginHookImport(contents) {
|
||||
const parsed = JSON.parse(contents);
|
||||
if (!isObject(parsed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { _type, __export_format } = parsed;
|
||||
if (_type !== 'export' || __export_format !== 4 || !Array.isArray(parsed.resources)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resources = {
|
||||
workspaces: [],
|
||||
requests: [],
|
||||
environments: [],
|
||||
};
|
||||
|
||||
for (const v of parsed.resources) {
|
||||
if (v._type === TYPES.workspace) {
|
||||
resources.workspaces.push(importWorkspace(v));
|
||||
} else if (v._type === TYPES.environment) {
|
||||
resources.environments.push(importEnvironment(v));
|
||||
} else if (v._type === TYPES.request) {
|
||||
resources.requests.push(importRequest(v));
|
||||
} else {
|
||||
console.log('UNKNOWN TYPE', v._type, JSON.stringify(v, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out any `null` values
|
||||
resources.requests = resources.requests.filter(Boolean);
|
||||
resources.environments = resources.environments.filter(Boolean);
|
||||
resources.workspaces = resources.workspaces.filter(Boolean);
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
function isObject(obj) {
|
||||
return Object.prototype.toString.call(obj) === '[object Object]';
|
||||
}
|
||||
68
src-tauri/Cargo.lock
generated
68
src-tauri/Cargo.lock
generated
@@ -133,6 +133,17 @@ dependencies = [
|
||||
"critical-section",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi 0.1.19",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
@@ -509,6 +520,30 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags 1.3.2",
|
||||
"clap_lex",
|
||||
"indexmap 1.9.3",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cobs"
|
||||
version = "0.2.3"
|
||||
@@ -1577,6 +1612,15 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.3"
|
||||
@@ -2392,7 +2436,7 @@ version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.3.3",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -2561,6 +2605,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
@@ -4012,6 +4062,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.5",
|
||||
"bytes",
|
||||
"clap",
|
||||
"cocoa 0.24.1",
|
||||
"dirs-next",
|
||||
"embed_plist",
|
||||
@@ -4238,6 +4289,21 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
|
||||
|
||||
[[package]]
|
||||
name = "thin-slice"
|
||||
version = "0.1.1"
|
||||
|
||||
@@ -29,7 +29,19 @@ reqwest = { version = "0.11.14", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time", "offline"] }
|
||||
tauri = { version = "1.3", features = ["config-toml", "devtools", "fs-read-file", "os-all", "protocol-asset", "shell-open", "system-tray", "updater", "window-start-dragging"] }
|
||||
tauri = { version = "1.3", features = [
|
||||
"cli",
|
||||
"config-toml",
|
||||
"devtools",
|
||||
"fs-read-file",
|
||||
"os-all",
|
||||
"protocol-asset",
|
||||
"shell-open",
|
||||
"system-tray",
|
||||
"updater",
|
||||
"window-start-dragging",
|
||||
"dialog-open",
|
||||
] }
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tokio = { version = "1.25.0", features = ["sync"] }
|
||||
uuid = "1.3.0"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE workspaces ADD COLUMN variables DEFAULT '[]' NOT NULL;
|
||||
@@ -1,3 +0,0 @@
|
||||
export function hello() {
|
||||
sayHello('Plugin');
|
||||
}
|
||||
@@ -160,16 +160,6 @@
|
||||
},
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n "
|
||||
},
|
||||
"3ec4710d28a7f38608c96798d971217ac97788bcb639089d0c5750c0d339bc9a": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE environments\n SET (name, variables, updated_at) = (?, ?, CURRENT_TIMESTAMP)\n WHERE id = ?;\n "
|
||||
},
|
||||
"448a1d1f1866ab42c0f81fcf8eb2930bf21dfdd43ca4831bc1a198cf45ac3732": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -180,6 +170,60 @@
|
||||
},
|
||||
"query": "\n DELETE FROM http_requests\n WHERE id = ?\n "
|
||||
},
|
||||
"5588db23df7f30dc75857e05395ebbcf2384e2ac0d7cb87f76d74c6d50781d7b": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT id, model, created_at, updated_at, name, description,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM workspaces\n "
|
||||
},
|
||||
"5aa070e61995f8b1724efaa94c5f0cef5a4be6efda5d70354ad449d7d4b5aee4": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -282,6 +326,16 @@
|
||||
},
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n "
|
||||
},
|
||||
"610223ad10b6e25926d486ba775a74b55625fcc4e6637d8a805d44ec3f3b9532": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO workspaces (id, name, description, variables)\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n description = excluded.description,\n variables = excluded.variables\n "
|
||||
},
|
||||
"62475fd9483fb5eda01c937949da2ef66ac7005b4be06b87aa6210d462348aca": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -452,16 +506,6 @@
|
||||
},
|
||||
"query": "\n DELETE FROM workspaces\n WHERE id = ?\n "
|
||||
},
|
||||
"86e32d6a6fadf35436f19b577a659c203a8d143cb3a8d6122951c5bf54a0888d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO environments (id, workspace_id, name, variables)\n VALUES (?, ?, ?, ?)\n "
|
||||
},
|
||||
"8947a2a90478277c42fe9b06bc1fa98197642a4d281a3dbc101be2c9c1fec36c": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -648,102 +692,6 @@
|
||||
},
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n "
|
||||
},
|
||||
"caf3f21bf291dfbd36446592066e96c1f83abe96f6ea9211a3e049eb9c58a8c8": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT id, model, created_at, updated_at, name, description\n FROM workspaces WHERE id = ?\n "
|
||||
},
|
||||
"cea4cae52f16ec78aca9a47b17117422d4f165e5a3b308c70fd1a180382475ea": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT id, model, created_at, updated_at, name, description\n FROM workspaces\n "
|
||||
},
|
||||
"ced098adb79c0ee64e223b6e02371ef253920a2c342275de0fa9c181529a4adc": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -850,24 +798,68 @@
|
||||
},
|
||||
"query": "\n INSERT INTO key_values (namespace, key, value)\n VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n value = excluded.value\n "
|
||||
},
|
||||
"e0f41023d877d94b7609ce910a71bd89c4827a558654b8ae14d85e6ba86990cf": {
|
||||
"dbe457087a7bccbca4c1d673aa8e547df04530a7f860a6ccd4e20126a7cdfa4f": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Null"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE workspaces SET (name, updated_at) =\n (?, CURRENT_TIMESTAMP) WHERE id = ?;\n "
|
||||
"query": "\n SELECT id, model, created_at, updated_at, name, description,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM workspaces WHERE id = ?\n "
|
||||
},
|
||||
"f116d8cf9aad828135bb8c3a4c8b8e6b857ae13303989e9133a33b2d1cf20e96": {
|
||||
"dcc2f405f8e29d0599d86bcde509187e9cc5fc647067eaa5c738cb24e2f081e5": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
"Right": 4
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO workspaces (id, name, description)\n VALUES (?, ?, ?)\n "
|
||||
"query": "\n INSERT INTO environments (\n id,\n workspace_id,\n name,\n variables\n )\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n variables = excluded.variables\n "
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ use std::collections::HashMap;
|
||||
use std::env::current_dir;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::Write;
|
||||
use std::process::exit;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
use tauri::{AppHandle, Menu, MenuItem, RunEvent, State, Submenu, Window, WindowUrl, Wry};
|
||||
@@ -30,9 +31,9 @@ use tokio::sync::Mutex;
|
||||
use window_ext::TrafficLightWindowExt;
|
||||
|
||||
mod models;
|
||||
mod plugin;
|
||||
mod render;
|
||||
mod window_ext;
|
||||
mod plugin;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct CustomResponse {
|
||||
@@ -72,8 +73,14 @@ async fn send_ephemeral_request(
|
||||
let pool = &*db_instance.lock().await;
|
||||
let response = models::HttpResponse::default();
|
||||
let environment_id2 = environment_id.unwrap_or("n/a").to_string();
|
||||
return actually_send_ephemeral_request(request, &response, &environment_id2, &app_handle, pool)
|
||||
.await;
|
||||
return actually_send_ephemeral_request(
|
||||
request,
|
||||
&response,
|
||||
&environment_id2,
|
||||
&app_handle,
|
||||
pool,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn actually_send_ephemeral_request(
|
||||
@@ -248,6 +255,25 @@ async fn actually_send_ephemeral_request(
|
||||
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
|
||||
}
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn import_data(
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
file_paths: Vec<&str>,
|
||||
workspace_id: Option<&str>,
|
||||
) -> Result<plugin::ImportedResources, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let workspace_id2 = workspace_id.unwrap_or_default();
|
||||
let imported = plugin::run_plugin_import(
|
||||
&window.app_handle(),
|
||||
pool,
|
||||
"insomnia-importer",
|
||||
file_paths.first().unwrap(),
|
||||
workspace_id2,
|
||||
)
|
||||
.await;
|
||||
Ok(imported)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn send_request(
|
||||
@@ -331,9 +357,15 @@ async fn create_workspace(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Workspace, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let created_workspace = models::create_workspace(name, "", pool)
|
||||
.await
|
||||
.expect("Failed to create workspace");
|
||||
let created_workspace = models::upsert_workspace(
|
||||
pool,
|
||||
models::Workspace {
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create workspace");
|
||||
|
||||
emit_and_return(&window, "created_model", created_workspace)
|
||||
}
|
||||
@@ -347,9 +379,17 @@ async fn create_environment(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Environment, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let created_environment = models::create_environment(workspace_id, name, variables, pool)
|
||||
.await
|
||||
.expect("Failed to create environment");
|
||||
let created_environment = models::upsert_environment(
|
||||
pool,
|
||||
models::Environment {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
name: name.to_string(),
|
||||
variables: Json(variables),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create environment");
|
||||
|
||||
emit_and_return(&window, "created_model", created_environment)
|
||||
}
|
||||
@@ -363,20 +403,15 @@ async fn create_request(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::HttpRequest, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let headers = Vec::new();
|
||||
let created_request = models::upsert_request(
|
||||
None,
|
||||
workspace_id,
|
||||
name,
|
||||
"GET",
|
||||
None,
|
||||
None,
|
||||
HashMap::new(),
|
||||
None,
|
||||
"",
|
||||
headers,
|
||||
sort_priority,
|
||||
pool,
|
||||
models::HttpRequest {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
name: name.to_string(),
|
||||
method: "GET".to_string(),
|
||||
sort_priority,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create request");
|
||||
@@ -405,7 +440,7 @@ async fn update_workspace(
|
||||
) -> Result<models::Workspace, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
let updated_workspace = models::update_workspace(workspace, pool)
|
||||
let updated_workspace = models::upsert_workspace(pool, workspace)
|
||||
.await
|
||||
.expect("Failed to update request");
|
||||
|
||||
@@ -420,14 +455,9 @@ async fn update_environment(
|
||||
) -> Result<models::Environment, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
let updated_environment = models::update_environment(
|
||||
environment.id.as_str(),
|
||||
environment.name.as_str(),
|
||||
environment.variables.0,
|
||||
pool,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update request");
|
||||
let updated_environment = models::upsert_environment(pool, environment)
|
||||
.await
|
||||
.expect("Failed to update environment");
|
||||
|
||||
emit_and_return(&window, "updated_model", updated_environment)
|
||||
}
|
||||
@@ -439,35 +469,9 @@ async fn update_request(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::HttpRequest, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
// TODO: Figure out how to make this better
|
||||
let b2;
|
||||
let body = match request.body {
|
||||
Some(b) => {
|
||||
b2 = b;
|
||||
Some(b2.as_str())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
// TODO: Figure out how to make this better
|
||||
let updated_request = models::upsert_request(
|
||||
Some(request.id.as_str()),
|
||||
request.workspace_id.as_str(),
|
||||
request.name.as_str(),
|
||||
request.method.as_str(),
|
||||
body,
|
||||
request.body_type,
|
||||
request.authentication.0,
|
||||
request.authentication_type,
|
||||
request.url.as_str(),
|
||||
request.headers.0,
|
||||
request.sort_priority,
|
||||
pool,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update request");
|
||||
|
||||
let updated_request = models::upsert_request(pool, request)
|
||||
.await
|
||||
.expect("Failed to update request");
|
||||
emit_and_return(&window, "updated_model", updated_request)
|
||||
}
|
||||
|
||||
@@ -598,10 +602,15 @@ async fn list_workspaces(
|
||||
.await
|
||||
.expect("Failed to find workspaces");
|
||||
if workspaces.is_empty() {
|
||||
let workspace =
|
||||
models::create_workspace("My Project", "This is the default workspace", pool)
|
||||
.await
|
||||
.expect("Failed to create workspace");
|
||||
let workspace = models::upsert_workspace(
|
||||
pool,
|
||||
models::Workspace {
|
||||
name: "My Project".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create workspace");
|
||||
Ok(vec![workspace])
|
||||
} else {
|
||||
Ok(workspaces)
|
||||
@@ -641,6 +650,7 @@ fn main() {
|
||||
let p_string = p.to_string_lossy().replace(' ', "%20");
|
||||
let url = format!("sqlite://{}?mode=rwc", p_string);
|
||||
println!("Connecting to database at {}", url);
|
||||
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.connect(url.as_str())
|
||||
@@ -648,12 +658,44 @@ fn main() {
|
||||
.expect("Failed to connect to database");
|
||||
|
||||
// Setup the DB handle
|
||||
let m = Mutex::new(pool);
|
||||
let m = Mutex::new(pool.clone());
|
||||
migrate_db(app.handle(), &m)
|
||||
.await
|
||||
.expect("Failed to migrate database");
|
||||
app.manage(m);
|
||||
|
||||
// TODO: Move this somewhere better
|
||||
match app.get_cli_matches() {
|
||||
Ok(matches) => {
|
||||
let cmd = matches.subcommand.unwrap_or_default();
|
||||
if cmd.name == "import" {
|
||||
let arg_file = cmd
|
||||
.matches
|
||||
.args
|
||||
.get("file")
|
||||
.unwrap()
|
||||
.value
|
||||
.as_str()
|
||||
.unwrap();
|
||||
plugin::run_plugin_import(
|
||||
&app.handle(),
|
||||
&pool,
|
||||
"insomnia-importer",
|
||||
arg_file,
|
||||
"wk_WN8Nrm2Awm",
|
||||
)
|
||||
.await;
|
||||
exit(0);
|
||||
} else if cmd.name == "hello" {
|
||||
plugin::run_plugin_hello(&app.handle(), "hello-world");
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Nothing found: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
@@ -671,6 +713,7 @@ fn main() {
|
||||
get_environment,
|
||||
get_request,
|
||||
get_workspace,
|
||||
import_data,
|
||||
list_environments,
|
||||
list_requests,
|
||||
list_responses,
|
||||
@@ -690,7 +733,6 @@ fn main() {
|
||||
let w = create_window(app_handle, None);
|
||||
w.restore_state(StateFlags::all())
|
||||
.expect("Failed to restore window state");
|
||||
plugin::test_plugins(&app_handle);
|
||||
}
|
||||
|
||||
// ExitRequested { api, .. } => {
|
||||
|
||||
@@ -7,8 +7,8 @@ use sqlx::types::chrono::NaiveDateTime;
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Workspace {
|
||||
pub id: String,
|
||||
pub model: String,
|
||||
@@ -16,10 +16,11 @@ pub struct Workspace {
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub variables: Json<Vec<EnvironmentVariable>>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Environment {
|
||||
pub id: String,
|
||||
pub workspace_id: String,
|
||||
@@ -30,35 +31,44 @@ pub struct Environment {
|
||||
pub variables: Json<Vec<EnvironmentVariable>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct EnvironmentVariable {
|
||||
#[serde(default)]
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct HttpRequestHeader {
|
||||
#[serde(default)]
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
fn default_http_request_method() -> String {
|
||||
"GET".to_string()
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct HttpRequest {
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub id: String,
|
||||
pub workspace_id: String,
|
||||
pub model: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub sort_priority: f64,
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
#[serde(default = "default_http_request_method")]
|
||||
pub method: String,
|
||||
pub body: Option<String>,
|
||||
pub body_type: Option<String>,
|
||||
@@ -67,15 +77,15 @@ pub struct HttpRequest {
|
||||
pub headers: Json<Vec<HttpRequestHeader>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct HttpResponseHeader {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct HttpResponse {
|
||||
pub id: String,
|
||||
pub model: String,
|
||||
@@ -94,8 +104,8 @@ pub struct HttpResponse {
|
||||
pub headers: Json<Vec<HttpResponseHeader>>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct KeyValue {
|
||||
pub model: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
@@ -153,7 +163,8 @@ pub async fn find_workspaces(pool: &Pool<Sqlite>) -> Result<Vec<Workspace>, sqlx
|
||||
sqlx::query_as!(
|
||||
Workspace,
|
||||
r#"
|
||||
SELECT id, model, created_at, updated_at, name, description
|
||||
SELECT id, model, created_at, updated_at, name, description,
|
||||
variables AS "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>"
|
||||
FROM workspaces
|
||||
"#,
|
||||
)
|
||||
@@ -165,7 +176,8 @@ pub async fn get_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace, s
|
||||
sqlx::query_as!(
|
||||
Workspace,
|
||||
r#"
|
||||
SELECT id, model, created_at, updated_at, name, description
|
||||
SELECT id, model, created_at, updated_at, name, description,
|
||||
variables AS "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>"
|
||||
FROM workspaces WHERE id = ?
|
||||
"#,
|
||||
id,
|
||||
@@ -193,27 +205,6 @@ pub async fn delete_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace
|
||||
Ok(workspace)
|
||||
}
|
||||
|
||||
pub async fn create_workspace(
|
||||
name: &str,
|
||||
description: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<Workspace, sqlx::Error> {
|
||||
let id = generate_id(Some("wk"));
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO workspaces (id, name, description)
|
||||
VALUES (?, ?, ?)
|
||||
"#,
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
get_workspace(&id, pool).await
|
||||
}
|
||||
|
||||
pub async fn find_environments(
|
||||
workspace_id: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
@@ -232,30 +223,6 @@ pub async fn find_environments(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_environment(
|
||||
workspace_id: &str,
|
||||
name: &str,
|
||||
variables: Vec<EnvironmentVariable>,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<Environment, sqlx::Error> {
|
||||
let id = generate_id(Some("en"));
|
||||
let trimmed_name = name.trim();
|
||||
let variables_json = Json(variables);
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO environments (id, workspace_id, name, variables)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"#,
|
||||
id,
|
||||
workspace_id,
|
||||
trimmed_name,
|
||||
variables_json,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_environment(&id, pool).await
|
||||
}
|
||||
|
||||
pub async fn delete_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environment, sqlx::Error> {
|
||||
let env = get_environment(id, pool).await?;
|
||||
let _ = sqlx::query!(
|
||||
@@ -271,26 +238,37 @@ pub async fn delete_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environ
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
pub async fn update_environment(
|
||||
id: &str,
|
||||
name: &str,
|
||||
variables: Vec<EnvironmentVariable>,
|
||||
pub async fn upsert_environment(
|
||||
pool: &Pool<Sqlite>,
|
||||
environment: Environment,
|
||||
) -> Result<Environment, sqlx::Error> {
|
||||
let variables_json = Json(variables);
|
||||
let id = match environment.id.as_str() {
|
||||
"" => generate_id(Some("ev")),
|
||||
_ => environment.id.to_string(),
|
||||
};
|
||||
let trimmed_name = environment.name.trim();
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE environments
|
||||
SET (name, variables, updated_at) = (?, ?, CURRENT_TIMESTAMP)
|
||||
WHERE id = ?;
|
||||
INSERT INTO environments (
|
||||
id,
|
||||
workspace_id,
|
||||
name,
|
||||
variables
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
name = excluded.name,
|
||||
variables = excluded.variables
|
||||
"#,
|
||||
name,
|
||||
variables_json,
|
||||
id,
|
||||
environment.workspace_id,
|
||||
trimmed_name,
|
||||
environment.variables,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_environment(id, pool).await
|
||||
get_environment(&id, pool).await
|
||||
}
|
||||
|
||||
pub async fn get_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environment, sqlx::Error> {
|
||||
@@ -315,60 +293,23 @@ pub async fn get_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environmen
|
||||
}
|
||||
|
||||
pub async fn duplicate_request(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, sqlx::Error> {
|
||||
let existing = get_request(id, pool).await?;
|
||||
|
||||
// TODO: Figure out how to make this better
|
||||
let b2;
|
||||
let body = match existing.body {
|
||||
Some(b) => {
|
||||
b2 = b;
|
||||
Some(b2.as_str())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
upsert_request(
|
||||
None,
|
||||
existing.workspace_id.as_str(),
|
||||
existing.name.as_str(),
|
||||
existing.method.as_str(),
|
||||
body,
|
||||
existing.body_type,
|
||||
existing.authentication.0,
|
||||
existing.authentication_type,
|
||||
existing.url.as_str(),
|
||||
existing.headers.0,
|
||||
existing.sort_priority + 0.001,
|
||||
pool,
|
||||
)
|
||||
.await
|
||||
let mut request = get_request(id, pool).await?.clone();
|
||||
request.id = "".to_string();
|
||||
upsert_request(pool, request).await
|
||||
}
|
||||
|
||||
pub async fn upsert_request(
|
||||
id: Option<&str>,
|
||||
workspace_id: &str,
|
||||
name: &str,
|
||||
method: &str,
|
||||
body: Option<&str>,
|
||||
body_type: Option<String>,
|
||||
authentication: HashMap<String, JsonValue>,
|
||||
authentication_type: Option<String>,
|
||||
url: &str,
|
||||
headers: Vec<HttpRequestHeader>,
|
||||
sort_priority: f64,
|
||||
pool: &Pool<Sqlite>,
|
||||
r: HttpRequest,
|
||||
) -> Result<HttpRequest, sqlx::Error> {
|
||||
let generated_id;
|
||||
let id = match id {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
generated_id = generate_id(Some("rq"));
|
||||
generated_id.as_str()
|
||||
}
|
||||
let id = match r.id.as_str() {
|
||||
"" => generate_id(Some("rq")),
|
||||
_ => r.id.to_string(),
|
||||
};
|
||||
let headers_json = Json(headers);
|
||||
let auth_json = Json(authentication);
|
||||
let trimmed_name = name.trim();
|
||||
let headers_json = Json(r.headers);
|
||||
let auth_json = Json(r.authentication);
|
||||
let trimmed_name = r.name.trim();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO http_requests (
|
||||
@@ -398,20 +339,21 @@ pub async fn upsert_request(
|
||||
sort_priority = excluded.sort_priority
|
||||
"#,
|
||||
id,
|
||||
workspace_id,
|
||||
r.workspace_id,
|
||||
trimmed_name,
|
||||
url,
|
||||
method,
|
||||
body,
|
||||
body_type,
|
||||
r.url,
|
||||
r.method,
|
||||
r.body,
|
||||
r.body_type,
|
||||
auth_json,
|
||||
authentication_type,
|
||||
r.authentication_type,
|
||||
headers_json,
|
||||
sort_priority,
|
||||
r.sort_priority,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_request(id, pool).await
|
||||
|
||||
get_request(&id, pool).await
|
||||
}
|
||||
|
||||
pub async fn find_requests(
|
||||
@@ -552,18 +494,29 @@ pub async fn update_response_if_id(
|
||||
return update_response(response, pool).await;
|
||||
}
|
||||
|
||||
pub async fn update_workspace(
|
||||
workspace: Workspace,
|
||||
pub async fn upsert_workspace(
|
||||
pool: &Pool<Sqlite>,
|
||||
workspace: Workspace,
|
||||
) -> Result<Workspace, sqlx::Error> {
|
||||
let id = match workspace.id.as_str() {
|
||||
"" => generate_id(Some("wk")),
|
||||
_ => workspace.id.to_string(),
|
||||
};
|
||||
let trimmed_name = workspace.name.trim();
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE workspaces SET (name, updated_at) =
|
||||
(?, CURRENT_TIMESTAMP) WHERE id = ?;
|
||||
INSERT INTO workspaces (id, name, description, variables)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
name = excluded.name,
|
||||
description = excluded.description,
|
||||
variables = excluded.variables
|
||||
"#,
|
||||
id,
|
||||
trimmed_name,
|
||||
workspace.id,
|
||||
workspace.description,
|
||||
workspace.variables,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::fs;
|
||||
|
||||
use boa_engine::{
|
||||
js_string,
|
||||
module::{ModuleLoader, SimpleModuleLoader},
|
||||
@@ -5,17 +7,96 @@ use boa_engine::{
|
||||
Context, JsArgs, JsNativeError, JsValue, Module, NativeFunction, Source,
|
||||
};
|
||||
use boa_runtime::Console;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub fn test_plugins(app_handle: &AppHandle) {
|
||||
use crate::models::{self, Environment, HttpRequest, Workspace};
|
||||
|
||||
pub fn run_plugin_hello(app_handle: &AppHandle, plugin_name: &str) {
|
||||
run_plugin(app_handle, plugin_name, "hello", &[]);
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||
pub struct ImportedResources {
|
||||
requests: Vec<HttpRequest>,
|
||||
environments: Vec<Environment>,
|
||||
workspaces: Vec<Workspace>,
|
||||
}
|
||||
|
||||
pub async fn run_plugin_import(
|
||||
app_handle: &AppHandle,
|
||||
pool: &Pool<Sqlite>,
|
||||
plugin_name: &str,
|
||||
file_path: &str,
|
||||
workspace_id: &str,
|
||||
) -> ImportedResources {
|
||||
let file = fs::read_to_string(file_path).expect("Unable to read file");
|
||||
let file_contents = file.as_str();
|
||||
let result_json = run_plugin(
|
||||
app_handle,
|
||||
plugin_name,
|
||||
"pluginHookImport",
|
||||
&[js_string!(file_contents).into()],
|
||||
);
|
||||
let resources: ImportedResources =
|
||||
serde_json::from_value(result_json).expect("failed to parse result json");
|
||||
let mut imported_resources = ImportedResources::default();
|
||||
|
||||
println!("Importing resources: {}", workspace_id.is_empty());
|
||||
if workspace_id.is_empty() {
|
||||
for w in resources.workspaces {
|
||||
println!("Importing workspace: {:?}", w);
|
||||
let x = models::upsert_workspace(&pool, w)
|
||||
.await
|
||||
.expect("Failed to create workspace");
|
||||
imported_resources.workspaces.push(x.clone());
|
||||
println!("Imported workspace: {}", x.name);
|
||||
}
|
||||
}
|
||||
|
||||
for mut e in resources.environments {
|
||||
if !workspace_id.is_empty() {
|
||||
e.workspace_id = workspace_id.to_string();
|
||||
}
|
||||
println!("Importing environment: {:?}", e);
|
||||
let x = models::upsert_environment(&pool, e)
|
||||
.await
|
||||
.expect("Failed to create environment");
|
||||
imported_resources.environments.push(x.clone());
|
||||
println!("Imported environment: {}", x.name);
|
||||
}
|
||||
|
||||
for mut r in resources.requests {
|
||||
if !workspace_id.is_empty() {
|
||||
r.workspace_id = workspace_id.to_string();
|
||||
}
|
||||
println!("Importing request: {:?}", r);
|
||||
let x = models::upsert_request(&pool, r)
|
||||
.await
|
||||
.expect("Failed to create request");
|
||||
imported_resources.requests.push(x.clone());
|
||||
println!("Imported request: {}", x.name);
|
||||
}
|
||||
|
||||
imported_resources
|
||||
}
|
||||
|
||||
fn run_plugin(
|
||||
app_handle: &AppHandle,
|
||||
plugin_name: &str,
|
||||
entrypoint: &str,
|
||||
js_args: &[JsValue],
|
||||
) -> serde_json::Value {
|
||||
let plugin_dir = app_handle
|
||||
.path_resolver()
|
||||
.resolve_resource("plugins/hello-world")
|
||||
.expect("failed to resolve plugin directory resource");
|
||||
let plugin_entry_file = app_handle
|
||||
.path_resolver()
|
||||
.resolve_resource("plugins/hello-world/index.js")
|
||||
.expect("failed to resolve plugin entry point resource");
|
||||
.resolve_resource("../plugins")
|
||||
.expect("failed to resolve plugin directory resource")
|
||||
.join(plugin_name);
|
||||
let plugin_index_file = plugin_dir.join("index.js");
|
||||
|
||||
println!("Plugin dir: {:?}", plugin_dir);
|
||||
|
||||
// Module loader for the specific plugin
|
||||
let loader = &SimpleModuleLoader::new(plugin_dir).expect("failed to create module loader");
|
||||
@@ -29,14 +110,14 @@ pub fn test_plugins(app_handle: &AppHandle) {
|
||||
add_runtime(context);
|
||||
add_globals(context);
|
||||
|
||||
let source = Source::from_filepath(&plugin_entry_file).expect("Error opening file");
|
||||
let source = Source::from_filepath(&plugin_index_file).expect("Error opening file");
|
||||
|
||||
// Can also pass a `Some(realm)` if you need to execute the module in another realm.
|
||||
let module = Module::parse(source, None, context).expect("failed to parse module");
|
||||
|
||||
// Insert parsed entrypoint into the module loader
|
||||
// TODO: Is this needed if loaded from file already?
|
||||
loader.insert(plugin_entry_file, module.clone());
|
||||
loader.insert(plugin_index_file, module.clone());
|
||||
|
||||
let _promise_result = module
|
||||
.load_link_evaluate(context)
|
||||
@@ -58,18 +139,22 @@ pub fn test_plugins(app_handle: &AppHandle) {
|
||||
|
||||
let namespace = module.namespace(context);
|
||||
|
||||
let entrypoint_fn = namespace
|
||||
.get(js_string!("entrypoint"), context)
|
||||
let result = namespace
|
||||
.get(js_string!(entrypoint), context)
|
||||
.expect("failed to get entrypoint")
|
||||
.as_callable()
|
||||
.cloned()
|
||||
.ok_or_else(|| JsNativeError::typ().with_message("export wasn't a function!"))
|
||||
.expect("Failed to get entrypoint");
|
||||
|
||||
// Actually call the entrypoint function
|
||||
let _result = entrypoint_fn
|
||||
.call(&JsValue::undefined(), &[], context)
|
||||
.expect("Failed to get entrypoint")
|
||||
.call(&JsValue::undefined(), js_args, context)
|
||||
.expect("Failed to call entrypoint");
|
||||
|
||||
match result.is_undefined() {
|
||||
true => json!(null), // to_json doesn't work with undefined (yet)
|
||||
false => result
|
||||
.to_json(context)
|
||||
.expect("failed to convert result to json"),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_runtime(context: &mut Context<'_>) {
|
||||
|
||||
@@ -12,6 +12,23 @@
|
||||
},
|
||||
"tauri": {
|
||||
"windows": [],
|
||||
"cli": {
|
||||
"description": "Yaak CLI",
|
||||
"longDescription": "This is the Yaak CLI, yo",
|
||||
"beforeHelp": "u can use it to build, develop and manage your Yaak application.",
|
||||
"afterHelp": "Have fun!",
|
||||
"args": [],
|
||||
"subcommands": {
|
||||
"import": {
|
||||
"args": [{
|
||||
"name": "file",
|
||||
"short": "f",
|
||||
"takesValue": true
|
||||
}]
|
||||
},
|
||||
"hello": {}
|
||||
}
|
||||
},
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"os": {
|
||||
@@ -36,6 +53,10 @@
|
||||
},
|
||||
"window": {
|
||||
"startDragging": true
|
||||
},
|
||||
"dialog": {
|
||||
"all": false,
|
||||
"open": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
@@ -54,7 +75,7 @@
|
||||
"longDescription": "The best cross-platform visual API client",
|
||||
"resources": [
|
||||
"migrations/*",
|
||||
"plugins/*"
|
||||
"../plugins/*"
|
||||
],
|
||||
"shortDescription": "The best API client",
|
||||
"targets": [
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useDialog } from './DialogContext';
|
||||
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
@@ -23,15 +22,14 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
const createEnvironment = useCreateEnvironment();
|
||||
const dialog = useDialog();
|
||||
const prompt = usePrompt();
|
||||
const routes = useAppRoutes();
|
||||
|
||||
const showEnvironmentDialog = useCallback(() => {
|
||||
dialog.show({
|
||||
title: 'Manage Environments',
|
||||
render: () => <EnvironmentEditDialog />,
|
||||
render: () => <EnvironmentEditDialog initialEnvironment={activeEnvironment} />,
|
||||
});
|
||||
}, [dialog]);
|
||||
}, [dialog, activeEnvironment]);
|
||||
|
||||
const items: DropdownItem[] = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||
import { useEnvironments } from '../hooks/useEnvironments';
|
||||
import type { Environment } from '../lib/models';
|
||||
import type { Environment, Workspace } from '../lib/models';
|
||||
import { Button } from './core/Button';
|
||||
import classNames from 'classnames';
|
||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { PairEditor } from './core/PairEditor';
|
||||
import type { PairEditorProps } from './core/PairEditor';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { IconButton } from './core/IconButton';
|
||||
@@ -19,12 +17,20 @@ import { usePrompt } from '../hooks/usePrompt';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { useWindowSize } from 'react-use';
|
||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||
|
||||
export const EnvironmentEditDialog = function () {
|
||||
const routes = useAppRoutes();
|
||||
interface Props {
|
||||
initialEnvironment: Environment | null;
|
||||
}
|
||||
|
||||
export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState<Environment | null>(
|
||||
initialEnvironment,
|
||||
);
|
||||
const environments = useEnvironments();
|
||||
const createEnvironment = useCreateEnvironment();
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
const showSidebar = windowSize.width > 500;
|
||||
@@ -39,19 +45,34 @@ export const EnvironmentEditDialog = function () {
|
||||
{showSidebar && (
|
||||
<aside className="grid grid-rows-[minmax(0,1fr)_auto] gap-y-0.5 h-full max-w-[250px] pr-4 border-r border-gray-100">
|
||||
<div className="min-w-0 h-full w-full overflow-y-scroll">
|
||||
<Button
|
||||
size="xs"
|
||||
color="custom"
|
||||
justify="start"
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'text-gray-600 hocus:text-gray-800',
|
||||
selectedEnvironment == null && 'bg-highlightSecondary !text-gray-900',
|
||||
)}
|
||||
onClick={() => {
|
||||
setSelectedEnvironment(null);
|
||||
}}
|
||||
>
|
||||
Base Environment
|
||||
</Button>
|
||||
{environments.map((e) => (
|
||||
<Button
|
||||
key={e.id}
|
||||
justify="start"
|
||||
size="xs"
|
||||
color="custom"
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'text-gray-600 hocus:text-gray-800',
|
||||
activeEnvironment?.id === e.id && 'bg-highlightSecondary !text-gray-900',
|
||||
selectedEnvironment?.id === e.id && 'bg-highlightSecondary !text-gray-900',
|
||||
)}
|
||||
justify="start"
|
||||
key={e.id}
|
||||
onClick={() => {
|
||||
routes.setEnvironment(e);
|
||||
setSelectedEnvironment(e);
|
||||
}}
|
||||
>
|
||||
{e.name}
|
||||
@@ -68,8 +89,8 @@ export const EnvironmentEditDialog = function () {
|
||||
</Button>
|
||||
</aside>
|
||||
)}
|
||||
{activeEnvironment != null ? (
|
||||
<EnvironmentEditor environment={activeEnvironment} />
|
||||
{activeWorkspace != null ? (
|
||||
<EnvironmentEditor environment={selectedEnvironment} workspace={activeWorkspace} />
|
||||
) : (
|
||||
<div className="flex w-full h-full items-center justify-center text-gray-400 italic">
|
||||
select an environment
|
||||
@@ -79,57 +100,72 @@ export const EnvironmentEditDialog = function () {
|
||||
);
|
||||
};
|
||||
|
||||
const EnvironmentEditor = function ({ environment }: { environment: Environment }) {
|
||||
const EnvironmentEditor = function ({
|
||||
environment,
|
||||
workspace,
|
||||
}: {
|
||||
environment: Environment | null;
|
||||
workspace: Workspace;
|
||||
}) {
|
||||
const environments = useEnvironments();
|
||||
const updateEnvironment = useUpdateEnvironment(environment.id);
|
||||
const updateEnvironment = useUpdateEnvironment(environment?.id ?? 'n/a');
|
||||
const updateWorkspace = useUpdateWorkspace(workspace.id);
|
||||
const deleteEnvironment = useDeleteEnvironment(environment);
|
||||
const variables = environment == null ? workspace.variables : environment.variables;
|
||||
const handleChange = useCallback<PairEditorProps['onChange']>(
|
||||
(variables) => {
|
||||
updateEnvironment.mutate({ variables });
|
||||
if (environment != null) {
|
||||
updateEnvironment.mutate({ variables });
|
||||
} else {
|
||||
updateWorkspace.mutate({ variables });
|
||||
}
|
||||
},
|
||||
[updateEnvironment],
|
||||
[updateWorkspace, updateEnvironment, environment],
|
||||
);
|
||||
|
||||
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
|
||||
const allVariableNames = environments.flatMap((e) => e.variables.map((v) => v.name));
|
||||
// Filter out empty strings and variables that already exist in the active environment
|
||||
const variableNames = allVariableNames.filter(
|
||||
(name) => name != '' && !environment.variables.find((v) => v.name === name),
|
||||
(name) => name != '' && !variables.find((v) => v.name === name),
|
||||
);
|
||||
return { options: variableNames.map((name) => ({ label: name, type: 'constant' })) };
|
||||
}, [environments, environment.variables]);
|
||||
}, [environments, variables]);
|
||||
|
||||
const prompt = usePrompt();
|
||||
const items = useMemo<DropdownItem[]>(
|
||||
() => [
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" size="sm" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
title: 'Rename Environment',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{environment.name}</InlineCode>
|
||||
</>
|
||||
),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: environment.name,
|
||||
});
|
||||
updateEnvironment.mutate({ name });
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
variant: 'danger',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" size="sm" />,
|
||||
onSelect: () => deleteEnvironment.mutate(),
|
||||
},
|
||||
],
|
||||
[deleteEnvironment, updateEnvironment, environment.name, prompt],
|
||||
const items = useMemo<DropdownItem[] | null>(
|
||||
() =>
|
||||
environment == null
|
||||
? null
|
||||
: [
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" size="sm" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
title: 'Rename Environment',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{environment.name}</InlineCode>
|
||||
</>
|
||||
),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: environment.name,
|
||||
});
|
||||
updateEnvironment.mutate({ name });
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
variant: 'danger',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" size="sm" />,
|
||||
onSelect: () => deleteEnvironment.mutate(),
|
||||
},
|
||||
],
|
||||
[deleteEnvironment, updateEnvironment, prompt, environment],
|
||||
);
|
||||
|
||||
const validateName = useCallback((name: string) => {
|
||||
@@ -141,10 +177,12 @@ const EnvironmentEditor = function ({ environment }: { environment: Environment
|
||||
return (
|
||||
<VStack space={2}>
|
||||
<HStack space={2} className="justify-between">
|
||||
<h1 className="text-xl">{environment.name}</h1>
|
||||
<Dropdown items={items}>
|
||||
<IconButton icon="gear" title="Environment Actions" size="sm" className="!h-auto w-8" />
|
||||
</Dropdown>
|
||||
<h1 className="text-xl">{environment?.name ?? 'Base Environment'}</h1>
|
||||
{items != null && (
|
||||
<Dropdown items={items}>
|
||||
<IconButton icon="gear" title="Environment Actions" size="sm" className="!h-auto w-8" />
|
||||
</Dropdown>
|
||||
)}
|
||||
</HStack>
|
||||
<PairEditor
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
@@ -153,8 +191,8 @@ const EnvironmentEditor = function ({ environment }: { environment: Environment
|
||||
valuePlaceholder="variable value"
|
||||
nameValidate={validateName}
|
||||
valueAutocompleteVariables={false}
|
||||
forceUpdateKey={environment.id}
|
||||
pairs={environment.variables}
|
||||
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
|
||||
pairs={variables}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</VStack>
|
||||
|
||||
@@ -85,9 +85,10 @@ export function RecentRequestsDropdown() {
|
||||
return (
|
||||
<Dropdown ref={dropdownRef} items={items}>
|
||||
<Button
|
||||
data-tauri-drag-region
|
||||
size="sm"
|
||||
className={classNames(
|
||||
'flex-[2] text-center text-gray-800 text-sm truncate pointer-events-none',
|
||||
'flex-[2] text-center text-gray-800 text-sm truncate pointer-events-auto',
|
||||
activeRequest === null && 'text-opacity-disabled italic',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
import type { HTMLAttributes, ReactElement } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import type { DropdownRef } from './core/Dropdown';
|
||||
import type { DropdownItem, DropdownProps, DropdownRef } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { HotKey } from './core/HotKey';
|
||||
import { Icon } from './core/Icon';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import type { Environment, HttpRequest, Workspace } from '../lib/models';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { pluralize } from '../lib/pluralize';
|
||||
|
||||
interface Props {
|
||||
requestId: string;
|
||||
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
|
||||
requestId: string | null;
|
||||
children: DropdownProps['children'];
|
||||
}
|
||||
|
||||
export function RequestActionsDropdown({ requestId, children }: Props) {
|
||||
const deleteRequest = useDeleteRequest(requestId);
|
||||
const duplicateRequest = useDuplicateRequest({ id: requestId, navigateAfter: true });
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
const routes = useAppRoutes();
|
||||
const dialog = useDialog();
|
||||
const { appearance, toggleAppearance } = useTheme();
|
||||
|
||||
useListenToTauriEvent('toggle_settings', () => {
|
||||
@@ -29,25 +36,88 @@ export function RequestActionsDropdown({ requestId, children }: Props) {
|
||||
duplicateRequest.mutate();
|
||||
});
|
||||
|
||||
const importData = useCallback(async () => {
|
||||
const selected = await open({
|
||||
multiple: true,
|
||||
filters: [
|
||||
{
|
||||
name: 'Export File',
|
||||
extensions: ['json'],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (selected == null || selected.length === 0) return;
|
||||
const imported: {
|
||||
workspaces: Workspace[];
|
||||
environments: Environment[];
|
||||
requests: HttpRequest[];
|
||||
} = await invoke('import_data', {
|
||||
filePaths: selected,
|
||||
workspaceId: null,
|
||||
});
|
||||
const importedWorkspace = imported.workspaces[0];
|
||||
|
||||
dialog.show({
|
||||
title: 'Import Complete',
|
||||
description: 'Imported the following:',
|
||||
size: 'dynamic',
|
||||
render: () => {
|
||||
const { workspaces, environments, requests } = imported;
|
||||
return (
|
||||
<div>
|
||||
<ul className="list-disc pl-6">
|
||||
<li>
|
||||
{workspaces.length} {pluralize('Workspace', workspaces.length)}
|
||||
</li>
|
||||
<li>
|
||||
{environments.length} {pluralize('Environment', environments.length)}
|
||||
</li>
|
||||
<li>
|
||||
{requests.length} {pluralize('Request', requests.length)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
if (importedWorkspace != null) {
|
||||
routes.navigate('workspace', {
|
||||
workspaceId: importedWorkspace.id,
|
||||
environmentId: imported.environments[0]?.id,
|
||||
});
|
||||
}
|
||||
}, [routes, dialog]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
ref={dropdownRef}
|
||||
items={[
|
||||
...(requestId != null
|
||||
? ([
|
||||
{
|
||||
key: 'duplicate',
|
||||
label: 'Duplicate',
|
||||
onSelect: duplicateRequest.mutate,
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
rightSlot: <HotKey modifier="Meta" keyName="D" />,
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
onSelect: deleteRequest.mutate,
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
},
|
||||
{ type: 'separator', label: 'Yaak Settings' },
|
||||
] as DropdownItem[])
|
||||
: []),
|
||||
{
|
||||
key: 'duplicate',
|
||||
label: 'Duplicate',
|
||||
onSelect: duplicateRequest.mutate,
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
rightSlot: <HotKey modifier="Meta" keyName="D" />,
|
||||
key: 'import',
|
||||
label: 'Import',
|
||||
onSelect: importData,
|
||||
leftSlot: <Icon icon="download" />,
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
onSelect: deleteRequest.mutate,
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
},
|
||||
{ type: 'separator', label: 'Yaak Settings' },
|
||||
{
|
||||
key: 'appearance',
|
||||
label: appearance === 'dark' ? 'Light Theme' : 'Dark Theme',
|
||||
|
||||
@@ -31,16 +31,14 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
<RecentRequestsDropdown />
|
||||
</div>
|
||||
<div className="flex-1 flex justify-end -mr-2 pointer-events-none">
|
||||
{activeRequest && (
|
||||
<RequestActionsDropdown requestId={activeRequest?.id}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Request Options"
|
||||
icon="gear"
|
||||
className="pointer-events-auto"
|
||||
/>
|
||||
</RequestActionsDropdown>
|
||||
)}
|
||||
<RequestActionsDropdown requestId={activeRequest?.id ?? null}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Request Options"
|
||||
icon="gear"
|
||||
className="pointer-events-auto"
|
||||
/>
|
||||
</RequestActionsDropdown>
|
||||
</div>
|
||||
</HStack>
|
||||
);
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
TriangleDownIcon,
|
||||
TriangleLeftIcon,
|
||||
TriangleRightIcon,
|
||||
DownloadIcon,
|
||||
UpdateIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import classNames from 'classnames';
|
||||
@@ -55,6 +56,7 @@ const icons = {
|
||||
dividerH: DividerHorizontalIcon,
|
||||
dotsH: DotsHorizontalIcon,
|
||||
dotsV: DotsVerticalIcon,
|
||||
download: DownloadIcon,
|
||||
drag: DragHandleDots2Icon,
|
||||
eye: EyeOpenIcon,
|
||||
eyeClosed: EyeClosedIcon,
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface Workspace extends BaseModel {
|
||||
readonly model: 'workspace';
|
||||
name: string;
|
||||
description: string;
|
||||
variables: EnvironmentVariable[];
|
||||
}
|
||||
|
||||
export interface EnvironmentVariable {
|
||||
|
||||
@@ -40,9 +40,10 @@ export const appThemeVariants: AppThemeColorVariant[] = [
|
||||
];
|
||||
|
||||
export type AppThemeLayer = 'root' | 'sidebar' | 'titlebar' | 'content' | 'above';
|
||||
export type AppThemeColors = Record<AppThemeColor, string>;
|
||||
|
||||
export interface AppThemeLayerStyle {
|
||||
colors: Record<AppThemeColor, string>;
|
||||
colors: AppThemeColors;
|
||||
blackPoint?: number;
|
||||
whitePoint?: number;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
import type { AppTheme } from './theme';
|
||||
import type { AppTheme, AppThemeColors } from './theme';
|
||||
import { generateCSS, toTailwindVariable } from './theme';
|
||||
|
||||
export type Appearance = 'dark' | 'light';
|
||||
|
||||
enum Theme {
|
||||
yaak = 'yaak',
|
||||
catppuccin = 'catppuccin',
|
||||
}
|
||||
|
||||
const themes: Record<Theme, AppThemeColors> = {
|
||||
yaak: {
|
||||
gray: '#6b5b98',
|
||||
red: '#ff417b',
|
||||
orange: '#fd9014',
|
||||
yellow: '#e8d13f',
|
||||
green: '#3fd265',
|
||||
blue: '#219dff',
|
||||
pink: '#ff6dff',
|
||||
violet: '#b176ff',
|
||||
},
|
||||
catppuccin: {
|
||||
gray: 'hsl(240, 23%, 47%)',
|
||||
red: 'hsl(343, 91%, 74%)',
|
||||
orange: 'hsl(23, 92%, 74%)',
|
||||
yellow: 'hsl(41, 86%, 72%)',
|
||||
green: 'hsl(115, 54%, 65%)',
|
||||
blue: 'hsl(217, 92%, 65%)',
|
||||
pink: 'hsl(316, 72%, 75%)',
|
||||
violet: 'hsl(267, 84%, 70%)',
|
||||
},
|
||||
};
|
||||
|
||||
const darkTheme: AppTheme = {
|
||||
name: 'Default Dark',
|
||||
appearance: 'dark',
|
||||
layers: {
|
||||
root: {
|
||||
blackPoint: 0.2,
|
||||
colors: {
|
||||
gray: '#6b5b98',
|
||||
red: '#ff417b',
|
||||
orange: '#fd9014',
|
||||
yellow: '#e8d13f',
|
||||
green: '#3fd265',
|
||||
blue: '#219dff',
|
||||
pink: '#ff6dff',
|
||||
violet: '#b176ff',
|
||||
},
|
||||
colors: themes.catppuccin,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user