Skip to content

Commit b91342d

Browse files
committed
Fix query enum null filters and Explore page UI issues
- Restore null/not-null filter support for enum columns in FsPulse queries - Add "No results found" message to Explore page structured tabs - Fix Explore page layout detachment when shrinking browser width - Add tests for EnumFilter null/not-null predicate generation
1 parent 0f976c3 commit b91342d

File tree

3 files changed

+127
-32
lines changed

3 files changed

+127
-32
lines changed

CHANGELOG.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
### Changed
11-
- **CLI simplified**: Running `fspulse` now starts the server by default; `fspulse serve` still works for backward compatibility
12-
- **TUI removed**: Terminal UI explorer (`src/explore/`) has been removed; all exploration is now through the web UI
13-
- **Legacy CLI commands removed**: Report commands and interactive CLI features removed; FsPulse is now a web-first application
14-
- **Removed `asm` feature from sha2**: Fixes x86_64 cross-compilation in CI workflow builds
15-
16-
### Removed
17-
- Unused dependencies: `crossterm`, `dialoguer`, `ratatui`, `tui-textarea`, `tabled`, `md-5`
18-
1910
### Added
2011
- **Access error tracking**: New `access` column on items tracks permission issues encountered during scanning
2112
- `Ok`: No access issues
@@ -27,9 +18,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2718
- **Change tracking for access state**: Change records now include `access_old` and `access_new` columns to track permission changes over time
2819

2920
### Changed
21+
- **CLI simplified**: Running `fspulse` now starts the server by default; `fspulse serve` still works for backward compatibility
22+
- **TUI removed**: Terminal UI explorer (`src/explore/`) has been removed; all exploration is now through the web UI
23+
- **Legacy CLI commands removed**: Report commands and interactive CLI features removed; FsPulse is now a web-first application
24+
- **Removed `asm` feature from sha2**: Fixes x86_64 cross-compilation in CI workflow builds
3025
- **Resilient scanning**: Permission errors no longer abort scans. Items with access issues are tracked and can be retried on subsequent scans when permissions are restored
3126
- **Hashing optimization**: Removed unnecessary `BufReader` wrapper for more efficient large file hashing
3227

28+
### Removed
29+
- Unused dependencies: `crossterm`, `dialoguer`, `ratatui`, `tui-textarea`, `tabled`, `md-5`
30+
31+
### Fixed
32+
- **Query null/not-null filters**: Restored ability to filter enum columns (like `val`, `access`, `change_type`) by `null` and `not null` in FsPulse queries
33+
- **Explore page empty results**: Structured tabs (Roots, Scans, Items, Changes, Alerts) now display "No results found" message when query returns no rows
34+
- **Explore page layout**: Fixed issue where outer card would detach from inner content when shrinking browser width on structured tabs
35+
3336
## [v0.3.1] - 2025-11-23
3437

3538
### Changed

frontend/src/pages/explore/DataExplorerView.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ export function DataExplorerView({ domain }: DataExplorerViewProps) {
215215
}
216216

217217
return (
218-
<div className="flex h-full gap-4">
218+
<div className="flex h-full gap-4 min-w-0">
219219
{/* Column Selector Card - Left Panel */}
220220
<Card className="w-96 flex flex-col">
221221
<CardContent className="flex-1 overflow-y-auto p-0">
@@ -351,7 +351,7 @@ export function DataExplorerView({ domain }: DataExplorerViewProps) {
351351
</Card>
352352

353353
{/* Data Table Card - Right Panel */}
354-
<Card className="flex-1 flex flex-col">
354+
<Card className="flex-1 flex flex-col min-w-0">
355355
<CardContent className="flex-1 overflow-auto p-0">
356356
{loading ? (
357357
<div className="p-4">Loading data...</div>
@@ -435,7 +435,11 @@ export function DataExplorerView({ domain }: DataExplorerViewProps) {
435435
</div>
436436
</div>
437437
</div>
438-
) : null}
438+
) : (
439+
<div className="flex items-center justify-center h-full text-muted-foreground">
440+
No results found
441+
</div>
442+
)}
439443
</CardContent>
440444
</Card>
441445

src/query/filter.rs

Lines changed: 108 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -504,23 +504,25 @@ type EnumParser = fn(&str) -> Option<i64>;
504504
/// Static map from column names to enum parsers
505505
static ENUM_PARSERS: Map<&'static str, EnumParser> = phf_map! {
506506
"scan_state" => ScanState::from_token,
507-
"item_type" => ItemType::from_token,
508-
"change_type" => ChangeType::from_token,
509-
"alert_type" => AlertType::from_token,
510-
"alert_status" => AlertStatus::from_token,
511-
"val" => ValidationState::from_token,
512-
"val_old" => ValidationState::from_token,
513-
"val_new" => ValidationState::from_token,
514-
"access" => Access::from_token,
515-
"access_old" => Access::from_token,
516-
"access_new" => Access::from_token,
507+
"item_type" => ItemType::from_token,
508+
"change_type" => ChangeType::from_token,
509+
"alert_type" => AlertType::from_token,
510+
"alert_status" => AlertStatus::from_token,
511+
"val" => ValidationState::from_token,
512+
"val_old" => ValidationState::from_token,
513+
"val_new" => ValidationState::from_token,
514+
"access" => Access::from_token,
515+
"access_old" => Access::from_token,
516+
"access_new" => Access::from_token,
517517
};
518518

519519
/// Filter for integer-backed enums (like scan_state)
520520
#[derive(Debug)]
521521
pub struct EnumFilter {
522522
enum_col_db: &'static str,
523523
enum_vals: Vec<i64>,
524+
match_null: bool,
525+
match_not_null: bool,
524526
}
525527

