Skip to content

Commit 0258558

Browse files
authored
Add configuration for which pages are shown in UI (#13)
* Configure which pages are shown in UI * Fix tests
1 parent 8c55eb2 commit 0258558

File tree

15 files changed

+719
-87
lines changed

15 files changed

+719
-87
lines changed

src/config/ui.rs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ pub struct UiConfig {
2828
/// Branding customization.
2929
#[serde(default)]
3030
pub branding: BrandingConfig,
31+
32+
/// Per-page visibility configuration.
33+
#[serde(default)]
34+
pub pages: PagesConfig,
3135
}
3236

3337
impl Default for UiConfig {
@@ -39,6 +43,7 @@ impl Default for UiConfig {
3943
chat: ChatConfig::default(),
4044
admin: AdminConfig::default(),
4145
branding: BrandingConfig::default(),
46+
pages: PagesConfig::default(),
4247
}
4348
}
4449
}
@@ -390,6 +395,117 @@ pub struct FooterLink {
390395
pub url: String,
391396
}
392397

398+
/// Page visibility status.
399+
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
400+
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
401+
#[serde(rename_all = "snake_case")]
402+
pub enum PageStatus {
403+
#[default]
404+
Enabled,
405+
Disabled,
406+
Notice,
407+
}
408+
409+
/// Per-page configuration. Accepts either a bare string (`"enabled"`) or an inline table
410+
/// (`{ status = "notice", notice_message = "..." }`).
411+
#[derive(Debug, Clone, Serialize, Deserialize)]
412+
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
413+
#[serde(untagged)]
414+
pub enum PageConfig {
415+
Simple(PageStatus),
416+
Detailed {
417+
status: PageStatus,
418+
#[serde(default)]
419+
notice_message: Option<String>,
420+
},
421+
}
422+
423+
impl Default for PageConfig {
424+
fn default() -> Self {
425+
Self::Simple(PageStatus::Enabled)
426+
}
427+
}
428+
429+
impl PageConfig {
430+
pub fn status(&self) -> &PageStatus {
431+
match self {
432+
Self::Simple(s) => s,
433+
Self::Detailed { status, .. } => status,
434+
}
435+
}
436+
437+
pub fn notice_message(&self) -> Option<&str> {
438+
match self {
439+
Self::Simple(_) => None,
440+
Self::Detailed { notice_message, .. } => notice_message.as_deref(),
441+
}
442+
}
443+
}
444+
445+
/// Per-page visibility for main UI pages.
446+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
447+
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
448+
#[serde(deny_unknown_fields)]
449+
pub struct PagesConfig {
450+
#[serde(default)]
451+
pub chat: PageConfig,
452+
#[serde(default)]
453+
pub studio: PageConfig,
454+
#[serde(default)]
455+
pub projects: PageConfig,
456+
#[serde(default)]
457+
pub teams: PageConfig,
458+
#[serde(default)]
459+
pub knowledge_bases: PageConfig,
460+
#[serde(default)]
461+
pub api_keys: PageConfig,
462+
#[serde(default)]
463+
pub providers: PageConfig,
464+
#[serde(default)]
465+
pub usage: PageConfig,
466+
#[serde(default)]
467+
pub admin: AdminPagesConfig,
468+
}
469+
470+
/// Per-page visibility for admin pages.
471+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
472+
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
473+
#[serde(deny_unknown_fields)]
474+
pub struct AdminPagesConfig {
475+
#[serde(default)]
476+
pub dashboard: PageConfig,
477+
#[serde(default)]
478+
pub organizations: PageConfig,
479+
#[serde(default)]
480+
pub projects: PageConfig,
481+
#[serde(default)]
482+
pub teams: PageConfig,
483+
#[serde(default)]
484+
pub service_accounts: PageConfig,
485+
#[serde(default)]
486+
pub users: PageConfig,
487+
#[serde(default)]
488+
pub sso: PageConfig,
489+
#[serde(default)]
490+
pub session_info: PageConfig,
491+
#[serde(default)]
492+
pub api_keys: PageConfig,
493+
#[serde(default)]
494+
pub providers: PageConfig,
495+
#[serde(default)]
496+
pub provider_health: PageConfig,
497+
#[serde(default)]
498+
pub knowledge_bases: PageConfig,
499+
#[serde(default)]
500+
pub pricing: PageConfig,
501+
#[serde(default)]
502+
pub usage: PageConfig,
503+
#[serde(default)]
504+
pub audit_logs: PageConfig,
505+
#[serde(default)]
506+
pub settings: PageConfig,
507+
}
508+
393509
/// Login page customization.
394510
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
395511
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
@@ -411,3 +527,53 @@ pub struct LoginConfig {
411527
#[serde(default = "default_true")]
412528
pub show_logo: bool,
413529
}
530+
531+
#[cfg(test)]
532+
mod tests {
533+
use super::*;
534+
535+
#[test]
536+
fn simple_string_status() {
537+
let toml = r#"chat = "enabled""#;
538+
let pages: PagesConfig = toml::from_str(toml).unwrap();
539+
assert_eq!(pages.chat.status(), &PageStatus::Enabled);
540+
}
541+
542+
#[test]
543+
fn detailed_table() {
544+
let toml = r#"
545+
[chat]
546+
status = "notice"
547+
notice_message = "Under maintenance"
548+
"#;
549+
let pages: PagesConfig = toml::from_str(toml).unwrap();
550+
assert_eq!(pages.chat.status(), &PageStatus::Notice);
551+
assert_eq!(pages.chat.notice_message(), Some("Under maintenance"));
552+
}
553+
554+
#[test]
555+
fn mixed_formats_with_defaults() {
556+
let toml = r#"
557+
chat = "disabled"
558+
studio = "enabled"
559+
"#;
560+
let pages: PagesConfig = toml::from_str(toml).unwrap();
561+
assert_eq!(pages.chat.status(), &PageStatus::Disabled);
562+
assert_eq!(pages.studio.status(), &PageStatus::Enabled);
563+
// Omitted fields default to enabled
564+
assert_eq!(pages.teams.status(), &PageStatus::Enabled);
565+
assert_eq!(pages.usage.status(), &PageStatus::Enabled);
566+
}
567+
568+
#[test]
569+
fn invalid_status_fails() {
570+
let toml = r#"chat = "bogus""#;
571+
assert!(toml::from_str::<PagesConfig>(toml).is_err());
572+
}
573+
574+
#[test]
575+
fn unknown_field_rejected() {
576+
let toml = r#"nonexistent_page = "enabled""#;
577+
assert!(toml::from_str::<PagesConfig>(toml).is_err());
578+
}
579+
}

