use crate::tui::app_states::compliance::DiffComplianceState;
use crate::tui::traits::{EventResult, Shortcut, ViewContext, ViewState};
use crossterm::event::{KeyCode, KeyEvent, MouseEvent};
pub struct ComplianceView {
inner: DiffComplianceState,
max_violations: usize,
export_requested: bool,
go_to_component_requested: bool,
toggle_group_entry_requested: bool,
}
impl ComplianceView {
pub(crate) fn new() -> Self {
Self {
inner: DiffComplianceState::new(),
max_violations: 0,
export_requested: false,
go_to_component_requested: false,
toggle_group_entry_requested: false,
}
}
pub(crate) const fn inner(&self) -> &DiffComplianceState {
&self.inner
}
pub(crate) fn inner_mut(&mut self) -> &mut DiffComplianceState {
&mut self.inner
}
pub(crate) fn set_max_violations(&mut self, max: usize) {
self.max_violations = max;
}
pub(crate) fn take_export_request(&mut self) -> bool {
let req = self.export_requested;
self.export_requested = false;
req
}
pub(crate) fn take_go_to_component_request(&mut self) -> bool {
let req = self.go_to_component_requested;
self.go_to_component_requested = false;
req
}
pub(crate) fn take_toggle_group_entry_request(&mut self) -> bool {
let req = self.toggle_group_entry_requested;
self.toggle_group_entry_requested = false;
req
}
}
impl Default for ComplianceView {
fn default() -> Self {
Self::new()
}
}
impl ViewState for ComplianceView {
fn handle_key(&mut self, key: KeyEvent, _ctx: &mut ViewContext) -> EventResult {
if self.inner.show_detail {
match key.code {
KeyCode::Esc | KeyCode::Enter => {
self.inner.show_detail = false;
return EventResult::Consumed;
}
_ => return EventResult::Consumed,
}
}
match key.code {
KeyCode::Left | KeyCode::Char('h') => {
self.inner.prev_standard();
EventResult::Consumed
}
KeyCode::Right | KeyCode::Char('l') => {
self.inner.next_standard();
EventResult::Consumed
}
KeyCode::Up | KeyCode::Char('k') => {
self.inner.select_prev();
EventResult::Consumed
}
KeyCode::Down | KeyCode::Char('j') => {
self.inner.select_next(self.max_violations);
EventResult::Consumed
}
KeyCode::Enter => {
if self.max_violations > 0 {
if self.inner.group_by_element {
self.toggle_group_entry_requested = true;
} else {
self.inner.show_detail = true;
}
}
EventResult::Consumed
}
KeyCode::Char('f') => {
self.inner.toggle_severity_filter();
let label = self.inner.severity_filter.label();
EventResult::status(format!("Compliance filter: {label}"))
}
KeyCode::Char('g') => {
self.inner.toggle_group_by_element();
EventResult::status(if self.inner.group_by_element {
"Violations grouped by component"
} else {
"Flat violation list"
})
}
KeyCode::Char('v') => {
self.inner.next_view_mode();
let mode_label = match self.inner.view_mode {
crate::tui::app_states::compliance::DiffComplianceViewMode::Overview => "Overview",
crate::tui::app_states::compliance::DiffComplianceViewMode::NewViolations => "New violations",
crate::tui::app_states::compliance::DiffComplianceViewMode::ResolvedViolations => "Resolved violations",
crate::tui::app_states::compliance::DiffComplianceViewMode::OldViolations => "Old SBOM violations",
crate::tui::app_states::compliance::DiffComplianceViewMode::NewSbomViolations => "New SBOM violations",
};
EventResult::status(format!("View: {mode_label}"))
}
KeyCode::Char('c') => {
if self.max_violations > 0 {
self.go_to_component_requested = true;
}
EventResult::Consumed
}
KeyCode::Char('E') => {
self.export_requested = true;
EventResult::Consumed
}
KeyCode::Home => {
self.inner.selected_violation = 0;
EventResult::Consumed
}
KeyCode::End | KeyCode::Char('G') => {
if self.max_violations > 0 {
self.inner.selected_violation = self.max_violations - 1;
}
EventResult::Consumed
}
KeyCode::PageUp => {
for _ in 0..crate::tui::constants::PAGE_SIZE {
self.inner.select_prev();
}
EventResult::Consumed
}
KeyCode::PageDown => {
for _ in 0..crate::tui::constants::PAGE_SIZE {
self.inner.select_next(self.max_violations);
}
EventResult::Consumed
}
_ => EventResult::Ignored,
}
}
fn handle_mouse(&mut self, _mouse: MouseEvent, _ctx: &mut ViewContext) -> EventResult {
EventResult::Ignored
}
fn title(&self) -> &'static str {
"Compliance"
}
fn shortcuts(&self) -> Vec<Shortcut> {
vec![
Shortcut::primary("j/k", "Navigate"),
Shortcut::new("h/l", "Switch standard"),
Shortcut::new("v", "View mode"),
Shortcut::new("Enter", "Detail"),
Shortcut::new("g", "Group"),
Shortcut::new("c", "Go to component"),
Shortcut::new("E", "Export"),
Shortcut::new("G", "Last"),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::traits::ViewMode;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn make_key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
fn make_ctx() -> ViewContext<'static> {
let status: &'static mut Option<String> = Box::leak(Box::new(None));
ViewContext {
mode: ViewMode::Diff,
focused: true,
width: 80,
height: 24,
tick: 0,
status_message: status,
}
}
#[test]
fn test_standard_navigation() {
let mut view = ComplianceView::new();
let mut ctx = make_ctx();
let initial = view.inner().selected_standard;
view.handle_key(make_key(KeyCode::Right), &mut ctx);
assert_ne!(view.inner().selected_standard, initial);
}
#[test]
fn test_violation_navigation() {
let mut view = ComplianceView::new();
view.set_max_violations(5);
let mut ctx = make_ctx();
view.handle_key(make_key(KeyCode::Down), &mut ctx);
assert_eq!(view.inner().selected_violation, 1);
view.handle_key(make_key(KeyCode::Up), &mut ctx);
assert_eq!(view.inner().selected_violation, 0);
}
#[test]
fn test_detail_toggle() {
let mut view = ComplianceView::new();
view.set_max_violations(3);
let mut ctx = make_ctx();
assert!(!view.inner().show_detail);
view.handle_key(make_key(KeyCode::Enter), &mut ctx);
assert!(view.inner().show_detail);
view.handle_key(make_key(KeyCode::Esc), &mut ctx);
assert!(!view.inner().show_detail);
}
#[test]
fn test_export_request() {
let mut view = ComplianceView::new();
let mut ctx = make_ctx();
view.handle_key(make_key(KeyCode::Char('E')), &mut ctx);
assert!(view.take_export_request());
assert!(!view.take_export_request()); }
#[test]
fn test_go_to_component_request() {
let mut view = ComplianceView::new();
view.set_max_violations(3);
let mut ctx = make_ctx();
view.handle_key(make_key(KeyCode::Char('c')), &mut ctx);
assert!(view.take_go_to_component_request());
assert!(!view.take_go_to_component_request()); }
#[test]
fn test_go_to_component_ignored_when_no_violations() {
let mut view = ComplianceView::new();
let mut ctx = make_ctx();
view.handle_key(make_key(KeyCode::Char('c')), &mut ctx);
assert!(!view.take_go_to_component_request());
}
}