mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 09:38:29 +02:00
Merge pull request #256
* Update environment model to get ready for request/folder environments * Folder environments in UI * Folder environments working * Tweaks and fixes * Tweak environment encryption UX * Tweak environment encryption UX * Address comments * Update fn name * Add tsc back to lint rules * Update src-web/components/EnvironmentEditor.tsx * Merge remote-tracking branch 'origin/folder-environments' into folder…
This commit is contained in:
@@ -12,6 +12,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsonwebtoken": "^9.0.2"
|
"jsonwebtoken": "^9.0.2"
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsonpath-plus": "^10.3.0"
|
"jsonpath-plus": "^10.3.0"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xmldom/xmldom": "^0.9.8",
|
"@xmldom/xmldom": "^0.9.8",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"shell-quote": "^1.8.1"
|
"shell-quote": "^1.8.1"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yaml": "^2.4.2"
|
"yaml": "^2.4.2"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"openapi-to-postmanv2": "^5.0.0",
|
"openapi-to-postmanv2": "^5.0.0",
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsonpath-plus": "^10.3.0"
|
"jsonpath-plus": "^10.3.0"
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsonpath-plus": "^10.3.0",
|
"jsonpath-plus": "^10.3.0",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-fns": "^4.1.0"
|
"date-fns": "^4.1.0"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xmldom/xmldom": "^0.9.8",
|
"@xmldom/xmldom": "^0.9.8",
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,14 +43,14 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
let app_handle = window.app_handle().clone();
|
let app_handle = window.app_handle().clone();
|
||||||
let plugin_manager = app_handle.state::<PluginManager>();
|
let plugin_manager = app_handle.state::<PluginManager>();
|
||||||
let (settings, workspace) = {
|
let settings = window.db().get_settings();
|
||||||
let db = window.db();
|
let workspace = window.db().get_workspace(&unrendered_request.workspace_id)?;
|
||||||
let settings = db.get_settings();
|
let environment_id = environment.map(|e| e.id);
|
||||||
let workspace = db.get_workspace(&unrendered_request.workspace_id)?;
|
let environment_chain = window.db().resolve_environments(
|
||||||
(settings, workspace)
|
&unrendered_request.workspace_id,
|
||||||
};
|
unrendered_request.folder_id.as_deref(),
|
||||||
let base_environment =
|
environment_id.as_deref(),
|
||||||
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
|
)?;
|
||||||
|
|
||||||
let response_id = og_response.id.clone();
|
let response_id = og_response.id.clone();
|
||||||
let response = Arc::new(Mutex::new(og_response.clone()));
|
let response = Arc::new(Mutex::new(og_response.clone()));
|
||||||
@@ -76,20 +76,17 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
RenderPurpose::Send,
|
RenderPurpose::Send,
|
||||||
);
|
);
|
||||||
|
|
||||||
let request =
|
let request = match render_http_request(&resolved_request, environment_chain, &cb).await {
|
||||||
match render_http_request(&resolved_request, &base_environment, environment.as_ref(), &cb)
|
Ok(r) => r,
|
||||||
.await
|
Err(e) => {
|
||||||
{
|
return Ok(response_err(
|
||||||
Ok(r) => r,
|
&app_handle,
|
||||||
Err(e) => {
|
&*response.lock().await,
|
||||||
return Ok(response_err(
|
e.to_string(),
|
||||||
&app_handle,
|
&update_source,
|
||||||
&*response.lock().await,
|
));
|
||||||
e.to_string(),
|
}
|
||||||
&update_source,
|
};
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut url_string = request.url.clone();
|
let mut url_string = request.url.clone();
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ use yaak_common::window::WorkspaceWindowTrait;
|
|||||||
use yaak_grpc::manager::{DynamicMessage, GrpcHandle};
|
use yaak_grpc::manager::{DynamicMessage, GrpcHandle};
|
||||||
use yaak_grpc::{Code, ServiceDefinition, deserialize_message, serialize_message};
|
use yaak_grpc::{Code, ServiceDefinition, deserialize_message, serialize_message};
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent, GrpcEventType,
|
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
|
||||||
GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, Plugin, Workspace, WorkspaceMeta,
|
GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, Plugin, Workspace,
|
||||||
|
WorkspaceMeta,
|
||||||
};
|
};
|
||||||
use yaak_models::query_manager::QueryManagerExt;
|
use yaak_models::query_manager::QueryManagerExt;
|
||||||
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
|
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
|
||||||
@@ -110,15 +111,11 @@ async fn cmd_render_template<R: Runtime>(
|
|||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
environment_id: Option<&str>,
|
environment_id: Option<&str>,
|
||||||
) -> YaakResult<String> {
|
) -> YaakResult<String> {
|
||||||
let environment = match environment_id {
|
let environment_chain =
|
||||||
Some(id) => app_handle.db().get_environment(id).ok(),
|
app_handle.db().resolve_environments(workspace_id, None, environment_id)?;
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
let base_environment = app_handle.db().get_base_environment(&workspace_id)?;
|
|
||||||
let result = render_template(
|
let result = render_template(
|
||||||
template,
|
template,
|
||||||
&base_environment,
|
environment_chain,
|
||||||
environment.as_ref(),
|
|
||||||
&PluginTemplateCallback::new(
|
&PluginTemplateCallback::new(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&PluginWindowContext::new(&window),
|
&PluginWindowContext::new(&window),
|
||||||
@@ -147,21 +144,19 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
|||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
||||||
) -> YaakResult<Vec<ServiceDefinition>> {
|
) -> YaakResult<Vec<ServiceDefinition>> {
|
||||||
let environment = match environment_id {
|
|
||||||
Some(id) => app_handle.db().get_environment(id).ok(),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
|
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
|
||||||
let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?;
|
let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?;
|
||||||
|
|
||||||
let base_environment =
|
let environment_chain = app_handle.db().resolve_environments(
|
||||||
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
|
&unrendered_request.workspace_id,
|
||||||
|
unrendered_request.folder_id.as_deref(),
|
||||||
|
environment_id,
|
||||||
|
)?;
|
||||||
let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?;
|
let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?;
|
||||||
|
|
||||||
let req = render_grpc_request(
|
let req = render_grpc_request(
|
||||||
&resolved_request,
|
&resolved_request,
|
||||||
&base_environment,
|
environment_chain,
|
||||||
environment.as_ref(),
|
|
||||||
&PluginTemplateCallback::new(
|
&PluginTemplateCallback::new(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&PluginWindowContext::new(&window),
|
&PluginWindowContext::new(&window),
|
||||||
@@ -196,20 +191,18 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
||||||
) -> YaakResult<String> {
|
) -> YaakResult<String> {
|
||||||
let environment = match environment_id {
|
|
||||||
Some(id) => app_handle.db().get_environment(id).ok(),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
|
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
|
||||||
let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?;
|
let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?;
|
||||||
let base_environment =
|
let environment_chain = app_handle.db().resolve_environments(
|
||||||
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
|
&unrendered_request.workspace_id,
|
||||||
|
unrendered_request.folder_id.as_deref(),
|
||||||
|
environment_id,
|
||||||
|
)?;
|
||||||
let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?;
|
let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?;
|
||||||
|
|
||||||
let request = render_grpc_request(
|
let request = render_grpc_request(
|
||||||
&resolved_request,
|
&resolved_request,
|
||||||
&base_environment,
|
environment_chain.clone(),
|
||||||
environment.as_ref(),
|
|
||||||
&PluginTemplateCallback::new(
|
&PluginTemplateCallback::new(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&PluginWindowContext::new(&window),
|
&PluginWindowContext::new(&window),
|
||||||
@@ -300,9 +293,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
let cb = {
|
let cb = {
|
||||||
let cancelled_rx = cancelled_rx.clone();
|
let cancelled_rx = cancelled_rx.clone();
|
||||||
let app_handle = app_handle.clone();
|
let app_handle = app_handle.clone();
|
||||||
|
let environment_chain = environment_chain.clone();
|
||||||
let window = window.clone();
|
let window = window.clone();
|
||||||
let base_environment = base_environment.clone();
|
|
||||||
let environment = environment.clone();
|
|
||||||
let base_msg = base_msg.clone();
|
let base_msg = base_msg.clone();
|
||||||
let method_desc = method_desc.clone();
|
let method_desc = method_desc.clone();
|
||||||
|
|
||||||
@@ -327,12 +319,12 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
let app_handle = app_handle.clone();
|
let app_handle = app_handle.clone();
|
||||||
let base_msg = base_msg.clone();
|
let base_msg = base_msg.clone();
|
||||||
let method_desc = method_desc.clone();
|
let method_desc = method_desc.clone();
|
||||||
|
let environment_chain = environment_chain.clone();
|
||||||
let msg = block_in_place(|| {
|
let msg = block_in_place(|| {
|
||||||
tauri::async_runtime::block_on(async {
|
tauri::async_runtime::block_on(async {
|
||||||
render_template(
|
render_template(
|
||||||
msg.as_str(),
|
msg.as_str(),
|
||||||
&base_environment,
|
environment_chain,
|
||||||
environment.as_ref(),
|
|
||||||
&PluginTemplateCallback::new(
|
&PluginTemplateCallback::new(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&PluginWindowContext::new(&window),
|
&PluginWindowContext::new(&window),
|
||||||
@@ -396,12 +388,12 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
let window = window.clone();
|
let window = window.clone();
|
||||||
let app_handle = app_handle.clone();
|
let app_handle = app_handle.clone();
|
||||||
let base_event = base_msg.clone();
|
let base_event = base_msg.clone();
|
||||||
|
let environment_chain = environment_chain.clone();
|
||||||
let req = request.clone();
|
let req = request.clone();
|
||||||
let msg = if req.message.is_empty() { "{}".to_string() } else { req.message };
|
let msg = if req.message.is_empty() { "{}".to_string() } else { req.message };
|
||||||
let msg = render_template(
|
let msg = render_template(
|
||||||
msg.as_str(),
|
msg.as_str(),
|
||||||
&base_environment.clone(),
|
environment_chain,
|
||||||
environment.as_ref(),
|
|
||||||
&PluginTemplateCallback::new(
|
&PluginTemplateCallback::new(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&PluginWindowContext::new(&window),
|
&PluginWindowContext::new(&window),
|
||||||
@@ -833,30 +825,25 @@ async fn cmd_get_http_authentication_config<R: Runtime>(
|
|||||||
plugin_manager: State<'_, PluginManager>,
|
plugin_manager: State<'_, PluginManager>,
|
||||||
auth_name: &str,
|
auth_name: &str,
|
||||||
values: HashMap<String, JsonPrimitive>,
|
values: HashMap<String, JsonPrimitive>,
|
||||||
request_id: &str,
|
request: AnyModel,
|
||||||
environment_id: Option<&str>,
|
environment_id: Option<&str>,
|
||||||
workspace_id: &str,
|
|
||||||
) -> YaakResult<GetHttpAuthenticationConfigResponse> {
|
) -> YaakResult<GetHttpAuthenticationConfigResponse> {
|
||||||
let base_environment = window.db().get_base_environment(&workspace_id)?;
|
let (workspace_id, folder_id) = match request.clone() {
|
||||||
let environment = match environment_id {
|
AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id),
|
||||||
Some(id) => match window.db().get_environment(id) {
|
AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id),
|
||||||
Ok(env) => Some(env),
|
AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id),
|
||||||
Err(e) => {
|
AnyModel::Folder(m) => (m.workspace_id, m.folder_id),
|
||||||
warn!("Failed to find environment by id {id} {}", e);
|
AnyModel::Workspace(m) => (m.id, None),
|
||||||
None
|
m => {
|
||||||
}
|
return Err(GenericError(format!("Unsupported model to call auth config {m:?}")));
|
||||||
},
|
},
|
||||||
None => None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let environment_chain =
|
||||||
|
window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?;
|
||||||
|
|
||||||
Ok(plugin_manager
|
Ok(plugin_manager
|
||||||
.get_http_authentication_config(
|
.get_http_authentication_config(&window, environment_chain, auth_name, values, request.id())
|
||||||
&window,
|
|
||||||
&base_environment,
|
|
||||||
environment.as_ref(),
|
|
||||||
auth_name,
|
|
||||||
values,
|
|
||||||
request_id,
|
|
||||||
)
|
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -907,30 +894,29 @@ 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>,
|
||||||
model_id: &str,
|
model: AnyModel,
|
||||||
workspace_id: &str,
|
|
||||||
environment_id: Option<&str>,
|
environment_id: Option<&str>,
|
||||||
) -> YaakResult<()> {
|
) -> YaakResult<()> {
|
||||||
let base_environment = window.db().get_base_environment(&workspace_id)?;
|
let (workspace_id, folder_id) = match model.clone() {
|
||||||
let environment = match environment_id {
|
AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id),
|
||||||
Some(id) => match window.db().get_environment(id) {
|
AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id),
|
||||||
Ok(env) => Some(env),
|
AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id),
|
||||||
Err(e) => {
|
AnyModel::Folder(m) => (m.workspace_id, m.folder_id),
|
||||||
warn!("Failed to find environment by id {id} {}", e);
|
AnyModel::Workspace(m) => (m.id, None),
|
||||||
None
|
m => {
|
||||||
}
|
return Err(GenericError(format!("Unsupported model to call auth {m:?}")));
|
||||||
},
|
}
|
||||||
None => None,
|
|
||||||
};
|
};
|
||||||
|
let environment_chain =
|
||||||
|
window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?;
|
||||||
Ok(plugin_manager
|
Ok(plugin_manager
|
||||||
.call_http_authentication_action(
|
.call_http_authentication_action(
|
||||||
&window,
|
&window,
|
||||||
&base_environment,
|
environment_chain,
|
||||||
environment.as_ref(),
|
|
||||||
auth_name,
|
auth_name,
|
||||||
action_index,
|
action_index,
|
||||||
values,
|
values,
|
||||||
model_id,
|
&model.id(),
|
||||||
)
|
)
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,20 +74,15 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
|
|
||||||
let workspace =
|
let workspace =
|
||||||
workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
|
workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
|
||||||
let environment = environment_from_window(&window);
|
let environment_id = environment_from_window(&window).map(|e| e.id);
|
||||||
let base_environment = app_handle
|
let environment_chain = window
|
||||||
.db()
|
.db()
|
||||||
.get_base_environment(&workspace.id)
|
.resolve_environments(&workspace.id, None, environment_id.as_deref())
|
||||||
.expect("Failed to get base environment");
|
.expect("Failed to resolve environments");
|
||||||
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
|
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
|
||||||
let grpc_request = render_grpc_request(
|
let grpc_request = render_grpc_request(&req.grpc_request, environment_chain, &cb)
|
||||||
&req.grpc_request,
|
.await
|
||||||
&base_environment,
|
.expect("Failed to render grpc request");
|
||||||
environment.as_ref(),
|
|
||||||
&cb,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to render grpc request");
|
|
||||||
Some(InternalEventPayload::RenderGrpcRequestResponse(RenderGrpcRequestResponse {
|
Some(InternalEventPayload::RenderGrpcRequestResponse(RenderGrpcRequestResponse {
|
||||||
grpc_request,
|
grpc_request,
|
||||||
}))
|
}))
|
||||||
@@ -98,20 +93,15 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
|
|
||||||
let workspace =
|
let workspace =
|
||||||
workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
|
workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
|
||||||
let environment = environment_from_window(&window);
|
let environment_id = environment_from_window(&window).map(|e| e.id);
|
||||||
let base_environment = app_handle
|
let environment_chain = window
|
||||||
.db()
|
.db()
|
||||||
.get_base_environment(&workspace.id)
|
.resolve_environments(&workspace.id, None, environment_id.as_deref())
|
||||||
.expect("Failed to get base environment");
|
.expect("Failed to resolve environments");
|
||||||
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
|
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
|
||||||
let http_request = render_http_request(
|
let http_request = render_http_request(&req.http_request, environment_chain, &cb)
|
||||||
&req.http_request,
|
.await
|
||||||
&base_environment,
|
.expect("Failed to render http request");
|
||||||
environment.as_ref(),
|
|
||||||
&cb,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to render http request");
|
|
||||||
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
|
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
|
||||||
http_request,
|
http_request,
|
||||||
}))
|
}))
|
||||||
@@ -122,13 +112,13 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
|
|
||||||
let workspace =
|
let workspace =
|
||||||
workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
|
workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
|
||||||
let environment = environment_from_window(&window);
|
let environment_id = environment_from_window(&window).map(|e| e.id);
|
||||||
let base_environment = app_handle
|
let environment_chain = window
|
||||||
.db()
|
.db()
|
||||||
.get_base_environment(&workspace.id)
|
.resolve_environments(&workspace.id, None, environment_id.as_deref())
|
||||||
.expect("Failed to get base environment");
|
.expect("Failed to resolve environments");
|
||||||
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
|
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
|
||||||
let data = render_json_value(req.data, &base_environment, environment.as_ref(), &cb)
|
let data = render_json_value(req.data, environment_chain, &cb)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to render template");
|
.expect("Failed to render template");
|
||||||
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
|
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
|
||||||
|
|||||||
@@ -5,35 +5,32 @@ use yaak_models::models::{
|
|||||||
Environment, 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::{TemplateCallback, parse_and_render, render_json_value_raw};
|
||||||
|
|
||||||
pub async fn render_template<T: TemplateCallback>(
|
pub async fn render_template<T: TemplateCallback>(
|
||||||
template: &str,
|
template: &str,
|
||||||
base_environment: &Environment,
|
environment_chain: Vec<Environment>,
|
||||||
environment: Option<&Environment>,
|
|
||||||
cb: &T,
|
cb: &T,
|
||||||
) -> yaak_templates::error::Result<String> {
|
) -> yaak_templates::error::Result<String> {
|
||||||
let vars = &make_vars_hashmap(base_environment, environment);
|
let vars = &make_vars_hashmap(environment_chain);
|
||||||
render(template, vars, cb).await
|
render(template, vars, cb).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_json_value<T: TemplateCallback>(
|
pub async fn render_json_value<T: TemplateCallback>(
|
||||||
value: Value,
|
value: Value,
|
||||||
base_environment: &Environment,
|
environment_chain: Vec<Environment>,
|
||||||
environment: Option<&Environment>,
|
|
||||||
cb: &T,
|
cb: &T,
|
||||||
) -> yaak_templates::error::Result<Value> {
|
) -> yaak_templates::error::Result<Value> {
|
||||||
let vars = &make_vars_hashmap(base_environment, environment);
|
let vars = &make_vars_hashmap(environment_chain);
|
||||||
render_json_value_raw(value, vars, cb).await
|
render_json_value_raw(value, vars, cb).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_grpc_request<T: TemplateCallback>(
|
pub async fn render_grpc_request<T: TemplateCallback>(
|
||||||
r: &GrpcRequest,
|
r: &GrpcRequest,
|
||||||
base_environment: &Environment,
|
environment_chain: Vec<Environment>,
|
||||||
environment: Option<&Environment>,
|
|
||||||
cb: &T,
|
cb: &T,
|
||||||
) -> yaak_templates::error::Result<GrpcRequest> {
|
) -> yaak_templates::error::Result<GrpcRequest> {
|
||||||
let vars = &make_vars_hashmap(base_environment, environment);
|
let vars = &make_vars_hashmap(environment_chain);
|
||||||
|
|
||||||
let mut metadata = Vec::new();
|
let mut metadata = Vec::new();
|
||||||
for p in r.metadata.clone() {
|
for p in r.metadata.clone() {
|
||||||
@@ -62,11 +59,10 @@ pub async fn render_grpc_request<T: TemplateCallback>(
|
|||||||
|
|
||||||
pub async fn render_http_request<T: TemplateCallback>(
|
pub async fn render_http_request<T: TemplateCallback>(
|
||||||
r: &HttpRequest,
|
r: &HttpRequest,
|
||||||
base_environment: &Environment,
|
environment_chain: Vec<Environment>,
|
||||||
environment: Option<&Environment>,
|
|
||||||
cb: &T,
|
cb: &T,
|
||||||
) -> yaak_templates::error::Result<HttpRequest> {
|
) -> yaak_templates::error::Result<HttpRequest> {
|
||||||
let vars = &make_vars_hashmap(base_environment, environment);
|
let vars = &make_vars_hashmap(environment_chain);
|
||||||
|
|
||||||
let mut url_parameters = Vec::new();
|
let mut url_parameters = Vec::new();
|
||||||
for p in r.url_parameters.clone() {
|
for p in r.url_parameters.clone() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
|
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, variables: Array<EnvironmentVariable>, color: string | null, parentModel: string, parentId: string | null, };
|
||||||
|
|
||||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub(crate) fn find_ssh_key() -> Option<PathBuf> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_current_branch(repo: &Repository) -> Result<Option<Branch>> {
|
pub(crate) fn get_current_branch(repo: &Repository) -> Result<Option<Branch<'_>>> {
|
||||||
for b in repo.branches(None)? {
|
for b in repo.branches(None)? {
|
||||||
let branch = b?.0;
|
let branch = b?.0;
|
||||||
if branch.is_head() {
|
if branch.is_head() {
|
||||||
@@ -101,7 +101,7 @@ pub(crate) fn get_default_remote_in_repo(repo: &Repository) -> Result<String> {
|
|||||||
return Ok(DEFAULT_REMOTE_NAME.into());
|
return Ok(DEFAULT_REMOTE_NAME.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// if only one remote exists pick that
|
// if only one remote exists, pick that
|
||||||
if remotes.len() == 1 {
|
if remotes.len() == 1 {
|
||||||
let first_remote = remotes
|
let first_remote = remotes
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
|||||||
|
|
||||||
export type EncryptedKey = { encryptedKey: string, };
|
export type EncryptedKey = { encryptedKey: string, };
|
||||||
|
|
||||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
|
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, variables: Array<EnvironmentVariable>, color: string | null, parentModel: string, parentId: string | null, };
|
||||||
|
|
||||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
-- Create temporary table for migration
|
||||||
|
CREATE TABLE environments__new
|
||||||
|
(
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
deleted_at DATETIME,
|
||||||
|
workspace_id TEXT NOT NULL
|
||||||
|
REFERENCES workspaces ON DELETE CASCADE,
|
||||||
|
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
variables TEXT DEFAULT '[]' NOT NULL,
|
||||||
|
model TEXT DEFAULT 'environment',
|
||||||
|
public BOOLEAN DEFAULT FALSE,
|
||||||
|
color TEXT,
|
||||||
|
|
||||||
|
-- NEW
|
||||||
|
parent_model TEXT DEFAULT 'workspace' NOT NULL,
|
||||||
|
parent_id TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Backfill the data from the old table
|
||||||
|
-- - base=1 -> (workspace, NULL)
|
||||||
|
-- - base=0 -> (environment, id_of_workspace_base) (fallback to workspace,NULL if none)
|
||||||
|
INSERT INTO environments__new
|
||||||
|
(id, created_at, updated_at, deleted_at, workspace_id, name, variables, model, public, color, parent_model, parent_id)
|
||||||
|
SELECT
|
||||||
|
e.id,
|
||||||
|
e.created_at,
|
||||||
|
e.updated_at,
|
||||||
|
e.deleted_at,
|
||||||
|
e.workspace_id,
|
||||||
|
e.name,
|
||||||
|
e.variables,
|
||||||
|
e.model,
|
||||||
|
e.public,
|
||||||
|
e.color,
|
||||||
|
CASE
|
||||||
|
WHEN e.base = 1 THEN 'workspace'
|
||||||
|
WHEN (
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM environments b
|
||||||
|
WHERE b.workspace_id = e.workspace_id AND b.base = 1
|
||||||
|
) > 0 THEN 'environment'
|
||||||
|
ELSE 'workspace'
|
||||||
|
END AS parent_model,
|
||||||
|
CASE
|
||||||
|
WHEN e.base = 1 THEN NULL
|
||||||
|
ELSE (
|
||||||
|
SELECT b.id
|
||||||
|
FROM environments b
|
||||||
|
WHERE b.workspace_id = e.workspace_id AND b.base = 1
|
||||||
|
ORDER BY b.created_at ASC, b.id ASC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
END AS parent_id
|
||||||
|
FROM environments e;
|
||||||
|
|
||||||
|
-- Move everything to the new table
|
||||||
|
DROP TABLE environments;
|
||||||
|
ALTER TABLE environments__new
|
||||||
|
RENAME TO environments;
|
||||||
@@ -89,7 +89,7 @@ impl<'a> DbContext<'a> {
|
|||||||
col: impl IntoColumnRef,
|
col: impl IntoColumnRef,
|
||||||
value: impl Into<SimpleExpr>,
|
value: impl Into<SimpleExpr>,
|
||||||
limit: Option<u64>,
|
limit: Option<u64>,
|
||||||
) -> crate::error::Result<Vec<M>>
|
) -> Result<Vec<M>>
|
||||||
where
|
where
|
||||||
M: Into<AnyModel> + Clone + UpsertModelInfo,
|
M: Into<AnyModel> + Clone + UpsertModelInfo,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ pub enum Error {
|
|||||||
|
|
||||||
#[error("Multiple base environments for {0}. Delete duplicates before continuing.")]
|
#[error("Multiple base environments for {0}. Delete duplicates before continuing.")]
|
||||||
MultipleBaseEnvironments(String),
|
MultipleBaseEnvironments(String),
|
||||||
|
|
||||||
|
#[error("Multiple folder environments for {0}. Delete duplicates before continuing.")]
|
||||||
|
MultipleFolderEnvironments(String),
|
||||||
|
|
||||||
#[error("unknown error")]
|
#[error("unknown error")]
|
||||||
Unknown,
|
Unknown,
|
||||||
|
|||||||
@@ -533,9 +533,10 @@ pub struct Environment {
|
|||||||
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub public: bool,
|
pub public: bool,
|
||||||
pub base: bool,
|
|
||||||
pub variables: Vec<EnvironmentVariable>,
|
pub variables: Vec<EnvironmentVariable>,
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
|
pub parent_model: String,
|
||||||
|
pub parent_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for Environment {
|
impl UpsertModelInfo for Environment {
|
||||||
@@ -568,7 +569,8 @@ impl UpsertModelInfo for Environment {
|
|||||||
(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)),
|
||||||
(WorkspaceId, self.workspace_id.into()),
|
(WorkspaceId, self.workspace_id.into()),
|
||||||
(Base, self.base.into()),
|
(ParentId, self.parent_id.into()),
|
||||||
|
(ParentModel, self.parent_model.into()),
|
||||||
(Color, self.color.into()),
|
(Color, self.color.into()),
|
||||||
(Name, self.name.trim().into()),
|
(Name, self.name.trim().into()),
|
||||||
(Public, self.public.into()),
|
(Public, self.public.into()),
|
||||||
@@ -579,7 +581,8 @@ impl UpsertModelInfo for Environment {
|
|||||||
fn update_columns() -> Vec<impl IntoIden> {
|
fn update_columns() -> Vec<impl IntoIden> {
|
||||||
vec![
|
vec![
|
||||||
EnvironmentIden::UpdatedAt,
|
EnvironmentIden::UpdatedAt,
|
||||||
EnvironmentIden::Base,
|
EnvironmentIden::ParentId,
|
||||||
|
EnvironmentIden::ParentModel,
|
||||||
EnvironmentIden::Color,
|
EnvironmentIden::Color,
|
||||||
EnvironmentIden::Name,
|
EnvironmentIden::Name,
|
||||||
EnvironmentIden::Public,
|
EnvironmentIden::Public,
|
||||||
@@ -598,7 +601,8 @@ impl UpsertModelInfo for Environment {
|
|||||||
workspace_id: row.get("workspace_id")?,
|
workspace_id: row.get("workspace_id")?,
|
||||||
created_at: row.get("created_at")?,
|
created_at: row.get("created_at")?,
|
||||||
updated_at: row.get("updated_at")?,
|
updated_at: row.get("updated_at")?,
|
||||||
base: row.get("base")?,
|
parent_id: row.get("parent_id")?,
|
||||||
|
parent_model: row.get("parent_model")?,
|
||||||
color: row.get("color")?,
|
color: row.get("color")?,
|
||||||
name: row.get("name")?,
|
name: row.get("name")?,
|
||||||
public: row.get("public")?,
|
public: row.get("public")?,
|
||||||
@@ -2072,6 +2076,17 @@ macro_rules! define_any_model {
|
|||||||
)*
|
)*
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AnyModel {
|
||||||
|
#[inline]
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
$(
|
||||||
|
AnyModel::$type(inner) => &inner.id,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$(
|
$(
|
||||||
impl From<$type> for AnyModel {
|
impl From<$type> for AnyModel {
|
||||||
fn from(value: $type) -> Self {
|
fn from(value: $type) -> Self {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::db_context::DbContext;
|
use crate::db_context::DbContext;
|
||||||
use crate::error::Error::{MissingBaseEnvironment, MultipleBaseEnvironments};
|
use crate::error::Error::{
|
||||||
|
MissingBaseEnvironment, MultipleBaseEnvironments, MultipleFolderEnvironments,
|
||||||
|
};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models::{Environment, EnvironmentIden, EnvironmentVariable};
|
use crate::models::{Environment, EnvironmentIden, EnvironmentVariable};
|
||||||
use crate::util::UpdateSource;
|
use crate::util::UpdateSource;
|
||||||
@@ -10,21 +12,31 @@ impl<'a> DbContext<'a> {
|
|||||||
self.find_one(EnvironmentIden::Id, id)
|
self.find_one(EnvironmentIden::Id, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_environment_by_folder_id(&self, folder_id: &str) -> Result<Option<Environment>> {
|
||||||
|
let environments: Vec<Environment> =
|
||||||
|
self.find_many(EnvironmentIden::ParentId, folder_id, None)?;
|
||||||
|
if environments.len() > 1 {
|
||||||
|
return Err(MultipleFolderEnvironments(folder_id.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(environments.get(0).cloned())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_base_environment(&self, workspace_id: &str) -> Result<Environment> {
|
pub fn get_base_environment(&self, workspace_id: &str) -> Result<Environment> {
|
||||||
let environments = self.list_environments_ensure_base(workspace_id)?;
|
let environments = self.list_environments_ensure_base(workspace_id)?;
|
||||||
let base_environments =
|
let base_environments = environments
|
||||||
environments.into_iter().filter(|e| e.base).collect::<Vec<Environment>>();
|
.into_iter()
|
||||||
|
.filter(|e| e.parent_id.is_none())
|
||||||
|
.collect::<Vec<Environment>>();
|
||||||
|
|
||||||
if base_environments.len() > 1 {
|
if base_environments.len() > 1 {
|
||||||
return Err(MultipleBaseEnvironments(workspace_id.to_string()));
|
return Err(MultipleBaseEnvironments(workspace_id.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let base_environment = base_environments.into_iter().find(|e| e.base).ok_or(
|
Ok(base_environments.first().cloned().ok_or(
|
||||||
// Should never happen because one should be created above if it does not exist
|
// Should never happen because one should be created above if it does not exist
|
||||||
MissingBaseEnvironment(workspace_id.to_string()),
|
MissingBaseEnvironment(workspace_id.to_string()),
|
||||||
)?;
|
)?)
|
||||||
|
|
||||||
Ok(base_environment)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lists environments and will create a base environment if one doesn't exist
|
/// Lists environments and will create a base environment if one doesn't exist
|
||||||
@@ -32,13 +44,12 @@ impl<'a> DbContext<'a> {
|
|||||||
let mut environments =
|
let mut environments =
|
||||||
self.find_many::<Environment>(EnvironmentIden::WorkspaceId, workspace_id, None)?;
|
self.find_many::<Environment>(EnvironmentIden::WorkspaceId, workspace_id, None)?;
|
||||||
|
|
||||||
let base_environment = environments.iter().find(|e| e.base);
|
let base_environment = environments.iter().find(|e| e.parent_id.is_none());
|
||||||
|
|
||||||
if let None = base_environment {
|
if let None = base_environment {
|
||||||
let e = self.upsert_environment(
|
let e = self.upsert_environment(
|
||||||
&Environment {
|
&Environment {
|
||||||
workspace_id: workspace_id.to_string(),
|
workspace_id: workspace_id.to_string(),
|
||||||
base: true,
|
|
||||||
name: "Global Variables".to_string(),
|
name: "Global Variables".to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -98,4 +109,43 @@ impl<'a> DbContext<'a> {
|
|||||||
source,
|
source,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn resolve_environments(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
folder_id: Option<&str>,
|
||||||
|
active_environment_id: Option<&str>,
|
||||||
|
) -> Result<Vec<Environment>> {
|
||||||
|
let mut environments = Vec::new();
|
||||||
|
|
||||||
|
if let Some(folder_id) = folder_id {
|
||||||
|
let folder = self.get_folder(folder_id)?;
|
||||||
|
|
||||||
|
// Add current folder's environment
|
||||||
|
if let Some(e) = self.get_environment_by_folder_id(folder_id)? {
|
||||||
|
environments.push(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recurse up
|
||||||
|
let ancestors = self.resolve_environments(
|
||||||
|
workspace_id,
|
||||||
|
folder.folder_id.as_deref(),
|
||||||
|
active_environment_id,
|
||||||
|
)?;
|
||||||
|
environments.extend(ancestors);
|
||||||
|
} else {
|
||||||
|
// Add active and base environments
|
||||||
|
if let Some(id) = active_environment_id {
|
||||||
|
if let Ok(e) = self.get_environment(&id) {
|
||||||
|
// Add active sub environment
|
||||||
|
environments.push(e);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the base environment
|
||||||
|
environments.push(self.get_base_environment(workspace_id)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(environments)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
use crate::connection_or_tx::ConnectionOrTx;
|
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::{Environment, EnvironmentIden, Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestHeader, HttpRequestIden, WebsocketRequest, WebsocketRequestIden};
|
||||||
Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestHeader,
|
|
||||||
HttpRequestIden, WebsocketRequest, WebsocketRequestIden,
|
|
||||||
};
|
|
||||||
use crate::util::UpdateSource;
|
use crate::util::UpdateSource;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@@ -37,6 +34,10 @@ impl<'a> DbContext<'a> {
|
|||||||
self.delete_websocket_request(&m, source)?;
|
self.delete_websocket_request(&m, source)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for e in self.find_many(EnvironmentIden::ParentId, fid, None)? {
|
||||||
|
self.delete_environment(&e, source)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Recurse down into child folders
|
// Recurse down into child folders
|
||||||
for folder in self.find_many::<Folder>(FolderIden::FolderId, fid, None)? {
|
for folder in self.find_many::<Folder>(FolderIden::FolderId, fid, None)? {
|
||||||
self.delete_folder(&folder, source)?;
|
self.delete_folder(&folder, source)?;
|
||||||
@@ -99,6 +100,17 @@ impl<'a> DbContext<'a> {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for m in self.find_many::<Environment>(EnvironmentIden::ParentId, fid, None)? {
|
||||||
|
self.upsert_environment(
|
||||||
|
&Environment {
|
||||||
|
id: "".into(),
|
||||||
|
parent_id: Some(new_folder.id.clone()),
|
||||||
|
..m
|
||||||
|
},
|
||||||
|
source,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
for m in self.find_many::<Folder>(FolderIden::FolderId, fid, None)? {
|
for m in self.find_many::<Folder>(FolderIden::FolderId, fid, None)? {
|
||||||
// Recurse down
|
// Recurse down
|
||||||
self.duplicate_folder(
|
self.duplicate_folder(
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ impl QueryManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connect(&self) -> DbContext {
|
pub fn connect(&self) -> DbContext<'_> {
|
||||||
let conn = self
|
let conn = self
|
||||||
.pool
|
.pool
|
||||||
.lock()
|
.lock()
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use crate::models::{Environment, EnvironmentVariable};
|
use crate::models::{Environment, EnvironmentVariable};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub fn make_vars_hashmap(
|
pub fn make_vars_hashmap(environment_chain: Vec<Environment>) -> HashMap<String, String> {
|
||||||
base_environment: &Environment,
|
|
||||||
environment: Option<&Environment>,
|
|
||||||
) -> HashMap<String, String> {
|
|
||||||
let mut variables = HashMap::new();
|
let mut variables = HashMap::new();
|
||||||
variables = add_variable_to_map(variables, &base_environment.variables);
|
|
||||||
|
|
||||||
if let Some(e) = environment {
|
for e in environment_chain.iter().rev() {
|
||||||
variables = add_variable_to_map(variables, &e.variables);
|
variables = add_variable_to_map(variables, &e.variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,4 +27,3 @@ fn add_variable_to_map(
|
|||||||
|
|
||||||
map
|
map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
|
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, variables: Array<EnvironmentVariable>, color: string | null, parentModel: string, parentId: string | null, };
|
||||||
|
|
||||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -584,8 +584,7 @@ impl PluginManager {
|
|||||||
pub async fn get_http_authentication_config<R: Runtime>(
|
pub async fn get_http_authentication_config<R: Runtime>(
|
||||||
&self,
|
&self,
|
||||||
window: &WebviewWindow<R>,
|
window: &WebviewWindow<R>,
|
||||||
base_environment: &Environment,
|
environment_chain: Vec<Environment>,
|
||||||
environment: Option<&Environment>,
|
|
||||||
auth_name: &str,
|
auth_name: &str,
|
||||||
values: HashMap<String, JsonPrimitive>,
|
values: HashMap<String, JsonPrimitive>,
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
@@ -596,7 +595,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 vars = &make_vars_hashmap(&base_environment, environment);
|
let vars = &make_vars_hashmap(environment_chain);
|
||||||
let cb = PluginTemplateCallback::new(
|
let cb = PluginTemplateCallback::new(
|
||||||
window.app_handle(),
|
window.app_handle(),
|
||||||
&PluginWindowContext::new(&window),
|
&PluginWindowContext::new(&window),
|
||||||
@@ -629,14 +628,13 @@ impl PluginManager {
|
|||||||
pub async fn call_http_authentication_action<R: Runtime>(
|
pub async fn call_http_authentication_action<R: Runtime>(
|
||||||
&self,
|
&self,
|
||||||
window: &WebviewWindow<R>,
|
window: &WebviewWindow<R>,
|
||||||
base_environment: &Environment,
|
environment_chain: Vec<Environment>,
|
||||||
environment: Option<&Environment>,
|
|
||||||
auth_name: &str,
|
auth_name: &str,
|
||||||
action_index: i32,
|
action_index: i32,
|
||||||
values: HashMap<String, JsonPrimitive>,
|
values: HashMap<String, JsonPrimitive>,
|
||||||
model_id: &str,
|
model_id: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let vars = &make_vars_hashmap(&base_environment, environment);
|
let vars = &make_vars_hashmap(environment_chain);
|
||||||
let rendered_values = render_json_value_raw(
|
let rendered_values = render_json_value_raw(
|
||||||
json!(values),
|
json!(values),
|
||||||
vars,
|
vars,
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use serde;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use tauri::path::BaseDirectory;
|
use tauri::path::BaseDirectory;
|
||||||
use tauri::{AppHandle, Manager, Runtime};
|
use tauri::{AppHandle, Manager, Runtime};
|
||||||
use tauri_plugin_shell::ShellExt;
|
|
||||||
use tauri_plugin_shell::process::CommandEvent;
|
use tauri_plugin_shell::process::CommandEvent;
|
||||||
|
use tauri_plugin_shell::ShellExt;
|
||||||
use tokio::sync::watch::Receiver;
|
use tokio::sync::watch::Receiver;
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
|
||||||
#[serde(default, rename_all = "camelCase")]
|
|
||||||
struct PortFile {
|
|
||||||
port: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start_nodejs_plugin_runtime<R: Runtime>(
|
pub async fn start_nodejs_plugin_runtime<R: Runtime>(
|
||||||
app: &AppHandle<R>,
|
app: &AppHandle<R>,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
|
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, variables: Array<EnvironmentVariable>, color: string | null, parentModel: string, parentId: string | null, };
|
||||||
|
|
||||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -109,24 +109,18 @@ pub(crate) async fn send<R: Runtime>(
|
|||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
ws_manager: State<'_, Mutex<WebsocketManager>>,
|
ws_manager: State<'_, Mutex<WebsocketManager>>,
|
||||||
) -> Result<WebsocketConnection> {
|
) -> Result<WebsocketConnection> {
|
||||||
let (connection, unrendered_request) = {
|
let connection = app_handle.db().get_websocket_connection(connection_id)?;
|
||||||
let db = app_handle.db();
|
let unrendered_request = app_handle.db().get_websocket_request(&connection.request_id)?;
|
||||||
let connection = db.get_websocket_connection(connection_id)?;
|
let environment_chain = app_handle.db().resolve_environments(
|
||||||
let unrendered_request = db.get_websocket_request(&connection.request_id)?;
|
&unrendered_request.workspace_id,
|
||||||
(connection, unrendered_request)
|
unrendered_request.folder_id.as_deref(),
|
||||||
};
|
environment_id,
|
||||||
let environment = match environment_id {
|
)?;
|
||||||
Some(id) => Some(app_handle.db().get_environment(id)?),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
let base_environment =
|
|
||||||
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
|
|
||||||
let (resolved_request, _auth_context_id) =
|
let (resolved_request, _auth_context_id) =
|
||||||
resolve_websocket_request(&window, &unrendered_request)?;
|
resolve_websocket_request(&window, &unrendered_request)?;
|
||||||
let request = render_websocket_request(
|
let request = render_websocket_request(
|
||||||
&resolved_request,
|
&resolved_request,
|
||||||
&base_environment,
|
environment_chain,
|
||||||
environment.as_ref(),
|
|
||||||
&PluginTemplateCallback::new(
|
&PluginTemplateCallback::new(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&PluginWindowContext::new(&window),
|
&PluginWindowContext::new(&window),
|
||||||
@@ -192,19 +186,17 @@ pub(crate) async fn connect<R: Runtime>(
|
|||||||
ws_manager: State<'_, Mutex<WebsocketManager>>,
|
ws_manager: State<'_, Mutex<WebsocketManager>>,
|
||||||
) -> Result<WebsocketConnection> {
|
) -> Result<WebsocketConnection> {
|
||||||
let unrendered_request = app_handle.db().get_websocket_request(request_id)?;
|
let unrendered_request = app_handle.db().get_websocket_request(request_id)?;
|
||||||
let environment = match environment_id {
|
let environment_chain = app_handle.db().resolve_environments(
|
||||||
Some(id) => Some(app_handle.db().get_environment(id)?),
|
&unrendered_request.workspace_id,
|
||||||
None => None,
|
unrendered_request.folder_id.as_deref(),
|
||||||
};
|
environment_id,
|
||||||
let base_environment =
|
)?;
|
||||||
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
|
|
||||||
let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?;
|
let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?;
|
||||||
let (resolved_request, auth_context_id) =
|
let (resolved_request, auth_context_id) =
|
||||||
resolve_websocket_request(&window, &unrendered_request)?;
|
resolve_websocket_request(&window, &unrendered_request)?;
|
||||||
let request = render_websocket_request(
|
let request = render_websocket_request(
|
||||||
&resolved_request,
|
&resolved_request,
|
||||||
&base_environment,
|
environment_chain,
|
||||||
environment.as_ref(),
|
|
||||||
&PluginTemplateCallback::new(
|
&PluginTemplateCallback::new(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&PluginWindowContext::new(&window),
|
&PluginWindowContext::new(&window),
|
||||||
@@ -305,7 +297,7 @@ pub(crate) async fn connect<R: Runtime>(
|
|||||||
|
|
||||||
// Add cookies to WS HTTP Upgrade
|
// Add cookies to WS HTTP Upgrade
|
||||||
if let Some(id) = cookie_jar_id {
|
if let Some(id) = cookie_jar_id {
|
||||||
let cookie_jar = app_handle.db().get_cookie_jar(id)?;
|
let cookie_jar = app_handle.db().get_cookie_jar(&id)?;
|
||||||
|
|
||||||
let cookies = cookie_jar
|
let cookies = cookie_jar
|
||||||
.cookies
|
.cookies
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ use yaak_templates::{parse_and_render, render_json_value_raw, TemplateCallback};
|
|||||||
|
|
||||||
pub async fn render_websocket_request<T: TemplateCallback>(
|
pub async fn render_websocket_request<T: TemplateCallback>(
|
||||||
r: &WebsocketRequest,
|
r: &WebsocketRequest,
|
||||||
base_environment: &Environment,
|
environment_chain: Vec<Environment>,
|
||||||
environment: Option<&Environment>,
|
|
||||||
cb: &T,
|
cb: &T,
|
||||||
) -> Result<WebsocketRequest> {
|
) -> Result<WebsocketRequest> {
|
||||||
let vars = &make_vars_hashmap(base_environment, environment);
|
let vars = &make_vars_hashmap(environment_chain);
|
||||||
|
|
||||||
let mut headers = Vec::new();
|
let mut headers = Vec::new();
|
||||||
for p in r.headers.clone() {
|
for p in r.headers.clone() {
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export const createEnvironmentAndActivate = createFastMutation<
|
|||||||
name,
|
name,
|
||||||
variables: [],
|
variables: [],
|
||||||
workspaceId,
|
workspaceId,
|
||||||
base: false,
|
parentId: baseEnvironment.id,
|
||||||
|
parentModel: 'environment',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: async (environmentId) => {
|
onSuccess: async (environmentId) => {
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
|||||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||||
import { createRequestAndNavigate } from '../lib/createRequestAndNavigate';
|
import { createRequestAndNavigate } from '../lib/createRequestAndNavigate';
|
||||||
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
|
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
|
||||||
import { showDialog, toggleDialog } from '../lib/dialog';
|
import { showDialog } from '../lib/dialog';
|
||||||
|
import { editEnvironment } from '../lib/editEnvironment';
|
||||||
import { renameModelWithPrompt } from '../lib/renameModelWithPrompt';
|
import { renameModelWithPrompt } from '../lib/renameModelWithPrompt';
|
||||||
import { resolvedModelNameWithFolders } from '../lib/resolvedModelName';
|
import { resolvedModelNameWithFolders } from '../lib/resolvedModelName';
|
||||||
import { router } from '../lib/router';
|
import { router } from '../lib/router';
|
||||||
@@ -40,7 +41,6 @@ import { HttpMethodTag } from './core/HttpMethodTag';
|
|||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { PlainInput } from './core/PlainInput';
|
import { PlainInput } from './core/PlainInput';
|
||||||
import { HStack } from './core/Stacks';
|
import { HStack } from './core/Stacks';
|
||||||
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
|
|
||||||
|
|
||||||
interface CommandPaletteGroup {
|
interface CommandPaletteGroup {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -125,15 +125,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
|||||||
key: 'environment.edit',
|
key: 'environment.edit',
|
||||||
label: 'Edit Environment',
|
label: 'Edit Environment',
|
||||||
action: 'environmentEditor.toggle',
|
action: 'environmentEditor.toggle',
|
||||||
onSelect: () => {
|
onSelect: () => editEnvironment(activeEnvironment),
|
||||||
toggleDialog({
|
|
||||||
id: 'environment-editor',
|
|
||||||
noPadding: true,
|
|
||||||
size: 'lg',
|
|
||||||
className: 'h-[80vh]',
|
|
||||||
render: () => <EnvironmentEditDialog initialEnvironment={activeEnvironment} />,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'environment.create',
|
key: 'environment.create',
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function ConfirmLargeResponse({ children, response }: Props) {
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
variant="border"
|
variant="border"
|
||||||
size="xs"
|
size="xs"
|
||||||
text={() => getResponseBodyText(response)}
|
text={() => getResponseBodyText({ responseId: response.id, filter: null })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||||
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
||||||
import { toggleDialog } from '../lib/dialog';
|
import { editEnvironment } from '../lib/editEnvironment';
|
||||||
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
|
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
|
||||||
import { Button } from './core/Button';
|
|
||||||
import type { ButtonProps } from './core/Button';
|
import type { ButtonProps } from './core/Button';
|
||||||
|
import { Button } from './core/Button';
|
||||||
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 { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
|
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
|
||||||
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -23,16 +22,6 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
|||||||
const { subEnvironments, baseEnvironment } = useEnvironmentsBreakdown();
|
const { subEnvironments, baseEnvironment } = useEnvironmentsBreakdown();
|
||||||
const activeEnvironment = useActiveEnvironment();
|
const activeEnvironment = useActiveEnvironment();
|
||||||
|
|
||||||
const showEnvironmentDialog = useCallback(() => {
|
|
||||||
toggleDialog({
|
|
||||||
id: 'environment-editor',
|
|
||||||
noPadding: true,
|
|
||||||
size: 'lg',
|
|
||||||
className: 'h-[80vh]',
|
|
||||||
render: () => <EnvironmentEditDialog initialEnvironment={activeEnvironment} />,
|
|
||||||
});
|
|
||||||
}, [activeEnvironment]);
|
|
||||||
|
|
||||||
const items: DropdownItem[] = useMemo(
|
const items: DropdownItem[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
...subEnvironments.map(
|
...subEnvironments.map(
|
||||||
@@ -55,14 +44,13 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
|||||||
? [{ type: 'separator', label: 'Environments' }]
|
? [{ type: 'separator', label: 'Environments' }]
|
||||||
: []) as DropdownItem[]),
|
: []) as DropdownItem[]),
|
||||||
{
|
{
|
||||||
key: 'edit',
|
|
||||||
label: 'Manage Environments',
|
label: 'Manage Environments',
|
||||||
hotKeyAction: 'environmentEditor.toggle',
|
hotKeyAction: 'environmentEditor.toggle',
|
||||||
leftSlot: <Icon icon="box" />,
|
leftSlot: <Icon icon="box" />,
|
||||||
onSelect: showEnvironmentDialog,
|
onSelect: () => editEnvironment(activeEnvironment),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[activeEnvironment?.id, subEnvironments, showEnvironmentDialog],
|
[subEnvironments, activeEnvironment],
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasBaseVars =
|
const hasBaseVars =
|
||||||
@@ -79,7 +67,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
|||||||
)}
|
)}
|
||||||
// If no environments, the button simply opens the dialog.
|
// If no environments, the button simply opens the dialog.
|
||||||
// NOTE: We don't create a new button because we want to reuse the hotkey from the menu items
|
// NOTE: We don't create a new button because we want to reuse the hotkey from the menu items
|
||||||
onClick={subEnvironments.length === 0 ? showEnvironmentDialog : undefined}
|
onClick={subEnvironments.length === 0 ? () => editEnvironment(null) : undefined}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
<EnvironmentColorIndicator environment={activeEnvironment ?? null} />
|
<EnvironmentColorIndicator environment={activeEnvironment ?? null} />
|
||||||
|
|||||||
@@ -1,42 +1,28 @@
|
|||||||
import type { Environment } from '@yaakapp-internal/models';
|
import type { Environment } from '@yaakapp-internal/models';
|
||||||
import { duplicateModel, patchModel } from '@yaakapp-internal/models';
|
import { duplicateModel, patchModel } from '@yaakapp-internal/models';
|
||||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { createEnvironmentAndActivate } from '../commands/createEnvironment';
|
import { createEnvironmentAndActivate } from '../commands/createEnvironment';
|
||||||
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
||||||
import { useIsEncryptionEnabled } from '../hooks/useIsEncryptionEnabled';
|
|
||||||
import { useKeyValue } from '../hooks/useKeyValue';
|
|
||||||
import { useRandomKey } from '../hooks/useRandomKey';
|
|
||||||
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
|
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
|
||||||
import { analyzeTemplate, convertTemplateToSecure } from '../lib/encryption';
|
import { isBaseEnvironment } from '../lib/model_util';
|
||||||
import { showPrompt } from '../lib/prompt';
|
import { showPrompt } from '../lib/prompt';
|
||||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||||
import {
|
|
||||||
setupOrConfigureEncryption,
|
|
||||||
withEncryptionEnabled,
|
|
||||||
} from '../lib/setupOrConfigureEncryption';
|
|
||||||
import { showColorPicker } from '../lib/showColorPicker';
|
import { showColorPicker } from '../lib/showColorPicker';
|
||||||
import { BadgeButton } from './core/BadgeButton';
|
|
||||||
import { Banner } from './core/Banner';
|
import { Banner } from './core/Banner';
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import { DismissibleBanner } from './core/DismissibleBanner';
|
|
||||||
import type { DropdownItem } from './core/Dropdown';
|
import type { DropdownItem } from './core/Dropdown';
|
||||||
import { ContextMenu } from './core/Dropdown';
|
import { ContextMenu } from './core/Dropdown';
|
||||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
|
||||||
import { Heading } from './core/Heading';
|
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
import { IconTooltip } from './core/IconTooltip';
|
import { IconTooltip } from './core/IconTooltip';
|
||||||
import { InlineCode } from './core/InlineCode';
|
import { InlineCode } from './core/InlineCode';
|
||||||
import type { PairWithId } from './core/PairEditor';
|
|
||||||
import { ensurePairId } from './core/PairEditor';
|
|
||||||
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
|
||||||
import { Separator } from './core/Separator';
|
import { Separator } from './core/Separator';
|
||||||
import { SplitLayout } from './core/SplitLayout';
|
import { SplitLayout } from './core/SplitLayout';
|
||||||
import { VStack } from './core/Stacks';
|
|
||||||
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
|
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
|
||||||
|
import { EnvironmentEditor } from './EnvironmentEditor';
|
||||||
|
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
initialEnvironment: Environment | null;
|
initialEnvironment: Environment | null;
|
||||||
@@ -97,13 +83,13 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
|||||||
<aside className="w-full min-w-0 pt-2">
|
<aside className="w-full min-w-0 pt-2">
|
||||||
<div className="min-w-0 h-full overflow-y-auto pt-1">
|
<div className="min-w-0 h-full overflow-y-auto pt-1">
|
||||||
{[baseEnvironment, ...otherBaseEnvironments].map((e) => (
|
{[baseEnvironment, ...otherBaseEnvironments].map((e) => (
|
||||||
<SidebarButton
|
<EnvironmentDialogSidebarButton
|
||||||
key={e.id}
|
key={e.id}
|
||||||
active={selectedEnvironment?.id == e.id}
|
active={selectedEnvironment?.id == e.id}
|
||||||
onClick={() => setSelectedEnvironmentId(e.id)}
|
onClick={() => setSelectedEnvironmentId(e.id)}
|
||||||
environment={e}
|
environment={e}
|
||||||
duplicateEnvironment={handleDuplicateEnvironment}
|
duplicateEnvironment={handleDuplicateEnvironment}
|
||||||
// Allow deleting base environment if there are multiples
|
// Allow deleting the base environment if there are multiples
|
||||||
deleteEnvironment={
|
deleteEnvironment={
|
||||||
otherBaseEnvironments.length > 0 ? handleDeleteEnvironment : null
|
otherBaseEnvironments.length > 0 ? handleDeleteEnvironment : null
|
||||||
}
|
}
|
||||||
@@ -121,7 +107,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{resolvedModelName(e)}
|
{resolvedModelName(e)}
|
||||||
</SidebarButton>
|
</EnvironmentDialogSidebarButton>
|
||||||
))}
|
))}
|
||||||
{subEnvironments.length > 0 && (
|
{subEnvironments.length > 0 && (
|
||||||
<div className="px-2">
|
<div className="px-2">
|
||||||
@@ -129,7 +115,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{subEnvironments.map((e) => (
|
{subEnvironments.map((e) => (
|
||||||
<SidebarButton
|
<EnvironmentDialogSidebarButton
|
||||||
key={e.id}
|
key={e.id}
|
||||||
active={selectedEnvironment?.id === e.id}
|
active={selectedEnvironment?.id === e.id}
|
||||||
environment={e}
|
environment={e}
|
||||||
@@ -139,7 +125,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
|||||||
deleteEnvironment={handleDeleteEnvironment}
|
deleteEnvironment={handleDeleteEnvironment}
|
||||||
>
|
>
|
||||||
{e.name}
|
{e.name}
|
||||||
</SidebarButton>
|
</EnvironmentDialogSidebarButton>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -153,7 +139,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<EnvironmentEditor
|
<EnvironmentEditor
|
||||||
className="pt-2 border-l border-border-subtle"
|
className="pl-4 pt-3 border-l border-border-subtle"
|
||||||
environment={selectedEnvironment}
|
environment={selectedEnvironment}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -162,139 +148,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const EnvironmentEditor = function ({
|
function EnvironmentDialogSidebarButton({
|
||||||
environment: selectedEnvironment,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
environment: Environment;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
const workspaceId = selectedEnvironment.workspaceId;
|
|
||||||
const isEncryptionEnabled = useIsEncryptionEnabled();
|
|
||||||
const valueVisibility = useKeyValue<boolean>({
|
|
||||||
namespace: 'global',
|
|
||||||
key: ['environmentValueVisibility', workspaceId],
|
|
||||||
fallback: false,
|
|
||||||
});
|
|
||||||
const { allEnvironments } = useEnvironmentsBreakdown();
|
|
||||||
const handleChange = useCallback(
|
|
||||||
(variables: PairWithId[]) => patchModel(selectedEnvironment, { variables }),
|
|
||||||
[selectedEnvironment],
|
|
||||||
);
|
|
||||||
const [forceUpdateKey, regenerateForceUpdateKey] = useRandomKey();
|
|
||||||
|
|
||||||
// Gather a list of env names from other environments to help the user get them aligned
|
|
||||||
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
|
|
||||||
const options: GenericCompletionOption[] = [];
|
|
||||||
if (selectedEnvironment.base) {
|
|
||||||
return { options };
|
|
||||||
}
|
|
||||||
|
|
||||||
const allVariables = allEnvironments.flatMap((e) => e?.variables);
|
|
||||||
const allVariableNames = new Set(allVariables.map((v) => v?.name));
|
|
||||||
for (const name of allVariableNames) {
|
|
||||||
const containingEnvs = allEnvironments.filter((e) =>
|
|
||||||
e.variables.some((v) => v.name === name),
|
|
||||||
);
|
|
||||||
const isAlreadyInActive = containingEnvs.find((e) => e.id === selectedEnvironment.id);
|
|
||||||
if (isAlreadyInActive) continue;
|
|
||||||
options.push({
|
|
||||||
label: name,
|
|
||||||
type: 'constant',
|
|
||||||
detail: containingEnvs.map((e) => e.name).join(', '),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return { options };
|
|
||||||
}, [selectedEnvironment.base, selectedEnvironment.id, allEnvironments]);
|
|
||||||
|
|
||||||
const validateName = useCallback((name: string) => {
|
|
||||||
// Empty just means the variable doesn't have a name yet and is unusable
|
|
||||||
if (name === '') return true;
|
|
||||||
return name.match(/^[a-z_][a-z0-9_-]*$/i) != null;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const valueType = !isEncryptionEnabled && valueVisibility.value ? 'text' : 'password';
|
|
||||||
const promptToEncrypt = useMemo(() => {
|
|
||||||
if (!isEncryptionEnabled) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return !selectedEnvironment.variables.every(
|
|
||||||
(v) => v.value === '' || analyzeTemplate(v.value) !== 'insecure',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [selectedEnvironment.variables, isEncryptionEnabled]);
|
|
||||||
|
|
||||||
const encryptEnvironment = (environment: Environment) => {
|
|
||||||
withEncryptionEnabled(async () => {
|
|
||||||
const encryptedVariables: PairWithId[] = [];
|
|
||||||
for (const variable of environment.variables) {
|
|
||||||
const value = variable.value ? await convertTemplateToSecure(variable.value) : '';
|
|
||||||
encryptedVariables.push(ensurePairId({ ...variable, value }));
|
|
||||||
}
|
|
||||||
await handleChange(encryptedVariables);
|
|
||||||
regenerateForceUpdateKey();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VStack space={4} className={classNames(className, 'pl-4')}>
|
|
||||||
<Heading className="w-full flex items-center gap-0.5">
|
|
||||||
<EnvironmentColorIndicator clickToEdit environment={selectedEnvironment ?? null} />
|
|
||||||
<div className="mr-2">{selectedEnvironment?.name}</div>
|
|
||||||
{isEncryptionEnabled ? (
|
|
||||||
promptToEncrypt ? (
|
|
||||||
<BadgeButton color="notice" onClick={() => encryptEnvironment(selectedEnvironment)}>
|
|
||||||
Encrypt All Variables
|
|
||||||
</BadgeButton>
|
|
||||||
) : (
|
|
||||||
<BadgeButton color="secondary" onClick={setupOrConfigureEncryption}>
|
|
||||||
Encryption Settings
|
|
||||||
</BadgeButton>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<BadgeButton color="secondary" onClick={() => valueVisibility.set((v) => !v)}>
|
|
||||||
{valueVisibility.value ? 'Hide Values' : 'Show Values'}
|
|
||||||
</BadgeButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Heading>
|
|
||||||
{selectedEnvironment.public && promptToEncrypt && (
|
|
||||||
<DismissibleBanner
|
|
||||||
id={`warn-unencrypted-${selectedEnvironment.id}`}
|
|
||||||
color="notice"
|
|
||||||
className="mr-3"
|
|
||||||
>
|
|
||||||
This environment is sharable. Ensure variable values are encrypted to avoid accidental
|
|
||||||
leaking of secrets during directory sync or data export.
|
|
||||||
</DismissibleBanner>
|
|
||||||
)}
|
|
||||||
<div className="h-full pr-2 pb-2 grid grid-rows-[minmax(0,1fr)] overflow-auto">
|
|
||||||
<PairOrBulkEditor
|
|
||||||
allowMultilineValues
|
|
||||||
preferenceName="environment"
|
|
||||||
nameAutocomplete={nameAutocomplete}
|
|
||||||
namePlaceholder="VAR_NAME"
|
|
||||||
nameValidate={validateName}
|
|
||||||
valueType={valueType}
|
|
||||||
valueAutocompleteVariables
|
|
||||||
valueAutocompleteFunctions
|
|
||||||
forceUpdateKey={`${selectedEnvironment.id}::${forceUpdateKey}`}
|
|
||||||
pairs={selectedEnvironment.variables}
|
|
||||||
onChange={handleChange}
|
|
||||||
stateKey={`environment.${selectedEnvironment.id}`}
|
|
||||||
forcedEnvironmentId={
|
|
||||||
// Editing the base environment should resolve variables using the active environment.
|
|
||||||
// Editing a sub environment should resolve variables as if it's the active environment
|
|
||||||
selectedEnvironment.base ? undefined : selectedEnvironment.id
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function SidebarButton({
|
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
active,
|
active,
|
||||||
@@ -359,7 +213,7 @@ function SidebarButton({
|
|||||||
{
|
{
|
||||||
label: 'Rename',
|
label: 'Rename',
|
||||||
leftSlot: <Icon icon="pencil" />,
|
leftSlot: <Icon icon="pencil" />,
|
||||||
hidden: environment.base,
|
hidden: isBaseEnvironment(environment),
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
const name = await showPrompt({
|
const name = await showPrompt({
|
||||||
id: 'rename-environment',
|
id: 'rename-environment',
|
||||||
@@ -392,23 +246,13 @@ function SidebarButton({
|
|||||||
{
|
{
|
||||||
label: environment.color ? 'Change Color' : 'Assign Color',
|
label: environment.color ? 'Change Color' : 'Assign Color',
|
||||||
leftSlot: <Icon icon="palette" />,
|
leftSlot: <Icon icon="palette" />,
|
||||||
hidden: environment.base,
|
hidden: isBaseEnvironment(environment),
|
||||||
onSelect: async () => showColorPicker(environment),
|
onSelect: async () => showColorPicker(environment),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `Make ${environment.public ? 'Private' : 'Sharable'}`,
|
label: `Make ${environment.public ? 'Private' : 'Sharable'}`,
|
||||||
leftSlot: <Icon icon={environment.public ? 'eye_closed' : 'eye'} />,
|
leftSlot: <Icon icon={environment.public ? 'eye_closed' : 'eye'} />,
|
||||||
rightSlot: (
|
rightSlot: <EnvironmentSharableTooltip />,
|
||||||
<IconTooltip
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
Sharable environments will be included in Directory Sync or data export. It is
|
|
||||||
recommended to encrypt all variable values within sharable environments to
|
|
||||||
prevent accidentally leaking secrets.
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
await patchModel(environment, { public: !environment.public });
|
await patchModel(environment, { public: !environment.public });
|
||||||
},
|
},
|
||||||
|
|||||||
169
src-web/components/EnvironmentEditor.tsx
Normal file
169
src-web/components/EnvironmentEditor.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import type { Environment } from '@yaakapp-internal/models';
|
||||||
|
import { patchModel } from '@yaakapp-internal/models';
|
||||||
|
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
||||||
|
import { useIsEncryptionEnabled } from '../hooks/useIsEncryptionEnabled';
|
||||||
|
import { useKeyValue } from '../hooks/useKeyValue';
|
||||||
|
import { useRandomKey } from '../hooks/useRandomKey';
|
||||||
|
import { analyzeTemplate, convertTemplateToSecure } from '../lib/encryption';
|
||||||
|
import { isBaseEnvironment } from '../lib/model_util';
|
||||||
|
import {
|
||||||
|
setupOrConfigureEncryption,
|
||||||
|
withEncryptionEnabled,
|
||||||
|
} from '../lib/setupOrConfigureEncryption';
|
||||||
|
import { BadgeButton } from './core/BadgeButton';
|
||||||
|
import { DismissibleBanner } from './core/DismissibleBanner';
|
||||||
|
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||||
|
import { Heading } from './core/Heading';
|
||||||
|
import type { PairWithId } from './core/PairEditor';
|
||||||
|
import { ensurePairId } from './core/PairEditor';
|
||||||
|
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
||||||
|
import { VStack } from './core/Stacks';
|
||||||
|
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
|
||||||
|
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
|
||||||
|
|
||||||
|
export function EnvironmentEditor({
|
||||||
|
environment: selectedEnvironment,
|
||||||
|
hideName,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
environment: Environment;
|
||||||
|
hideName?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const workspaceId = selectedEnvironment.workspaceId;
|
||||||
|
const isEncryptionEnabled = useIsEncryptionEnabled();
|
||||||
|
const valueVisibility = useKeyValue<boolean>({
|
||||||
|
namespace: 'global',
|
||||||
|
key: ['environmentValueVisibility', workspaceId],
|
||||||
|
fallback: false,
|
||||||
|
});
|
||||||
|
const { allEnvironments } = useEnvironmentsBreakdown();
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(variables: PairWithId[]) => patchModel(selectedEnvironment, { variables }),
|
||||||
|
[selectedEnvironment],
|
||||||
|
);
|
||||||
|
const [forceUpdateKey, regenerateForceUpdateKey] = useRandomKey();
|
||||||
|
|
||||||
|
// Gather a list of env names from other environments to help the user get them aligned
|
||||||
|
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
|
||||||
|
const options: GenericCompletionOption[] = [];
|
||||||
|
if (isBaseEnvironment(selectedEnvironment)) {
|
||||||
|
return { options };
|
||||||
|
}
|
||||||
|
|
||||||
|
const allVariables = allEnvironments.flatMap((e) => e?.variables);
|
||||||
|
const allVariableNames = new Set(allVariables.map((v) => v?.name));
|
||||||
|
for (const name of allVariableNames) {
|
||||||
|
const containingEnvs = allEnvironments.filter((e) =>
|
||||||
|
e.variables.some((v) => v.name === name),
|
||||||
|
);
|
||||||
|
const isAlreadyInActive = containingEnvs.find((e) => e.id === selectedEnvironment.id);
|
||||||
|
if (isAlreadyInActive) continue;
|
||||||
|
options.push({
|
||||||
|
label: name,
|
||||||
|
type: 'constant',
|
||||||
|
detail: containingEnvs.map((e) => e.name).join(', '),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { options };
|
||||||
|
}, [selectedEnvironment, allEnvironments]);
|
||||||
|
|
||||||
|
const validateName = useCallback((name: string) => {
|
||||||
|
// Empty just means the variable doesn't have a name yet and is unusable
|
||||||
|
if (name === '') return true;
|
||||||
|
return name.match(/^[a-z_][a-z0-9_-]*$/i) != null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const valueType = !isEncryptionEnabled && valueVisibility.value ? 'text' : 'password';
|
||||||
|
const allVariableAreEncrypted = useMemo(
|
||||||
|
() =>
|
||||||
|
selectedEnvironment.variables.every(
|
||||||
|
(v) => v.value === '' || analyzeTemplate(v.value) !== 'insecure',
|
||||||
|
),
|
||||||
|
[selectedEnvironment.variables],
|
||||||
|
);
|
||||||
|
|
||||||
|
const encryptEnvironment = (environment: Environment) => {
|
||||||
|
withEncryptionEnabled(async () => {
|
||||||
|
const encryptedVariables: PairWithId[] = [];
|
||||||
|
for (const variable of environment.variables) {
|
||||||
|
const value = variable.value ? await convertTemplateToSecure(variable.value) : '';
|
||||||
|
encryptedVariables.push(ensurePairId({ ...variable, value }));
|
||||||
|
}
|
||||||
|
await handleChange(encryptedVariables);
|
||||||
|
regenerateForceUpdateKey();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack space={4} className={className}>
|
||||||
|
<Heading className="w-full flex items-center gap-0.5">
|
||||||
|
<EnvironmentColorIndicator clickToEdit environment={selectedEnvironment ?? null} />
|
||||||
|
{!hideName && <div className="mr-2">{selectedEnvironment?.name}</div>}
|
||||||
|
{isEncryptionEnabled ? (
|
||||||
|
!allVariableAreEncrypted ? (
|
||||||
|
<BadgeButton color="notice" onClick={() => encryptEnvironment(selectedEnvironment)}>
|
||||||
|
Encrypt All Variables
|
||||||
|
</BadgeButton>
|
||||||
|
) : (
|
||||||
|
<BadgeButton color="secondary" onClick={setupOrConfigureEncryption}>
|
||||||
|
Encryption Settings
|
||||||
|
</BadgeButton>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<BadgeButton color="secondary" onClick={() => valueVisibility.set((v) => !v)}>
|
||||||
|
{valueVisibility.value ? 'Hide Values' : 'Show Values'}
|
||||||
|
</BadgeButton>
|
||||||
|
)}
|
||||||
|
<BadgeButton
|
||||||
|
color="secondary"
|
||||||
|
rightSlot={<EnvironmentSharableTooltip />}
|
||||||
|
onClick={async () => {
|
||||||
|
await patchModel(selectedEnvironment, { public: !selectedEnvironment.public });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedEnvironment.public ? 'Sharable' : 'Private'}
|
||||||
|
</BadgeButton>
|
||||||
|
</Heading>
|
||||||
|
{selectedEnvironment.public && (!isEncryptionEnabled || !allVariableAreEncrypted) && (
|
||||||
|
<DismissibleBanner
|
||||||
|
id={`warn-unencrypted-${selectedEnvironment.id}`}
|
||||||
|
color="notice"
|
||||||
|
className="mr-3"
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'Encrypt Variables',
|
||||||
|
onClick: () => encryptEnvironment(selectedEnvironment),
|
||||||
|
color: 'primary',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
This sharable environment contains plain-text secrets
|
||||||
|
</DismissibleBanner>
|
||||||
|
)}
|
||||||
|
<div className="h-full pr-2 pb-2 grid grid-rows-[minmax(0,1fr)] overflow-auto">
|
||||||
|
<PairOrBulkEditor
|
||||||
|
allowMultilineValues
|
||||||
|
preferenceName="environment"
|
||||||
|
nameAutocomplete={nameAutocomplete}
|
||||||
|
namePlaceholder="VAR_NAME"
|
||||||
|
nameValidate={validateName}
|
||||||
|
valueType={valueType}
|
||||||
|
valueAutocompleteVariables
|
||||||
|
valueAutocompleteFunctions
|
||||||
|
forceUpdateKey={`${selectedEnvironment.id}::${forceUpdateKey}`}
|
||||||
|
pairs={selectedEnvironment.variables}
|
||||||
|
onChange={handleChange}
|
||||||
|
stateKey={`environment.${selectedEnvironment.id}`}
|
||||||
|
forcedEnvironmentId={
|
||||||
|
// Editing the base environment should resolve variables using the active environment.
|
||||||
|
// Editing a sub environment should resolve variables as if it's the active environment
|
||||||
|
isBaseEnvironment(selectedEnvironment) ? undefined : selectedEnvironment.id
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src-web/components/EnvironmentSharableTooltip.tsx
Normal file
8
src-web/components/EnvironmentSharableTooltip.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { IconTooltip } from './core/IconTooltip';
|
||||||
|
|
||||||
|
export function EnvironmentSharableTooltip() {
|
||||||
|
return (
|
||||||
|
<IconTooltip content="Sharable environments are included in Directory Sync and data export." />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
import { foldersAtom, patchModel } from '@yaakapp-internal/models';
|
import { createWorkspaceModel, foldersAtom, patchModel } from '@yaakapp-internal/models';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useAuthTab } from '../hooks/useAuthTab';
|
import { useAuthTab } from '../hooks/useAuthTab';
|
||||||
|
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
||||||
import { useHeadersTab } from '../hooks/useHeadersTab';
|
import { useHeadersTab } from '../hooks/useHeadersTab';
|
||||||
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
||||||
|
import { Button } from './core/Button';
|
||||||
|
import { CountBadge } from './core/CountBadge';
|
||||||
import { Input } from './core/Input';
|
import { Input } from './core/Input';
|
||||||
|
import { Link } from './core/Link';
|
||||||
import { VStack } from './core/Stacks';
|
import { 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 { EmptyStateText } from './EmptyStateText';
|
||||||
|
import { EnvironmentEditor } from './EnvironmentEditor';
|
||||||
import { HeadersEditor } from './HeadersEditor';
|
import { HeadersEditor } from './HeadersEditor';
|
||||||
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
||||||
import { MarkdownEditor } from './MarkdownEditor';
|
import { MarkdownEditor } from './MarkdownEditor';
|
||||||
@@ -19,9 +25,10 @@ interface Props {
|
|||||||
|
|
||||||
const TAB_AUTH = 'auth';
|
const TAB_AUTH = 'auth';
|
||||||
const TAB_HEADERS = 'headers';
|
const TAB_HEADERS = 'headers';
|
||||||
|
const TAB_VARIABLES = 'variables';
|
||||||
const TAB_GENERAL = 'general';
|
const TAB_GENERAL = 'general';
|
||||||
|
|
||||||
export type FolderSettingsTab = typeof TAB_AUTH | typeof TAB_HEADERS | typeof TAB_GENERAL;
|
export type FolderSettingsTab = typeof TAB_AUTH | typeof TAB_HEADERS | typeof TAB_GENERAL | typeof TAB_VARIABLES;
|
||||||
|
|
||||||
export function FolderSettingsDialog({ folderId, tab }: Props) {
|
export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||||
const folders = useAtomValue(foldersAtom);
|
const folders = useAtomValue(foldersAtom);
|
||||||
@@ -30,6 +37,11 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
const authTab = useAuthTab(TAB_AUTH, folder);
|
const authTab = useAuthTab(TAB_AUTH, folder);
|
||||||
const headersTab = useHeadersTab(TAB_HEADERS, folder);
|
const headersTab = useHeadersTab(TAB_HEADERS, folder);
|
||||||
const inheritedHeaders = useInheritedHeaders(folder);
|
const inheritedHeaders = useInheritedHeaders(folder);
|
||||||
|
const environments = useEnvironmentsBreakdown();
|
||||||
|
const folderEnvironment = environments.allEnvironments.find(
|
||||||
|
(e) => e.parentModel === 'folder' && e.parentId === folderId,
|
||||||
|
);
|
||||||
|
const numVars = (folderEnvironment?.variables ?? []).filter((v) => v.name).length;
|
||||||
|
|
||||||
const tabs = useMemo<TabItem[]>(() => {
|
const tabs = useMemo<TabItem[]>(() => {
|
||||||
if (folder == null) return [];
|
if (folder == null) return [];
|
||||||
@@ -39,10 +51,15 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
value: TAB_GENERAL,
|
value: TAB_GENERAL,
|
||||||
label: 'General',
|
label: 'General',
|
||||||
},
|
},
|
||||||
...authTab,
|
|
||||||
...headersTab,
|
...headersTab,
|
||||||
|
...authTab,
|
||||||
|
{
|
||||||
|
value: TAB_VARIABLES,
|
||||||
|
label: 'Variables',
|
||||||
|
rightSlot: numVars > 0 ? <CountBadge count={numVars} /> : null,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}, [authTab, folder, headersTab]);
|
}, [authTab, folder, headersTab, numVars]);
|
||||||
|
|
||||||
if (folder == null) return null;
|
if (folder == null) return null;
|
||||||
|
|
||||||
@@ -85,6 +102,38 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
stateKey={`headers.${folder.id}`}
|
stateKey={`headers.${folder.id}`}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
<TabContent value={TAB_VARIABLES} className="pt-3 overflow-y-auto h-full px-4">
|
||||||
|
{folderEnvironment == null ? (
|
||||||
|
<EmptyStateText>
|
||||||
|
<VStack alignItems="center" space={1.5}>
|
||||||
|
<p>
|
||||||
|
Override{' '}
|
||||||
|
<Link href="https://feedback.yaak.app/help/articles/3284139-environments-and-variables">
|
||||||
|
Variables
|
||||||
|
</Link>{' '}
|
||||||
|
for requests within this folder.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="border"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
await createWorkspaceModel({
|
||||||
|
workspaceId: folder.workspaceId,
|
||||||
|
parentModel: 'folder',
|
||||||
|
parentId: folder.id,
|
||||||
|
model: 'environment',
|
||||||
|
name: 'Folder Environment',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create Folder Environment
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</EmptyStateText>
|
||||||
|
) : (
|
||||||
|
<EnvironmentEditor hideName environment={folderEnvironment} />
|
||||||
|
)}
|
||||||
|
</TabContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
|||||||
const authConfig = useHttpAuthenticationConfig(
|
const authConfig = useHttpAuthenticationConfig(
|
||||||
model.authenticationType,
|
model.authenticationType,
|
||||||
model.authentication,
|
model.authentication,
|
||||||
model.id,
|
model,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
|||||||
value: TAB_DATA,
|
value: TAB_DATA,
|
||||||
label: 'Directory Sync',
|
label: 'Directory Sync',
|
||||||
},
|
},
|
||||||
...authTab,
|
|
||||||
...headersTab,
|
...headersTab,
|
||||||
|
...authTab,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<TabContent value={TAB_AUTH} className="overflow-y-auto h-full px-4">
|
<TabContent value={TAB_AUTH} className="overflow-y-auto h-full px-4">
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
|
import type { Color } from '@yaakapp-internal/plugins';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useKeyValue } from '../../hooks/useKeyValue';
|
import { useKeyValue } from '../../hooks/useKeyValue';
|
||||||
import type { BannerProps } from './Banner';
|
import type { BannerProps } from './Banner';
|
||||||
import { Banner } from './Banner';
|
import { Banner } from './Banner';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
|
import { HStack } from './Stacks';
|
||||||
|
|
||||||
export function DismissibleBanner({
|
export function DismissibleBanner({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
id,
|
id,
|
||||||
|
actions,
|
||||||
...props
|
...props
|
||||||
}: BannerProps & { id: string }) {
|
}: BannerProps & { id: string; actions?: { label: string; onClick: () => void; color?: Color }[] }) {
|
||||||
const { set: setDismissed, value: dismissed } = useKeyValue<boolean>({
|
const { set: setDismissed, value: dismissed } = useKeyValue<boolean>({
|
||||||
namespace: 'global',
|
namespace: 'global',
|
||||||
key: ['dismiss-banner', id],
|
key: ['dismiss-banner', id],
|
||||||
@@ -19,17 +22,34 @@ export function DismissibleBanner({
|
|||||||
if (dismissed) return null;
|
if (dismissed) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Banner className={classNames(className, 'relative grid grid-cols-[1fr_auto] gap-3')} {...props}>
|
<Banner
|
||||||
|
className={classNames(className, 'relative grid grid-cols-[1fr_auto] gap-3')}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
<Button
|
<HStack space={1.5}>
|
||||||
variant="border"
|
{actions?.map((a, i) => (
|
||||||
color={props.color}
|
<Button
|
||||||
size="xs"
|
key={a.label + i}
|
||||||
onClick={() => setDismissed((d) => !d)}
|
variant="border"
|
||||||
title="Dismiss message"
|
color={a.color ?? props.color}
|
||||||
>
|
size="xs"
|
||||||
Dismiss
|
onClick={a.onClick}
|
||||||
</Button>
|
title="Dismiss message"
|
||||||
|
>
|
||||||
|
{a.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="border"
|
||||||
|
color={props.color}
|
||||||
|
size="xs"
|
||||||
|
onClick={() => setDismissed((d) => !d)}
|
||||||
|
title="Dismiss message"
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
</Banner>
|
</Banner>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -519,7 +519,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
'x-theme-menu',
|
'x-theme-menu',
|
||||||
'outline-none my-1 pointer-events-auto fixed z-50',
|
'outline-none my-1 pointer-events-auto fixed z-40',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{showTriangle && (
|
{showTriangle && (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { emacs } from '@replit/codemirror-emacs';
|
|||||||
import { vim } from '@replit/codemirror-vim';
|
import { vim } from '@replit/codemirror-vim';
|
||||||
|
|
||||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||||
import type { EditorKeymap, EnvironmentVariable } from '@yaakapp-internal/models';
|
import type { EditorKeymap } from '@yaakapp-internal/models';
|
||||||
import { settingsAtom } from '@yaakapp-internal/models';
|
import { settingsAtom } from '@yaakapp-internal/models';
|
||||||
import type { EditorLanguage, TemplateFunction } from '@yaakapp-internal/plugins';
|
import type { EditorLanguage, TemplateFunction } from '@yaakapp-internal/plugins';
|
||||||
import { parseTemplate } from '@yaakapp-internal/templates';
|
import { parseTemplate } from '@yaakapp-internal/templates';
|
||||||
@@ -28,10 +28,12 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { activeEnvironmentIdAtom } from '../../../hooks/useActiveEnvironment';
|
import { activeEnvironmentIdAtom } from '../../../hooks/useActiveEnvironment';
|
||||||
|
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
|
||||||
import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables';
|
import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables';
|
||||||
import { useRequestEditor } from '../../../hooks/useRequestEditor';
|
import { useRequestEditor } from '../../../hooks/useRequestEditor';
|
||||||
import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions';
|
import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions';
|
||||||
import { showDialog } from '../../../lib/dialog';
|
import { showDialog } from '../../../lib/dialog';
|
||||||
|
import { editEnvironment } from '../../../lib/editEnvironment';
|
||||||
import { tryFormatJson, tryFormatXml } from '../../../lib/formatters';
|
import { tryFormatJson, tryFormatXml } from '../../../lib/formatters';
|
||||||
import { withEncryptionEnabled } from '../../../lib/setupOrConfigureEncryption';
|
import { withEncryptionEnabled } from '../../../lib/setupOrConfigureEncryption';
|
||||||
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
|
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
|
||||||
@@ -96,7 +98,7 @@ export interface EditorProps {
|
|||||||
|
|
||||||
const stateFields = { history: historyField, folds: foldState };
|
const stateFields = { history: historyField, folds: foldState };
|
||||||
|
|
||||||
const emptyVariables: EnvironmentVariable[] = [];
|
const emptyVariables: WrappedEnvironmentVariable[] = [];
|
||||||
const emptyExtension: Extension = [];
|
const emptyExtension: Extension = [];
|
||||||
|
|
||||||
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||||
@@ -306,24 +308,9 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onClickVariable = useCallback(
|
const onClickVariable = useCallback(
|
||||||
async (_v: EnvironmentVariable, tagValue: string, startPos: number) => {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const initialTokens = parseTemplate(tagValue);
|
async (v: WrappedEnvironmentVariable, _tagValue: string, _startPos: number) => {
|
||||||
showDialog({
|
editEnvironment(v.environment);
|
||||||
size: 'dynamic',
|
|
||||||
id: 'template-variable',
|
|
||||||
title: 'Change Variable',
|
|
||||||
render: ({ hide }) => (
|
|
||||||
<TemplateVariableDialog
|
|
||||||
hide={hide}
|
|
||||||
initialTokens={initialTokens}
|
|
||||||
onChange={(insert) => {
|
|
||||||
cm.current?.view.dispatch({
|
|
||||||
changes: [{ from: startPos, to: startPos + tagValue.length, insert }],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ import {
|
|||||||
rectangularSelection,
|
rectangularSelection,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { tags as t } from '@lezer/highlight';
|
import { tags as t } from '@lezer/highlight';
|
||||||
import type { EnvironmentVariable } from '@yaakapp-internal/models';
|
|
||||||
import { graphql } from 'cm6-graphql';
|
import { graphql } from 'cm6-graphql';
|
||||||
import type { GraphQLSchema } from 'graphql';
|
import type { GraphQLSchema } from 'graphql';
|
||||||
import { activeRequestIdAtom } from '../../../hooks/useActiveRequestId';
|
import { activeRequestIdAtom } from '../../../hooks/useActiveRequestId';
|
||||||
|
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
|
||||||
import { jotaiStore } from '../../../lib/jotai';
|
import { jotaiStore } from '../../../lib/jotai';
|
||||||
import { renderMarkdown } from '../../../lib/markdown';
|
import { renderMarkdown } from '../../../lib/markdown';
|
||||||
import { pluralizeCount } from '../../../lib/pluralize';
|
import { pluralizeCount } from '../../../lib/pluralize';
|
||||||
@@ -110,8 +110,8 @@ export function getLanguageExtension({
|
|||||||
graphQLSchema,
|
graphQLSchema,
|
||||||
}: {
|
}: {
|
||||||
useTemplating: boolean;
|
useTemplating: boolean;
|
||||||
environmentVariables: EnvironmentVariable[];
|
environmentVariables: WrappedEnvironmentVariable[];
|
||||||
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
|
onClickVariable: (option: WrappedEnvironmentVariable, tagValue: string, startPos: number) => void;
|
||||||
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
|
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
|
||||||
onClickPathParameter: (name: string) => void;
|
onClickPathParameter: (name: string) => void;
|
||||||
completionOptions: TwigCompletionOption[];
|
completionOptions: TwigCompletionOption[];
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { LanguageSupport } from '@codemirror/language';
|
|||||||
import { LRLanguage } from '@codemirror/language';
|
import { LRLanguage } from '@codemirror/language';
|
||||||
import type { Extension } from '@codemirror/state';
|
import type { Extension } from '@codemirror/state';
|
||||||
import { parseMixed } from '@lezer/common';
|
import { parseMixed } from '@lezer/common';
|
||||||
import type { EnvironmentVariable } from '@yaakapp-internal/models';
|
import type { WrappedEnvironmentVariable } from '../../../../hooks/useEnvironmentVariables';
|
||||||
import type { GenericCompletionConfig } from '../genericCompletion';
|
import type { GenericCompletionConfig } from '../genericCompletion';
|
||||||
import { genericCompletion } from '../genericCompletion';
|
import { genericCompletion } from '../genericCompletion';
|
||||||
import { textLanguage } from '../text/extension';
|
import { textLanguage } from '../text/extension';
|
||||||
@@ -21,10 +21,10 @@ export function twig({
|
|||||||
extraExtensions,
|
extraExtensions,
|
||||||
}: {
|
}: {
|
||||||
base: LanguageSupport;
|
base: LanguageSupport;
|
||||||
environmentVariables: EnvironmentVariable[];
|
environmentVariables: WrappedEnvironmentVariable[];
|
||||||
completionOptions: TwigCompletionOption[];
|
completionOptions: TwigCompletionOption[];
|
||||||
autocomplete?: GenericCompletionConfig;
|
autocomplete?: GenericCompletionConfig;
|
||||||
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
|
onClickVariable: (option: WrappedEnvironmentVariable, tagValue: string, startPos: number) => void;
|
||||||
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
|
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
|
||||||
onClickPathParameter: (name: string) => void;
|
onClickPathParameter: (name: string) => void;
|
||||||
extraExtensions: Extension[];
|
extraExtensions: Extension[];
|
||||||
@@ -33,9 +33,11 @@ export function twig({
|
|||||||
|
|
||||||
const variableOptions: TwigCompletionOption[] =
|
const variableOptions: TwigCompletionOption[] =
|
||||||
environmentVariables.map((v) => ({
|
environmentVariables.map((v) => ({
|
||||||
...v,
|
name: v.variable.name,
|
||||||
|
value: v.variable.value,
|
||||||
type: 'variable',
|
type: 'variable',
|
||||||
label: v.name,
|
label: v.variable.name,
|
||||||
|
description: `Inherited from ${v.source}`,
|
||||||
onClick: (rawTag: string, startPos: number) => onClickVariable(v, rawTag, startPos),
|
onClick: (rawTag: string, startPos: number) => onClickVariable(v, rawTag, startPos),
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
|
|||||||
@@ -94,13 +94,16 @@ function templateTags(
|
|||||||
(o) => o.name === name || (o.type === 'function' && o.aliases?.includes(name)),
|
(o) => o.name === name || (o.type === 'function' && o.aliases?.includes(name)),
|
||||||
);
|
);
|
||||||
if (option == null) {
|
if (option == null) {
|
||||||
|
const from = node.from; // Cache here so the reference doesn't change
|
||||||
option = {
|
option = {
|
||||||
invalid: true,
|
invalid: true,
|
||||||
type: 'variable',
|
type: 'variable',
|
||||||
name: inner,
|
name: inner,
|
||||||
value: null,
|
value: null,
|
||||||
label: inner,
|
label: inner,
|
||||||
onClick: () => onClickMissingVariable(name, rawTag, node.from),
|
onClick: () => {
|
||||||
|
onClickMissingVariable(name, rawTag, from);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CsvViewer({ response, className }: Props) {
|
export function CsvViewer({ response, className }: Props) {
|
||||||
const body = useResponseBodyText(response);
|
const body = useResponseBodyText({ response, filter: null });
|
||||||
|
|
||||||
const parsed = useMemo(() => {
|
const parsed = useMemo(() => {
|
||||||
if (body.data == null) return null;
|
if (body.data == null) return null;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Props) {
|
export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Props) {
|
||||||
const rawTextBody = useResponseBodyText({ responseId: response.id, filter: null });
|
const rawTextBody = useResponseBodyText({ response, filter: null });
|
||||||
const contentType = getContentTypeFromHeaders(response.headers);
|
const contentType = getContentTypeFromHeaders(response.headers);
|
||||||
const language = languageFromContentType(contentType, rawTextBody.data ?? '');
|
const language = languageFromContentType(contentType, rawTextBody.data ?? '');
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Prop
|
|||||||
text={rawTextBody.data}
|
text={rawTextBody.data}
|
||||||
pretty={pretty}
|
pretty={pretty}
|
||||||
className={textViewerClassName}
|
className={textViewerClassName}
|
||||||
responseId={response.id}
|
response={response}
|
||||||
requestId={response.requestId}
|
requestId={response.requestId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function JsonViewer({ response, className }: Props) {
|
export function JsonViewer({ response, className }: Props) {
|
||||||
const rawBody = useResponseBodyText(response);
|
const rawBody = useResponseBodyText({ response, filter: null });
|
||||||
|
|
||||||
if (rawBody.isLoading || rawBody.data == null) return null;
|
if (rawBody.isLoading || rawBody.data == null) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SvgViewer({ response }: Props) {
|
export function SvgViewer({ response }: Props) {
|
||||||
const rawTextBody = useResponseBodyText(response);
|
const rawTextBody = useResponseBodyText({ response, filter: null });
|
||||||
const [src, setSrc] = useState<string | null>(null);
|
const [src, setSrc] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
@@ -18,13 +19,13 @@ interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
text: string;
|
text: string;
|
||||||
language: EditorProps['language'];
|
language: EditorProps['language'];
|
||||||
responseId: string;
|
response: HttpResponse;
|
||||||
requestId: string;
|
requestId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useFilterText = createGlobalState<Record<string, string | null>>({});
|
const useFilterText = createGlobalState<Record<string, string | null>>({});
|
||||||
|
|
||||||
export function TextViewer({ language, text, responseId, requestId, pretty, className }: Props) {
|
export function TextViewer({ language, text, response, requestId, pretty, className }: Props) {
|
||||||
const [filterTextMap, setFilterTextMap] = useFilterText();
|
const [filterTextMap, setFilterTextMap] = useFilterText();
|
||||||
const filterText = filterTextMap[requestId] ?? null;
|
const filterText = filterTextMap[requestId] ?? null;
|
||||||
const debouncedFilterText = useDebouncedValue(filterText);
|
const debouncedFilterText = useDebouncedValue(filterText);
|
||||||
@@ -36,7 +37,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isSearching = filterText != null;
|
const isSearching = filterText != null;
|
||||||
const filteredResponse = useResponseBodyText({ responseId, filter: debouncedFilterText ?? null });
|
const filteredResponse = useResponseBodyText({ response, filter: debouncedFilterText ?? null });
|
||||||
|
|
||||||
const toggleSearch = useCallback(() => {
|
const toggleSearch = useCallback(() => {
|
||||||
if (isSearching) {
|
if (isSearching) {
|
||||||
@@ -69,7 +70,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
|
|||||||
defaultValue={filterText}
|
defaultValue={filterText}
|
||||||
onKeyDown={(e) => e.key === 'Escape' && toggleSearch()}
|
onKeyDown={(e) => e.key === 'Escape' && toggleSearch()}
|
||||||
onChange={setFilterText}
|
onChange={setFilterText}
|
||||||
stateKey={`filter.${responseId}`}
|
stateKey={`filter.${response.id}`}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
@@ -96,7 +97,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
|
|||||||
isSearching,
|
isSearching,
|
||||||
language,
|
language,
|
||||||
requestId,
|
requestId,
|
||||||
responseId,
|
response,
|
||||||
setFilterText,
|
setFilterText,
|
||||||
toggleSearch,
|
toggleSearch,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface Props {
|
|||||||
|
|
||||||
export function WebPageViewer({ response }: Props) {
|
export function WebPageViewer({ response }: Props) {
|
||||||
const { url } = response;
|
const { url } = response;
|
||||||
const body = useResponseBodyText(response).data ?? '';
|
const body = useResponseBodyText({ response, filter: null }).data ?? '';
|
||||||
|
|
||||||
const contentForIframe: string | undefined = useMemo(() => {
|
const contentForIframe: string | undefined = useMemo(() => {
|
||||||
if (body.includes('<head>')) {
|
if (body.includes('<head>')) {
|
||||||
|
|||||||
@@ -37,11 +37,6 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
|||||||
const items = useMemo((): DropdownItem[] => {
|
const items = useMemo((): DropdownItem[] => {
|
||||||
if (child.model === 'folder') {
|
if (child.model === 'folder') {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
label: 'Send All',
|
|
||||||
leftSlot: <Icon icon="send_horizontal" />,
|
|
||||||
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.id)),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
leftSlot: <Icon icon="settings" />,
|
leftSlot: <Icon icon="settings" />,
|
||||||
@@ -52,6 +47,11 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
|||||||
leftSlot: <Icon icon="copy" />,
|
leftSlot: <Icon icon="copy" />,
|
||||||
onSelect: () => duplicateModelById(child.model, child.id),
|
onSelect: () => duplicateModelById(child.model, child.id),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Send All',
|
||||||
|
leftSlot: <Icon icon="send_horizontal" />,
|
||||||
|
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.id)),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ import { useEnvironmentVariables } from './useEnvironmentVariables';
|
|||||||
|
|
||||||
export function useActiveEnvironmentVariables() {
|
export function useActiveEnvironmentVariables() {
|
||||||
const activeEnvironment = useAtomValue(activeEnvironmentAtom);
|
const activeEnvironment = useAtomValue(activeEnvironmentAtom);
|
||||||
return useEnvironmentVariables(activeEnvironment?.id ?? null);
|
return useEnvironmentVariables(activeEnvironment?.id ?? null).map((v) => v.variable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function useCopyHttpResponse(response: HttpResponse) {
|
|||||||
return useFastMutation({
|
return useFastMutation({
|
||||||
mutationKey: ['copy_http_response', response.id],
|
mutationKey: ['copy_http_response', response.id],
|
||||||
async mutationFn() {
|
async mutationFn() {
|
||||||
const body = await getResponseBodyText(response);
|
const body = await getResponseBodyText({ responseId: response.id, filter: null });
|
||||||
copyToClipboard(body);
|
copyToClipboard(body);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
10
src-web/hooks/useEnvironmentValueVisibility.ts
Normal file
10
src-web/hooks/useEnvironmentValueVisibility.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Environment } from '@yaakapp-internal/models';
|
||||||
|
import { useKeyValue } from './useKeyValue';
|
||||||
|
|
||||||
|
export function useEnvironmentValueVisibility(environment: Environment) {
|
||||||
|
return useKeyValue<boolean>({
|
||||||
|
namespace: 'global',
|
||||||
|
key: ['environmentValueVisibility', environment.workspaceId],
|
||||||
|
fallback: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,25 +1,52 @@
|
|||||||
import type { EnvironmentVariable } from '@yaakapp-internal/models';
|
import type { Environment, EnvironmentVariable } from '@yaakapp-internal/models';
|
||||||
import { environmentsAtom } from '@yaakapp-internal/models';
|
import { foldersAtom } from '@yaakapp-internal/models';
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { jotaiStore } from '../lib/jotai';
|
||||||
|
import { useActiveRequest } from './useActiveRequest';
|
||||||
import { useEnvironmentsBreakdown } from './useEnvironmentsBreakdown';
|
import { useEnvironmentsBreakdown } from './useEnvironmentsBreakdown';
|
||||||
|
import { useParentFolders } from './useParentFolders';
|
||||||
|
|
||||||
export function useEnvironmentVariables(environmentId: string | null) {
|
export function useEnvironmentVariables(environmentId: string | null) {
|
||||||
const { baseEnvironment } = useEnvironmentsBreakdown();
|
const { baseEnvironment, folderEnvironments, subEnvironments } = useEnvironmentsBreakdown();
|
||||||
const activeEnvironment =
|
const activeEnvironment = subEnvironments.find((e) => e.id === environmentId) ?? null;
|
||||||
useAtomValue(environmentsAtom).find((e) => e.id === environmentId) ?? null;
|
const activeRequest = useActiveRequest();
|
||||||
|
const parentFolders = useParentFolders(activeRequest);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const varMap: Record<string, EnvironmentVariable> = {};
|
const varMap: Record<string, WrappedEnvironmentVariable> = {};
|
||||||
|
const folderVariables = parentFolders.flatMap((f) =>
|
||||||
|
wrapVariables(folderEnvironments.find((fe) => fe.parentId === f.id) ?? null),
|
||||||
|
);
|
||||||
|
|
||||||
const allVariables = [
|
const allVariables = [
|
||||||
...(baseEnvironment?.variables ?? []),
|
...folderVariables,
|
||||||
...(activeEnvironment?.variables ?? []),
|
...wrapVariables(activeEnvironment),
|
||||||
|
...wrapVariables(baseEnvironment),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const v of allVariables) {
|
for (const v of allVariables) {
|
||||||
if (!v.enabled || !v.name) continue;
|
if (!v.variable.enabled || !v.variable.name || v.variable.name in varMap) {
|
||||||
varMap[v.name] = v;
|
continue;
|
||||||
|
}
|
||||||
|
varMap[v.variable.name] = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.values(varMap);
|
return Object.values(varMap);
|
||||||
}, [activeEnvironment, baseEnvironment]);
|
}, [activeEnvironment, baseEnvironment, folderEnvironments, parentFolders]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WrappedEnvironmentVariable {
|
||||||
|
variable: EnvironmentVariable;
|
||||||
|
environment: Environment;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapVariables(e: Environment | null): WrappedEnvironmentVariable[] {
|
||||||
|
if (e == null) return [];
|
||||||
|
const folders = jotaiStore.get(foldersAtom);
|
||||||
|
return e.variables.map((v) => {
|
||||||
|
const folder = e.parentModel === 'folder' ? folders.find((f) => f.id === e.parentId) : null;
|
||||||
|
const source = folder?.name ?? e.name;
|
||||||
|
return { variable: v, environment: e, source };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import { useMemo } from 'react';
|
|||||||
export function useEnvironmentsBreakdown() {
|
export function useEnvironmentsBreakdown() {
|
||||||
const allEnvironments = useAtomValue(environmentsAtom);
|
const allEnvironments = useAtomValue(environmentsAtom);
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const baseEnvironments = allEnvironments.filter((e) => e.base) ?? [];
|
const baseEnvironments = allEnvironments.filter((e) => e.parentId == null) ?? [];
|
||||||
const subEnvironments = allEnvironments.filter((e) => !e.base) ?? [];
|
const subEnvironments =
|
||||||
|
allEnvironments.filter((e) => e.parentModel === 'environment' && e.parentId != null) ?? [];
|
||||||
|
const folderEnvironments =
|
||||||
|
allEnvironments.filter((e) => e.parentModel === 'folder' && e.parentId != null) ?? [];
|
||||||
|
|
||||||
const baseEnvironment = baseEnvironments[0] ?? null;
|
const baseEnvironment = baseEnvironments[0] ?? null;
|
||||||
const otherBaseEnvironments =
|
const otherBaseEnvironments =
|
||||||
baseEnvironments.filter((e) => e.id !== baseEnvironment?.id) ?? [];
|
baseEnvironments.filter((e) => e.id !== baseEnvironment?.id) ?? [];
|
||||||
return { allEnvironments, baseEnvironment, subEnvironments, otherBaseEnvironments };
|
return { allEnvironments, baseEnvironment, subEnvironments, folderEnvironments, otherBaseEnvironments };
|
||||||
}, [allEnvironments]);
|
}, [allEnvironments]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const hotkeys: Record<HotkeyAction, string[]> = {
|
|||||||
'request_switcher.prev': ['Control+Tab'],
|
'request_switcher.prev': ['Control+Tab'],
|
||||||
'request_switcher.toggle': ['CmdCtrl+p'],
|
'request_switcher.toggle': ['CmdCtrl+p'],
|
||||||
'settings.show': ['CmdCtrl+,'],
|
'settings.show': ['CmdCtrl+,'],
|
||||||
'sidebar.delete_selected_item': ['Backspace'],
|
'sidebar.delete_selected_item': ['Delete'],
|
||||||
'sidebar.focus': ['CmdCtrl+b'],
|
'sidebar.focus': ['CmdCtrl+b'],
|
||||||
'url_bar.focus': ['CmdCtrl+l'],
|
'url_bar.focus': ['CmdCtrl+l'],
|
||||||
'workspace_settings.show': ['CmdCtrl+;'],
|
'workspace_settings.show': ['CmdCtrl+;'],
|
||||||
@@ -98,7 +98,7 @@ export function useHotKey(
|
|||||||
|
|
||||||
// Don't add key if not holding modifier
|
// Don't add key if not holding modifier
|
||||||
const isValidKeymapKey =
|
const isValidKeymapKey =
|
||||||
e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.key === 'Backspace';
|
e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.key === 'Backspace' || e.key === 'Delete';
|
||||||
if (!isValidKeymapKey) {
|
if (!isValidKeymapKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { activeWorkspaceIdAtom } from './useActiveWorkspace';
|
|||||||
export function useHttpAuthenticationConfig(
|
export function useHttpAuthenticationConfig(
|
||||||
authName: string | null,
|
authName: string | null,
|
||||||
values: Record<string, JsonPrimitive>,
|
values: Record<string, JsonPrimitive>,
|
||||||
requestId: string,
|
request: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace,
|
||||||
) {
|
) {
|
||||||
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
|
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
|
||||||
const environmentId = useAtomValue(activeEnvironmentIdAtom);
|
const environmentId = useAtomValue(activeEnvironmentIdAtom);
|
||||||
@@ -37,7 +37,7 @@ export function useHttpAuthenticationConfig(
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'http_authentication_config',
|
'http_authentication_config',
|
||||||
requestId,
|
request,
|
||||||
authName,
|
authName,
|
||||||
values,
|
values,
|
||||||
responseKey,
|
responseKey,
|
||||||
@@ -53,8 +53,7 @@ export function useHttpAuthenticationConfig(
|
|||||||
{
|
{
|
||||||
authName,
|
authName,
|
||||||
values,
|
values,
|
||||||
requestId,
|
request,
|
||||||
workspaceId,
|
|
||||||
environmentId,
|
environmentId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -63,17 +62,16 @@ export function useHttpAuthenticationConfig(
|
|||||||
...config,
|
...config,
|
||||||
actions: config.actions?.map((a, i) => ({
|
actions: config.actions?.map((a, i) => ({
|
||||||
...a,
|
...a,
|
||||||
call: async ({
|
call: async (
|
||||||
id: modelId,
|
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace,
|
||||||
}: 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,
|
||||||
modelId,
|
model,
|
||||||
environmentId,
|
environmentId,
|
||||||
workspaceId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ensure the config is refreshed after the action is done
|
// Ensure the config is refreshed after the action is done
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function useIntrospectGraphQL(
|
|||||||
return setError(response.error);
|
return setError(response.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bodyText = await getResponseBodyText(response);
|
const bodyText = await getResponseBodyText({ responseId: response.id, filter: null });
|
||||||
if (response.status < 200 || response.status >= 300) {
|
if (response.status < 200 || response.status >= 300) {
|
||||||
return setError(
|
return setError(
|
||||||
`Request failed with status ${response.status}.\nThe response text is:\n\n${bodyText}`,
|
`Request failed with status ${response.status}.\nThe response text is:\n\n${bodyText}`,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {useAtomValue} from "jotai/index";
|
import { useAtomValue } from 'jotai';
|
||||||
import {activeWorkspaceMetaAtom} from "./useActiveWorkspace";
|
import { activeWorkspaceMetaAtom } from './useActiveWorkspace';
|
||||||
|
|
||||||
export function useIsEncryptionEnabled() {
|
export function useIsEncryptionEnabled() {
|
||||||
const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
|
const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
|
||||||
return workspaceMeta?.encryptionKey != null;
|
return workspaceMeta?.encryptionKey != null;
|
||||||
}
|
}
|
||||||
|
|||||||
24
src-web/hooks/useParentFolders.ts
Normal file
24
src-web/hooks/useParentFolders.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
|
||||||
|
import { foldersAtom } from '@yaakapp-internal/models';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export function useParentFolders(m: Folder | HttpRequest | GrpcRequest | WebsocketRequest | null) {
|
||||||
|
const folders = useAtomValue(foldersAtom);
|
||||||
|
|
||||||
|
return useMemo(() => getParentFolders(folders, m), [folders, m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParentFolders(
|
||||||
|
folders: Folder[],
|
||||||
|
currentModel: Folder | HttpRequest | GrpcRequest | WebsocketRequest | null,
|
||||||
|
): Folder[] {
|
||||||
|
if (currentModel == null) return [];
|
||||||
|
|
||||||
|
const folder = currentModel.folderId ? folders.find((f) => f.id === currentModel.folderId) : null;
|
||||||
|
if (folder == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [folder, ...getParentFolders(folders, folder)];
|
||||||
|
}
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||||
import { getResponseBodyText } from '../lib/responseBody';
|
import { getResponseBodyText } from '../lib/responseBody';
|
||||||
|
|
||||||
export function useResponseBodyText({
|
export function useResponseBodyText({
|
||||||
responseId,
|
response,
|
||||||
filter,
|
filter,
|
||||||
}: {
|
}: {
|
||||||
responseId: string;
|
response: HttpResponse;
|
||||||
filter: string | null;
|
filter: string | null;
|
||||||
}) {
|
}) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['response_body_text', responseId, filter ?? ''],
|
queryKey: [
|
||||||
queryFn: () => getResponseBodyText({ responseId, filter }),
|
'response_body_text',
|
||||||
|
response.id,
|
||||||
|
response.updatedAt,
|
||||||
|
response.contentLength,
|
||||||
|
filter ?? '',
|
||||||
|
],
|
||||||
|
queryFn: () => getResponseBodyText({ responseId: response.id, filter }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
18
src-web/lib/editEnvironment.tsx
Normal file
18
src-web/lib/editEnvironment.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { Environment } from '@yaakapp-internal/models';
|
||||||
|
import { openFolderSettings } from '../commands/openFolderSettings';
|
||||||
|
import { EnvironmentEditDialog } from '../components/EnvironmentEditDialog';
|
||||||
|
import { toggleDialog } from './dialog';
|
||||||
|
|
||||||
|
export function editEnvironment(environment: Environment | null) {
|
||||||
|
if (environment?.parentModel === 'folder' && environment.parentId != null) {
|
||||||
|
openFolderSettings(environment.parentId, 'variables');
|
||||||
|
} else {
|
||||||
|
toggleDialog({
|
||||||
|
id: 'environment-editor',
|
||||||
|
noPadding: true,
|
||||||
|
size: 'lg',
|
||||||
|
className: 'h-[80vh]',
|
||||||
|
render: () => <EnvironmentEditDialog initialEnvironment={environment} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AnyModel, Cookie, HttpResponseHeader } from '@yaakapp-internal/models';
|
import type { AnyModel, Cookie, Environment, HttpResponseHeader } from '@yaakapp-internal/models';
|
||||||
import { getMimeTypeFromContentType } from './contentType';
|
import { getMimeTypeFromContentType } from './contentType';
|
||||||
|
|
||||||
export const BODY_TYPE_NONE = null;
|
export const BODY_TYPE_NONE = null;
|
||||||
@@ -47,3 +47,7 @@ export function getCharsetFromContentType(headers: HttpResponseHeader[]): string
|
|||||||
const mimeType = getMimeTypeFromContentType(contentType);
|
const mimeType = getMimeTypeFromContentType(contentType);
|
||||||
return mimeType.parameters.get('charset') ?? null;
|
return mimeType.parameters.get('charset') ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isBaseEnvironment(environment: Environment): boolean {
|
||||||
|
return environment.parentId == null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --force",
|
"dev": "vite dev --force",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/commands": "^6.8.1",
|
"@codemirror/commands": "^6.8.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user