mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-07 05:15:20 +02:00
Request Inheritance (#209)
This commit is contained in:
@@ -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 '[]';
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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
@@ -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
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, };
|
||||||
|
|||||||
@@ -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, };
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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, };
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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, };
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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} />;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
});
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}`}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
}
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user