Request Inheritance (#209)

This commit is contained in:
Gregory Schier
2025-05-23 08:13:25 -07:00
committed by GitHub
parent 13d959799a
commit 4cd2e9cd31
41 changed files with 1031 additions and 403 deletions
@@ -4,11 +4,9 @@ export type Environment = { model: "environment", id: string, workspaceId: strin
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, }; export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, description: string, name: string, defaultAuthentication: ParentAuthentication, defaultHeaders: Array<HttpRequestHeader>, sortPriority: number, };
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, }; export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, }; export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
@@ -22,6 +20,8 @@ export type HttpResponseState = "initialized" | "connected" | "closed";
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, }; export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
export type ParentAuthentication = { authentication: Record<string, any>, authenticationType: string | null, };
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, }; export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, }; export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, defaultAuthentication: ParentAuthentication, defaultHeaders: Array<HttpRequestHeader>, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
@@ -0,0 +1,15 @@
-- Auth
ALTER TABLE workspaces
ADD COLUMN authentication TEXT NOT NULL DEFAULT '{}';
ALTER TABLE folders
ADD COLUMN authentication TEXT NOT NULL DEFAULT '{}';
ALTER TABLE workspaces
ADD COLUMN authentication_type TEXT;
ALTER TABLE folders
ADD COLUMN authentication_type TEXT;
-- Headers
ALTER TABLE workspaces
ADD COLUMN headers TEXT NOT NULL DEFAULT '[]';
ALTER TABLE folders
ADD COLUMN headers TEXT NOT NULL DEFAULT '[]';
+8 -3
View File
@@ -5,6 +5,7 @@ use KeyAndValueRef::{Ascii, Binary};
use tauri::{Manager, Runtime, WebviewWindow}; use tauri::{Manager, Runtime, WebviewWindow};
use yaak_grpc::{KeyAndValueRef, MetadataMap}; use yaak_grpc::{KeyAndValueRef, MetadataMap};
use yaak_models::models::GrpcRequest; use yaak_models::models::GrpcRequest;
use yaak_models::query_manager::QueryManagerExt;
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader}; use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader};
use yaak_plugins::manager::PluginManager; use yaak_plugins::manager::PluginManager;
@@ -27,7 +28,8 @@ pub(crate) async fn build_metadata<R: Runtime>(
let mut metadata = BTreeMap::new(); let mut metadata = BTreeMap::new();
// Add the rest of metadata // Add the rest of metadata
for h in request.clone().metadata { let resolved_metadata = window.db().resolve_metadata_for_grpc_request(&request)?;
for h in resolved_metadata {
if h.name.is_empty() && h.value.is_empty() { if h.name.is_empty() && h.value.is_empty() {
continue; continue;
} }
@@ -39,8 +41,11 @@ pub(crate) async fn build_metadata<R: Runtime>(
metadata.insert(h.name, h.value); metadata.insert(h.name, h.value);
} }
if let Some(auth_name) = request.authentication_type.clone() { let (authentication_type, authentication) =
let auth = request.authentication.clone(); window.db().resolve_auth_for_grpc_request(&request)?;
if let Some(auth_name) = authentication_type.clone() {
let auth = authentication.clone();
let plugin_req = CallHttpAuthenticationRequest { let plugin_req = CallHttpAuthenticationRequest {
context_id: format!("{:x}", md5::compute(request.id.clone())), context_id: format!("{:x}", md5::compute(request.id.clone())),
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(), values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
+51 -39
View File
@@ -84,7 +84,7 @@ pub async fn send_http_request<R: Runtime>(
} }
}; };
let mut url_string = request.url; let mut url_string = request.url.clone();
url_string = ensure_proto(&url_string); url_string = ensure_proto(&url_string);
if !url_string.starts_with("http://") && !url_string.starts_with("https://") { if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
@@ -227,7 +227,9 @@ pub async fn send_http_request<R: Runtime>(
// ); // );
// } // }
for h in request.headers.clone() { let resolved_headers = window.db().resolve_headers_for_http_request(&request)?;
for h in resolved_headers {
if h.name.is_empty() && h.value.is_empty() { if h.name.is_empty() && h.value.is_empty() {
continue; continue;
} }
@@ -255,7 +257,7 @@ pub async fn send_http_request<R: Runtime>(
} }
let request_body = request.body.clone(); let request_body = request.body.clone();
if let Some(body_type) = &request.body_type { if let Some(body_type) = &request.body_type.clone() {
if body_type == "graphql" { if body_type == "graphql" {
let query = get_str_h(&request_body, "query"); let query = get_str_h(&request_body, "query");
let variables = get_str_h(&request_body, "variables"); let variables = get_str_h(&request_body, "variables");
@@ -376,7 +378,7 @@ pub async fn send_http_request<R: Runtime>(
}; };
} }
// Set file path if it is not empty // Set a file path if it is not empty
if !file_path.is_empty() { if !file_path.is_empty() {
let filename = PathBuf::from(file_path) let filename = PathBuf::from(file_path)
.file_name() .file_name()
@@ -426,43 +428,53 @@ pub async fn send_http_request<R: Runtime>(
} }
}; };
// Apply authentication let (authentication_type, authentication) =
window.db().resolve_auth_for_http_request(&request)?;
if let Some(auth_name) = request.authentication_type.to_owned() { match authentication_type {
let req = CallHttpAuthenticationRequest { None => {
context_id: format!("{:x}", md5::compute(request.id)), // No authentication found. Not even inherited
values: serde_json::from_value(serde_json::to_value(&request.authentication).unwrap()) }
.unwrap(), Some(authentication_type) if authentication_type == "none" => {
url: sendable_req.url().to_string(), // Explicitly no authentication
method: sendable_req.method().to_string(), }
headers: sendable_req Some(authentication_type) => {
.headers() let req = CallHttpAuthenticationRequest {
.iter() context_id: format!("{:x}", md5::compute(request.id)),
.map(|(name, value)| HttpHeader { values: serde_json::from_value(serde_json::to_value(&authentication).unwrap())
name: name.to_string(), .unwrap(),
value: value.to_str().unwrap_or_default().to_string(), url: sendable_req.url().to_string(),
}) method: sendable_req.method().to_string(),
.collect(), headers: sendable_req
}; .headers()
let auth_result = plugin_manager.call_http_authentication(&window, &auth_name, req).await; .iter()
let plugin_result = match auth_result { .map(|(name, value)| HttpHeader {
Ok(r) => r, name: name.to_string(),
Err(e) => { value: value.to_str().unwrap_or_default().to_string(),
return Ok(response_err( })
&app_handle, .collect(),
&*response.lock().await, };
e.to_string(), let auth_result =
&update_source, plugin_manager.call_http_authentication(&window, &authentication_type, req).await;
)); let plugin_result = match auth_result {
Ok(r) => r,
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
&update_source,
));
}
};
let headers = sendable_req.headers_mut();
for header in plugin_result.set_headers {
headers.insert(
HeaderName::from_str(&header.name).unwrap(),
HeaderValue::from_str(&header.value).unwrap(),
);
} }
};
let headers = sendable_req.headers_mut();
for header in plugin_result.set_headers {
headers.insert(
HeaderName::from_str(&header.name).unwrap(),
HeaderValue::from_str(&header.value).unwrap(),
);
} }
} }
+2 -2
View File
@@ -917,10 +917,10 @@ async fn cmd_call_http_authentication_action<R: Runtime>(
auth_name: &str, auth_name: &str,
action_index: i32, action_index: i32,
values: HashMap<String, JsonPrimitive>, values: HashMap<String, JsonPrimitive>,
request_id: &str, model_id: &str,
) -> YaakResult<()> { ) -> YaakResult<()> {
Ok(plugin_manager Ok(plugin_manager
.call_http_authentication_action(&window, auth_name, action_index, values, request_id) .call_http_authentication_action(&window, auth_name, action_index, values, model_id)
.await?) .await?)
} }
+2 -2
View File
@@ -2,7 +2,7 @@ use serde_json::Value;
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use yaak_http::apply_path_placeholders; use yaak_http::apply_path_placeholders;
use yaak_models::models::{ use yaak_models::models::{
Environment, GrpcMetadataEntry, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter, Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter,
}; };
use yaak_models::render::make_vars_hashmap; use yaak_models::render::make_vars_hashmap;
use yaak_templates::{parse_and_render, render_json_value_raw, TemplateCallback}; use yaak_templates::{parse_and_render, render_json_value_raw, TemplateCallback};
@@ -37,7 +37,7 @@ pub async fn render_grpc_request<T: TemplateCallback>(
let mut metadata = Vec::new(); let mut metadata = Vec::new();
for p in r.metadata.clone() { for p in r.metadata.clone() {
metadata.push(GrpcMetadataEntry { metadata.push(HttpRequestHeader {
enabled: p.enabled, enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await?, name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await?, value: render(p.value.as_str(), vars, cb).await?,
+26 -5
View File
@@ -32,6 +32,7 @@ var import_node_fs = require("node:fs");
async function getAccessToken(ctx, { async function getAccessToken(ctx, {
accessTokenUrl, accessTokenUrl,
scope, scope,
audience,
params, params,
grantType, grantType,
credentialsInBody, credentialsInBody,
@@ -56,6 +57,7 @@ async function getAccessToken(ctx, {
] ]
}; };
if (scope) httpRequest.body.form.push({ name: "scope", value: scope }); if (scope) httpRequest.body.form.push({ name: "scope", value: scope });
if (scope) httpRequest.body.form.push({ name: "audience", value: audience });
if (credentialsInBody) { if (credentialsInBody) {
httpRequest.body.form.push({ name: "client_id", value: clientId }); httpRequest.body.form.push({ name: "client_id", value: clientId });
httpRequest.body.form.push({ name: "client_secret", value: clientSecret }); httpRequest.body.form.push({ name: "client_secret", value: clientSecret });
@@ -64,10 +66,10 @@ async function getAccessToken(ctx, {
httpRequest.headers.push({ name: "Authorization", value }); httpRequest.headers.push({ name: "Authorization", value });
} }
const resp = await ctx.httpRequest.send({ httpRequest }); const resp = await ctx.httpRequest.send({ httpRequest });
const body = resp.bodyPath ? (0, import_node_fs.readFileSync)(resp.bodyPath, "utf8") : "";
if (resp.status < 200 || resp.status >= 300) { if (resp.status < 200 || resp.status >= 300) {
throw new Error("Failed to fetch access token with status=" + resp.status); throw new Error("Failed to fetch access token with status=" + resp.status + " and body=" + body);
} }
const body = (0, import_node_fs.readFileSync)(resp.bodyPath ?? "", "utf8");
let response; let response;
try { try {
response = JSON.parse(body); response = JSON.parse(body);
@@ -168,10 +170,10 @@ async function getOrRefreshAccessToken(ctx, contextId, {
await deleteToken(ctx, contextId); await deleteToken(ctx, contextId);
return null; return null;
} }
const body = resp.bodyPath ? (0, import_node_fs2.readFileSync)(resp.bodyPath, "utf8") : "";
if (resp.status < 200 || resp.status >= 300) { if (resp.status < 200 || resp.status >= 300) {
throw new Error("Failed to fetch access token with status=" + resp.status); throw new Error("Failed to refresh access token with status=" + resp.status + " and body=" + body);
} }
const body = (0, import_node_fs2.readFileSync)(resp.bodyPath ?? "", "utf8");
let response; let response;
try { try {
response = JSON.parse(body); response = JSON.parse(body);
@@ -201,6 +203,7 @@ async function getAuthorizationCode(ctx, contextId, {
redirectUri, redirectUri,
scope, scope,
state, state,
audience,
credentialsInBody, credentialsInBody,
pkce pkce
}) { }) {
@@ -220,6 +223,7 @@ async function getAuthorizationCode(ctx, contextId, {
if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri); if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri);
if (scope) authorizationUrl.searchParams.set("scope", scope); if (scope) authorizationUrl.searchParams.set("scope", scope);
if (state) authorizationUrl.searchParams.set("state", state); if (state) authorizationUrl.searchParams.set("state", state);
if (audience) authorizationUrl.searchParams.set("audience", audience);
if (pkce) { if (pkce) {
const verifier = pkce.codeVerifier || createPkceCodeVerifier(); const verifier = pkce.codeVerifier || createPkceCodeVerifier();
const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD; const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD;
@@ -256,6 +260,7 @@ async function getAuthorizationCode(ctx, contextId, {
clientId, clientId,
clientSecret, clientSecret,
scope, scope,
audience,
credentialsInBody, credentialsInBody,
params: [ params: [
{ name: "code", value: code }, { name: "code", value: code },
@@ -291,6 +296,7 @@ async function getClientCredentials(ctx, contextId, {
clientId, clientId,
clientSecret, clientSecret,
scope, scope,
audience,
credentialsInBody credentialsInBody
}) { }) {
const token = await getToken(ctx, contextId); const token = await getToken(ctx, contextId);
@@ -299,6 +305,7 @@ async function getClientCredentials(ctx, contextId, {
const response = await getAccessToken(ctx, { const response = await getAccessToken(ctx, {
grantType: "client_credentials", grantType: "client_credentials",
accessTokenUrl, accessTokenUrl,
audience,
clientId, clientId,
clientSecret, clientSecret,
scope, scope,
@@ -315,7 +322,8 @@ function getImplicit(ctx, contextId, {
clientId, clientId,
redirectUri, redirectUri,
scope, scope,
state state,
audience
}) { }) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const token = await getToken(ctx, contextId); const token = await getToken(ctx, contextId);
@@ -327,6 +335,7 @@ function getImplicit(ctx, contextId, {
if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri); if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri);
if (scope) authorizationUrl.searchParams.set("scope", scope); if (scope) authorizationUrl.searchParams.set("scope", scope);
if (state) authorizationUrl.searchParams.set("state", state); if (state) authorizationUrl.searchParams.set("state", state);
if (audience) authorizationUrl.searchParams.set("audience", audience);
if (responseType.includes("id_token")) { if (responseType.includes("id_token")) {
authorizationUrl.searchParams.set("nonce", String(Math.floor(Math.random() * 9999999999999) + 1)); authorizationUrl.searchParams.set("nonce", String(Math.floor(Math.random() * 9999999999999) + 1));
} }
@@ -366,6 +375,7 @@ async function getPassword(ctx, contextId, {
username, username,
password, password,
credentialsInBody, credentialsInBody,
audience,
scope scope
}) { }) {
const token = await getOrRefreshAccessToken(ctx, contextId, { const token = await getOrRefreshAccessToken(ctx, contextId, {
@@ -383,6 +393,7 @@ async function getPassword(ctx, contextId, {
clientId, clientId,
clientSecret, clientSecret,
scope, scope,
audience,
grantType: "password", grantType: "password",
credentialsInBody, credentialsInBody,
params: [ params: [
@@ -530,6 +541,12 @@ var plugin = {
optional: true, optional: true,
dynamic: hiddenIfNot(["authorization_code", "implicit"]) dynamic: hiddenIfNot(["authorization_code", "implicit"])
}, },
{
type: "text",
name: "audience",
label: "Audience",
optional: true
},
{ {
type: "checkbox", type: "checkbox",
name: "usePkce", name: "usePkce",
@@ -635,6 +652,7 @@ var plugin = {
clientSecret: stringArg(values, "clientSecret"), clientSecret: stringArg(values, "clientSecret"),
redirectUri: stringArgOrNull(values, "redirectUri"), redirectUri: stringArgOrNull(values, "redirectUri"),
scope: stringArgOrNull(values, "scope"), scope: stringArgOrNull(values, "scope"),
audience: stringArgOrNull(values, "audience"),
state: stringArgOrNull(values, "state"), state: stringArgOrNull(values, "state"),
credentialsInBody, credentialsInBody,
pkce: values.usePkce ? { pkce: values.usePkce ? {
@@ -650,6 +668,7 @@ var plugin = {
redirectUri: stringArgOrNull(values, "redirectUri"), redirectUri: stringArgOrNull(values, "redirectUri"),
responseType: stringArg(values, "responseType"), responseType: stringArg(values, "responseType"),
scope: stringArgOrNull(values, "scope"), scope: stringArgOrNull(values, "scope"),
audience: stringArgOrNull(values, "audience"),
state: stringArgOrNull(values, "state") state: stringArgOrNull(values, "state")
}); });
} else if (grantType === "client_credentials") { } else if (grantType === "client_credentials") {
@@ -659,6 +678,7 @@ var plugin = {
clientId: stringArg(values, "clientId"), clientId: stringArg(values, "clientId"),
clientSecret: stringArg(values, "clientSecret"), clientSecret: stringArg(values, "clientSecret"),
scope: stringArgOrNull(values, "scope"), scope: stringArgOrNull(values, "scope"),
audience: stringArgOrNull(values, "audience"),
credentialsInBody credentialsInBody
}); });
} else if (grantType === "password") { } else if (grantType === "password") {
@@ -670,6 +690,7 @@ var plugin = {
username: stringArg(values, "username"), username: stringArg(values, "username"),
password: stringArg(values, "password"), password: stringArg(values, "password"),
scope: stringArgOrNull(values, "scope"), scope: stringArgOrNull(values, "scope"),
audience: stringArgOrNull(values, "audience"),
credentialsInBody credentialsInBody
}); });
} else { } else {
+17 -14
View File
@@ -542,20 +542,23 @@ function pairsToDataParameters(keyedPairs) {
} }
for (const p of pairs) { for (const p of pairs) {
if (typeof p !== "string") continue; if (typeof p !== "string") continue;
const [name, value] = p.split("="); let params = p.split("&");
if (p.startsWith("@")) { for (const param of params) {
dataParameters.push({ const [name, value] = param.split("=");
name: name ?? "", if (param.startsWith("@")) {
value: "", dataParameters.push({
filePath: p.slice(1), name: name ?? "",
enabled: true value: "",
}); filePath: param.slice(1),
} else { enabled: true
dataParameters.push({ });
name: name ?? "", } else {
value: flagName === "data-urlencode" ? encodeURIComponent(value ?? "") : value ?? "", dataParameters.push({
enabled: true name: name ?? "",
}); value: flagName === "data-urlencode" ? encodeURIComponent(value ?? "") : value ?? "",
enabled: true
});
}
} }
} }
} }
+3 -5
View File
@@ -4,11 +4,9 @@ export type Environment = { model: "environment", id: string, workspaceId: strin
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, }; export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, }; export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, }; export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
@@ -20,4 +18,4 @@ export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environ
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, }; export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, }; export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
+7 -5
View File
@@ -18,7 +18,7 @@ export type Environment = { model: "environment", id: string, workspaceId: strin
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, }; export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, }; export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, };
@@ -28,9 +28,7 @@ export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, up
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end"; export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, }; export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, }; export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
@@ -50,6 +48,10 @@ export type ModelChangeEvent = { "type": "upsert" } | { "type": "delete" };
export type ModelPayload = { model: AnyModel, updateSource: UpdateSource, change: ModelChangeEvent, }; export type ModelPayload = { model: AnyModel, updateSource: UpdateSource, change: ModelChangeEvent, };
export type ParentAuthentication = { authentication: Record<string, any>, authenticationType: string | null, };
export type ParentHeaders = { headers: Array<HttpRequestHeader>, };
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, }; export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, }; export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
@@ -76,6 +78,6 @@ export type WebsocketMessageType = "text" | "binary";
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, }; export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, }; export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, }; export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
+70 -36
View File
@@ -219,8 +219,13 @@ pub struct Workspace {
pub id: String, pub id: String,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
pub name: String,
#[ts(type = "Record<string, any>")]
pub authentication: BTreeMap<String, Value>,
pub authentication_type: Option<String>,
pub description: String, pub description: String,
pub headers: Vec<HttpRequestHeader>,
pub name: String,
pub encryption_key_challenge: Option<String>, pub encryption_key_challenge: Option<String>,
// Settings // Settings
@@ -261,6 +266,9 @@ impl UpsertModelInfo for Workspace {
(CreatedAt, upsert_date(source, self.created_at)), (CreatedAt, upsert_date(source, self.created_at)),
(UpdatedAt, upsert_date(source, self.updated_at)), (UpdatedAt, upsert_date(source, self.updated_at)),
(Name, self.name.trim().into()), (Name, self.name.trim().into()),
(Authentication, serde_json::to_string(&self.authentication)?.into()),
(AuthenticationType, self.authentication_type.into()),
(Headers, serde_json::to_string(&self.headers)?.into()),
(Description, self.description.into()), (Description, self.description.into()),
(EncryptionKeyChallenge, self.encryption_key_challenge.into()), (EncryptionKeyChallenge, self.encryption_key_challenge.into()),
(SettingFollowRedirects, self.setting_follow_redirects.into()), (SettingFollowRedirects, self.setting_follow_redirects.into()),
@@ -273,6 +281,9 @@ impl UpsertModelInfo for Workspace {
vec![ vec![
WorkspaceIden::UpdatedAt, WorkspaceIden::UpdatedAt,
WorkspaceIden::Name, WorkspaceIden::Name,
WorkspaceIden::Authentication,
WorkspaceIden::AuthenticationType,
WorkspaceIden::Headers,
WorkspaceIden::Description, WorkspaceIden::Description,
WorkspaceIden::EncryptionKeyChallenge, WorkspaceIden::EncryptionKeyChallenge,
WorkspaceIden::SettingRequestTimeout, WorkspaceIden::SettingRequestTimeout,
@@ -286,6 +297,8 @@ impl UpsertModelInfo for Workspace {
where where
Self: Sized, Self: Sized,
{ {
let headers: String = row.get("headers")?;
let authentication: String = row.get("authentication")?;
Ok(Self { Ok(Self {
id: row.get("id")?, id: row.get("id")?,
model: row.get("model")?, model: row.get("model")?,
@@ -294,6 +307,9 @@ impl UpsertModelInfo for Workspace {
name: row.get("name")?, name: row.get("name")?,
description: row.get("description")?, description: row.get("description")?,
encryption_key_challenge: row.get("encryption_key_challenge")?, encryption_key_challenge: row.get("encryption_key_challenge")?,
headers: serde_json::from_str(&headers).unwrap_or_default(),
authentication: serde_json::from_str(&authentication).unwrap_or_default(),
authentication_type: row.get("authentication_type")?,
setting_follow_redirects: row.get("setting_follow_redirects")?, setting_follow_redirects: row.get("setting_follow_redirects")?,
setting_request_timeout: row.get("setting_request_timeout")?, setting_request_timeout: row.get("setting_request_timeout")?,
setting_validate_certificates: row.get("setting_validate_certificates")?, setting_validate_certificates: row.get("setting_validate_certificates")?,
@@ -581,6 +597,22 @@ pub struct EnvironmentVariable {
pub id: Option<String>, pub id: Option<String>,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
pub struct ParentAuthentication {
#[ts(type = "Record<string, any>")]
pub authentication: BTreeMap<String, Value>,
pub authentication_type: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
pub struct ParentHeaders {
pub headers: Vec<HttpRequestHeader>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")] #[ts(export, export_to = "gen_models.ts")]
@@ -594,8 +626,12 @@ pub struct Folder {
pub workspace_id: String, pub workspace_id: String,
pub folder_id: Option<String>, pub folder_id: Option<String>,
pub name: String, #[ts(type = "Record<string, any>")]
pub authentication: BTreeMap<String, Value>,
pub authentication_type: Option<String>,
pub description: String, pub description: String,
pub headers: Vec<HttpRequestHeader>,
pub name: String,
pub sort_priority: f32, pub sort_priority: f32,
} }
@@ -630,8 +666,11 @@ impl UpsertModelInfo for Folder {
(UpdatedAt, upsert_date(source, self.updated_at)), (UpdatedAt, upsert_date(source, self.updated_at)),
(WorkspaceId, self.workspace_id.into()), (WorkspaceId, self.workspace_id.into()),
(FolderId, self.folder_id.into()), (FolderId, self.folder_id.into()),
(Name, self.name.trim().into()), (Authentication, serde_json::to_string(&self.authentication)?.into()),
(AuthenticationType, self.authentication_type.into()),
(Headers, serde_json::to_string(&self.headers)?.into()),
(Description, self.description.into()), (Description, self.description.into()),
(Name, self.name.trim().into()),
(SortPriority, self.sort_priority.into()), (SortPriority, self.sort_priority.into()),
]) ])
} }
@@ -640,6 +679,9 @@ impl UpsertModelInfo for Folder {
vec![ vec![
FolderIden::UpdatedAt, FolderIden::UpdatedAt,
FolderIden::Name, FolderIden::Name,
FolderIden::Authentication,
FolderIden::AuthenticationType,
FolderIden::Headers,
FolderIden::Description, FolderIden::Description,
FolderIden::FolderId, FolderIden::FolderId,
FolderIden::SortPriority, FolderIden::SortPriority,
@@ -650,6 +692,8 @@ impl UpsertModelInfo for Folder {
where where
Self: Sized, Self: Sized,
{ {
let headers: String = row.get("headers")?;
let authentication: String = row.get("authentication")?;
Ok(Self { Ok(Self {
id: row.get("id")?, id: row.get("id")?,
model: row.get("model")?, model: row.get("model")?,
@@ -660,6 +704,9 @@ impl UpsertModelInfo for Folder {
folder_id: row.get("folder_id")?, folder_id: row.get("folder_id")?,
name: row.get("name")?, name: row.get("name")?,
description: row.get("description")?, description: row.get("description")?,
headers: serde_json::from_str(&headers).unwrap_or_default(),
authentication_type: row.get("authentication_type")?,
authentication: serde_json::from_str(&authentication).unwrap_or_default(),
}) })
} }
} }
@@ -782,28 +829,28 @@ impl UpsertModelInfo for HttpRequest {
] ]
} }
fn from_row(r: &Row) -> rusqlite::Result<Self> { fn from_row(row: &Row) -> rusqlite::Result<Self> {
let url_parameters: String = r.get("url_parameters")?; let url_parameters: String = row.get("url_parameters")?;
let body: String = r.get("body")?; let body: String = row.get("body")?;
let authentication: String = r.get("authentication")?; let authentication: String = row.get("authentication")?;
let headers: String = r.get("headers")?; let headers: String = row.get("headers")?;
Ok(Self { Ok(Self {
id: r.get("id")?, id: row.get("id")?,
model: r.get("model")?, model: row.get("model")?,
workspace_id: r.get("workspace_id")?, workspace_id: row.get("workspace_id")?,
created_at: r.get("created_at")?, created_at: row.get("created_at")?,
updated_at: r.get("updated_at")?, updated_at: row.get("updated_at")?,
authentication: serde_json::from_str(authentication.as_str()).unwrap_or_default(), authentication: serde_json::from_str(authentication.as_str()).unwrap_or_default(),
authentication_type: r.get("authentication_type")?, authentication_type: row.get("authentication_type")?,
body: serde_json::from_str(body.as_str()).unwrap_or_default(), body: serde_json::from_str(body.as_str()).unwrap_or_default(),
body_type: r.get("body_type")?, body_type: row.get("body_type")?,
description: r.get("description")?, description: row.get("description")?,
folder_id: r.get("folder_id")?, folder_id: row.get("folder_id")?,
headers: serde_json::from_str(headers.as_str()).unwrap_or_default(), headers: serde_json::from_str(headers.as_str()).unwrap_or_default(),
method: r.get("method")?, method: row.get("method")?,
name: r.get("name")?, name: row.get("name")?,
sort_priority: r.get("sort_priority")?, sort_priority: row.get("sort_priority")?,
url: r.get("url")?, url: row.get("url")?,
url_parameters: serde_json::from_str(url_parameters.as_str()).unwrap_or_default(), url_parameters: serde_json::from_str(url_parameters.as_str()).unwrap_or_default(),
}) })
} }
@@ -992,7 +1039,7 @@ impl UpsertModelInfo for WebsocketRequest {
(WorkspaceId, self.workspace_id.into()), (WorkspaceId, self.workspace_id.into()),
(FolderId, self.folder_id.as_ref().map(|s| s.as_str()).into()), (FolderId, self.folder_id.as_ref().map(|s| s.as_str()).into()),
(Authentication, serde_json::to_string(&self.authentication)?.into()), (Authentication, serde_json::to_string(&self.authentication)?.into()),
(AuthenticationType, self.authentication_type.as_ref().map(|s| s.as_str()).into()), (AuthenticationType, self.authentication_type.into()),
(Description, self.description.into()), (Description, self.description.into()),
(Headers, serde_json::to_string(&self.headers)?.into()), (Headers, serde_json::to_string(&self.headers)?.into()),
(Message, self.message.into()), (Message, self.message.into()),
@@ -1295,19 +1342,6 @@ impl UpsertModelInfo for HttpResponse {
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
pub struct GrpcMetadataEntry {
#[serde(default = "default_true")]
#[ts(optional, as = "Option<bool>")]
pub enabled: bool,
pub name: String,
pub value: String,
#[ts(optional, as = "Option<String>")]
pub id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")] #[ts(export, export_to = "gen_models.ts")]
@@ -1326,7 +1360,7 @@ pub struct GrpcRequest {
pub authentication: BTreeMap<String, Value>, pub authentication: BTreeMap<String, Value>,
pub description: String, pub description: String,
pub message: String, pub message: String,
pub metadata: Vec<GrpcMetadataEntry>, pub metadata: Vec<HttpRequestHeader>,
pub method: Option<String>, pub method: Option<String>,
pub name: String, pub name: String,
pub service: Option<String>, pub service: Option<String>,
+40 -2
View File
@@ -2,10 +2,12 @@ use crate::connection_or_tx::ConnectionOrTx;
use crate::db_context::DbContext; use crate::db_context::DbContext;
use crate::error::Result; use crate::error::Result;
use crate::models::{ use crate::models::{
Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestIden, Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestHeader,
WebsocketRequest, WebsocketRequestIden, HttpRequestIden, WebsocketRequest, WebsocketRequestIden,
}; };
use crate::util::UpdateSource; use crate::util::UpdateSource;
use serde_json::Value;
use std::collections::BTreeMap;
impl<'a> DbContext<'a> { impl<'a> DbContext<'a> {
pub fn get_folder(&self, id: &str) -> Result<Folder> { pub fn get_folder(&self, id: &str) -> Result<Folder> {
@@ -110,4 +112,40 @@ impl<'a> DbContext<'a> {
Ok(new_folder) Ok(new_folder)
} }
pub fn resolve_auth_for_folder(
&self,
folder: Folder,
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
if let Some(at) = folder.authentication_type {
return Ok((Some(at), folder.authentication));
}
if let Some(folder_id) = folder.folder_id {
let folder = self.get_folder(&folder_id)?;
return self.resolve_auth_for_folder(folder);
}
let workspace = self.get_workspace(&folder.workspace_id)?;
Ok(self.resolve_auth_for_workspace(&workspace))
}
pub fn resolve_headers_for_folder(&self, folder: &Folder) -> Result<Vec<HttpRequestHeader>> {
let mut headers = Vec::new();
if let Some(folder_id) = folder.folder_id.clone() {
let parent_folder = self.get_folder(&folder_id)?;
let mut folder_headers = self.resolve_headers_for_folder(&parent_folder)?;
// NOTE: Add parent headers first, so overrides are logical
headers.append(&mut folder_headers);
} else {
let workspace = self.get_workspace(&folder.workspace_id)?;
let mut workspace_headers = self.resolve_headers_for_workspace(&workspace);
headers.append(&mut workspace_headers);
}
headers.append(&mut folder.headers.clone());
Ok(headers)
}
} }
@@ -1,7 +1,9 @@
use crate::db_context::DbContext; use crate::db_context::DbContext;
use crate::error::Result; use crate::error::Result;
use crate::models::{GrpcRequest, GrpcRequestIden}; use crate::models::{GrpcRequest, GrpcRequestIden, HttpRequestHeader};
use crate::util::UpdateSource; use crate::util::UpdateSource;
use serde_json::Value;
use std::collections::BTreeMap;
impl<'a> DbContext<'a> { impl<'a> DbContext<'a> {
pub fn get_grpc_request(&self, id: &str) -> Result<GrpcRequest> { pub fn get_grpc_request(&self, id: &str) -> Result<GrpcRequest> {
@@ -48,4 +50,43 @@ impl<'a> DbContext<'a> {
) -> Result<GrpcRequest> { ) -> Result<GrpcRequest> {
self.upsert(grpc_request, source) self.upsert(grpc_request, source)
} }
pub fn resolve_auth_for_grpc_request(
&self,
grpc_request: &GrpcRequest,
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
if let Some(at) = grpc_request.authentication_type.clone() {
return Ok((Some(at), grpc_request.authentication.clone()));
}
if let Some(folder_id) = grpc_request.folder_id.clone() {
let folder = self.get_folder(&folder_id)?;
return self.resolve_auth_for_folder(folder);
}
let workspace = self.get_workspace(&grpc_request.workspace_id)?;
Ok(self.resolve_auth_for_workspace(&workspace))
}
pub fn resolve_metadata_for_grpc_request(
&self,
grpc_request: &GrpcRequest,
) -> Result<Vec<HttpRequestHeader>> {
// Resolved headers should be from furthest to closest ancestor, to override logically.
let mut metadata = Vec::new();
if let Some(folder_id) = grpc_request.folder_id.clone() {
let parent_folder = self.get_folder(&folder_id)?;
let mut folder_headers = self.resolve_headers_for_folder(&parent_folder)?;
metadata.append(&mut folder_headers);
} else {
let workspace = self.get_workspace(&grpc_request.workspace_id)?;
let mut workspace_metadata = self.resolve_headers_for_workspace(&workspace);
metadata.append(&mut workspace_metadata);
}
metadata.append(&mut grpc_request.metadata.clone());
Ok(metadata)
}
} }
@@ -1,7 +1,9 @@
use crate::db_context::DbContext; use crate::db_context::DbContext;
use crate::error::Result; use crate::error::Result;
use crate::models::{HttpRequest, HttpRequestIden}; use crate::models::{HttpRequest, HttpRequestHeader, HttpRequestIden};
use crate::util::UpdateSource; use crate::util::UpdateSource;
use serde_json::Value;
use std::collections::BTreeMap;
impl<'a> DbContext<'a> { impl<'a> DbContext<'a> {
pub fn get_http_request(&self, id: &str) -> Result<HttpRequest> { pub fn get_http_request(&self, id: &str) -> Result<HttpRequest> {
@@ -48,4 +50,43 @@ impl<'a> DbContext<'a> {
) -> Result<HttpRequest> { ) -> Result<HttpRequest> {
self.upsert(http_request, source) self.upsert(http_request, source)
} }
pub fn resolve_auth_for_http_request(
&self,
http_request: &HttpRequest,
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
if let Some(at) = http_request.authentication_type.clone() {
return Ok((Some(at), http_request.authentication.clone()));
}
if let Some(folder_id) = http_request.folder_id.clone() {
let folder = self.get_folder(&folder_id)?;
return self.resolve_auth_for_folder(folder);
}
let workspace = self.get_workspace(&http_request.workspace_id)?;
Ok(self.resolve_auth_for_workspace(&workspace))
}
pub fn resolve_headers_for_http_request(
&self,
http_request: &HttpRequest,
) -> Result<Vec<HttpRequestHeader>> {
// Resolved headers should be from furthest to closest ancestor, to override logically.
let mut headers = Vec::new();
if let Some(folder_id) = http_request.folder_id.clone() {
let parent_folder = self.get_folder(&folder_id)?;
let mut folder_headers = self.resolve_headers_for_folder(&parent_folder)?;
headers.append(&mut folder_headers);
} else {
let workspace = self.get_workspace(&http_request.workspace_id)?;
let mut workspace_headers = self.resolve_headers_for_workspace(&workspace);
headers.append(&mut workspace_headers);
}
headers.append(&mut http_request.headers.clone());
Ok(headers)
}
} }
@@ -1,7 +1,9 @@
use crate::db_context::DbContext; use crate::db_context::DbContext;
use crate::error::Result; use crate::error::Result;
use crate::models::{WebsocketRequest, WebsocketRequestIden}; use crate::models::{HttpRequestHeader, WebsocketRequest, WebsocketRequestIden};
use crate::util::UpdateSource; use crate::util::UpdateSource;
use serde_json::Value;
use std::collections::BTreeMap;
impl<'a> DbContext<'a> { impl<'a> DbContext<'a> {
pub fn get_websocket_request(&self, id: &str) -> Result<WebsocketRequest> { pub fn get_websocket_request(&self, id: &str) -> Result<WebsocketRequest> {
@@ -48,4 +50,47 @@ impl<'a> DbContext<'a> {
) -> Result<WebsocketRequest> { ) -> Result<WebsocketRequest> {
self.upsert(websocket_request, source) self.upsert(websocket_request, source)
} }
pub fn resolve_auth_for_websocket_request(
&self,
websocket_request: &WebsocketRequest,
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
if let Some(at) = websocket_request.authentication_type.clone() {
return Ok((Some(at), websocket_request.authentication.clone()));
}
if let Some(folder_id) = websocket_request.folder_id.clone() {
let folder = self.get_folder(&folder_id)?;
return self.resolve_auth_for_folder(folder);
}
let workspace = self.get_workspace(&websocket_request.workspace_id)?;
Ok(self.resolve_auth_for_workspace(&workspace))
}
pub fn resolve_headers_for_websocket_request(
&self,
websocket_request: &WebsocketRequest,
) -> Result<Vec<HttpRequestHeader>> {
let workspace = self.get_workspace(&websocket_request.workspace_id)?;
// Resolved headers should be from furthest to closest ancestor, to override logically.
let mut headers = Vec::new();
headers.append(&mut workspace.headers.clone());
if let Some(folder_id) = websocket_request.folder_id.clone() {
let parent_folder = self.get_folder(&folder_id)?;
let mut folder_headers = self.resolve_headers_for_folder(&parent_folder)?;
headers.append(&mut folder_headers);
} else {
let workspace = self.get_workspace(&websocket_request.workspace_id)?;
let mut workspace_headers = self.resolve_headers_for_workspace(&workspace);
headers.append(&mut workspace_headers);
}
headers.append(&mut websocket_request.headers.clone());
Ok(headers)
}
} }
@@ -1,10 +1,12 @@
use crate::db_context::DbContext; use crate::db_context::DbContext;
use crate::error::Result; use crate::error::Result;
use crate::models::{ use crate::models::{
EnvironmentIden, FolderIden, GrpcRequestIden, HttpRequestIden, WebsocketRequestIden, Workspace, EnvironmentIden, FolderIden, GrpcRequestIden, HttpRequestHeader, HttpRequestIden,
WorkspaceIden, WebsocketRequestIden, Workspace, WorkspaceIden,
}; };
use crate::util::UpdateSource; use crate::util::UpdateSource;
use serde_json::Value;
use std::collections::BTreeMap;
impl<'a> DbContext<'a> { impl<'a> DbContext<'a> {
pub fn get_workspace(&self, id: &str) -> Result<Workspace> { pub fn get_workspace(&self, id: &str) -> Result<Workspace> {
@@ -65,4 +67,15 @@ impl<'a> DbContext<'a> {
pub fn upsert_workspace(&self, w: &Workspace, source: &UpdateSource) -> Result<Workspace> { pub fn upsert_workspace(&self, w: &Workspace, source: &UpdateSource) -> Result<Workspace> {
self.upsert(w, source) self.upsert(w, source)
} }
pub fn resolve_auth_for_workspace(
&self,
workspace: &Workspace,
) -> (Option<String>, BTreeMap<String, Value>) {
(workspace.authentication_type.clone(), workspace.authentication.clone())
}
pub fn resolve_headers_for_workspace(&self, workspace: &Workspace) -> Vec<HttpRequestHeader> {
workspace.headers.clone()
}
} }
@@ -4,11 +4,9 @@ export type Environment = { model: "environment", id: string, workspaceId: strin
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, }; export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, }; export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, }; export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
@@ -24,4 +22,4 @@ export type HttpUrlParameter = { enabled?: boolean, name: string, value: string,
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, }; export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, }; export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
+2 -2
View File
@@ -537,7 +537,7 @@ impl PluginManager {
auth_name: &str, auth_name: &str,
action_index: i32, action_index: i32,
values: HashMap<String, JsonPrimitive>, values: HashMap<String, JsonPrimitive>,
request_id: &str, model_id: &str,
) -> Result<()> { ) -> Result<()> {
let results = self.get_http_authentication_summaries(window).await?; let results = self.get_http_authentication_summaries(window).await?;
let plugin = results let plugin = results
@@ -545,7 +545,7 @@ impl PluginManager {
.find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None }) .find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None })
.ok_or(PluginNotFoundErr(auth_name.into()))?; .ok_or(PluginNotFoundErr(auth_name.into()))?;
let context_id = format!("{:x}", md5::compute(request_id.to_string())); let context_id = format!("{:x}", md5::compute(model_id.to_string()));
self.send_to_plugin_and_wait( self.send_to_plugin_and_wait(
&PluginWindowContext::new(window), &PluginWindowContext::new(window),
&plugin, &plugin,
+3 -5
View File
@@ -4,11 +4,9 @@ export type Environment = { model: "environment", id: string, workspaceId: strin
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, }; export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, }; export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, }; export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
@@ -22,4 +20,4 @@ export type SyncState = { model: "sync_state", id: string, workspaceId: string,
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, }; export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, }; export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
+21 -2
View File
@@ -206,9 +206,28 @@ pub(crate) async fn connect<R: Runtime>(
) )
.await?; .await?;
let (authentication_type, authentication) =
window.db().resolve_auth_for_websocket_request(&request)?;
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
if let Some(auth_name) = request.authentication_type.clone() {
let auth = request.authentication.clone(); let resolved_headers = window.db().resolve_headers_for_websocket_request(&request)?;
for h in resolved_headers {
if h.name.is_empty() && h.value.is_empty() {
continue;
}
if !h.enabled {
continue;
}
headers.insert(
HeaderName::from_str(&h.name).unwrap(),
HeaderValue::from_str(&h.value).unwrap(),
);
}
if let Some(auth_name) = authentication_type.clone() {
let auth = authentication.clone();
let plugin_req = CallHttpAuthenticationRequest { let plugin_req = CallHttpAuthenticationRequest {
context_id: format!("{:x}", md5::compute(request_id.to_string())), context_id: format!("{:x}", md5::compute(request_id.to_string())),
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(), values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
+14
View File
@@ -0,0 +1,14 @@
import type { FolderSettingsTab } from '../components/FolderSettingsDialog';
import { FolderSettingsDialog } from '../components/FolderSettingsDialog';
import { showDialog } from '../lib/dialog';
export function openFolderSettings(folderId: string, tab?: FolderSettingsTab) {
showDialog({
id: 'folder-settings',
title: 'Folder Settings',
size: 'lg',
className: 'h-[50rem]',
noPadding: true,
render: () => <FolderSettingsDialog folderId={folderId} tab={tab} />,
});
}
+18 -16
View File
@@ -1,20 +1,22 @@
import { WorkspaceSettingsDialog } from '../components/WorkspaceSettingsDialog'; import type {
WorkspaceSettingsTab} from '../components/WorkspaceSettingsDialog';
import {
WorkspaceSettingsDialog
} from '../components/WorkspaceSettingsDialog';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { createFastMutation } from '../hooks/useFastMutation';
import { showDialog } from '../lib/dialog'; import { showDialog } from '../lib/dialog';
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from '../lib/jotai';
export const openWorkspaceSettings = createFastMutation<void, string>({ export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) {
mutationKey: ['open_workspace_settings'], const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
async mutationFn() { showDialog({
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom); id: 'workspace-settings',
showDialog({ title: 'Workspace Settings',
id: 'workspace-settings', size: 'lg',
title: 'Workspace Settings', className: 'h-[50rem]',
size: 'md', noPadding: true,
render({ hide }) { render({ hide }) {
return <WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} />; return <WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />;
}, },
}); });
}, }
});
+73 -18
View File
@@ -1,36 +1,91 @@
import { foldersAtom, patchModel } from '@yaakapp-internal/models'; import { foldersAtom, patchModel } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useMemo, useState } from 'react';
import { useAuthTab } from '../hooks/useAuthTab';
import { useHeadersTab } from '../hooks/useHeadersTab';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { Input } from './core/Input'; import { Input } from './core/Input';
import { VStack } from './core/Stacks'; import { VStack } from './core/Stacks';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor'; import { MarkdownEditor } from './MarkdownEditor';
interface Props { interface Props {
folderId: string | null; folderId: string | null;
tab?: FolderSettingsTab;
} }
export function FolderSettingsDialog({ folderId }: Props) { const TAB_AUTH = 'auth';
const TAB_HEADERS = 'headers';
const TAB_GENERAL = 'general';
export type FolderSettingsTab = typeof TAB_AUTH | typeof TAB_HEADERS | typeof TAB_GENERAL;
export function FolderSettingsDialog({ folderId, tab }: Props) {
const folders = useAtomValue(foldersAtom); const folders = useAtomValue(foldersAtom);
const folder = folders.find((f) => f.id === folderId); const folder = folders.find((f) => f.id === folderId) ?? null;
const [activeTab, setActiveTab] = useState<string>(tab ?? TAB_GENERAL);
const authTab = useAuthTab(TAB_AUTH, folder);
const headersTab = useHeadersTab(TAB_HEADERS, folder);
const inheritedHeaders = useInheritedHeaders(folder);
const tabs = useMemo<TabItem[]>(() => {
if (folder == null) return [];
return [
{
value: TAB_GENERAL,
label: 'General',
},
...authTab,
...headersTab,
];
}, [authTab, folder, headersTab]);
if (folder == null) return null; if (folder == null) return null;
return ( return (
<VStack space={3} className="pb-3"> <Tabs
<Input value={activeTab}
label="Folder Name" onChangeValue={setActiveTab}
defaultValue={folder.name} label="Folder Settings"
onChange={(name) => patchModel(folder, { name })} className="px-1.5 pb-2"
stateKey={`name.${folder.id}`} addBorders
/> tabs={tabs}
>
<TabContent value={TAB_AUTH} className="pt-3 overflow-y-auto h-full px-4">
<HttpAuthenticationEditor model={folder} />
</TabContent>
<TabContent value={TAB_GENERAL} className="pt-3 overflow-y-auto h-full px-4">
<VStack space={3} className="pb-3">
<Input
label="Folder Name"
defaultValue={folder.name}
onChange={(name) => patchModel(folder, { name })}
stateKey={`name.${folder.id}`}
/>
<MarkdownEditor <MarkdownEditor
name="folder-description" name="folder-description"
placeholder="Folder description" placeholder="Folder description"
className="min-h-[10rem] border border-border px-2" className="min-h-[10rem] border border-border px-2"
defaultValue={folder.description} defaultValue={folder.description}
stateKey={`description.${folder.id}`} stateKey={`description.${folder.id}`}
onChange={(description) => patchModel(folder, { description })} onChange={(description) => patchModel(folder, { description })}
/> />
</VStack> </VStack>
</TabContent>
<TabContent value={TAB_HEADERS} className="pt-3 overflow-y-auto h-full px-4">
<HeadersEditor
inheritedHeaders={inheritedHeaders}
forceUpdateKey={folder.id}
headers={folder.headers}
onChange={(headers) => patchModel(folder, { headers })}
stateKey={`headers.${folder.id}`}
/>
</TabContent>
</Tabs>
); );
} }
+1 -3
View File
@@ -343,9 +343,7 @@ function SetupSyncDropdown({ workspaceMeta }: { workspaceMeta: WorkspaceMeta })
color: 'success', color: 'success',
label: 'Open Workspace Settings', label: 'Open Workspace Settings',
leftSlot: <Icon icon="settings" />, leftSlot: <Icon icon="settings" />,
onSelect() { onSelect: openWorkspaceSettings,
openWorkspaceSettings.mutate();
},
}, },
{ type: 'separator' }, { type: 'separator' },
{ {
+18 -43
View File
@@ -1,10 +1,12 @@
import { type GrpcMetadataEntry, type GrpcRequest, patchModel } from '@yaakapp-internal/models'; import { type GrpcRequest, type HttpRequestHeader, patchModel } from '@yaakapp-internal/models';
import classNames from 'classnames'; import classNames from 'classnames';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import React, { useCallback, useMemo, useRef } from 'react'; import React, { useCallback, useMemo, useRef } from 'react';
import { useAuthTab } from '../hooks/useAuthTab';
import { useContainerSize } from '../hooks/useContainerQuery'; import { useContainerSize } from '../hooks/useContainerQuery';
import type { ReflectResponseService } from '../hooks/useGrpc'; import type { ReflectResponseService } from '../hooks/useGrpc';
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication'; import { useHeadersTab } from '../hooks/useHeadersTab';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { useKeyValue } from '../hooks/useKeyValue'; import { useKeyValue } from '../hooks/useKeyValue';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { resolvedModelName } from '../lib/resolvedModelName'; import { resolvedModelName } from '../lib/resolvedModelName';
@@ -12,13 +14,13 @@ import { Button } from './core/Button';
import { CountBadge } from './core/CountBadge'; import { CountBadge } from './core/CountBadge';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { PlainInput } from './core/PlainInput'; import { PlainInput } from './core/PlainInput';
import { RadioDropdown } from './core/RadioDropdown'; import { RadioDropdown } from './core/RadioDropdown';
import { HStack, VStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
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';
import { GrpcEditor } from './GrpcEditor'; import { GrpcEditor } from './GrpcEditor';
import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor'; import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor'; import { MarkdownEditor } from './MarkdownEditor';
import { UrlBar } from './UrlBar'; import { UrlBar } from './UrlBar';
@@ -64,7 +66,9 @@ export function GrpcRequestPane({
onCancel, onCancel,
onSend, onSend,
}: Props) { }: Props) {
const authentication = useHttpAuthenticationSummaries(); const authTab = useAuthTab(TAB_AUTH, activeRequest);
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, 'Metadata');
const inheritedHeaders = useInheritedHeaders(activeRequest);
const { value: activeTabs, set: setActiveTabs } = useKeyValue<Record<string, string>>({ const { value: activeTabs, set: setActiveTabs } = useKeyValue<Record<string, string>>({
namespace: 'no_sync', namespace: 'no_sync',
key: 'grpcRequestActiveTabs', key: 'grpcRequestActiveTabs',
@@ -130,42 +134,15 @@ export function GrpcRequestPane({
const tabs: TabItem[] = useMemo( const tabs: TabItem[] = useMemo(
() => [ () => [
{ value: TAB_MESSAGE, label: 'Message' }, { value: TAB_MESSAGE, label: 'Message' },
{ ...metadataTab,
value: TAB_AUTH, ...authTab,
label: 'Auth',
options: {
value: activeRequest.authenticationType,
items: [
...authentication.map((a) => ({
label: a.label || 'UNKNOWN',
shortLabel: a.shortLabel,
value: a.name,
})),
{ type: 'separator' },
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
],
onChange: async (authenticationType) => {
let authentication: GrpcRequest['authentication'] = activeRequest.authentication;
if (activeRequest.authenticationType !== authenticationType) {
authentication = {
// Reset auth if changing types
};
}
await patchModel(activeRequest, {
authenticationType,
authentication,
});
},
},
},
{ value: TAB_METADATA, label: 'Metadata' },
{ {
value: TAB_DESCRIPTION, value: TAB_DESCRIPTION,
label: 'Info', label: 'Info',
rightSlot: activeRequest.description && <CountBadge count={true} />, rightSlot: activeRequest.description && <CountBadge count={true} />,
}, },
], ],
[activeRequest, authentication], [activeRequest.description, authTab, metadataTab],
); );
const activeTab = activeTabs?.[activeRequest.id]; const activeTab = activeTabs?.[activeRequest.id];
@@ -177,7 +154,7 @@ export function GrpcRequestPane({
); );
const handleMetadataChange = useCallback( const handleMetadataChange = useCallback(
(metadata: GrpcMetadataEntry[]) => patchModel(activeRequest, { metadata }), (metadata: HttpRequestHeader[]) => patchModel(activeRequest, { metadata }),
[activeRequest], [activeRequest],
); );
@@ -307,17 +284,15 @@ export function GrpcRequestPane({
/> />
</TabContent> </TabContent>
<TabContent value={TAB_AUTH}> <TabContent value={TAB_AUTH}>
<HttpAuthenticationEditor request={activeRequest} /> <HttpAuthenticationEditor model={activeRequest} />
</TabContent> </TabContent>
<TabContent value={TAB_METADATA}> <TabContent value={TAB_METADATA}>
<PairOrBulkEditor <HeadersEditor
preferenceName="grpc_metadata" inheritedHeaders={inheritedHeaders}
valueAutocompleteVariables
nameAutocompleteVariables
pairs={activeRequest.metadata}
onChange={handleMetadataChange}
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
stateKey={`grpc_metadata.${activeRequest.id}`} headers={activeRequest.metadata}
stateKey={`headers.${activeRequest.id}`}
onChange={handleMetadataChange}
/> />
</TabContent> </TabContent>
<TabContent value={TAB_DESCRIPTION}> <TabContent value={TAB_DESCRIPTION}>
+70 -25
View File
@@ -5,36 +5,81 @@ import { connections } from '../lib/data/connections';
import { encodings } from '../lib/data/encodings'; import { encodings } from '../lib/data/encodings';
import { headerNames } from '../lib/data/headerNames'; import { headerNames } from '../lib/data/headerNames';
import { mimeTypes } from '../lib/data/mimetypes'; import { mimeTypes } from '../lib/data/mimetypes';
import { Banner } from './core/Banner';
import { CountBadge } from './core/CountBadge';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion'; import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import type { InputProps } from './core/Input'; import type { InputProps } from './core/Input';
import type { Pair, PairEditorProps } from './core/PairEditor'; import type { Pair, PairEditorProps } from './core/PairEditor';
import { ensurePairId, PairEditorRow } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor'; import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { HStack } from './core/Stacks';
type Props = { type Props = {
forceUpdateKey: string; forceUpdateKey: string;
headers: HttpRequestHeader[]; headers: HttpRequestHeader[];
inheritedHeaders?: HttpRequestHeader[];
stateKey: string; stateKey: string;
onChange: (headers: HttpRequestHeader[]) => void; onChange: (headers: HttpRequestHeader[]) => void;
label?: string;
}; };
export function HeadersEditor({ stateKey, headers, onChange, forceUpdateKey }: Props) { export function HeadersEditor({
stateKey,
headers,
inheritedHeaders,
onChange,
forceUpdateKey,
}: Props) {
const validInheritedHeaders =
inheritedHeaders?.filter((pair) => pair.enabled && (pair.name || pair.value)) ?? [];
return ( return (
<PairOrBulkEditor <div className="@container w-full h-full grid grid-rows-[auto_minmax(0,1fr)]">
forceUpdateKey={forceUpdateKey} {validInheritedHeaders.length > 0 ? (
nameAutocomplete={nameAutocomplete} <Banner className="!py-0 mb-1.5 border-dashed" color="secondary">
nameAutocompleteFunctions <details>
nameAutocompleteVariables <summary className="py-1.5 text-sm !cursor-default !select-none opacity-70 hover:opacity-100">
namePlaceholder="Header-Name" <HStack>
nameValidate={validateHttpHeader} Inherited <CountBadge count={validInheritedHeaders.length} />
onChange={onChange} </HStack>
pairs={headers} </summary>
preferenceName="headers" <div className="pb-2">
stateKey={stateKey} {validInheritedHeaders?.map((pair, i) => (
valueType={valueType} <PairEditorRow
valueAutocomplete={valueAutocomplete} key={pair.id + '.' + i}
valueAutocompleteFunctions index={i}
valueAutocompleteVariables disabled
/> disableDrag
className="py-1"
onChange={() => {}}
onEnd={() => {}}
onMove={() => {}}
pair={ensurePairId(pair)}
stateKey={null}
/>
))}
</div>
</details>
</Banner>
) : (
<span />
)}
<PairOrBulkEditor
forceUpdateKey={forceUpdateKey}
nameAutocomplete={nameAutocomplete}
nameAutocompleteFunctions
nameAutocompleteVariables
namePlaceholder="Header-Name"
nameValidate={validateHttpHeader}
onChange={onChange}
pairs={headers}
preferenceName="headers"
stateKey={stateKey}
valueType={valueType}
valueAutocomplete={valueAutocomplete}
valueAutocompleteFunctions
valueAutocompleteVariables
/>
</div>
); );
} }
@@ -51,14 +96,14 @@ const headerOptionsMap: Record<string, string[]> = {
const valueType = (pair: Pair): InputProps['type'] => { const valueType = (pair: Pair): InputProps['type'] => {
const name = pair.name.toLowerCase().trim(); const name = pair.name.toLowerCase().trim();
if ( if (
name.includes('authorization') || name.includes('authorization') ||
name.includes('api-key') || name.includes('api-key') ||
name.includes('access-token') || name.includes('access-token') ||
name.includes('auth') || name.includes('auth') ||
name.includes('secret') || name.includes('secret') ||
name.includes('token') || name.includes('token') ||
name === 'cookie' || name === 'cookie' ||
name === 'set-cookie' name === 'set-cookie'
) { ) {
return 'password'; return 'password';
} else { } else {
+68 -18
View File
@@ -1,34 +1,84 @@
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models'; import type {
Folder,
GrpcRequest,
HttpRequest,
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models'; import { patchModel } from '@yaakapp-internal/models';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { openFolderSettings } from '../commands/openFolderSettings';
import { openWorkspaceSettings } from '../commands/openWorkspaceSettings';
import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig'; import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig';
import { useInheritedAuthentication } from '../hooks/useInheritedAuthentication';
import { resolvedModelName } from '../lib/resolvedModelName';
import { Checkbox } from './core/Checkbox'; import { Checkbox } from './core/Checkbox';
import type { DropdownItem } from './core/Dropdown'; import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown'; import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import { HStack } from './core/Stacks'; import { HStack } from './core/Stacks';
import { DynamicForm } from './DynamicForm'; import { DynamicForm } from './DynamicForm';
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from './EmptyStateText';
interface Props { interface Props {
request: HttpRequest | GrpcRequest | WebsocketRequest; model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace;
} }
export function HttpAuthenticationEditor({ request }: Props) { export function HttpAuthenticationEditor({ model }: Props) {
const inheritedAuth = useInheritedAuthentication(model);
const authConfig = useHttpAuthenticationConfig( const authConfig = useHttpAuthenticationConfig(
request.authenticationType, model.authenticationType,
request.authentication, model.authentication,
request.id, model.id,
); );
const handleChange = useCallback( const handleChange = useCallback(
(authentication: Record<string, boolean>) => patchModel(request, { authentication }), async (authentication: Record<string, boolean>) => await patchModel(model, { authentication }),
[request], [model],
); );
if (authConfig.data == null) { if (model.authenticationType === 'none') {
return <EmptyStateText>No Authentication {request.authenticationType}</EmptyStateText>; return <EmptyStateText>No authentication</EmptyStateText>;
}
if (model.authenticationType != null && authConfig.data == null) {
return (
<EmptyStateText>
Unknown authentication <InlineCode>{authConfig.data}</InlineCode>
</EmptyStateText>
);
}
if (inheritedAuth == null) {
return <EmptyStateText>Authentication not configured</EmptyStateText>;
}
if (inheritedAuth.authenticationType === 'none') {
return <EmptyStateText>No authentication</EmptyStateText>;
}
const wasAuthInherited = inheritedAuth?.id !== model.id;
if (wasAuthInherited) {
const name = resolvedModelName(inheritedAuth);
const cta = inheritedAuth.model === 'workspace' ? 'Workspace' : name;
return (
<EmptyStateText>
<p>
Inherited from{' '}
<button
className="underline hover:text-text"
onClick={() => {
if (inheritedAuth.model === 'folder') openFolderSettings(inheritedAuth.id, 'auth');
else openWorkspaceSettings('auth');
}}
>
{cta}
</button>
</p>
</EmptyStateText>
);
} }
return ( return (
@@ -36,17 +86,17 @@ export function HttpAuthenticationEditor({ request }: Props) {
<HStack space={2} className="mb-2" alignItems="center"> <HStack space={2} className="mb-2" alignItems="center">
<Checkbox <Checkbox
className="w-full" className="w-full"
checked={!request.authentication.disabled} checked={!model.authentication.disabled}
onChange={(disabled) => handleChange({ ...request.authentication, disabled: !disabled })} onChange={(disabled) => handleChange({ ...model.authentication, disabled: !disabled })}
title="Enabled" title="Enabled"
/> />
{authConfig.data.actions && authConfig.data.actions.length > 0 && ( {authConfig.data?.actions && authConfig.data.actions.length > 0 && (
<Dropdown <Dropdown
items={authConfig.data.actions.map( items={authConfig.data.actions.map(
(a): DropdownItem => ({ (a): DropdownItem => ({
label: a.label, label: a.label,
leftSlot: a.icon ? <Icon icon={a.icon} /> : null, leftSlot: a.icon ? <Icon icon={a.icon} /> : null,
onSelect: () => a.call(request), onSelect: () => a.call(model),
}), }),
)} )}
> >
@@ -55,12 +105,12 @@ export function HttpAuthenticationEditor({ request }: Props) {
)} )}
</HStack> </HStack>
<DynamicForm <DynamicForm
disabled={request.authentication.disabled} disabled={model.authentication.disabled}
autocompleteVariables autocompleteVariables
autocompleteFunctions autocompleteFunctions
stateKey={`auth.${request.id}.${request.authenticationType}`} stateKey={`auth.${model.id}.${model.authenticationType}`}
inputs={authConfig.data.args} inputs={authConfig.data?.args ?? []}
data={request.authentication} data={model.authentication}
onChange={handleChange} onChange={handleChange}
/> />
</div> </div>
+20 -36
View File
@@ -6,13 +6,15 @@ import { atom, useAtomValue } from 'jotai';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId'; import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useAuthTab } from '../hooks/useAuthTab';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse'; import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication'; import { useHeadersTab } from '../hooks/useHeadersTab';
import { useImportCurl } from '../hooks/useImportCurl'; import { useImportCurl } from '../hooks/useImportCurl';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { useKeyValue } from '../hooks/useKeyValue'; import { useKeyValue } from '../hooks/useKeyValue';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse'; import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor'; import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest'; import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { deepEqualAtom } from '../lib/atoms'; import { deepEqualAtom } from '../lib/atoms';
@@ -44,12 +46,12 @@ import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from './EmptyStateText';
import { FormMultipartEditor } from './FormMultipartEditor'; import { FormMultipartEditor } from './FormMultipartEditor';
import { FormUrlencodedEditor } from './FormUrlencodedEditor'; import { FormUrlencodedEditor } from './FormUrlencodedEditor';
import { GraphQLEditor } from './GraphQLEditor';
import { HeadersEditor } from './HeadersEditor'; import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor'; import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor'; import { MarkdownEditor } from './MarkdownEditor';
import { UrlBar } from './UrlBar'; import { UrlBar } from './UrlBar';
import { UrlParametersEditor } from './UrlParameterEditor'; import { UrlParametersEditor } from './UrlParameterEditor';
import { GraphQLEditor } from './GraphQLEditor';
interface Props { interface Props {
style: CSSProperties; style: CSSProperties;
@@ -85,7 +87,9 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null); const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor(); const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
const contentType = getContentTypeFromHeaders(activeRequest.headers); const contentType = getContentTypeFromHeaders(activeRequest.headers);
const authentication = useHttpAuthenticationSummaries(); const authTab = useAuthTab(TAB_AUTH, activeRequest);
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
const inheritedHeaders = useInheritedHeaders(activeRequest);
const handleContentTypeChange = useCallback( const handleContentTypeChange = useCallback(
async (contentType: string | null, patch: Partial<Omit<HttpRequest, 'headers'>> = {}) => { async (contentType: string | null, patch: Partial<Omit<HttpRequest, 'headers'>> = {}) => {
@@ -214,42 +218,21 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
rightSlot: <CountBadge count={urlParameterPairs.length} />, rightSlot: <CountBadge count={urlParameterPairs.length} />,
label: 'Params', label: 'Params',
}, },
{ ...headersTab,
value: TAB_HEADERS, ...authTab,
label: 'Headers',
rightSlot: <CountBadge count={activeRequest.headers.filter((h) => h.name).length} />,
},
{
value: TAB_AUTH,
label: 'Auth',
options: {
value: activeRequest.authenticationType,
items: [
...authentication.map((a) => ({
label: a.label || 'UNKNOWN',
shortLabel: a.shortLabel,
value: a.name,
})),
{ type: 'separator' },
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
],
onChange: async (authenticationType) => {
let authentication: HttpRequest['authentication'] = activeRequest.authentication;
if (activeRequest.authenticationType !== authenticationType) {
authentication = {
// Reset auth if changing types
};
}
await patchModel(activeRequest, { authenticationType, authentication });
},
},
},
{ {
value: TAB_DESCRIPTION, value: TAB_DESCRIPTION,
label: 'Info', label: 'Info',
}, },
], ],
[activeRequest, authentication, handleContentTypeChange, numParams, urlParameterPairs.length], [
activeRequest,
authTab,
handleContentTypeChange,
headersTab,
numParams,
urlParameterPairs.length,
],
); );
const { mutate: sendRequest } = useSendAnyHttpRequest(); const { mutate: sendRequest } = useSendAnyHttpRequest();
@@ -372,10 +355,11 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
tabListClassName="mt-2 !mb-1.5" tabListClassName="mt-2 !mb-1.5"
> >
<TabContent value={TAB_AUTH}> <TabContent value={TAB_AUTH}>
<HttpAuthenticationEditor request={activeRequest} /> <HttpAuthenticationEditor model={activeRequest} />
</TabContent> </TabContent>
<TabContent value={TAB_HEADERS}> <TabContent value={TAB_HEADERS}>
<HeadersEditor <HeadersEditor
inheritedHeaders={inheritedHeaders}
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`} forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
headers={activeRequest.headers} headers={activeRequest.headers}
stateKey={`headers.${activeRequest.id}`} stateKey={`headers.${activeRequest.id}`}
+1 -1
View File
@@ -17,7 +17,7 @@ export default function RouteError({ error }: { error: unknown }) {
<FormattedError> <FormattedError>
{message} {message}
{stack && ( {stack && (
<details className="mt-3 select-autotext-xs"> <details className="mt-3 select-auto text-xs">
<summary className="!cursor-default !select-none">Stack Trace</summary> <summary className="!cursor-default !select-none">Stack Trace</summary>
<div className="mt-2 text-xs">{stack}</div> <div className="mt-2 text-xs">{stack}</div>
</details> </details>
+13 -39
View File
@@ -1,4 +1,4 @@
import type { HttpRequest, WebsocketRequest } from '@yaakapp-internal/models'; import type { WebsocketRequest } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models'; import { patchModel } from '@yaakapp-internal/models';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins'; import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import { closeWebsocket, connectWebsocket, sendWebsocket } from '@yaakapp-internal/ws'; import { closeWebsocket, connectWebsocket, sendWebsocket } from '@yaakapp-internal/ws';
@@ -9,13 +9,15 @@ import React, { useCallback, useMemo } from 'react';
import { getActiveCookieJar } from '../hooks/useActiveCookieJar'; import { getActiveCookieJar } from '../hooks/useActiveCookieJar';
import { getActiveEnvironment } from '../hooks/useActiveEnvironment'; import { getActiveEnvironment } from '../hooks/useActiveEnvironment';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId'; import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useAuthTab } from '../hooks/useAuthTab';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse'; import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication'; import { useHeadersTab } from '../hooks/useHeadersTab';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { useKeyValue } from '../hooks/useKeyValue'; import { useKeyValue } from '../hooks/useKeyValue';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse'; import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { activeWebsocketConnectionAtom } from '../hooks/usePinnedWebsocketConnection'; import { activeWebsocketConnectionAtom } from '../hooks/usePinnedWebsocketConnection';
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor'; import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
import {allRequestsAtom} from "../hooks/useAllRequests";
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { deepEqualAtom } from '../lib/atoms'; import { deepEqualAtom } from '../lib/atoms';
import { languageFromContentType } from '../lib/contentType'; import { languageFromContentType } from '../lib/contentType';
@@ -69,7 +71,9 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
}); });
const forceUpdateKey = useRequestUpdateKey(activeRequest.id); const forceUpdateKey = useRequestUpdateKey(activeRequest.id);
const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor(); const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
const authentication = useHttpAuthenticationSummaries(); const authTab = useAuthTab(TAB_AUTH, activeRequest);
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
const inheritedHeaders = useInheritedHeaders(activeRequest);
const { urlParameterPairs, urlParametersKey } = useMemo(() => { const { urlParameterPairs, urlParametersKey } = useMemo(() => {
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map( const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
@@ -99,45 +103,14 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
rightSlot: <CountBadge count={urlParameterPairs.length} />, rightSlot: <CountBadge count={urlParameterPairs.length} />,
label: 'Params', label: 'Params',
}, },
{ ...headersTab,
value: TAB_HEADERS, ...authTab,
label: 'Headers',
rightSlot: <CountBadge count={activeRequest.headers.filter((h) => h.name).length} />,
},
{
value: TAB_AUTH,
label: 'Auth',
options: {
value: activeRequest.authenticationType,
items: [
...authentication.map((a) => ({
label: a.label || 'UNKNOWN',
shortLabel: a.shortLabel,
value: a.name,
})),
{ type: 'separator' },
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
],
onChange: async (authenticationType) => {
let authentication: HttpRequest['authentication'] = activeRequest.authentication;
if (activeRequest.authenticationType !== authenticationType) {
authentication = {
// Reset auth if changing types
};
}
await patchModel(activeRequest, {
authenticationType,
authentication,
});
},
},
},
{ {
value: TAB_DESCRIPTION, value: TAB_DESCRIPTION,
label: 'Info', label: 'Info',
}, },
]; ];
}, [activeRequest, authentication, urlParameterPairs.length]); }, [authTab, headersTab, urlParameterPairs.length]);
const { activeResponse } = usePinnedHttpResponse(activeRequestId); const { activeResponse } = usePinnedHttpResponse(activeRequestId);
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null); const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
@@ -266,10 +239,11 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
tabListClassName="mt-2 !mb-1.5" tabListClassName="mt-2 !mb-1.5"
> >
<TabContent value={TAB_AUTH}> <TabContent value={TAB_AUTH}>
<HttpAuthenticationEditor request={activeRequest} /> <HttpAuthenticationEditor model={activeRequest} />
</TabContent> </TabContent>
<TabContent value={TAB_HEADERS}> <TabContent value={TAB_HEADERS}>
<HeadersEditor <HeadersEditor
inheritedHeaders={inheritedHeaders}
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
headers={activeRequest.headers} headers={activeRequest.headers}
stateKey={`headers.${activeRequest.id}`} stateKey={`headers.${activeRequest.id}`}
@@ -49,7 +49,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
label: 'Workspace Settings', label: 'Workspace Settings',
leftSlot: <Icon icon="settings" />, leftSlot: <Icon icon="settings" />,
hotKeyAction: 'workspace_settings.show', hotKeyAction: 'workspace_settings.show',
onSelect: () => openWorkspaceSettings.mutate(), onSelect: openWorkspaceSettings,
}, },
{ {
label: revealInFinderText, label: revealInFinderText,
+86 -45
View File
@@ -1,5 +1,9 @@
import { patchModel, workspaceMetasAtom, workspacesAtom } from '@yaakapp-internal/models'; import { patchModel, workspaceMetasAtom, workspacesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useState } from 'react';
import { useAuthTab } from '../hooks/useAuthTab';
import { useHeadersTab } from '../hooks/useHeadersTab';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
import { router } from '../lib/router'; import { router } from '../lib/router';
import { Banner } from './core/Banner'; import { Banner } from './core/Banner';
@@ -8,6 +12,9 @@ import { InlineCode } from './core/InlineCode';
import { PlainInput } from './core/PlainInput'; import { PlainInput } from './core/PlainInput';
import { Separator } from './core/Separator'; import { Separator } from './core/Separator';
import { HStack, VStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor'; import { MarkdownEditor } from './MarkdownEditor';
import { SyncToFilesystemSetting } from './SyncToFilesystemSetting'; import { SyncToFilesystemSetting } from './SyncToFilesystemSetting';
import { WorkspaceEncryptionSetting } from './WorkspaceEncryptionSetting'; import { WorkspaceEncryptionSetting } from './WorkspaceEncryptionSetting';
@@ -15,11 +22,22 @@ import { WorkspaceEncryptionSetting } from './WorkspaceEncryptionSetting';
interface Props { interface Props {
workspaceId: string | null; workspaceId: string | null;
hide: () => void; hide: () => void;
tab?: WorkspaceSettingsTab;
} }
export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) { const TAB_AUTH = 'auth';
const TAB_HEADERS = 'headers';
const TAB_GENERAL = 'general';
export type WorkspaceSettingsTab = typeof TAB_AUTH | typeof TAB_HEADERS | typeof TAB_GENERAL;
export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
const workspace = useAtomValue(workspacesAtom).find((w) => w.id === workspaceId); const workspace = useAtomValue(workspacesAtom).find((w) => w.id === workspaceId);
const workspaceMeta = useAtomValue(workspaceMetasAtom).find((m) => m.workspaceId === workspaceId); const workspaceMeta = useAtomValue(workspaceMetasAtom).find((m) => m.workspaceId === workspaceId);
const [activeTab, setActiveTab] = useState<string>(tab ?? TAB_GENERAL);
const authTab = useAuthTab(TAB_AUTH, workspace ?? null);
const headersTab = useHeadersTab(TAB_HEADERS, workspace ?? null);
const inheritedHeaders = useInheritedHeaders(workspace ?? null);
if (workspace == null) { if (workspace == null) {
return ( return (
@@ -37,53 +55,76 @@ export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) {
); );
return ( return (
<VStack space={4} alignItems="start" className="pb-3 h-full"> <Tabs
<PlainInput value={activeTab}
required onChangeValue={setActiveTab}
hideLabel label="Folder Settings"
placeholder="Workspace Name" className="px-1.5 pb-2"
label="Name" addBorders
defaultValue={workspace.name} tabs={[{ value: TAB_GENERAL, label: 'General' }, ...authTab, ...headersTab]}
className="!text-base font-sans" >
onChange={(name) => patchModel(workspace, { name })} <TabContent value={TAB_AUTH} className="pt-3 overflow-y-auto h-full px-4">
/> <HttpAuthenticationEditor model={workspace} />
</TabContent>
<TabContent value={TAB_HEADERS} className="pt-3 overflow-y-auto h-full px-4">
<HeadersEditor
inheritedHeaders={inheritedHeaders}
forceUpdateKey={workspace.id}
headers={workspace.headers}
onChange={(headers) => patchModel(workspace, { headers })}
stateKey={`headers.${workspace.id}`}
/>
</TabContent>
<TabContent value={TAB_GENERAL} className="pt-3 overflow-y-auto h-full px-4">
<VStack space={4} alignItems="start" className="pb-3 h-full">
<PlainInput
required
hideLabel
placeholder="Workspace Name"
label="Name"
defaultValue={workspace.name}
className="!text-base font-sans"
onChange={(name) => patchModel(workspace, { name })}
/>
<MarkdownEditor <MarkdownEditor
name="workspace-description" name="workspace-description"
placeholder="Workspace description" placeholder="Workspace description"
className="min-h-[3rem] max-h-[25rem] border border-border px-2" className="min-h-[3rem] max-h-[25rem] border border-border px-2"
defaultValue={workspace.description} defaultValue={workspace.description}
stateKey={`description.${workspace.id}`} stateKey={`description.${workspace.id}`}
onChange={(description) => patchModel(workspace, { description })} onChange={(description) => patchModel(workspace, { description })}
heightMode="auto" heightMode="auto"
/> />
<SyncToFilesystemSetting <SyncToFilesystemSetting
value={{ filePath: workspaceMeta.settingSyncDir }} value={{ filePath: workspaceMeta.settingSyncDir }}
onCreateNewWorkspace={hide} onCreateNewWorkspace={hide}
onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })} onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })}
/> />
<WorkspaceEncryptionSetting size="xs" /> <WorkspaceEncryptionSetting size="xs" />
<Separator className="my-4" /> <Separator className="my-4" />
<HStack alignItems="center" justifyContent="between" className="w-full"> <HStack alignItems="center" justifyContent="between" className="w-full">
<Button <Button
onClick={async () => { onClick={async () => {
const didDelete = await deleteModelWithConfirm(workspace); const didDelete = await deleteModelWithConfirm(workspace);
if (didDelete) { if (didDelete) {
hide(); // Only hide if actually deleted workspace hide(); // Only hide if actually deleted workspace
await router.navigate({ to: '/' }); await router.navigate({ to: '/' });
} }
}} }}
color="danger" color="danger"
variant="border" variant="border"
size="xs" size="xs"
> >
Delete Workspace Delete Workspace
</Button> </Button>
<InlineCode className="select-text cursor-text">{workspaceId}</InlineCode> <InlineCode className="select-text cursor-text">{workspaceId}</InlineCode>
</HStack> </HStack>
</VStack> </VStack>
</TabContent>
</Tabs>
); );
} }
+1
View File
@@ -52,6 +52,7 @@ export function Checkbox({
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<Icon <Icon
size="sm" size="sm"
className={classNames(disabled && 'opacity-disabled')}
icon={checked === 'indeterminate' ? 'minus' : checked ? 'check' : 'empty'} icon={checked === 'indeterminate' ? 'minus' : checked ? 'check' : 'empty'}
/> />
</div> </div>
+22 -8
View File
@@ -290,6 +290,8 @@ type PairEditorRowProps = {
onFocus?: (pair: PairWithId) => void; onFocus?: (pair: PairWithId) => void;
onSubmit?: (pair: PairWithId) => void; onSubmit?: (pair: PairWithId) => void;
isLast?: boolean; isLast?: boolean;
disabled?: boolean;
disableDrag?: boolean;
index: number; index: number;
} & Pick< } & Pick<
PairEditorProps, PairEditorProps,
@@ -311,21 +313,23 @@ type PairEditorRowProps = {
| 'valueValidate' | 'valueValidate'
>; >;
function PairEditorRow({ export function PairEditorRow({
allowFileValues, allowFileValues,
allowMultilineValues, allowMultilineValues,
className, className,
forcedEnvironmentId, disableDrag,
disabled,
forceFocusNamePairId, forceFocusNamePairId,
forceFocusValuePairId, forceFocusValuePairId,
forceUpdateKey, forceUpdateKey,
forcedEnvironmentId,
index, index,
isLast, isLast,
nameAutocomplete, nameAutocomplete,
namePlaceholder,
nameValidate,
nameAutocompleteFunctions, nameAutocompleteFunctions,
nameAutocompleteVariables, nameAutocompleteVariables,
namePlaceholder,
nameValidate,
onChange, onChange,
onDelete, onDelete,
onEnd, onEnd,
@@ -461,12 +465,12 @@ function PairEditorRow({
<Checkbox <Checkbox
hideLabel hideLabel
title={pair.enabled ? 'Disable item' : 'Enable item'} title={pair.enabled ? 'Disable item' : 'Enable item'}
disabled={isLast} disabled={isLast || disabled}
checked={isLast ? false : !!pair.enabled} checked={isLast ? false : !!pair.enabled}
className={classNames(isLast && '!opacity-disabled')} className={classNames(isLast && '!opacity-disabled')}
onChange={handleChangeEnabled} onChange={handleChangeEnabled}
/> />
{!isLast ? ( {!isLast && !disableDrag ? (
<div <div
className={classNames( className={classNames(
'py-2 h-7 w-4 flex items-center', 'py-2 h-7 w-4 flex items-center',
@@ -502,6 +506,7 @@ function PairEditorRow({
ref={nameInputRef} ref={nameInputRef}
hideLabel hideLabel
stateKey={`name.${pair.id}.${stateKey}`} stateKey={`name.${pair.id}.${stateKey}`}
disabled={disabled}
wrapLines={false} wrapLines={false}
readOnly={pair.readOnlyName} readOnly={pair.readOnlyName}
size="sm" size="sm"
@@ -523,12 +528,19 @@ function PairEditorRow({
)} )}
<div className="w-full grid grid-cols-[minmax(0,1fr)_auto] gap-1 items-center"> <div className="w-full grid grid-cols-[minmax(0,1fr)_auto] gap-1 items-center">
{pair.isFile ? ( {pair.isFile ? (
<SelectFile inline size="xs" filePath={pair.value} onChange={handleChangeValueFile} /> <SelectFile
disabled={disabled}
inline
size="xs"
filePath={pair.value}
onChange={handleChangeValueFile}
/>
) : isLast ? ( ) : isLast ? (
// Use PlainInput for last ones because there's a unique bug where clicking below // Use PlainInput for last ones because there's a unique bug where clicking below
// the Codemirror input focuses it. // the Codemirror input focuses it.
<PlainInput <PlainInput
hideLabel hideLabel
disabled={disabled}
size="sm" size="sm"
containerClassName={classNames(isLast && 'border-dashed')} containerClassName={classNames(isLast && 'border-dashed')}
label="Value" label="Value"
@@ -553,6 +565,7 @@ function PairEditorRow({
stateKey={`value.${pair.id}.${stateKey}`} stateKey={`value.${pair.id}.${stateKey}`}
wrapLines={false} wrapLines={false}
size="sm" size="sm"
disabled={disabled}
containerClassName={classNames(isLast && 'border-dashed')} containerClassName={classNames(isLast && 'border-dashed')}
validate={valueValidate} validate={valueValidate}
forcedEnvironmentId={forcedEnvironmentId} forcedEnvironmentId={forcedEnvironmentId}
@@ -585,8 +598,9 @@ function PairEditorRow({
<IconButton <IconButton
iconSize="sm" iconSize="sm"
size="xs" size="xs"
icon={isLast ? 'empty' : 'chevron_down'} icon={(isLast || disabled) ? 'empty' : 'chevron_down'}
title="Select form data type" title="Select form data type"
className="text-text-subtle"
/> />
</Dropdown> </Dropdown>
)} )}
+1 -1
View File
@@ -8,7 +8,7 @@ export type RadioDropdownItem<T = string | null> =
| { | {
type?: 'default'; type?: 'default';
label: ReactNode; label: ReactNode;
shortLabel?: string; shortLabel?: ReactNode;
value: T; value: T;
rightSlot?: ReactNode; rightSlot?: ReactNode;
} }
@@ -1,6 +1,7 @@
import { duplicateModelById, getModel, workspacesAtom } from '@yaakapp-internal/models'; import { duplicateModelById, getModel, workspacesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { openFolderSettings } from '../../commands/openFolderSettings';
import { useCreateDropdownItems } from '../../hooks/useCreateDropdownItems'; import { useCreateDropdownItems } from '../../hooks/useCreateDropdownItems';
import { useHttpRequestActions } from '../../hooks/useHttpRequestActions'; import { useHttpRequestActions } from '../../hooks/useHttpRequestActions';
import { useMoveToWorkspace } from '../../hooks/useMoveToWorkspace'; import { useMoveToWorkspace } from '../../hooks/useMoveToWorkspace';
@@ -8,13 +9,11 @@ import { useSendAnyHttpRequest } from '../../hooks/useSendAnyHttpRequest';
import { useSendManyRequests } from '../../hooks/useSendManyRequests'; import { useSendManyRequests } from '../../hooks/useSendManyRequests';
import { deleteModelWithConfirm } from '../../lib/deleteModelWithConfirm'; import { deleteModelWithConfirm } from '../../lib/deleteModelWithConfirm';
import { showDialog } from '../../lib/dialog';
import { duplicateRequestAndNavigate } from '../../lib/duplicateRequestAndNavigate'; import { duplicateRequestAndNavigate } from '../../lib/duplicateRequestAndNavigate';
import { renameModelWithPrompt } from '../../lib/renameModelWithPrompt'; import { renameModelWithPrompt } from '../../lib/renameModelWithPrompt';
import type { DropdownItem } from '../core/Dropdown'; import type { DropdownItem } from '../core/Dropdown';
import { ContextMenu } from '../core/Dropdown'; import { ContextMenu } from '../core/Dropdown';
import { Icon } from '../core/Icon'; import { Icon } from '../core/Icon';
import { FolderSettingsDialog } from '../FolderSettingsDialog';
import type { SidebarTreeNode } from './Sidebar'; import type { SidebarTreeNode } from './Sidebar';
interface Props { interface Props {
@@ -44,13 +43,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
{ {
label: 'Settings', label: 'Settings',
leftSlot: <Icon icon="settings" />, leftSlot: <Icon icon="settings" />,
onSelect: () => onSelect: () => openFolderSettings(child.id),
showDialog({
id: 'folder-settings',
title: 'Folder Settings',
size: 'md',
render: () => <FolderSettingsDialog folderId={child.id} />,
}),
}, },
{ {
label: 'Duplicate', label: 'Duplicate',
+64
View File
@@ -0,0 +1,64 @@
import type { Folder } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import { useMemo } from 'react';
import { IconTooltip } from '../components/core/IconTooltip';
import { HStack } from '../components/core/Stacks';
import type { TabItem } from '../components/core/Tabs/Tabs';
import { useHttpAuthenticationSummaries } from './useHttpAuthentication';
import type { AuthenticatedModel} from './useInheritedAuthentication';
import { useInheritedAuthentication } from './useInheritedAuthentication';
export function useAuthTab<T extends string>(tabValue: T, model: AuthenticatedModel | null) {
const authentication = useHttpAuthenticationSummaries();
const inheritedAuth = useInheritedAuthentication(model);
return useMemo<TabItem[]>(() => {
if (model == null) return [];
const tab: TabItem = {
value: tabValue,
label: 'Auth',
options: {
value: model.authenticationType,
items: [
...authentication.map((a) => ({
label: a.label || 'UNKNOWN',
shortLabel: a.shortLabel,
value: a.name,
})),
{ type: 'separator' },
{
label: 'Inherit from Parent',
shortLabel:
inheritedAuth != null && inheritedAuth.authenticationType != 'none' ? (
<HStack space={1.5}>
{authentication.find((a) => a.name === inheritedAuth.authenticationType)
?.shortLabel ?? 'UNKNOWN'}
<IconTooltip
icon="magic_wand"
iconSize="xs"
content="Authenticatin was inherited from an ancestor"
/>
</HStack>
) : (
'Auth'
),
value: null,
},
{ label: 'No Auth', shortLabel: 'No Auth', value: 'none' },
],
onChange: async (authenticationType) => {
let authentication: Folder['authentication'] = model.authentication;
if (model.authenticationType !== authenticationType) {
authentication = {
// Reset auth if changing types
};
}
await patchModel(model, { authentication, authenticationType });
},
},
};
return [tab];
}, [authentication, inheritedAuth, model, tabValue]);
}
+31
View File
@@ -0,0 +1,31 @@
import React, { useMemo } from 'react';
import { CountBadge } from '../components/core/CountBadge';
import type { TabItem } from '../components/core/Tabs/Tabs';
import type { HeaderModel } from './useInheritedHeaders';
import { useInheritedHeaders } from './useInheritedHeaders';
export function useHeadersTab<T extends string>(
tabValue: T,
model: HeaderModel | null,
label?: string,
) {
const inheritedHeaders = useInheritedHeaders(model);
return useMemo<TabItem[]>(() => {
if (model == null) return [];
const allHeaders = [
...inheritedHeaders,
...(model.model === 'grpc_request' ? model.metadata : model.headers),
];
const numHeaders = allHeaders.filter((h) => h.name).length;
const tab: TabItem = {
value: tabValue,
label: label ?? 'Headers',
rightSlot: <CountBadge count={numHeaders} />,
};
return [tab];
}, [inheritedHeaders, label, model, tabValue]);
}
+11 -3
View File
@@ -1,5 +1,11 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models'; import type {
Folder,
GrpcRequest,
HttpRequest,
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
import { httpResponsesAtom } from '@yaakapp-internal/models'; import { httpResponsesAtom } from '@yaakapp-internal/models';
import type { GetHttpAuthenticationConfigResponse, JsonPrimitive } from '@yaakapp-internal/plugins'; import type { GetHttpAuthenticationConfigResponse, JsonPrimitive } from '@yaakapp-internal/plugins';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
@@ -49,13 +55,15 @@ export function useHttpAuthenticationConfig(
...config, ...config,
actions: config.actions?.map((a, i) => ({ actions: config.actions?.map((a, i) => ({
...a, ...a,
call: async ({ id: requestId }: HttpRequest | GrpcRequest | WebsocketRequest) => { call: async ({
id: modelId,
}: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace) => {
await invokeCmd('cmd_call_http_authentication_action', { await invokeCmd('cmd_call_http_authentication_action', {
pluginRefId: config.pluginRefId, pluginRefId: config.pluginRefId,
actionIndex: i, actionIndex: i,
authName, authName,
values, values,
requestId, modelId,
}); });
// Ensure the config is refreshed after the action is done // Ensure the config is refreshed after the action is done
@@ -0,0 +1,50 @@
import type {
Folder,
GrpcRequest,
HttpRequest,
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
import { foldersAtom, workspacesAtom } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
const ancestorsAtom = atom(function (get) {
return [...get(foldersAtom), ...get(workspacesAtom)];
});
export type AuthenticatedModel = HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace;
export function useInheritedAuthentication(
baseModel: AuthenticatedModel | null,
) {
const parents = useAtomValue(ancestorsAtom);
if (baseModel == null) return null;
const next = (child: AuthenticatedModel) => {
// We hit the top
if (child.model === 'workspace') {
return child.authenticationType == null ? null : child;
}
// Has valid auth
if (child.authenticationType !== null) {
return child;
}
// Recurse up the tree
const parent = parents.find((p) => {
if (child.folderId) return p.id === child.folderId;
else return p.id === child.workspaceId;
});
// Failed to find parent (should never happen)
if (parent == null) {
return null;
}
return next(parent);
};
return next(baseModel);
}
+46
View File
@@ -0,0 +1,46 @@
import type {
Folder,
GrpcRequest,
HttpRequest,
HttpRequestHeader,
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
import { foldersAtom, workspacesAtom } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
const ancestorsAtom = atom(function (get) {
return [...get(foldersAtom), ...get(workspacesAtom)];
});
export type HeaderModel = HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace;
export function useInheritedHeaders(baseModel: HeaderModel | null) {
const parents = useAtomValue(ancestorsAtom);
if (baseModel == null) return [];
if (baseModel.model === 'workspace') return [];
const next = (child: HeaderModel): HttpRequestHeader[] => {
// Short-circuit
if (child.model === 'workspace') {
return [];
}
// Recurse up the tree
const parent = parents.find((p) => {
if (child.folderId) return p.id === child.folderId;
else return p.id === child.workspaceId;
});
// Failed to find parent (should never happen)
if (parent == null) {
return [];
}
const headers = next(parent);
return [...headers, ...parent.headers];
};
return next(baseModel);
}