Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions src/config/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ pub struct UiConfig {
/// Branding customization.
#[serde(default)]
pub branding: BrandingConfig,

/// Per-page visibility configuration.
#[serde(default)]
pub pages: PagesConfig,
}

impl Default for UiConfig {
Expand All @@ -39,6 +43,7 @@ impl Default for UiConfig {
chat: ChatConfig::default(),
admin: AdminConfig::default(),
branding: BrandingConfig::default(),
pages: PagesConfig::default(),
}
}
}
Expand Down Expand Up @@ -390,6 +395,117 @@ pub struct FooterLink {
pub url: String,
}

/// Page visibility status.
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum PageStatus {
#[default]
Enabled,
Disabled,
Notice,
}

/// Per-page configuration. Accepts either a bare string (`"enabled"`) or an inline table
/// (`{ status = "notice", notice_message = "..." }`).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum PageConfig {
Simple(PageStatus),
Detailed {
status: PageStatus,
#[serde(default)]
notice_message: Option<String>,
},
}

impl Default for PageConfig {
fn default() -> Self {
Self::Simple(PageStatus::Enabled)
}
}

impl PageConfig {
pub fn status(&self) -> &PageStatus {
match self {
Self::Simple(s) => s,
Self::Detailed { status, .. } => status,
}
}

pub fn notice_message(&self) -> Option<&str> {
match self {
Self::Simple(_) => None,
Self::Detailed { notice_message, .. } => notice_message.as_deref(),
}
}
}

/// Per-page visibility for main UI pages.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct PagesConfig {
#[serde(default)]
pub chat: PageConfig,
#[serde(default)]
pub studio: PageConfig,
#[serde(default)]
pub projects: PageConfig,
#[serde(default)]
pub teams: PageConfig,
#[serde(default)]
pub knowledge_bases: PageConfig,
#[serde(default)]
pub api_keys: PageConfig,
#[serde(default)]
pub providers: PageConfig,
#[serde(default)]
pub usage: PageConfig,
#[serde(default)]
pub admin: AdminPagesConfig,
}

/// Per-page visibility for admin pages.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct AdminPagesConfig {
#[serde(default)]
pub dashboard: PageConfig,
#[serde(default)]
pub organizations: PageConfig,
#[serde(default)]
pub projects: PageConfig,
#[serde(default)]
pub teams: PageConfig,
#[serde(default)]
pub service_accounts: PageConfig,
#[serde(default)]
pub users: PageConfig,
#[serde(default)]
pub sso: PageConfig,
#[serde(default)]
pub session_info: PageConfig,
#[serde(default)]
pub api_keys: PageConfig,
#[serde(default)]
pub providers: PageConfig,
#[serde(default)]
pub provider_health: PageConfig,
#[serde(default)]
pub knowledge_bases: PageConfig,
#[serde(default)]
pub pricing: PageConfig,
#[serde(default)]
pub usage: PageConfig,
#[serde(default)]
pub audit_logs: PageConfig,
#[serde(default)]
pub settings: PageConfig,
}

/// Login page customization.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
Expand All @@ -411,3 +527,53 @@ pub struct LoginConfig {
#[serde(default = "default_true")]
pub show_logo: bool,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn simple_string_status() {
let toml = r#"chat = "enabled""#;
let pages: PagesConfig = toml::from_str(toml).unwrap();
assert_eq!(pages.chat.status(), &PageStatus::Enabled);
}

#[test]
fn detailed_table() {
let toml = r#"
[chat]
status = "notice"
notice_message = "Under maintenance"
"#;
let pages: PagesConfig = toml::from_str(toml).unwrap();
assert_eq!(pages.chat.status(), &PageStatus::Notice);
assert_eq!(pages.chat.notice_message(), Some("Under maintenance"));
}

#[test]
fn mixed_formats_with_defaults() {
let toml = r#"
chat = "disabled"
studio = "enabled"
"#;
let pages: PagesConfig = toml::from_str(toml).unwrap();
assert_eq!(pages.chat.status(), &PageStatus::Disabled);
assert_eq!(pages.studio.status(), &PageStatus::Enabled);
// Omitted fields default to enabled
assert_eq!(pages.teams.status(), &PageStatus::Enabled);
assert_eq!(pages.usage.status(), &PageStatus::Enabled);
}

#[test]
fn invalid_status_fails() {
let toml = r#"chat = "bogus""#;
assert!(toml::from_str::<PagesConfig>(toml).is_err());
}

#[test]
fn unknown_field_rejected() {
let toml = r#"nonexistent_page = "enabled""#;
assert!(toml::from_str::<PagesConfig>(toml).is_err());
}
}
94 changes: 92 additions & 2 deletions src/routes/admin/ui_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use serde::Serialize;
use crate::{
AppState,
config::{
AdminConfig, AuthMode, BrandingConfig, ChatConfig, ColorPalette, CustomFont, FontsConfig,
LoginConfig, UiConfig,
AdminConfig, AdminPagesConfig, AuthMode, BrandingConfig, ChatConfig, ColorPalette,
CustomFont, FontsConfig, LoginConfig, PageConfig, PageStatus, PagesConfig, UiConfig,
},
};

Expand All @@ -17,6 +17,7 @@ pub struct UiConfigResponse {
pub admin: AdminResponse,
pub auth: AuthResponse,
pub sovereignty: SovereigntyUiResponse,
pub pages: PagesResponse,
}

