Simple auth schemes

This commit is contained in:
Gregory Schier
2023-03-29 09:03:38 -07:00
parent af9755c513
commit 0f58986b4c
18 changed files with 392 additions and 157 deletions

22
package-lock.json generated
View File

@@ -39,6 +39,7 @@
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0", "react-helmet-async": "^1.3.0",
"react-hook-form": "^7.43.8",
"react-router-dom": "^6.8.1", "react-router-dom": "^6.8.1",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"uuid": "^9.0.0" "uuid": "^9.0.0"
@@ -6334,6 +6335,21 @@
"react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" "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": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -12107,6 +12123,12 @@
"shallowequal": "^1.1.0" "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": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

1
src-tauri/Cargo.lock generated
View File

@@ -5169,6 +5169,7 @@ dependencies = [
name = "yaak-app" name = "yaak-app"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"base64 0.21.0",
"chrono", "chrono",
"cocoa", "cocoa",
"deno_ast", "deno_ast",

View File

@@ -14,23 +14,24 @@ strip = true # Automatically strip symbols from the binary.
tauri-build = { version = "1.2", features = [] } tauri-build = { version = "1.2", features = [] }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
objc = { version = "0.2.7" } objc = "0.2.7"
cocoa = { version = "0.24.1" } cocoa = "0.24.1"
[dependencies] [dependencies]
serde_json = { version = "1.0", features = ["raw_value"] } serde_json = { version = "1.0", features = ["raw_value"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2", features = ["config-toml", "devtools", "shell-open", "system-tray", "updater", "window-start-dragging"] } 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"] } reqwest = { version = "0.11.14", features = ["json"] }
tokio = { version = "1.25.0", features = ["sync"] } tokio = { version = "1.25.0", features = ["sync"] }
futures = { version = "0.3.26" } futures = "0.3.26"
deno_core = { version = "0.174.0" } deno_core = "0.174.0"
deno_ast = { version = "0.24.0", features = ["transpiling"] } deno_ast = { version = "0.24.0", features = ["transpiling"] }
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time", "offline"] } sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time", "offline"] }
uuid = { version = "1.3.0" } uuid = "1.3.0"
rand = { version = "0.8.5" } rand = "0.8.5"
chrono = { version = "0.4.23", features = ["serde"] } chrono = { version = "0.4.23", features = ["serde"] }
base64 = "0.21.0"
[features] [features]
# by default Tauri runs in production mode # by default Tauri runs in production mode

View File

