refined client configuration manager flow
This commit is contained in:
@@ -365,8 +365,7 @@ fn save_client_config(config: AppConfig) -> Result<(), String> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn open_client_config_manager() -> Result<(), String> {
|
fn open_client_config_manager() -> Result<(), String> {
|
||||||
open_config_manager_window();
|
open_config_manager_window().map_err(|e| e.to_string())
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ pub enum ClientConfigError {
|
|||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
#[error("failed to parse client config: {0}")]
|
#[error("failed to parse client config: {0}")]
|
||||||
Parse(#[from] serde_json::Error),
|
Parse(#[from] serde_json::Error),
|
||||||
|
#[error("failed to build client config manager window: {0}")]
|
||||||
|
Window(tauri::Error),
|
||||||
|
#[error("failed to show client config manager window: {0}")]
|
||||||
|
ShowWindow(tauri::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static CLIENT_CONFIG_MANAGER_WINDOW_LABEL: &str = "client_config_manager";
|
pub static CLIENT_CONFIG_MANAGER_WINDOW_LABEL: &str = "client_config_manager";
|
||||||
@@ -48,38 +52,34 @@ fn strip_trailing_slash(value: &str) -> String {
|
|||||||
value.trim_end_matches('/').to_string()
|
value.trim_end_matches('/').to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_http_url(value: &str) -> Option<String> {
|
||||||
|
if value.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attempts = [value.to_string(), format!("https://{value}")];
|
||||||
|
|
||||||
|
for attempt in attempts {
|
||||||
|
if let Ok(parsed) = Url::parse(&attempt) {
|
||||||
|
if matches!(parsed.scheme(), "http" | "https") {
|
||||||
|
return Some(strip_trailing_slash(parsed.as_str()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn sanitize(mut config: AppConfig) -> AppConfig {
|
fn sanitize(mut config: AppConfig) -> AppConfig {
|
||||||
// Trim and normalize optional api_base_url
|
|
||||||
config.api_base_url = config
|
config.api_base_url = config
|
||||||
.api_base_url
|
.api_base_url
|
||||||
.and_then(|v| {
|
.and_then(|v| parse_http_url(v.trim()))
|
||||||
let trimmed = v.trim().to_string();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
// ensure scheme present and no double prefixes
|
|
||||||
if let Ok(parsed) = Url::parse(&trimmed) {
|
|
||||||
Some(strip_trailing_slash(parsed.as_str()))
|
|
||||||
} else if let Ok(parsed) = Url::parse(&format!("https://{trimmed}")) {
|
|
||||||
Some(strip_trailing_slash(parsed.as_str()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.or_else(|| Some(DEFAULT_API_BASE_URL.to_string()))
|
.or_else(|| Some(DEFAULT_API_BASE_URL.to_string()))
|
||||||
.map(|v| strip_trailing_slash(&v));
|
.map(|v| strip_trailing_slash(&v));
|
||||||
|
|
||||||
let auth_url_trimmed = config.auth.auth_url.trim();
|
let auth_url_trimmed = config.auth.auth_url.trim();
|
||||||
if auth_url_trimmed.is_empty() {
|
config.auth.auth_url = parse_http_url(auth_url_trimmed)
|
||||||
config.auth.auth_url = DEFAULT_AUTH_URL.to_string();
|
.unwrap_or_else(|| DEFAULT_AUTH_URL.to_string());
|
||||||
} else if let Ok(parsed) = Url::parse(auth_url_trimmed) {
|
|
||||||
config.auth.auth_url = strip_trailing_slash(parsed.as_str());
|
|
||||||
} else if let Ok(parsed) = Url::parse(&format!("https://{auth_url_trimmed}")) {
|
|
||||||
config.auth.auth_url = strip_trailing_slash(parsed.as_str());
|
|
||||||
} else {
|
|
||||||
config.auth.auth_url = DEFAULT_AUTH_URL.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.auth.audience.trim().is_empty() {
|
if config.auth.audience.trim().is_empty() {
|
||||||
config.auth.audience = DEFAULT_JWT_AUDIENCE.to_string();
|
config.auth.audience = DEFAULT_JWT_AUDIENCE.to_string();
|
||||||
@@ -139,23 +139,28 @@ pub fn save_app_config(config: AppConfig) -> Result<AppConfig, ClientConfigError
|
|||||||
|
|
||||||
let sanitized = sanitize(config);
|
let sanitized = sanitize(config);
|
||||||
let serialized = serde_json::to_string_pretty(&sanitized)?;
|
let serialized = serde_json::to_string_pretty(&sanitized)?;
|
||||||
fs::write(&path, serialized)?;
|
|
||||||
|
let temp_path = path.with_extension("tmp");
|
||||||
|
fs::write(&temp_path, serialized)?;
|
||||||
|
fs::rename(&temp_path, &path)?;
|
||||||
|
|
||||||
Ok(sanitized)
|
Ok(sanitized)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_config_manager_window() {
|
pub fn open_config_manager_window() -> Result<(), ClientConfigError> {
|
||||||
let app_handle = get_app_handle();
|
let app_handle = get_app_handle();
|
||||||
let existing_webview_window = app_handle.get_window(CLIENT_CONFIG_MANAGER_WINDOW_LABEL);
|
let existing_webview_window = app_handle.get_window(CLIENT_CONFIG_MANAGER_WINDOW_LABEL);
|
||||||
|
|
||||||
if let Some(window) = existing_webview_window {
|
if let Some(window) = existing_webview_window {
|
||||||
if let Err(e) = window.show() {
|
if let Err(e) = window.show() {
|
||||||
error!("Failed to show client config manager window: {e}");
|
error!("Failed to show client config manager window: {e}");
|
||||||
|
return Err(ClientConfigError::ShowWindow(e));
|
||||||
}
|
}
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Starting client config manager...");
|
info!("Starting client config manager...");
|
||||||
let webview_window = match tauri::WebviewWindowBuilder::new(
|
match tauri::WebviewWindowBuilder::new(
|
||||||
app_handle,
|
app_handle,
|
||||||
CLIENT_CONFIG_MANAGER_WINDOW_LABEL,
|
CLIENT_CONFIG_MANAGER_WINDOW_LABEL,
|
||||||
tauri::WebviewUrl::App("/client-config-manager".into()),
|
tauri::WebviewUrl::App("/client-config-manager".into()),
|
||||||
@@ -164,15 +169,14 @@ pub fn open_config_manager_window() {
|
|||||||
.inner_size(600.0, 500.0)
|
.inner_size(600.0, 500.0)
|
||||||
.build()
|
.build()
|
||||||
{
|
{
|
||||||
Ok(window) => {
|
Ok(_) => {
|
||||||
info!("Client config manager window builder succeeded");
|
info!("Client config manager window builder succeeded");
|
||||||
window
|
info!("Client config manager window initialized successfully.");
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to build client config manager window: {}", e);
|
error!("Failed to build client config manager window: {}", e);
|
||||||
return;
|
Err(ClientConfigError::Window(e))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
info!("Client config manager window initialized successfully.");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
let saving = false;
|
let saving = false;
|
||||||
let errorMessage = "";
|
let errorMessage = "";
|
||||||
let successMessage = "";
|
let successMessage = "";
|
||||||
|
let restartError = "";
|
||||||
|
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -36,11 +37,51 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
if (!form.auth.auth_url.trim()) {
|
||||||
|
return "Auth URL is required";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(form.auth.auth_url.trim());
|
||||||
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||||
|
return "Auth URL must start with http or https";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return "Auth URL must be a valid URL";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!form.auth.audience.trim()) {
|
||||||
|
return "JWT audience is required";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.api_base_url?.trim()) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(
|
||||||
|
form.api_base_url.trim().startsWith("http")
|
||||||
|
? form.api_base_url.trim()
|
||||||
|
: `https://${form.api_base_url.trim()}`
|
||||||
|
);
|
||||||
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||||
|
return "API base URL must start with http or https";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return "API base URL must be a valid URL";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
if (saving) return;
|
if (saving) return;
|
||||||
|
errorMessage = validate();
|
||||||
|
if (errorMessage) return;
|
||||||
|
|
||||||
saving = true;
|
saving = true;
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
successMessage = "";
|
successMessage = "";
|
||||||
|
restartError = "";
|
||||||
try {
|
try {
|
||||||
await invoke("save_client_config", {
|
await invoke("save_client_config", {
|
||||||
config: {
|
config: {
|
||||||
@@ -52,8 +93,7 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
successMessage = "Configuration saved. Please restart the app.";
|
successMessage = "Configuration saved. Restart to apply changes.";
|
||||||
await invoke("restart_app");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorMessage = `Failed to save config: ${err}`;
|
errorMessage = `Failed to save config: ${err}`;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -61,6 +101,15 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const restart = async () => {
|
||||||
|
restartError = "";
|
||||||
|
try {
|
||||||
|
await invoke("restart_app");
|
||||||
|
} catch (err) {
|
||||||
|
restartError = `Restart failed: ${err}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMount(loadConfig);
|
onMount(loadConfig);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -95,6 +144,9 @@
|
|||||||
{#if successMessage}
|
{#if successMessage}
|
||||||
<p class="text-sm text-success">{successMessage}</p>
|
<p class="text-sm text-success">{successMessage}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if restartError}
|
||||||
|
<p class="text-sm text-error">{restartError}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -105,5 +157,9 @@
|
|||||||
>
|
>
|
||||||
{saving ? "Saving..." : "Save"}
|
{saving ? "Saving..." : "Save"}
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-outline" on:click={restart}>
|
||||||
|
Restart app
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user