mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-11 20:00:29 +01:00
Simple auth schemes
This commit is contained in:
22
package-lock.json
generated
22
package-lock.json
generated
@@ -39,6 +39,7 @@
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-hook-form": "^7.43.8",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-use": "^17.4.0",
|
||||
"uuid": "^9.0.0"
|
||||
@@ -6334,6 +6335,21 @@
|
||||
"react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.43.8",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.8.tgz",
|
||||
"integrity": "sha512-BQm+Ge5KjTk1EchDBRhdP8Pkb7MArO2jFF+UWYr3rtvh6197khi22uloLqlWeuY02ItlCzPunPsFt1/q9wQKnw==",
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-hook-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -12107,6 +12123,12 @@
|
||||
"shallowequal": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"react-hook-form": {
|
||||
"version": "7.43.8",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.8.tgz",
|
||||
"integrity": "sha512-BQm+Ge5KjTk1EchDBRhdP8Pkb7MArO2jFF+UWYr3rtvh6197khi22uloLqlWeuY02ItlCzPunPsFt1/q9wQKnw==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -5169,6 +5169,7 @@ dependencies = [
|
||||
name = "yaak-app"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"chrono",
|
||||
"cocoa",
|
||||
"deno_ast",
|
||||
|
||||
@@ -14,23 +14,24 @@ strip = true # Automatically strip symbols from the binary.
|
||||
tauri-build = { version = "1.2", features = [] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = { version = "0.2.7" }
|
||||
cocoa = { version = "0.24.1" }
|
||||
objc = "0.2.7"
|
||||
cocoa = "0.24.1"
|
||||
|
||||
[dependencies]
|
||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.2", features = ["config-toml", "devtools", "shell-open", "system-tray", "updater", "window-start-dragging"] }
|
||||
http = { version = "0.2.8" }
|
||||
http = "0.2.8"
|
||||
reqwest = { version = "0.11.14", features = ["json"] }
|
||||
tokio = { version = "1.25.0", features = ["sync"] }
|
||||
futures = { version = "0.3.26" }
|
||||
deno_core = { version = "0.174.0" }
|
||||
futures = "0.3.26"
|
||||
deno_core = "0.174.0"
|
||||
deno_ast = { version = "0.24.0", features = ["transpiling"] }
|
||||
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time", "offline"] }
|
||||
uuid = { version = "1.3.0" }
|
||||
rand = { version = "0.8.5" }
|
||||
uuid = "1.3.0"
|
||||
rand = "0.8.5"
|
||||
chrono = { version = "0.4.23", features = ["serde"] }
|
||||
base64 = "0.21.0"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
|
||||
use base64::Engine;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::env::current_dir;
|
||||
use std::fs::create_dir_all;
|
||||
|
||||
use http::header::{HeaderName, ACCEPT, USER_AGENT};
|
||||
use http::{HeaderMap, HeaderValue, Method};
|
||||
use http::header::{ACCEPT, HeaderName, USER_AGENT};
|
||||
use reqwest::redirect::Policy;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use serde::Serialize;
|
||||
use sqlx::migrate::Migrator;
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use sqlx::types::{Json};
|
||||
use tauri::{AppHandle, Menu, MenuItem, State, Submenu, TitleBarStyle, Window, Wry};
|
||||
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent};
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use tauri::regex::Regex;
|
||||
use tauri::{AppHandle, Menu, MenuItem, Runtime, State, Submenu, TitleBarStyle, Window, Wry};
|
||||
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use window_ext::WindowExt;
|
||||
@@ -133,6 +135,41 @@ async fn actually_send_ephemeral_request(
|
||||
headers.insert(header_name, header_value);
|
||||
}
|
||||
|
||||
if let Some(b) = &request.authentication_type {
|
||||
let empty_value = &serde_json::to_value("").unwrap();
|
||||
if b == "basic" {
|
||||
let a = request.authentication.0;
|
||||
let auth = format!(
|
||||
"{}:{}",
|
||||
a.get("username")
|
||||
.unwrap_or(empty_value)
|
||||
.as_str()
|
||||
.unwrap_or(""),
|
||||
a.get("password")
|
||||
.unwrap_or(empty_value)
|
||||
.as_str()
|
||||
.unwrap_or(""),
|
||||
);
|
||||
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
|
||||
);
|
||||
} else if b == "bearer" {
|
||||
let token = request
|
||||
.authentication
|
||||
.0
|
||||
.get("token")
|
||||
.unwrap_or(empty_value)
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
|
||||
.expect("Failed to create method");
|
||||
let builder = client.request(m, url_string.to_string()).headers(headers);
|
||||
@@ -151,7 +188,8 @@ async fn actually_send_ephemeral_request(
|
||||
|
||||
let resp = client.execute(sendable_req).await;
|
||||
|
||||
let p = window.app_handle()
|
||||
let p = window
|
||||
.app_handle()
|
||||
.path_resolver()
|
||||
.resolve_resource("plugins/plugin.ts")
|
||||
.expect("failed to resolve resource");
|
||||
@@ -177,7 +215,10 @@ async fn actually_send_ephemeral_request(
|
||||
response = models::update_response_if_id(response, window.label(), pool)
|
||||
.await
|
||||
.expect("Failed to update response");
|
||||
window.app_handle().emit_all("updated_response", &response).unwrap();
|
||||
window
|
||||
.app_handle()
|
||||
.emit_all("updated_response", &response)
|
||||
.unwrap();
|
||||
Ok(response)
|
||||
}
|
||||
Err(e) => response_err(response, e.to_string(), window, pool).await,
|
||||
@@ -196,10 +237,14 @@ async fn send_request(
|
||||
.await
|
||||
.expect("Failed to get request");
|
||||
|
||||
let response = models::create_response(&req.id, 0, "", 0, None, "", vec![], window.label(), pool)
|
||||
.await
|
||||
.expect("Failed to create response");
|
||||
window.app_handle().emit_all("updated_response", &response).unwrap();
|
||||
let response =
|
||||
models::create_response(&req.id, 0, "", 0, None, "", vec![], window.label(), pool)
|
||||
.await
|
||||
.expect("Failed to create response");
|
||||
window
|
||||
.app_handle()
|
||||
.emit_all("updated_response", &response)
|
||||
.unwrap();
|
||||
|
||||
actually_send_ephemeral_request(req, response, window, pool).await?;
|
||||
Ok(())
|
||||
@@ -215,7 +260,10 @@ async fn response_err(
|
||||
response = models::update_response_if_id(response, window.label(), pool)
|
||||
.await
|
||||
.expect("Failed to update response");
|
||||
window.app_handle().emit_all("updated_response", &response).unwrap();
|
||||
window
|
||||
.app_handle()
|
||||
.emit_all("updated_response", &response)
|
||||
.unwrap();
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -294,10 +342,11 @@ async fn create_request(
|
||||
window.label(),
|
||||
pool,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create request");
|
||||
.await
|
||||
.expect("Failed to create request");
|
||||
|
||||
window.app_handle()
|
||||
window
|
||||
.app_handle()
|
||||
.emit_all("updated_request", &created_request)
|
||||
.unwrap();
|
||||
|
||||
@@ -314,7 +363,10 @@ async fn duplicate_request(
|
||||
let request = models::duplicate_request(id, window.label(), pool)
|
||||
.await
|
||||
.expect("Failed to duplicate request");
|
||||
window.app_handle().emit_all("updated_request", &request).unwrap();
|
||||
window
|
||||
.app_handle()
|
||||
.emit_all("updated_request", &request)
|
||||
.unwrap();
|
||||
Ok(request.id)
|
||||
}
|
||||
|
||||
@@ -352,10 +404,11 @@ async fn update_request(
|
||||
window.label(),
|
||||
pool,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update request");
|
||||
.await
|
||||
.expect("Failed to update request");
|
||||
|
||||
window.app_handle()
|
||||
window
|
||||
.app_handle()
|
||||
.emit_all("updated_request", updated_request)
|
||||
.unwrap();
|
||||
|
||||
@@ -444,10 +497,14 @@ async fn workspaces(
|
||||
.await
|
||||
.expect("Failed to find workspaces");
|
||||
if workspaces.is_empty() {
|
||||
let workspace =
|
||||
models::create_workspace("My Project", "This is the default workspace", window.label(), pool)
|
||||
.await
|
||||
.expect("Failed to create workspace");
|
||||
let workspace = models::create_workspace(
|
||||
"My Project",
|
||||
"This is the default workspace",
|
||||
window.label(),
|
||||
pool,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create workspace");
|
||||
Ok(vec![workspace])
|
||||
} else {
|
||||
Ok(workspaces)
|
||||
@@ -597,15 +654,15 @@ fn create_window(handle: AppHandle<Wry>) -> Window<Wry> {
|
||||
window_id,
|
||||
tauri::WindowUrl::App("workspaces".into()),
|
||||
)
|
||||
.menu(menu)
|
||||
.fullscreen(false)
|
||||
.resizable(true)
|
||||
.inner_size(1100.0, 600.0)
|
||||
.hidden_title(true)
|
||||
.title("Yaak")
|
||||
.title_bar_style(TitleBarStyle::Overlay)
|
||||
.build()
|
||||
.expect("failed to build window");
|
||||
.menu(menu)
|
||||
.fullscreen(false)
|
||||
.resizable(true)
|
||||
.inner_size(1100.0, 600.0)
|
||||
.hidden_title(true)
|
||||
.title("Yaak")
|
||||
.title_bar_style(TitleBarStyle::Overlay)
|
||||
.build()
|
||||
.expect("failed to build window");
|
||||
|
||||
let win2 = win.clone();
|
||||
win.on_menu_event(move |event| {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -189,7 +189,11 @@ pub async fn create_workspace(
|
||||
get_workspace(&id, pool).await
|
||||
}
|
||||
|
||||
pub async fn duplicate_request(id: &str, updated_by: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, sqlx::Error> {
|
||||
pub async fn duplicate_request(
|
||||
id: &str,
|
||||
updated_by: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<HttpRequest, sqlx::Error> {
|
||||
let existing = get_request(id, pool)
|
||||
.await
|
||||
.expect("Failed to get request to duplicate");
|
||||
|
||||
@@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { persistQueryClient } from '@tanstack/react-query-persist-client';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { MotionConfig } from 'framer-motion';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
@@ -16,11 +17,13 @@ import type { SidebarDisplay } from '../hooks/useSidebarDisplay';
|
||||
import { sidebarDisplayDefaultValue, sidebarDisplayKey } from '../hooks/useSidebarDisplay';
|
||||
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
||||
import { DEFAULT_FONT_SIZE } from '../lib/constants';
|
||||
import { debounce } from '../lib/debounce';
|
||||
import { extractKeyValue, getKeyValue, setKeyValue } from '../lib/keyValueStore';
|
||||
import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/models';
|
||||
import { AppRouter } from './AppRouter';
|
||||
import { DialogProvider } from './DialogContext';
|
||||
import { appWindow, WebviewWindow } from '@tauri-apps/api/window';
|
||||
|
||||
const UPDATE_DEBOUNCE_MILLIS = 500;
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -33,7 +36,7 @@ const queryClient = new QueryClient({
|
||||
|
||||
const localStoragePersister = createSyncStoragePersister({
|
||||
storage: window.localStorage,
|
||||
throttleTime: 1000,
|
||||
throttleTime: 1000, // 1 second
|
||||
});
|
||||
|
||||
persistQueryClient({
|
||||
@@ -42,40 +45,47 @@ persistQueryClient({
|
||||
maxAge: 1000 * 60 * 60 * 24, // 24 hours
|
||||
});
|
||||
|
||||
await listen('updated_key_value', ({ payload: keyValue }: { payload: KeyValue }) => {
|
||||
if (keyValue.updatedBy === appWindow.label) return;
|
||||
queryClient.setQueryData(keyValueQueryKey(keyValue), extractKeyValue(keyValue));
|
||||
});
|
||||
await listen(
|
||||
'updated_key_value',
|
||||
debounce(({ payload: keyValue }: { payload: KeyValue }) => {
|
||||
if (keyValue.updatedBy === appWindow.label) return;
|
||||
queryClient.setQueryData(keyValueQueryKey(keyValue), extractKeyValue(keyValue));
|
||||
}, UPDATE_DEBOUNCE_MILLIS),
|
||||
);
|
||||
|
||||
await listen('updated_request', ({ payload: request }: { payload: HttpRequest }) => {
|
||||
if (request.updatedBy === appWindow.label) return;
|
||||
await listen(
|
||||
'updated_request',
|
||||
debounce(({ payload: request }: { payload: HttpRequest }) => {
|
||||
if (request.updatedBy === appWindow.label) return;
|
||||
|
||||
queryClient.setQueryData(
|
||||
requestsQueryKey(request.workspaceId),
|
||||
(requests: HttpRequest[] = []) => {
|
||||
const newRequests = [];
|
||||
let found = false;
|
||||
for (const r of requests) {
|
||||
if (r.id === request.id) {
|
||||
found = true;
|
||||
newRequests.push(request);
|
||||
} else {
|
||||
newRequests.push(r);
|
||||
queryClient.setQueryData(
|
||||
requestsQueryKey(request.workspaceId),
|
||||
(requests: HttpRequest[] = []) => {
|
||||
const newRequests = [];
|
||||
let found = false;
|
||||
for (const r of requests) {
|
||||
if (r.id === request.id) {
|
||||
found = true;
|
||||
newRequests.push(request);
|
||||
} else {
|
||||
newRequests.push(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
newRequests.push(request);
|
||||
}
|
||||
return newRequests;
|
||||
},
|
||||
);
|
||||
});
|
||||
if (!found) {
|
||||
newRequests.push(request);
|
||||
}
|
||||
return newRequests;
|
||||
},
|
||||
);
|
||||
}, UPDATE_DEBOUNCE_MILLIS),
|
||||
);
|
||||
|
||||
await listen('updated_response', ({ payload: response }: { payload: HttpResponse }) => {
|
||||
queryClient.setQueryData(
|
||||
responsesQueryKey(response.requestId),
|
||||
(responses: HttpResponse[] = []) => {
|
||||
if (response.updatedBy === appWindow.label) return;
|
||||
// We want updates from every response
|
||||
// if (response.updatedBy === appWindow.label) return;
|
||||
|
||||
const newResponses = [];
|
||||
let found = false;
|
||||
@@ -95,26 +105,29 @@ await listen('updated_response', ({ payload: response }: { payload: HttpResponse
|
||||
);
|
||||
});
|
||||
|
||||
await listen('updated_workspace', ({ payload: workspace }: { payload: Workspace }) => {
|
||||
queryClient.setQueryData(workspacesQueryKey(), (workspaces: Workspace[] = []) => {
|
||||
if (workspace.updatedBy === appWindow.label) return;
|
||||
await listen(
|
||||
'updated_workspace',
|
||||
debounce(({ payload: workspace }: { payload: Workspace }) => {
|
||||
queryClient.setQueryData(workspacesQueryKey(), (workspaces: Workspace[] = []) => {
|
||||
if (workspace.updatedBy === appWindow.label) return;
|
||||
|
||||
const newWorkspaces = [];
|
||||
let found = false;
|
||||
for (const w of workspaces) {
|
||||
if (w.id === workspace.id) {
|
||||
found = true;
|
||||
newWorkspaces.push(workspace);
|
||||
} else {
|
||||
newWorkspaces.push(w);
|
||||
const newWorkspaces = [];
|
||||
let found = false;
|
||||
for (const w of workspaces) {
|
||||
if (w.id === workspace.id) {
|
||||
found = true;
|
||||
newWorkspaces.push(workspace);
|
||||
} else {
|
||||
newWorkspaces.push(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
newWorkspaces.push(workspace);
|
||||
}
|
||||
return newWorkspaces;
|
||||
});
|
||||
});
|
||||
if (!found) {
|
||||
newWorkspaces.push(workspace);
|
||||
}
|
||||
return newWorkspaces;
|
||||
});
|
||||
}, UPDATE_DEBOUNCE_MILLIS),
|
||||
);
|
||||
|
||||
await listen(
|
||||
'deleted_model',
|
||||
|
||||
42
src-web/components/BasicAuth.tsx
Normal file
42
src-web/components/BasicAuth.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { Input } from './core/Input';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
requestId: string;
|
||||
authentication: HttpRequest['authentication'];
|
||||
}
|
||||
|
||||
export function BasicAuth({ requestId, authentication }: Props) {
|
||||
const updateRequest = useUpdateRequest(requestId);
|
||||
|
||||
return (
|
||||
<VStack className="my-2" space={2}>
|
||||
<Input
|
||||
label="Username"
|
||||
name="username"
|
||||
size="sm"
|
||||
defaultValue={`${authentication.username}`}
|
||||
onChange={(username: string) => {
|
||||
updateRequest.mutate((r) => ({
|
||||
...r,
|
||||
authentication: { password: r.authentication.password, username },
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label="Password"
|
||||
name="password"
|
||||
size="sm"
|
||||
defaultValue={`${authentication.password}`}
|
||||
onChange={(password: string) => {
|
||||
updateRequest.mutate((r) => ({
|
||||
...r,
|
||||
authentication: { username: r.authentication.username, password },
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
30
src-web/components/BearerAuth.tsx
Normal file
30
src-web/components/BearerAuth.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { Input } from './core/Input';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
requestId: string;
|
||||
authentication: HttpRequest['authentication'];
|
||||
}
|
||||
|
||||
export function BearerAuth({ requestId, authentication }: Props) {
|
||||
const updateRequest = useUpdateRequest(requestId);
|
||||
|
||||
return (
|
||||
<VStack className="my-2" space={2}>
|
||||
<Input
|
||||
label="Token"
|
||||
name="token"
|
||||
size="sm"
|
||||
defaultValue={`${authentication.token}`}
|
||||
onChange={(token: string) => {
|
||||
updateRequest.mutate((r) => ({
|
||||
...r,
|
||||
authentication: { token },
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,17 @@ import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import type { HttpHeader, HttpRequest } from '../lib/models';
|
||||
import { BODY_TYPE_GRAPHQL, BODY_TYPE_JSON, BODY_TYPE_NONE, BODY_TYPE_XML } from '../lib/models';
|
||||
import {
|
||||
AUTH_TYPE_BASIC,
|
||||
AUTH_TYPE_BEARER,
|
||||
AUTH_TYPE_NONE,
|
||||
BODY_TYPE_GRAPHQL,
|
||||
BODY_TYPE_JSON,
|
||||
BODY_TYPE_NONE,
|
||||
BODY_TYPE_XML,
|
||||
} from '../lib/models';
|
||||
import { BasicAuth } from './BasicAuth';
|
||||
import { BearerAuth } from './BearerAuth';
|
||||
import { Editor } from './core/Editor';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
@@ -43,10 +53,11 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
options: {
|
||||
value: activeRequest.bodyType,
|
||||
items: [
|
||||
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
|
||||
{ label: 'JSON', value: BODY_TYPE_JSON },
|
||||
{ label: 'XML', value: BODY_TYPE_XML },
|
||||
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
|
||||
{ type: 'separator' },
|
||||
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
|
||||
],
|
||||
onChange: async (bodyType) => {
|
||||
const patch: Partial<HttpRequest> = { bodyType };
|
||||
@@ -76,21 +87,36 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
options: {
|
||||
value: activeRequest.authenticationType,
|
||||
items: [
|
||||
{ label: 'No Auth', shortLabel: 'Auth', value: null },
|
||||
{ label: 'Basic', value: 'basic' },
|
||||
{ label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC },
|
||||
{ label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER },
|
||||
{ type: 'separator' },
|
||||
{ label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE },
|
||||
],
|
||||
onChange: async (a) => {
|
||||
await updateRequest.mutate({
|
||||
authenticationType: a,
|
||||
authentication: { username: '', password: '' },
|
||||
});
|
||||
onChange: async (authenticationType) => {
|
||||
let authentication: HttpRequest['authentication'] = activeRequest?.authentication;
|
||||
if (authenticationType === AUTH_TYPE_BASIC) {
|
||||
authentication = {
|
||||
username: authentication.username ?? '',
|
||||
password: authentication.password ?? '',
|
||||
};
|
||||
} else if (authenticationType === AUTH_TYPE_BEARER) {
|
||||
authentication = {
|
||||
token: authentication.token ?? '',
|
||||
};
|
||||
}
|
||||
await updateRequest.mutate({ authenticationType, authentication });
|
||||
},
|
||||
},
|
||||
},
|
||||
{ value: 'params', label: 'URL Params' },
|
||||
{ value: 'headers', label: 'Headers' },
|
||||
],
|
||||
[activeRequest?.bodyType, activeRequest?.headers, activeRequest?.authenticationType],
|
||||
[
|
||||
activeRequest?.bodyType,
|
||||
activeRequest?.headers,
|
||||
activeRequest?.authenticationType,
|
||||
activeRequest?.authentication,
|
||||
],
|
||||
);
|
||||
|
||||
const handleBodyChange = useCallback((body: string) => updateRequest.mutate({ body }), []);
|
||||
@@ -123,24 +149,38 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
label="Request body"
|
||||
>
|
||||
<TabContent value="auth">
|
||||
<div className="flex items-center justify-center min-h-[5rem]">
|
||||
<header>Hello</header>
|
||||
</div>
|
||||
{activeRequest.authenticationType === AUTH_TYPE_BASIC ? (
|
||||
<BasicAuth
|
||||
key={forceUpdateKey}
|
||||
requestId={activeRequest.id}
|
||||
authentication={activeRequest.authentication}
|
||||
/>
|
||||
) : activeRequest.authenticationType === AUTH_TYPE_BEARER ? (
|
||||
<BearerAuth
|
||||
key={forceUpdateKey}
|
||||
requestId={activeRequest.id}
|
||||
authentication={activeRequest.authentication}
|
||||
/>
|
||||
) : (
|
||||
<EmptyStateText>
|
||||
No Authentication {activeRequest.authenticationType}
|
||||
</EmptyStateText>
|
||||
)}
|
||||
</TabContent>
|
||||
<TabContent value="headers">
|
||||
<HeaderEditor
|
||||
key={`${activeRequest.id}::${forceUpdateHeaderEditorKey}`}
|
||||
key={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
|
||||
headers={activeRequest.headers}
|
||||
onChange={handleHeadersChange}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value="params">
|
||||
<ParametersEditor key={activeRequestId} parameters={[]} onChange={() => null} />
|
||||
<ParametersEditor key={forceUpdateKey} parameters={[]} onChange={() => null} />
|
||||
</TabContent>
|
||||
<TabContent value="body" className="pl-3 mt-1">
|
||||
{activeRequest.bodyType === BODY_TYPE_JSON ? (
|
||||
<Editor
|
||||
key={activeRequest.id}
|
||||
key={forceUpdateKey}
|
||||
useTemplating
|
||||
placeholder="..."
|
||||
className="!bg-gray-50"
|
||||
@@ -152,7 +192,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_XML ? (
|
||||
<Editor
|
||||
key={activeRequest.id}
|
||||
key={forceUpdateKey}
|
||||
useTemplating
|
||||
placeholder="..."
|
||||
className="!bg-gray-50"
|
||||
@@ -163,7 +203,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
|
||||
<GraphQLEditor
|
||||
key={activeRequest.id}
|
||||
key={forceUpdateKey}
|
||||
baseRequest={activeRequest}
|
||||
className="!bg-gray-50"
|
||||
defaultValue={activeRequest?.body ?? ''}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { dialog } from '@tauri-apps/api';
|
||||
import classnames from 'classnames';
|
||||
import type { ForwardedRef, KeyboardEvent } from 'react';
|
||||
import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
@@ -99,14 +98,15 @@ function SidebarItems({
|
||||
|
||||
const shouldUpdateAll = afterPriority - beforePriority < 1;
|
||||
if (shouldUpdateAll) {
|
||||
newRequests.forEach((r, i) => {
|
||||
updateRequest.mutate({ id: r.id, sortPriority: i * 1000 });
|
||||
newRequests.forEach(({ id }, i) => {
|
||||
const sortPriority = i * 1000;
|
||||
const update = (r: HttpRequest) => ({ ...r, sortPriority });
|
||||
updateRequest.mutate({ id, update });
|
||||
});
|
||||
} else {
|
||||
updateRequest.mutate({
|
||||
id: requestId,
|
||||
sortPriority: afterPriority - (afterPriority - beforePriority) / 2,
|
||||
});
|
||||
const sortPriority = afterPriority - (afterPriority - beforePriority) / 2;
|
||||
const update = (r: HttpRequest) => ({ ...r, sortPriority });
|
||||
updateRequest.mutate({ id: requestId, update });
|
||||
}
|
||||
},
|
||||
[hoveredIndex, requests],
|
||||
@@ -149,7 +149,7 @@ const _SidebarItem = forwardRef(function SidebarItem(
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
|
||||
const handleSubmitNameEdit = useCallback(async (el: HTMLInputElement) => {
|
||||
await updateRequest.mutate({ name: el.value });
|
||||
await updateRequest.mutate((r) => ({ ...r, name: el.value }));
|
||||
setEditing(false);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@ import { Portal } from '../Portal';
|
||||
import { Separator } from './Separator';
|
||||
import { VStack } from './Stacks';
|
||||
|
||||
export type DropdownItemSeparator = {
|
||||
type: 'separator';
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export type DropdownItem =
|
||||
| {
|
||||
type?: 'default';
|
||||
@@ -18,10 +23,7 @@ export type DropdownItem =
|
||||
rightSlot?: ReactNode;
|
||||
onSelect?: () => void;
|
||||
}
|
||||
| {
|
||||
type: 'separator';
|
||||
label?: string;
|
||||
};
|
||||
| DropdownItemSeparator;
|
||||
|
||||
export interface DropdownProps {
|
||||
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
|
||||
|
||||
@@ -69,7 +69,7 @@ export function Input({
|
||||
htmlFor={id}
|
||||
className={classnames(
|
||||
labelClassName,
|
||||
'font-semibold text-sm uppercase text-gray-700',
|
||||
'font-semibold text-xs uppercase text-gray-700',
|
||||
hideLabel && 'sr-only',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { DropdownProps } from './Dropdown';
|
||||
import type { DropdownItemSeparator, DropdownProps } from './Dropdown';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
export interface RadioDropdownItem<T> {
|
||||
label: string;
|
||||
shortLabel?: string;
|
||||
value: T;
|
||||
}
|
||||
export type RadioDropdownItem =
|
||||
| {
|
||||
type?: 'default';
|
||||
label: string;
|
||||
shortLabel?: string;
|
||||
value: string | null;
|
||||
}
|
||||
| DropdownItemSeparator;
|
||||
|
||||
export interface RadioDropdownProps<T = string | null> {
|
||||
value: T;
|
||||
onChange: (value: T) => void;
|
||||
items: RadioDropdownItem<T>[];
|
||||
export interface RadioDropdownProps {
|
||||
value: string | null;
|
||||
onChange: (value: string | null) => void;
|
||||
items: RadioDropdownItem[];
|
||||
children: DropdownProps['children'];
|
||||
}
|
||||
|
||||
export function RadioDropdown<T>({ value, items, onChange, children }: RadioDropdownProps<T>) {
|
||||
export function RadioDropdown({ value, items, onChange, children }: RadioDropdownProps) {
|
||||
const dropdownItems = useMemo(
|
||||
() =>
|
||||
items.map(({ label, shortLabel, value: v }) => ({
|
||||
label,
|
||||
shortLabel,
|
||||
onSelect: () => onChange(v),
|
||||
leftSlot: <Icon icon={value === v ? 'check' : 'empty'} />,
|
||||
})),
|
||||
items.map((item) => {
|
||||
if (item.type === 'separator') {
|
||||
return item;
|
||||
} else {
|
||||
return {
|
||||
label: item.label,
|
||||
shortLabel: item.shortLabel,
|
||||
onSelect: () => onChange(item.value),
|
||||
leftSlot: <Icon icon={value === item.value ? 'check' : 'empty'} />,
|
||||
};
|
||||
}
|
||||
}),
|
||||
[value, items],
|
||||
);
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export const VStack = forwardRef(function VStack(
|
||||
});
|
||||
|
||||
type BaseStackProps = HTMLAttributes<HTMLElement> & {
|
||||
as?: ComponentType | 'ul';
|
||||
as?: ComponentType | 'ul' | 'form';
|
||||
space?: keyof typeof gapClasses;
|
||||
alignItems?: 'start' | 'center';
|
||||
justifyContent?: 'start' | 'center' | 'end';
|
||||
|
||||
@@ -82,7 +82,9 @@ export function Tabs({
|
||||
isActive ? 'bg-gray-100 text-gray-800' : 'text-gray-600 hover:text-gray-900',
|
||||
);
|
||||
if ('options' in t) {
|
||||
const option = t.options.items.find((i) => i.value === t.options?.value);
|
||||
const option = t.options.items.find(
|
||||
(i) => 'value' in i && i.value === t.options?.value,
|
||||
);
|
||||
return (
|
||||
<RadioDropdown
|
||||
key={t.value}
|
||||
@@ -96,7 +98,9 @@ export function Tabs({
|
||||
onClick={isActive ? undefined : () => handleTabChange(t.value)}
|
||||
className={btnClassName}
|
||||
>
|
||||
{option?.shortLabel ?? option?.label ?? 'Unknown'}
|
||||
{option && 'shortLabel' in option
|
||||
? option.shortLabel
|
||||
: option?.label ?? 'Unknown'}
|
||||
<Icon icon="triangleDown" className="-mr-1.5" />
|
||||
</Button>
|
||||
</RadioDropdown>
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { getRequest } from '../lib/store';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
|
||||
export function useUpdateAnyRequest() {
|
||||
return useMutation<void, unknown, Partial<HttpRequest> & { id: string }>({
|
||||
mutationFn: async (patch) => {
|
||||
const request = await getRequest(patch.id);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<void, unknown, { id: string; update: (r: HttpRequest) => HttpRequest }>({
|
||||
mutationFn: async ({ id, update }) => {
|
||||
const request = await getRequest(id);
|
||||
if (request === null) {
|
||||
throw new Error("Can't update a null request");
|
||||
}
|
||||
|
||||
const updatedRequest = { ...request, ...patch };
|
||||
await invoke('update_request', { request: updatedRequest });
|
||||
await invoke('update_request', { request: update(request) });
|
||||
},
|
||||
onMutate: async ({ id, update }) => {
|
||||
const request = await getRequest(id);
|
||||
if (request === null) return;
|
||||
queryClient.setQueryData(
|
||||
requestsQueryKey(request?.workspaceId),
|
||||
(requests: HttpRequest[] | undefined) =>
|
||||
requests?.map((r) => (r.id === request.id ? update(r) : r)),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,27 +6,25 @@ import { requestsQueryKey } from './useRequests';
|
||||
|
||||
export function useUpdateRequest(id: string | null) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, unknown, Partial<HttpRequest>>({
|
||||
mutationFn: async (patch) => {
|
||||
return useMutation<void, unknown, Partial<HttpRequest> | ((r: HttpRequest) => HttpRequest)>({
|
||||
mutationFn: async (v) => {
|
||||
const request = await getRequest(id);
|
||||
if (request == null) {
|
||||
throw new Error("Can't update a null request");
|
||||
}
|
||||
|
||||
const updatedRequest = { ...request, ...patch };
|
||||
|
||||
console.log('UPDATING REQUEST', patch);
|
||||
await invoke('update_request', {
|
||||
request: updatedRequest,
|
||||
});
|
||||
const newRequest = typeof v === 'function' ? v(request) : { ...request, ...v };
|
||||
await invoke('update_request', { request: newRequest });
|
||||
},
|
||||
onMutate: async (patch) => {
|
||||
onMutate: async (v) => {
|
||||
const request = await getRequest(id);
|
||||
if (request === null) return;
|
||||
|
||||
const newRequest = typeof v === 'function' ? v(request) : { ...request, ...v };
|
||||
queryClient.setQueryData(
|
||||
requestsQueryKey(request?.workspaceId),
|
||||
(requests: HttpRequest[] | undefined) =>
|
||||
requests?.map((r) => (r.id === request.id ? { ...r, ...patch } : r)),
|
||||
requests?.map((r) => (r.id === newRequest.id ? newRequest : r)),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ export const BODY_TYPE_XML = 'text/xml';
|
||||
|
||||
export const AUTH_TYPE_NONE = null;
|
||||
export const AUTH_TYPE_BASIC = 'basic';
|
||||
export const AUTH_TYPE_BEARER = 'bearer';
|
||||
|
||||
export interface HttpRequest extends BaseModel {
|
||||
readonly workspaceId: string;
|
||||
@@ -33,7 +34,7 @@ export interface HttpRequest extends BaseModel {
|
||||
url: string;
|
||||
body: string | null;
|
||||
bodyType: string | null;
|
||||
authentication: any | null;
|
||||
authentication: Record<string, string | number | boolean | null | undefined>;
|
||||
authenticationType: string | null;
|
||||
auth: Record<string, string | number | null>;
|
||||
authType: string | null;
|
||||
|
||||
Reference in New Issue
Block a user