#[derive(Debug, Serialize)]
Expand Down Expand Up @@ -155,6 +156,94 @@ pub struct CustomFieldDefResponse {
pub description: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct PageConfigResponse {
pub status: PageStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub notice_message: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct PagesResponse {
pub chat: PageConfigResponse,
pub studio: PageConfigResponse,
pub projects: PageConfigResponse,
pub teams: PageConfigResponse,
pub knowledge_bases: PageConfigResponse,
pub api_keys: PageConfigResponse,
pub providers: PageConfigResponse,
pub usage: PageConfigResponse,
pub admin: AdminPagesResponse,
}

#[derive(Debug, Serialize)]
pub struct AdminPagesResponse {
pub dashboard: PageConfigResponse,
pub organizations: PageConfigResponse,
pub projects: PageConfigResponse,
pub teams: PageConfigResponse,
pub service_accounts: PageConfigResponse,
pub users: PageConfigResponse,
pub sso: PageConfigResponse,
pub session_info: PageConfigResponse,
pub api_keys: PageConfigResponse,
pub providers: PageConfigResponse,
pub provider_health: PageConfigResponse,
pub knowledge_bases: PageConfigResponse,
pub pricing: PageConfigResponse,
pub usage: PageConfigResponse,
pub audit_logs: PageConfigResponse,
pub settings: PageConfigResponse,
}

impl From<&PageConfig> for PageConfigResponse {
fn from(config: &PageConfig) -> Self {
Self {
status: config.status().clone(),
notice_message: config.notice_message().map(String::from),
}
}
}

impl From<&PagesConfig> for PagesResponse {
fn from(config: &PagesConfig) -> Self {
Self {
chat: PageConfigResponse::from(&config.chat),
studio: PageConfigResponse::from(&config.studio),
projects: PageConfigResponse::from(&config.projects),
teams: PageConfigResponse::from(&config.teams),
knowledge_bases: PageConfigResponse::from(&config.knowledge_bases),
api_keys: PageConfigResponse::from(&config.api_keys),
providers: PageConfigResponse::from(&config.providers),
usage: PageConfigResponse::from(&config.usage),
admin: AdminPagesResponse::from(&config.admin),
}
}
}

impl From<&AdminPagesConfig> for AdminPagesResponse {
fn from(config: &AdminPagesConfig) -> Self {
Self {
dashboard: PageConfigResponse::from(&config.dashboard),
organizations: PageConfigResponse::from(&config.organizations),
projects: PageConfigResponse::from(&config.projects),
teams: PageConfigResponse::from(&config.teams),
service_accounts: PageConfigResponse::from(&config.service_accounts),
users: PageConfigResponse::from(&config.users),
sso: PageConfigResponse::from(&config.sso),
session_info: PageConfigResponse::from(&config.session_info),
api_keys: PageConfigResponse::from(&config.api_keys),
providers: PageConfigResponse::from(&config.providers),
provider_health: PageConfigResponse::from(&config.provider_health),
knowledge_bases: PageConfigResponse::from(&config.knowledge_bases),
pricing: PageConfigResponse::from(&config.pricing),
usage: PageConfigResponse::from(&config.usage),
audit_logs: PageConfigResponse::from(&config.audit_logs),
settings: PageConfigResponse::from(&config.settings),
}
}
}

impl From<&UiConfig> for UiConfigResponse {
fn from(config: &UiConfig) -> Self {
Self {
Expand All @@ -165,6 +254,7 @@ impl From<&UiConfig> for UiConfigResponse {
sovereignty: SovereigntyUiResponse {
custom_fields: vec![],
},
pages: PagesResponse::from(&config.pages),
}
}
}
Expand Down
33 changes: 32 additions & 1 deletion src/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,12 @@ fn error_response(status: u16, message: &str) -> Response {

/// Create a minimal config suitable for WASM browser operation.
fn wasm_default_config() -> config::GatewayConfig {
use config::{PageConfig, PageStatus};

let notice = |msg: &str| PageConfig::Detailed {
status: PageStatus::Notice,
notice_message: Some(format!("This feature requires Hadrian Server. {msg}")),
};
config::GatewayConfig {
server: config::ServerConfig {
allow_loopback_urls: true,
Expand All @@ -474,7 +480,32 @@ fn wasm_default_config() -> config::GatewayConfig {
limits: config::LimitsConfig::default(),
features: config::FeaturesConfig::default(),
observability: config::ObservabilityConfig::default(),
ui: config::UiConfig::default(),
ui: config::UiConfig {
pages: config::PagesConfig {
teams: notice("Team management needs multi-user authentication."),
api_keys: notice("API key management is not available in browser mode."),
usage: notice("Usage tracking is not available in browser mode."),
admin: config::AdminPagesConfig {
organizations: notice(
"Organization management is not available in browser mode.",
),
teams: notice("Team management needs multi-user authentication."),
service_accounts: notice("Service accounts are not available in browser mode."),
users: notice("User management is not available in browser mode."),
sso: notice("Single sign-on is not available in browser mode."),
api_keys: notice("API key management is not available in browser mode."),
provider_health: notice(
"Provider health monitoring is not available in browser mode.",
),
pricing: notice("Pricing configuration requires usage tracking."),
usage: notice("Usage tracking is not available in browser mode."),
audit_logs: notice("Audit logging is not available in browser mode."),
..Default::default()
},
..Default::default()
},
..Default::default()
},
docs: config::DocsConfig::default(),
pricing: pricing::PricingConfig::default(),
secrets: config::SecretsConfig::None,
Expand Down
Loading
Loading