src/routes/admin/ui_config.rs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use serde::Serialize;
44
use crate::{
55
AppState,
66
config::{
7-
AdminConfig, AuthMode, BrandingConfig, ChatConfig, ColorPalette, CustomFont, FontsConfig,
8-
LoginConfig, UiConfig,
7+
AdminConfig, AdminPagesConfig, AuthMode, BrandingConfig, ChatConfig, ColorPalette,
8+
CustomFont, FontsConfig, LoginConfig, PageConfig, PageStatus, PagesConfig, UiConfig,
99
},
1010
};
1111

@@ -17,6 +17,7 @@ pub struct UiConfigResponse {
1717
pub admin: AdminResponse,
1818
pub auth: AuthResponse,
1919
pub sovereignty: SovereigntyUiResponse,
20+
pub pages: PagesResponse,
2021
}
2122

2223
#[derive(Debug, Serialize)]
@@ -155,6 +156,94 @@ pub struct CustomFieldDefResponse {
155156
pub description: Option<String>,
156157
}
157158

159+
#[derive(Debug, Serialize)]
160+
pub struct PageConfigResponse {
161+
pub status: PageStatus,
162+
#[serde(skip_serializing_if = "Option::is_none")]
163+
pub notice_message: Option<String>,
164+
}
165+
166+
#[derive(Debug, Serialize)]
167+
pub struct PagesResponse {
168+
pub chat: PageConfigResponse,
169+
pub studio: PageConfigResponse,
170+
pub projects: PageConfigResponse,
171+
pub teams: PageConfigResponse,
172+
pub knowledge_bases: PageConfigResponse,
173+
pub api_keys: PageConfigResponse,
174+
pub providers: PageConfigResponse,
175+
pub usage: PageConfigResponse,
176+
pub admin: AdminPagesResponse,
177+
}
178+
179+
#[derive(Debug, Serialize)]
180+
pub struct AdminPagesResponse {
181+
pub dashboard: PageConfigResponse,
182+
pub organizations: PageConfigResponse,
183+
pub projects: PageConfigResponse,
184+
pub teams: PageConfigResponse,
185+
pub service_accounts: PageConfigResponse,
186+
pub users: PageConfigResponse,
187+
pub sso: PageConfigResponse,
188+
pub session_info: PageConfigResponse,
189+
pub api_keys: PageConfigResponse,
190+
pub providers: PageConfigResponse,
191+
pub provider_health: PageConfigResponse,
192+
pub knowledge_bases: PageConfigResponse,
193+
pub pricing: PageConfigResponse,
194+
pub usage: PageConfigResponse,
195+
pub audit_logs: PageConfigResponse,
196+
pub settings: PageConfigResponse,
197+
}
198+
199+
impl From<&PageConfig> for PageConfigResponse {
200+
fn from(config: &PageConfig) -> Self {
201+
Self {
202+
status: config.status().clone(),
203+
notice_message: config.notice_message().map(String::from),
204+
}
205+
}
206+
}
207+
208+
impl From<&PagesConfig> for PagesResponse {
209+
fn from(config: &PagesConfig) -> Self {
210+
Self {
211+
chat: PageConfigResponse::from(&config.chat),
212+
studio: PageConfigResponse::from(&config.studio),
213+
projects: PageConfigResponse::from(&config.projects),
214+
teams: PageConfigResponse::from(&config.teams),
215+
knowledge_bases: PageConfigResponse::from(&config.knowledge_bases),
216+
api_keys: PageConfigResponse::from(&config.api_keys),
217+
providers: PageConfigResponse::from(&config.providers),
218+
usage: PageConfigResponse::from(&config.usage),
219+
admin: AdminPagesResponse::from(&config.admin),
220+
}
221+
}
222+
}
223+
224+
impl From<&AdminPagesConfig> for AdminPagesResponse {
225+
fn from(config: &AdminPagesConfig) -> Self {
226+
Self {
227+
dashboard: PageConfigResponse::from(&config.dashboard),
228+
organizations: PageConfigResponse::from(&config.organizations),
229+
projects: PageConfigResponse::from(&config.projects),
230+
teams: PageConfigResponse::from(&config.teams),
231+
service_accounts: PageConfigResponse::from(&config.service_accounts),
232+
users: PageConfigResponse::from(&config.users),
233+
sso: PageConfigResponse::from(&config.sso),
234+
session_info: PageConfigResponse::from(&config.session_info),
235+
api_keys: PageConfigResponse::from(&config.api_keys),
236+
providers: PageConfigResponse::from(&config.providers),
237+
provider_health: PageConfigResponse::from(&config.provider_health),
238+
knowledge_bases: PageConfigResponse::from(&config.knowledge_bases),
239+
pricing: PageConfigResponse::from(&config.pricing),
240+
usage: PageConfigResponse::from(&config.usage),
241+
audit_logs: PageConfigResponse::from(&config.audit_logs),
242+
settings: PageConfigResponse::from(&config.settings),
243+
}
244+
}
245+
}
246+
158247
impl From<&UiConfig> for UiConfigResponse {
159248
fn from(config: &UiConfig) -> Self {
160249
Self {
@@ -165,6 +254,7 @@ impl From<&UiConfig> for UiConfigResponse {
165254
sovereignty: SovereigntyUiResponse {
166255
custom_fields: vec![],
167256
},
257+
pages: PagesResponse::from(&config.pages),
168258
}
169259
}
170260
}

src/wasm.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,12 @@ fn error_response(status: u16, message: &str) -> Response {
458458

459459
/// Create a minimal config suitable for WASM browser operation.
460460
fn wasm_default_config() -> config::GatewayConfig {
461+
use config::{PageConfig, PageStatus};
462+
463+
let notice = |msg: &str| PageConfig::Detailed {
464+
status: PageStatus::Notice,
465+
notice_message: Some(format!("This feature requires Hadrian Server. {msg}")),
466+
};
461467
config::GatewayConfig {
462468
server: config::ServerConfig {
463469
allow_loopback_urls: true,
@@ -474,7 +480,32 @@ fn wasm_default_config() -> config::GatewayConfig {
474480
limits: config::LimitsConfig::default(),
475481
features: config::FeaturesConfig::default(),
476482
observability: config::ObservabilityConfig::default(),
477-
ui: config::UiConfig::default(),
483+
ui: config::UiConfig {
484+
pages: config::PagesConfig {
485+
teams: notice("Team management needs multi-user authentication."),
486+
api_keys: notice("API key management is not available in browser mode."),
487+
usage: notice("Usage tracking is not available in browser mode."),
488+
admin: config::AdminPagesConfig {
489+
organizations: notice(
490+
"Organization management is not available in browser mode.",
491+
),
492+
teams: notice("Team management needs multi-user authentication."),
493+
service_accounts: notice("Service accounts are not available in browser mode."),
494+
users: notice("User management is not available in browser mode."),
495+
sso: notice("Single sign-on is not available in browser mode."),
496+
api_keys: notice("API key management is not available in browser mode."),
497+
provider_health: notice(
498+
"Provider health monitoring is not available in browser mode.",
499+
),
500+
pricing: notice("Pricing configuration requires usage tracking."),
501+
usage: notice("Usage tracking is not available in browser mode."),
502+
audit_logs: notice("Audit logging is not available in browser mode."),
503+
..Default::default()
504+
},
505+
..Default::default()
506+
},
507+
..Default::default()
508+
},
478509
docs: config::DocsConfig::default(),
479510
pricing: pricing::PricingConfig::default(),
480511
secrets: config::SecretsConfig::None,

0 commit comments

Comments
 (0)