526528
impl Filter for EnumFilter {
@@ -529,10 +531,32 @@ impl Filter for EnumFilter {
529531
let mut pred_vec: Vec<Box<dyn ToSql>> = Vec::new();
530532
let mut first = true;
531533

532-
if self.enum_vals.len() > 1 {
534+
let mut pred_count = self.enum_vals.len();
535+
if self.match_null {
536+
pred_count += 1
537+
};
538+
if self.match_not_null {
539+
pred_count += 1
540+
};
541+
542+
if pred_count > 1 {
533543
pred_str.push('(');
534544
}
535545

546+
// if match_null is true, it will always be first
547+
if self.match_null {
548+
first = false;
549+
pred_str.push_str(&format!("({} IS NULL)", &self.enum_col_db));
550+
}
551+
552+
if self.match_not_null {
553+
match first {
554+
true => first = false,
555+
false => pred_str.push_str(" OR "),
556+
}
557+
pred_str.push_str(&format!("({} IS NOT NULL)", &self.enum_col_db));
558+
}
559+
536560
for enum_val in &self.enum_vals {
537561
match first {
538562
true => first = false,
@@ -543,7 +567,7 @@ impl Filter for EnumFilter {
543567
pred_vec.push(Box::new(*enum_val));
544568
}
545569

546-
if self.enum_vals.len() > 1 {
570+
if pred_count > 1 {
547571
pred_str.push(')');
548572
}
549573

@@ -556,6 +580,8 @@ impl EnumFilter {
556580
EnumFilter {
557581
enum_col_db,
558582
enum_vals: Vec::new(),
583+
match_null: false,
584+
match_not_null: false,
559585
}
560586
}
561587

@@ -583,14 +609,20 @@ impl EnumFilter {
583609

584610
// Parse each enum value using the parser
585611
for enum_val_pair in iter {
586-
let token = enum_val_pair.as_str();
587-
match parser(token) {
588-
Some(db_val) => enum_filter.enum_vals.push(db_val),
589-
None => {
590-
return Err(FsPulseError::CustomParsingError(format!(
591-
"Invalid {} value: '{}'",
592-
enum_col, token
593-
)))
612+
match enum_val_pair.as_rule() {
613+
Rule::null => enum_filter.match_null = true,
614+
Rule::not_null => enum_filter.match_not_null = true,
615+
_ => {
616+
let token_str = enum_val_pair.as_str();
617+
match parser(token_str) {
618+
Some(db_val) => enum_filter.enum_vals.push(db_val),
619+
None => {
620+
return Err(FsPulseError::CustomParsingError(format!(
621+
"Invalid {} value: '{}'",
622+
enum_col, token_str
623+
)))
624+
}
625+
}
594626
}
595627
}
596628
}
@@ -1525,6 +1557,8 @@ mod tests {
15251557
let filter = EnumFilter {
15261558
enum_col_db: "state",
15271559
enum_vals: vec![1],
1560+
match_null: false,
1561+
match_not_null: false,
15281562
};
15291563

15301564
let result = filter.to_predicate_parts();
@@ -1539,6 +1573,8 @@ mod tests {
15391573
let filter = EnumFilter {
15401574
enum_col_db: "state",
15411575
enum_vals: vec![1, 4, 6],
1576+
match_null: false,
1577+
match_not_null: false,
15421578
};
15431579

15441580
let result = filter.to_predicate_parts();
@@ -1548,6 +1584,58 @@ mod tests {
15481584
assert_eq!(pred_vec.len(), 3);
15491585
}
15501586

1587+
#[test]
1588+
fn test_enum_filter_predicate_null_only() {
1589+
let filter = EnumFilter {
1590+
enum_col_db: "state",
1591+
enum_vals: vec![],
1592+
match_null: true,
1593+
match_not_null: false,
1594+
};
1595+
let (pred_str, pred_vec) = filter.to_predicate_parts().unwrap();
1596+
assert_eq!(pred_str, "(state IS NULL)");
1597+
assert_eq!(pred_vec.len(), 0);
1598+
}
1599+
1600+
#[test]
1601+
fn test_enum_filter_predicate_not_null_only() {
1602+
let filter = EnumFilter {
1603+
enum_col_db: "state",
1604+
enum_vals: vec![],
1605+
match_null: false,
1606+
match_not_null: true,
1607+
};
1608+
let (pred_str, pred_vec) = filter.to_predicate_parts().unwrap();
1609+
assert_eq!(pred_str, "(state IS NOT NULL)");
1610+
assert_eq!(pred_vec.len(), 0);
1611+
}
1612+
1613+
#[test]
1614+
fn test_enum_filter_predicate_null_with_values() {
1615+
let filter = EnumFilter {
1616+
enum_col_db: "state",
1617+
enum_vals: vec![1, 2],
1618+
match_null: true,
1619+
match_not_null: false,
1620+
};
1621+
let (pred_str, pred_vec) = filter.to_predicate_parts().unwrap();
1622+
assert_eq!(pred_str, "((state IS NULL) OR (state = ?) OR (state = ?))");
1623+
assert_eq!(pred_vec.len(), 2);
1624+
}
1625+
1626+
#[test]
1627+
fn test_enum_filter_predicate_null_and_not_null() {
1628+
let filter = EnumFilter {
1629+
enum_col_db: "state",
1630+
enum_vals: vec![],
1631+
match_null: true,
1632+
match_not_null: true,
1633+
};
1634+
let (pred_str, pred_vec) = filter.to_predicate_parts().unwrap();
1635+
assert_eq!(pred_str, "((state IS NULL) OR (state IS NOT NULL))");
1636+
assert_eq!(pred_vec.len(), 0);
1637+
}
1638+
15511639
// ==================================================================================
15521640
// Int Filter Tests
15531641
// ==================================================================================

0 commit comments

Comments
 (0)