From 1cd6e0af06bb64815b65e8fd80eb0c29312a58d2 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 2 Nov 2023 18:08:43 -0700 Subject: [PATCH] Initial "plugin" system with importer (#7) --- Makefile | 4 + plugins/hello-world/greet.js | 4 + .../plugins => plugins}/hello-world/index.js | 6 +- .../Insomnia_hello-world.json | 100 ++++++++ .../importers/environment.js | 23 ++ .../insomnia-importer/importers/request.js | 28 ++ .../insomnia-importer/importers/workspace.js | 14 + plugins/insomnia-importer/index.js | 50 ++++ src-tauri/Cargo.lock | 68 ++++- src-tauri/Cargo.toml | 14 +- .../20231103004111_workspace-variables.sql | 1 + src-tauri/plugins/hello-world/hello.js | 3 - src-tauri/sqlx-data.json | 240 +++++++++--------- src-tauri/src/main.rs | 172 ++++++++----- src-tauri/src/models.rs | 229 +++++++---------- src-tauri/src/plugin.rs | 117 +++++++-- src-tauri/tauri.conf.json | 23 +- .../components/EnvironmentActionsDropdown.tsx | 6 +- src-web/components/EnvironmentEditDialog.tsx | 148 +++++++---- src-web/components/RecentRequestsDropdown.tsx | 3 +- src-web/components/RequestActionsDropdown.tsx | 106 ++++++-- src-web/components/WorkspaceHeader.tsx | 18 +- src-web/components/core/Icon.tsx | 2 + src-web/lib/models.ts | 1 + src-web/lib/theme/theme.ts | 3 +- src-web/lib/theme/window.ts | 41 ++- 26 files changed, 972 insertions(+), 452 deletions(-) create mode 100644 plugins/hello-world/greet.js rename {src-tauri/plugins => plugins}/hello-world/index.js (61%) create mode 100644 plugins/insomnia-importer/Insomnia_hello-world.json create mode 100644 plugins/insomnia-importer/importers/environment.js create mode 100644 plugins/insomnia-importer/importers/request.js create mode 100644 plugins/insomnia-importer/importers/workspace.js create mode 100644 plugins/insomnia-importer/index.js create mode 100644 src-tauri/migrations/20231103004111_workspace-variables.sql delete mode 100644 src-tauri/plugins/hello-world/hello.js diff --git a/Makefile b/Makefile index c656c66b..928422de 100644 --- a/Makefile +++ b/Makefile @@ -5,3 +5,7 @@ sqlx-prepare: dev: npm run tauri-dev + + +build: + ./node_modules/.bin/tauri build diff --git a/plugins/hello-world/greet.js b/plugins/hello-world/greet.js new file mode 100644 index 00000000..9e23f000 --- /dev/null +++ b/plugins/hello-world/greet.js @@ -0,0 +1,4 @@ +export function greet() { + // Call Rust-provided fn! + sayHello('Plugin'); +} diff --git a/src-tauri/plugins/hello-world/index.js b/plugins/hello-world/index.js similarity index 61% rename from src-tauri/plugins/hello-world/index.js rename to plugins/hello-world/index.js index bb1ab725..b739c110 100644 --- a/src-tauri/plugins/hello-world/index.js +++ b/plugins/hello-world/index.js @@ -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]+/)); } diff --git a/plugins/insomnia-importer/Insomnia_hello-world.json b/plugins/insomnia-importer/Insomnia_hello-world.json new file mode 100644 index 00000000..b73a0063 --- /dev/null +++ b/plugins/insomnia-importer/Insomnia_hello-world.json @@ -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" + } + ] +} diff --git a/plugins/insomnia-importer/importers/environment.js b/plugins/insomnia-importer/importers/environment.js new file mode 100644 index 00000000..f22a3f48 --- /dev/null +++ b/plugins/insomnia-importer/importers/environment.js @@ -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}`, + })), + }; +} diff --git a/plugins/insomnia-importer/importers/request.js b/plugins/insomnia-importer/importers/request.js new file mode 100644 index 00000000..4393db61 --- /dev/null +++ b/plugins/insomnia-importer/importers/request.js @@ -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, + })), + }; +} diff --git a/plugins/insomnia-importer/importers/workspace.js b/plugins/insomnia-importer/importers/workspace.js new file mode 100644 index 00000000..8ff73f7a --- /dev/null +++ b/plugins/insomnia-importer/importers/workspace.js @@ -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, + }; +} diff --git a/plugins/insomnia-importer/index.js b/plugins/insomnia-importer/index.js new file mode 100644 index 00000000..6bf1f1e1 --- /dev/null +++ b/plugins/insomnia-importer/index.js @@ -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]'; +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index dea7457e..a8fc04f8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -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" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3ed33118..768dc993 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" diff --git a/src-tauri/migrations/20231103004111_workspace-variables.sql b/src-tauri/migrations/20231103004111_workspace-variables.sql new file mode 100644 index 00000000..5873f120 --- /dev/null +++ b/src-tauri/migrations/20231103004111_workspace-variables.sql @@ -0,0 +1 @@ +ALTER TABLE workspaces ADD COLUMN variables DEFAULT '[]' NOT NULL; diff --git a/src-tauri/plugins/hello-world/hello.js b/src-tauri/plugins/hello-world/hello.js deleted file mode 100644 index 1e784c8a..00000000 --- a/src-tauri/plugins/hello-world/hello.js +++ /dev/null @@ -1,3 +0,0 @@ -export function hello() { - sayHello('Plugin'); -} diff --git a/src-tauri/sqlx-data.json b/src-tauri/sqlx-data.json index a7caf6c5..df202aa1 100644 --- a/src-tauri/sqlx-data.json +++ b/src-tauri/sqlx-data.json @@ -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>\"\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>", + "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>\"\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>\"\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>\"\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>", + "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>\"\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 " } } \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 182ca7b8..07e81d00 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -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, + db_instance: State<'_, Mutex>>, + file_paths: Vec<&str>, + workspace_id: Option<&str>, +) -> Result { + 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>>, ) -> Result { 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>>, ) -> Result { 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>>, ) -> Result { 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 { 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 { 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>>, ) -> Result { 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, .. } => { diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index d72e94a2..47ebcee1 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -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>, } -#[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>, } -#[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, pub body_type: Option, @@ -67,15 +77,15 @@ pub struct HttpRequest { pub headers: Json>, } -#[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>, } -#[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) -> Result, 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>" FROM workspaces "#, ) @@ -165,7 +176,8 @@ pub async fn get_workspace(id: &str, pool: &Pool) -> Result>" FROM workspaces WHERE id = ? "#, id, @@ -193,27 +205,6 @@ pub async fn delete_workspace(id: &str, pool: &Pool) -> Result, -) -> Result { - 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, @@ -232,30 +223,6 @@ pub async fn find_environments( .await } -pub async fn create_environment( - workspace_id: &str, - name: &str, - variables: Vec, - pool: &Pool, -) -> Result { - 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) -> Result { let env = get_environment(id, pool).await?; let _ = sqlx::query!( @@ -271,26 +238,37 @@ pub async fn delete_environment(id: &str, pool: &Pool) -> Result, +pub async fn upsert_environment( pool: &Pool, + environment: Environment, ) -> Result { - 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) -> Result { @@ -315,60 +293,23 @@ pub async fn get_environment(id: &str, pool: &Pool) -> Result) -> Result { - 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, - authentication: HashMap, - authentication_type: Option, - url: &str, - headers: Vec, - sort_priority: f64, pool: &Pool, + r: HttpRequest, ) -> Result { - 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, + workspace: Workspace, ) -> Result { + 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?; diff --git a/src-tauri/src/plugin.rs b/src-tauri/src/plugin.rs index 93184e35..27c67cfc 100644 --- a/src-tauri/src/plugin.rs +++ b/src-tauri/src/plugin.rs @@ -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, + environments: Vec, + workspaces: Vec, +} + +pub async fn run_plugin_import( + app_handle: &AppHandle, + pool: &Pool, + 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<'_>) { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6c409991..2707a57b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -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": [ diff --git a/src-web/components/EnvironmentActionsDropdown.tsx b/src-web/components/EnvironmentActionsDropdown.tsx index f7f5919f..6f10c75c 100644 --- a/src-web/components/EnvironmentActionsDropdown.tsx +++ b/src-web/components/EnvironmentActionsDropdown.tsx @@ -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: () => , + render: () => , }); - }, [dialog]); + }, [dialog, activeEnvironment]); const items: DropdownItem[] = useMemo( () => diff --git a/src-web/components/EnvironmentEditDialog.tsx b/src-web/components/EnvironmentEditDialog.tsx index bdb259bc..375b4349 100644 --- a/src-web/components/EnvironmentEditDialog.tsx +++ b/src-web/components/EnvironmentEditDialog.tsx @@ -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( + 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 && ( )} - {activeEnvironment != null ? ( - + {activeWorkspace != null ? ( + ) : (
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( (variables) => { - updateEnvironment.mutate({ variables }); + if (environment != null) { + updateEnvironment.mutate({ variables }); + } else { + updateWorkspace.mutate({ variables }); + } }, - [updateEnvironment], + [updateWorkspace, updateEnvironment, environment], ); const nameAutocomplete = useMemo(() => { 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( - () => [ - { - key: 'rename', - label: 'Rename', - leftSlot: , - onSelect: async () => { - const name = await prompt({ - title: 'Rename Environment', - description: ( - <> - Enter a new name for {environment.name} - - ), - name: 'name', - label: 'Name', - defaultValue: environment.name, - }); - updateEnvironment.mutate({ name }); - }, - }, - { - key: 'delete', - variant: 'danger', - label: 'Delete', - leftSlot: , - onSelect: () => deleteEnvironment.mutate(), - }, - ], - [deleteEnvironment, updateEnvironment, environment.name, prompt], + const items = useMemo( + () => + environment == null + ? null + : [ + { + key: 'rename', + label: 'Rename', + leftSlot: , + onSelect: async () => { + const name = await prompt({ + title: 'Rename Environment', + description: ( + <> + Enter a new name for {environment.name} + + ), + name: 'name', + label: 'Name', + defaultValue: environment.name, + }); + updateEnvironment.mutate({ name }); + }, + }, + { + key: 'delete', + variant: 'danger', + label: 'Delete', + leftSlot: , + onSelect: () => deleteEnvironment.mutate(), + }, + ], + [deleteEnvironment, updateEnvironment, prompt, environment], ); const validateName = useCallback((name: string) => { @@ -141,10 +177,12 @@ const EnvironmentEditor = function ({ environment }: { environment: Environment return ( -

{environment.name}

- - - +

{environment?.name ?? 'Base Environment'}

+ {items != null && ( + + + + )}
diff --git a/src-web/components/RecentRequestsDropdown.tsx b/src-web/components/RecentRequestsDropdown.tsx index 84634398..e9ca20af 100644 --- a/src-web/components/RecentRequestsDropdown.tsx +++ b/src-web/components/RecentRequestsDropdown.tsx @@ -85,9 +85,10 @@ export function RecentRequestsDropdown() { return (
- {activeRequest && ( - - - - )} + + +
); diff --git a/src-web/components/core/Icon.tsx b/src-web/components/core/Icon.tsx index dd16b4f1..6380f98f 100644 --- a/src-web/components/core/Icon.tsx +++ b/src-web/components/core/Icon.tsx @@ -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, diff --git a/src-web/lib/models.ts b/src-web/lib/models.ts index db0bb0f9..d234157e 100644 --- a/src-web/lib/models.ts +++ b/src-web/lib/models.ts @@ -20,6 +20,7 @@ export interface Workspace extends BaseModel { readonly model: 'workspace'; name: string; description: string; + variables: EnvironmentVariable[]; } export interface EnvironmentVariable { diff --git a/src-web/lib/theme/theme.ts b/src-web/lib/theme/theme.ts index 98585144..a52d8d01 100644 --- a/src-web/lib/theme/theme.ts +++ b/src-web/lib/theme/theme.ts @@ -40,9 +40,10 @@ export const appThemeVariants: AppThemeColorVariant[] = [ ]; export type AppThemeLayer = 'root' | 'sidebar' | 'titlebar' | 'content' | 'above'; +export type AppThemeColors = Record; export interface AppThemeLayerStyle { - colors: Record; + colors: AppThemeColors; blackPoint?: number; whitePoint?: number; } diff --git a/src-web/lib/theme/window.ts b/src-web/lib/theme/window.ts index 0d7ebdee..28b13ca1 100644 --- a/src-web/lib/theme/window.ts +++ b/src-web/lib/theme/window.ts @@ -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 = { + 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, }, }, };