@@ -1,27 +1,29 @@
#![cfg_attr( #![cfg_attr(
all(not(debug_assertions), target_os = "windows"), all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[macro_use] #[macro_use]
extern crate objc; extern crate objc;
use base64::Engine;
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
use std::env::current_dir; use std::env::current_dir;
use std::fs::create_dir_all; use std::fs::create_dir_all;
use http::header::{HeaderName, ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderValue, Method}; use http::{HeaderMap, HeaderValue, Method};
use http::header::{ACCEPT, HeaderName, USER_AGENT};
use reqwest::redirect::Policy; use reqwest::redirect::Policy;
use sqlx::{Pool, Sqlite}; use serde::Serialize;
use sqlx::migrate::Migrator; use sqlx::migrate::Migrator;
use sqlx::sqlite::SqlitePoolOptions; use sqlx::sqlite::SqlitePoolOptions;
use sqlx::types::{Json}; use sqlx::types::{Json, JsonValue};
use tauri::{AppHandle, Menu, MenuItem, State, Submenu, TitleBarStyle, Window, Wry}; use sqlx::{Pool, Sqlite};
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent};
use tauri::regex::Regex; 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 tokio::sync::Mutex;
use window_ext::WindowExt; use window_ext::WindowExt;
@@ -133,6 +135,41 @@ async fn actually_send_ephemeral_request(
headers.insert(header_name, header_value); 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()) let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
.expect("Failed to create method"); .expect("Failed to create method");
let builder = client.request(m, url_string.to_string()).headers(headers); 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 resp = client.execute(sendable_req).await;
let p = window.app_handle() let p = window
.app_handle()
.path_resolver() .path_resolver()
.resolve_resource("plugins/plugin.ts") .resolve_resource("plugins/plugin.ts")
.expect("failed to resolve resource"); .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) response = models::update_response_if_id(response, window.label(), pool)
.await .await
.expect("Failed to update response"); .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) Ok(response)
} }
Err(e) => response_err(response, e.to_string(), window, pool).await, Err(e) => response_err(response, e.to_string(), window, pool).await,
@@ -196,10 +237,14 @@ async fn send_request(
.await .await
.expect("Failed to get request"); .expect("Failed to get request");
let response = models::create_response(&req.id, 0, "", 0, None, "", vec![], window.label(), pool) let response =
.await models::create_response(&req.id, 0, "", 0, None, "", vec![], window.label(), pool)
.expect("Failed to create response"); .await
window.app_handle().emit_all("updated_response", &response).unwrap(); .expect("Failed to create response");
window
.app_handle()
.emit_all("updated_response", &response)
.unwrap();
actually_send_ephemeral_request(req, response, window, pool).await?; actually_send_ephemeral_request(req, response, window, pool).await?;
Ok(()) Ok(())
@@ -215,7 +260,10 @@ async fn response_err(
response = models::update_response_if_id(response, window.label(), pool) response = models::update_response_if_id(response, window.label(), pool)
.await .await
.expect("Failed to update response"); .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) Ok(response)
} }
@@ -294,10 +342,11 @@ async fn create_request(
window.label(), window.label(),
pool, pool,
) )
.await .await
.expect("Failed to create request"); .expect("Failed to create request");
window.app_handle() window
.app_handle()
.emit_all("updated_request", &created_request) .emit_all("updated_request", &created_request)
.unwrap(); .unwrap();
@@ -314,7 +363,10 @@ async fn duplicate_request(
let request = models::duplicate_request(id, window.label(), pool) let request = models::duplicate_request(id, window.label(), pool)
.await .await
.expect("Failed to duplicate request"); .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) Ok(request.id)
} }
@@ -352,10 +404,11 @@ async fn update_request(
window.label(), window.label(),
pool, pool,
) )
.await .await
.expect("Failed to update request"); .expect("Failed to update request");
window.app_handle() window
.app_handle()
.emit_all("updated_request", updated_request) .emit_all("updated_request", updated_request)
.unwrap(); .unwrap();
@@ -444,10 +497,14 @@ async fn workspaces(
.await .await
.expect("Failed to find workspaces"); .expect("Failed to find workspaces");
if workspaces.is_empty() { if workspaces.is_empty() {
let workspace = let workspace = models::create_workspace(
models::create_workspace("My Project", "This is the default workspace", window.label(), pool) "My Project",
.await "This is the default workspace",
.expect("Failed to create workspace"); window.label(),
pool,
)
.await
.expect("Failed to create workspace");
Ok(vec![workspace]) Ok(vec![workspace])
} else { } else {
Ok(workspaces) Ok(workspaces)
@@ -597,15 +654,15 @@ fn create_window(handle: AppHandle<Wry>) -> Window<Wry> {
window_id, window_id,
tauri::WindowUrl::App("workspaces".into()), tauri::WindowUrl::App("workspaces".into()),
) )
.menu(menu) .menu(menu)
.fullscreen(false) .fullscreen(false)
.resizable(true) .resizable(true)
.inner_size(1100.0, 600.0) .inner_size(1100.0, 600.0)
.hidden_title(true) .hidden_title(true)
.title("Yaak") .title("Yaak")
.title_bar_style(TitleBarStyle::Overlay) .title_bar_style(TitleBarStyle::Overlay)
.build() .build()
.expect("failed to build window"); .expect("failed to build window");
let win2 = win.clone(); let win2 = win.clone();
win.on_menu_event(move |event| { win.on_menu_event(move |event| {

View File

@@ -1,9 +1,9 @@
use std::collections::HashMap;
use rand::distributions::{Alphanumeric, DistString}; use rand::distributions::{Alphanumeric, DistString};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::types::chrono::NaiveDateTime; use sqlx::types::chrono::NaiveDateTime;
use sqlx::types::{Json, JsonValue}; use sqlx::types::{Json, JsonValue};
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use std::collections::HashMap;
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)] #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -189,7 +189,11 @@ pub async fn create_workspace(
get_workspace(&id, pool).await 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) let existing = get_request(id, pool)
.await .await
.expect("Failed to get request to duplicate"); .expect("Failed to get request to duplicate");

View File

@@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { persistQueryClient } from '@tanstack/react-query-persist-client'; import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
import { appWindow } from '@tauri-apps/api/window';
import { MotionConfig } from 'framer-motion'; import { MotionConfig } from 'framer-motion';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
@@ -16,11 +17,13 @@ import type { SidebarDisplay } from '../hooks/useSidebarDisplay';
import { sidebarDisplayDefaultValue, sidebarDisplayKey } from '../hooks/useSidebarDisplay'; import { sidebarDisplayDefaultValue, sidebarDisplayKey } from '../hooks/useSidebarDisplay';
import { workspacesQueryKey } from '../hooks/useWorkspaces'; import { workspacesQueryKey } from '../hooks/useWorkspaces';
import { DEFAULT_FONT_SIZE } from '../lib/constants'; import { DEFAULT_FONT_SIZE } from '../lib/constants';
import { debounce } from '../lib/debounce';
import { extractKeyValue, getKeyValue, setKeyValue } from '../lib/keyValueStore'; import { extractKeyValue, getKeyValue, setKeyValue } from '../lib/keyValueStore';
import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/models'; import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/models';
import { AppRouter } from './AppRouter'; import { AppRouter } from './AppRouter';
import { DialogProvider } from './DialogContext'; import { DialogProvider } from './DialogContext';
import { appWindow, WebviewWindow } from '@tauri-apps/api/window';
const UPDATE_DEBOUNCE_MILLIS = 500;
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@@ -33,7 +36,7 @@ const queryClient = new QueryClient({
const localStoragePersister = createSyncStoragePersister({ const localStoragePersister = createSyncStoragePersister({
storage: window.localStorage, storage: window.localStorage,
throttleTime: 1000, throttleTime: 1000, // 1 second
}); });
persistQueryClient({ persistQueryClient({
@@ -42,40 +45,47 @@ persistQueryClient({
maxAge: 1000 * 60 * 60 * 24, // 24 hours maxAge: 1000 * 60 * 60 * 24, // 24 hours
}); });
await listen('updated_key_value', ({ payload: keyValue }: { payload: KeyValue }) => { await listen(
if (keyValue.updatedBy === appWindow.label) return; 'updated_key_value',
queryClient.setQueryData(keyValueQueryKey(keyValue), extractKeyValue(keyValue)); 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 }) => { await listen(
if (request.updatedBy === appWindow.label) return; 'updated_request',
debounce(({ payload: request }: { payload: HttpRequest }) => {
if (request.updatedBy === appWindow.label) return;
queryClient.setQueryData( queryClient.setQueryData(
requestsQueryKey(request.workspaceId), requestsQueryKey(request.workspaceId),
(requests: HttpRequest[] = []) => { (requests: HttpRequest[] = []) => {
const newRequests = []; const newRequests = [];
let found = false; let found = false;
for (const r of requests) { for (const r of requests) {
if (r.id === request.id) { if (r.id === request.id) {
found = true; found = true;
newRequests.push(request); newRequests.push(request);
} else { } else {
newRequests.push(r); newRequests.push(r);
}
} }
} if (!found) {
if (!found) { newRequests.push(request);
newRequests.push(request); }
} return newRequests;
return newRequests; },
}, );
); }, UPDATE_DEBOUNCE_MILLIS),
}); );
await listen('updated_response', ({ payload: response }: { payload: HttpResponse }) => { await listen('updated_response', ({ payload: response }: { payload: HttpResponse }) => {
queryClient.setQueryData( queryClient.setQueryData(
responsesQueryKey(response.requestId), responsesQueryKey(response.requestId),
(responses: HttpResponse[] = []) => { (responses: HttpResponse[] = []) => {
if (response.updatedBy === appWindow.label) return; // We want updates from every response
// if (response.updatedBy === appWindow.label) return;
const newResponses = []; const newResponses = [];
let found = false; let found = false;
@@ -95,26 +105,29 @@ await listen('updated_response', ({ payload: response }: { payload: HttpResponse
); );
}); });
await listen('updated_workspace', ({ payload: workspace }: { payload: Workspace }) => { await listen(
queryClient.setQueryData(workspacesQueryKey(), (workspaces: Workspace[] = []) => { 'updated_workspace',
if (workspace.updatedBy === appWindow.label) return; debounce(({ payload: workspace }: { payload: Workspace }) => {
queryClient.setQueryData(workspacesQueryKey(), (workspaces: Workspace[] = []) => {
if (workspace.updatedBy === appWindow.label) return;
const newWorkspaces = []; const newWorkspaces = [];
let found = false; let found = false;
for (const w of workspaces) { for (const w of workspaces) {
if (w.id === workspace.id) { if (w.id === workspace.id) {
found = true; found = true;
newWorkspaces.push(workspace); newWorkspaces.push(workspace);
} else { } else {
newWorkspaces.push(w); newWorkspaces.push(w);
}
} }
} if (!found) {
if (!found) { newWorkspaces.push(workspace);
newWorkspaces.push(workspace); }
} return newWorkspaces;
return newWorkspaces; });
}); }, UPDATE_DEBOUNCE_MILLIS),
}); );
await listen( await listen(
'deleted_model', 'deleted_model',

View 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>
);
}

View 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>
);
}

View File

@@ -7,7 +7,17 @@ import { useKeyValue } from '../hooks/useKeyValue';
import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { tryFormatJson } from '../lib/formatters'; import { tryFormatJson } from '../lib/formatters';
import type { HttpHeader, HttpRequest } from '../lib/models'; 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 { Editor } from './core/Editor';
import type { TabItem } from './core/Tabs/Tabs'; import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } 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: { options: {
value: activeRequest.bodyType, value: activeRequest.bodyType,
items: [ items: [
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
{ label: 'JSON', value: BODY_TYPE_JSON }, { label: 'JSON', value: BODY_TYPE_JSON },
{ label: 'XML', value: BODY_TYPE_XML }, { label: 'XML', value: BODY_TYPE_XML },
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL }, { label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
{ type: 'separator' },
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
], ],
onChange: async (bodyType) => { onChange: async (bodyType) => {
const patch: Partial<HttpRequest> = { bodyType }; const patch: Partial<HttpRequest> = { bodyType };
@@ -76,21 +87,36 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
options: { options: {
value: activeRequest.authenticationType, value: activeRequest.authenticationType,
items: [ items: [
{ label: 'No Auth', shortLabel: 'Auth', value: null }, { label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC },
{ label: 'Basic', value: 'basic' }, { label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER },
{ type: 'separator' },
{ label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE },
], ],
onChange: async (a) => { onChange: async (authenticationType) => {
await updateRequest.mutate({ let authentication: HttpRequest['authentication'] = activeRequest?.authentication;
authenticationType: a, if (authenticationType === AUTH_TYPE_BASIC) {
authentication: { username: '', password: '' }, 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: 'params', label: 'URL Params' },
{ value: 'headers', label: 'Headers' }, { 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 }), []); const handleBodyChange = useCallback((body: string) => updateRequest.mutate({ body }), []);
@@ -123,24 +149,38 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
label="Request body" label="Request body"
> >
<TabContent value="auth"> <TabContent value="auth">
<div className="flex items-center justify-center min-h-[5rem]"> {activeRequest.authenticationType === AUTH_TYPE_BASIC ? (
<header>Hello</header> <BasicAuth
</div> 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>
<TabContent value="headers"> <TabContent value="headers">
<HeaderEditor <HeaderEditor
key={`${activeRequest.id}::${forceUpdateHeaderEditorKey}`} key={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
headers={activeRequest.headers} headers={activeRequest.headers}
onChange={handleHeadersChange} onChange={handleHeadersChange}
/> />
</TabContent> </TabContent>
<TabContent value="params"> <TabContent value="params">
<ParametersEditor key={activeRequestId} parameters={[]} onChange={() => null} /> <ParametersEditor key={forceUpdateKey} parameters={[]} onChange={() => null} />
</TabContent> </TabContent>
<TabContent value="body" className="pl-3 mt-1"> <TabContent value="body" className="pl-3 mt-1">
{activeRequest.bodyType === BODY_TYPE_JSON ? ( {activeRequest.bodyType === BODY_TYPE_JSON ? (
<Editor <Editor
key={activeRequest.id} key={forceUpdateKey}
useTemplating useTemplating
placeholder="..." placeholder="..."
className="!bg-gray-50" className="!bg-gray-50"
@@ -152,7 +192,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
/> />
) : activeRequest.bodyType === BODY_TYPE_XML ? ( ) : activeRequest.bodyType === BODY_TYPE_XML ? (
<Editor <Editor
key={activeRequest.id} key={forceUpdateKey}
useTemplating useTemplating
placeholder="..." placeholder="..."
className="!bg-gray-50" className="!bg-gray-50"
@@ -163,7 +203,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
/> />
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? ( ) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
<GraphQLEditor <GraphQLEditor
key={activeRequest.id} key={forceUpdateKey}
baseRequest={activeRequest} baseRequest={activeRequest}
className="!bg-gray-50" className="!bg-gray-50"
defaultValue={activeRequest?.body ?? ''} defaultValue={activeRequest?.body ?? ''}

View File

@@ -1,4 +1,3 @@
import { dialog } from '@tauri-apps/api';
import classnames from 'classnames'; import classnames from 'classnames';
import type { ForwardedRef, KeyboardEvent } from 'react'; import type { ForwardedRef, KeyboardEvent } from 'react';
import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react'; import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react';
@@ -99,14 +98,15 @@ function SidebarItems({
const shouldUpdateAll = afterPriority - beforePriority < 1; const shouldUpdateAll = afterPriority - beforePriority < 1;
if (shouldUpdateAll) { if (shouldUpdateAll) {
newRequests.forEach((r, i) => { newRequests.forEach(({ id }, i) => {
updateRequest.mutate({ id: r.id, sortPriority: i * 1000 }); const sortPriority = i * 1000;
const update = (r: HttpRequest) => ({ ...r, sortPriority });
updateRequest.mutate({ id, update });
}); });
} else { } else {
updateRequest.mutate({ const sortPriority = afterPriority - (afterPriority - beforePriority) / 2;
id: requestId, const update = (r: HttpRequest) => ({ ...r, sortPriority });
sortPriority: afterPriority - (afterPriority - beforePriority) / 2, updateRequest.mutate({ id: requestId, update });
});
} }
}, },
[hoveredIndex, requests], [hoveredIndex, requests],
@@ -149,7 +149,7 @@ const _SidebarItem = forwardRef(function SidebarItem(
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
const handleSubmitNameEdit = useCallback(async (el: HTMLInputElement) => { const handleSubmitNameEdit = useCallback(async (el: HTMLInputElement) => {
await updateRequest.mutate({ name: el.value }); await updateRequest.mutate((r) => ({ ...r, name: el.value }));
setEditing(false); setEditing(false);
}, []); }, []);

View File

@@ -8,6 +8,11 @@ import { Portal } from '../Portal';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { VStack } from './Stacks'; import { VStack } from './Stacks';
export type DropdownItemSeparator = {
type: 'separator';
label?: string;
};
export type DropdownItem = export type DropdownItem =
| { | {
type?: 'default'; type?: 'default';
@@ -18,10 +23,7 @@ export type DropdownItem =
rightSlot?: ReactNode; rightSlot?: ReactNode;
onSelect?: () => void; onSelect?: () => void;
} }
| { | DropdownItemSeparator;
type: 'separator';
label?: string;
};
export interface DropdownProps { export interface DropdownProps {
children: ReactElement<HTMLAttributes<HTMLButtonElement>>; children: ReactElement<HTMLAttributes<HTMLButtonElement>>;

View File

@@ -69,7 +69,7 @@ export function Input({
htmlFor={id} htmlFor={id}
className={classnames( className={classnames(
labelClassName, labelClassName,
'font-semibold text-sm uppercase text-gray-700', 'font-semibold text-xs uppercase text-gray-700',
hideLabel && 'sr-only', hideLabel && 'sr-only',
)} )}
> >

View File

@@ -1,30 +1,39 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { DropdownProps } from './Dropdown'; import type { DropdownItemSeparator, DropdownProps } from './Dropdown';
import { Dropdown } from './Dropdown'; import { Dropdown } from './Dropdown';
import { Icon } from './Icon'; import { Icon } from './Icon';
export interface RadioDropdownItem<T> { export type RadioDropdownItem =
label: string; | {
shortLabel?: string; type?: 'default';
value: T; label: string;
} shortLabel?: string;
value: string | null;
}
| DropdownItemSeparator;
export interface RadioDropdownProps<T = string | null> { export interface RadioDropdownProps {
value: T; value: string | null;
onChange: (value: T) => void; onChange: (value: string | null) => void;
items: RadioDropdownItem<T>[]; items: RadioDropdownItem[];
children: DropdownProps['children']; children: DropdownProps['children'];
} }
export function RadioDropdown<T>({ value, items, onChange, children }: RadioDropdownProps<T>) { export function RadioDropdown({ value, items, onChange, children }: RadioDropdownProps) {
const dropdownItems = useMemo( const dropdownItems = useMemo(
() => () =>
items.map(({ label, shortLabel, value: v }) => ({ items.map((item) => {
label, if (item.type === 'separator') {
shortLabel, return item;
onSelect: () => onChange(v), } else {
leftSlot: <Icon icon={value === v ? 'check' : 'empty'} />, return {
})), label: item.label,
shortLabel: item.shortLabel,
onSelect: () => onChange(item.value),
leftSlot: <Icon icon={value === item.value ? 'check' : 'empty'} />,
};
}
}),
[value, items], [value, items],
); );

View File

@@ -54,7 +54,7 @@ export const VStack = forwardRef(function VStack(
}); });
type BaseStackProps = HTMLAttributes<HTMLElement> & { type BaseStackProps = HTMLAttributes<HTMLElement> & {
as?: ComponentType | 'ul'; as?: ComponentType | 'ul' | 'form';
space?: keyof typeof gapClasses; space?: keyof typeof gapClasses;
alignItems?: 'start' | 'center'; alignItems?: 'start' | 'center';
justifyContent?: 'start' | 'center' | 'end'; justifyContent?: 'start' | 'center' | 'end';

View File

@@ -82,7 +82,9 @@ export function Tabs({
isActive ? 'bg-gray-100 text-gray-800' : 'text-gray-600 hover:text-gray-900', isActive ? 'bg-gray-100 text-gray-800' : 'text-gray-600 hover:text-gray-900',
); );
if ('options' in t) { 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 ( return (
<RadioDropdown <RadioDropdown
key={t.value} key={t.value}
@@ -96,7 +98,9 @@ export function Tabs({
onClick={isActive ? undefined : () => handleTabChange(t.value)} onClick={isActive ? undefined : () => handleTabChange(t.value)}
className={btnClassName} className={btnClassName}
> >
{option?.shortLabel ?? option?.label ?? 'Unknown'} {option && 'shortLabel' in option
? option.shortLabel
: option?.label ?? 'Unknown'}
<Icon icon="triangleDown" className="-mr-1.5" /> <Icon icon="triangleDown" className="-mr-1.5" />
</Button> </Button>
</RadioDropdown> </RadioDropdown>

View File

@@ -1,18 +1,29 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { getRequest } from '../lib/store'; import { getRequest } from '../lib/store';
import { requestsQueryKey } from './useRequests';
export function useUpdateAnyRequest() { export function useUpdateAnyRequest() {
return useMutation<void, unknown, Partial<HttpRequest> & { id: string }>({ const queryClient = useQueryClient();
mutationFn: async (patch) => {
const request = await getRequest(patch.id); return useMutation<void, unknown, { id: string; update: (r: HttpRequest) => HttpRequest }>({
mutationFn: async ({ id, update }) => {
const request = await getRequest(id);
if (request === null) { if (request === null) {
throw new Error("Can't update a null request"); throw new Error("Can't update a null request");
} }
const updatedRequest = { ...request, ...patch }; await invoke('update_request', { request: update(request) });
await invoke('update_request', { request: updatedRequest }); },
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)),
);
}, },
}); });
} }

View File

@@ -6,27 +6,25 @@ import { requestsQueryKey } from './useRequests';
export function useUpdateRequest(id: string | null) { export function useUpdateRequest(id: string | null) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<void, unknown, Partial<HttpRequest>>({ return useMutation<void, unknown, Partial<HttpRequest> | ((r: HttpRequest) => HttpRequest)>({
mutationFn: async (patch) => { mutationFn: async (v) => {
const request = await getRequest(id); const request = await getRequest(id);
if (request == null) { if (request == null) {
throw new Error("Can't update a null request"); throw new Error("Can't update a null request");
} }
const updatedRequest = { ...request, ...patch }; const newRequest = typeof v === 'function' ? v(request) : { ...request, ...v };
await invoke('update_request', { request: newRequest });
console.log('UPDATING REQUEST', patch);
await invoke('update_request', {
request: updatedRequest,
});
}, },
onMutate: async (patch) => { onMutate: async (v) => {
const request = await getRequest(id); const request = await getRequest(id);
if (request === null) return; if (request === null) return;
const newRequest = typeof v === 'function' ? v(request) : { ...request, ...v };
queryClient.setQueryData( queryClient.setQueryData(
requestsQueryKey(request?.workspaceId), requestsQueryKey(request?.workspaceId),
(requests: HttpRequest[] | undefined) => (requests: HttpRequest[] | undefined) =>
requests?.map((r) => (r.id === request.id ? { ...r, ...patch } : r)), requests?.map((r) => (r.id === newRequest.id ? newRequest : r)),
); );
}, },
}); });

View File

@@ -24,6 +24,7 @@ export const BODY_TYPE_XML = 'text/xml';
export const AUTH_TYPE_NONE = null; export const AUTH_TYPE_NONE = null;
export const AUTH_TYPE_BASIC = 'basic'; export const AUTH_TYPE_BASIC = 'basic';
export const AUTH_TYPE_BEARER = 'bearer';
export interface HttpRequest extends BaseModel { export interface HttpRequest extends BaseModel {
readonly workspaceId: string; readonly workspaceId: string;
@@ -33,7 +34,7 @@ export interface HttpRequest extends BaseModel {
url: string; url: string;
body: string | null; body: string | null;
bodyType: string | null; bodyType: string | null;
authentication: any | null; authentication: Record<string, string | number | boolean | null | undefined>;
authenticationType: string | null; authenticationType: string | null;
auth: Record<string, string | number | null>; auth: Record<string, string | number | null>;
authType: string | null; authType: string | null;