Some checks failed
CI / Deploy MCP (push) Has been cancelled
CI / Detect Changes (push) Has been cancelled
CI / Format (push) Successful in 3s
CI / Clippy (push) Successful in 3m58s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / Format (pull_request) Successful in 3s
CI / Deploy Agent (push) Has been cancelled
CI / Deploy Dashboard (push) Has been cancelled
CI / Deploy Docs (push) Has been cancelled
CI / Clippy (pull_request) Successful in 4m1s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
New page at /mcp-servers to register, view, and manage MCP server instances. Shows endpoint config, enabled tools, and access tokens with reveal/regenerate controls. Includes McpServerConfig model in compliance-core, MongoDB collection accessor, server functions for list/add/delete/regenerate-token, sidebar nav entry, and full CSS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
163 lines
6.1 KiB
Rust
163 lines
6.1 KiB
Rust
use compliance_core::models::auth::AuthInfo;
|
|
use dioxus::prelude::*;
|
|
use dioxus_free_icons::icons::bs_icons::*;
|
|
use dioxus_free_icons::Icon;
|
|
|
|
use crate::app::Route;
|
|
|
|
struct NavItem {
|
|
label: &'static str,
|
|
route: Route,
|
|
icon: Element,
|
|
}
|
|
|
|
#[component]
|
|
pub fn Sidebar() -> Element {
|
|
let current_route = use_route::<Route>();
|
|
let mut collapsed = use_signal(|| true);
|
|
|
|
let nav_items = [
|
|
NavItem {
|
|
label: "Overview",
|
|
route: Route::OverviewPage {},
|
|
icon: rsx! { Icon { icon: BsSpeedometer2, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "Repositories",
|
|
route: Route::RepositoriesPage {},
|
|
icon: rsx! { Icon { icon: BsFolder2Open, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "Findings",
|
|
route: Route::FindingsPage {},
|
|
icon: rsx! { Icon { icon: BsShieldExclamation, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "SBOM",
|
|
route: Route::SbomPage {},
|
|
icon: rsx! { Icon { icon: BsBoxSeam, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "Issues",
|
|
route: Route::IssuesPage {},
|
|
icon: rsx! { Icon { icon: BsListTask, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "Code Graph",
|
|
route: Route::GraphIndexPage {},
|
|
icon: rsx! { Icon { icon: BsDiagram3, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "AI Chat",
|
|
route: Route::ChatIndexPage {},
|
|
icon: rsx! { Icon { icon: BsChatDots, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "DAST",
|
|
route: Route::DastOverviewPage {},
|
|
icon: rsx! { Icon { icon: BsBug, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "MCP Servers",
|
|
route: Route::McpServersPage {},
|
|
icon: rsx! { Icon { icon: BsPlug, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "Settings",
|
|
route: Route::SettingsPage {},
|
|
icon: rsx! { Icon { icon: BsGear, width: 18, height: 18 } },
|
|
},
|
|
];
|
|
|
|
let docs_url = option_env!("DOCS_URL").unwrap_or("/docs");
|
|
|
|
let sidebar_class = if collapsed() {
|
|
"sidebar collapsed"
|
|
} else {
|
|
"sidebar"
|
|
};
|
|
|
|
rsx! {
|
|
nav { class: "{sidebar_class}",
|
|
div { class: "sidebar-header",
|
|
Icon { icon: BsShieldCheck, width: 24, height: 24 }
|
|
if !collapsed() {
|
|
h1 { "Compliance Scanner" }
|
|
}
|
|
}
|
|
div { class: "sidebar-nav",
|
|
for item in nav_items {
|
|
{
|
|
let is_active = match (¤t_route, &item.route) {
|
|
(Route::FindingDetailPage { .. }, Route::FindingsPage {}) => true,
|
|
(Route::GraphIndexPage {}, Route::GraphIndexPage {}) => true,
|
|
(Route::GraphExplorerPage { .. }, Route::GraphIndexPage {}) => true,
|
|
(Route::ImpactAnalysisPage { .. }, Route::GraphIndexPage {}) => true,
|
|
(Route::ChatPage { .. }, Route::ChatIndexPage {}) => true,
|
|
(Route::DastTargetsPage {}, Route::DastOverviewPage {}) => true,
|
|
(Route::DastFindingsPage {}, Route::DastOverviewPage {}) => true,
|
|
(Route::DastFindingDetailPage { .. }, Route::DastOverviewPage {}) => true,
|
|
(a, b) => a == b,
|
|
};
|
|
let class = if is_active { "nav-item active" } else { "nav-item" };
|
|
rsx! {
|
|
Link {
|
|
to: item.route.clone(),
|
|
class: class,
|
|
{item.icon}
|
|
if !collapsed() {
|
|
span { "{item.label}" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
a {
|
|
href: "{docs_url}",
|
|
target: "_blank",
|
|
class: "nav-item",
|
|
Icon { icon: BsBook, width: 18, height: 18 }
|
|
if !collapsed() {
|
|
span { "Docs" }
|
|
}
|
|
}
|
|
button {
|
|
class: "sidebar-toggle",
|
|
onclick: move |_| collapsed.set(!collapsed()),
|
|
if collapsed() {
|
|
Icon { icon: BsChevronRight, width: 14, height: 14 }
|
|
} else {
|
|
Icon { icon: BsChevronLeft, width: 14, height: 14 }
|
|
}
|
|
}
|
|
{
|
|
let auth_info = use_context::<Signal<AuthInfo>>();
|
|
let info = auth_info();
|
|
let initials = info.name.chars().next().unwrap_or('U').to_uppercase().to_string();
|
|
let user_class = if collapsed() { "sidebar-user sidebar-user-collapsed" } else { "sidebar-user" };
|
|
rsx! {
|
|
div { class: "{user_class}",
|
|
div { class: "user-avatar",
|
|
if info.avatar_url.is_empty() {
|
|
span { class: "avatar-initials", "{initials}" }
|
|
} else {
|
|
img { src: "{info.avatar_url}", alt: "avatar", class: "avatar-img" }
|
|
}
|
|
}
|
|
if !collapsed() {
|
|
span { class: "user-name", "{info.name}" }
|
|
}
|
|
a {
|
|
href: "/logout",
|
|
class: if collapsed() { "logout-btn logout-btn-collapsed" } else { "logout-btn" },
|
|
title: "Sign out",
|
|
Icon { icon: BsBoxArrowRight, width: 16, height: 16 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|