git.iko.soy/parseable-android
on master help sign in
Add column value filtering with include/exclude from log entries (#25)

- Add (+) and (-) filter buttons on each field row in expanded log entries to quickly include (=) or exclude (!=) a specific value - Show existing column values as suggestion chips in the filter bottom sheet when using = or != operators, extracted from currently loaded logs - Clear value input when switching columns in the filter sheet - Suggestion chips are filtered by typed text for quick narrowing https://claude.ai/code/session_016NWPYf3z8Ly94vq8WX6kJ5 Co-authored-by: Claude <noreply@anthropic.com>

commit3b7ea3321cd3fcc3f3047cc67d5e3c24e553390d authoriko <mail@iko.soy> dateMon 16 Feb 2026, 13:36 (7 months ago) parente98fa28
1 file changed+115−14
app/src/main/java/com/parseable/android/ui/screens/logviewer/LogViewerScreen.kt+115−14
app/src/main/java/com/parseable/android/ui/screens/logviewer/LogViewerScreen.kt+115−14
@@ -393,6 +393,12 @@ fun LogViewerScreen(
393393 )
394394 }
395395 },
396+ onFilterInclude = { column, value ->
397+ viewModel.addFilter(column, "=", value)
398+ },
399+ onFilterExclude = { column, value ->
400+ viewModel.addFilter(column, "!=", value)
401+ },
396402 )
397403 }
398404
@@ -445,6 +451,7 @@ fun LogViewerScreen(
445451 if (showFilterSheet) {
446452 FilterBottomSheet(
447453 columns = state.columns,
454+ logs = state.logs,
448455 onDismiss = { showFilterSheet = false },
449456 onApplyFilter = { column, operator, value ->
450457 viewModel.addFilter(column, operator, value)
@@ -681,6 +688,8 @@ fun LogEntryCard(
681688 isExpanded: Boolean,
682689 onClick: () -> Unit,
683690 onCopied: () -> Unit = {},
691+ onFilterInclude: (column: String, value: String) -> Unit = { _, _ -> },
692+ onFilterExclude: (column: String, value: String) -> Unit = { _, _ -> },
684693 clipboardManager: android.content.ClipboardManager,
685694) {
686695 val severity = remember(logEntry) { detectSeverity(logEntry) }
@@ -761,25 +770,55 @@ fun LogEntryCard(
761770 }
762771 HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
763772 logEntry.entries.forEach { (key, value) ->
773+ val displayValue = formatJsonValue(value)
764774 Row(
765775 modifier = Modifier
766776 .fillMaxWidth()
767777 .padding(vertical = 2.dp),
778+ verticalAlignment = Alignment.CenterVertically,
768779 ) {
769− Text(
770− text = "$key: ",
771− style = MaterialTheme.typography.bodySmall,
772− fontWeight = FontWeight.Bold,
773− fontFamily = FontFamily.Monospace,
774− color = MaterialTheme.colorScheme.primary,
775− fontSize = 12.sp,
776− )
777− Text(
778− text = formatJsonValue(value),
779− style = MaterialTheme.typography.bodySmall,
780− fontFamily = FontFamily.Monospace,
781− fontSize = 12.sp,
782− )
780+ Column(modifier = Modifier.weight(1f)) {
781+ Row {
782+ Text(
783+ text = "$key: ",
784+ style = MaterialTheme.typography.bodySmall,
785+ fontWeight = FontWeight.Bold,
786+ fontFamily = FontFamily.Monospace,
787+ color = MaterialTheme.colorScheme.primary,
788+ fontSize = 12.sp,
789+ )
790+ Text(
791+ text = displayValue,
792+ style = MaterialTheme.typography.bodySmall,
793+ fontFamily = FontFamily.Monospace,
794+ fontSize = 12.sp,
795+ )
796+ }
797+ }
798+ // Filter include (=) button
799+ IconButton(
800+ onClick = { onFilterInclude(key, displayValue) },
801+ modifier = Modifier.size(28.dp),
802+ ) {
803+ Icon(
804+ Icons.Filled.AddCircle,
805+ contentDescription = "Filter for $key = $displayValue",
806+ modifier = Modifier.size(16.dp),
807+ tint = MaterialTheme.colorScheme.primary,
808+ )
809+ }
810+ // Filter exclude (!=) button
811+ IconButton(
812+ onClick = { onFilterExclude(key, displayValue) },
813+ modifier = Modifier.size(28.dp),
814+ ) {
815+ Icon(
816+ Icons.Filled.RemoveCircle,
817+ contentDescription = "Filter out $key = $displayValue",
818+ modifier = Modifier.size(16.dp),
819+ tint = MaterialTheme.colorScheme.error,
820+ )
821+ }
783822 }
784823 }
785824 }
@@ -799,6 +838,7 @@ private fun formatJsonValue(element: JsonElement): String {
799838@Composable
800839fun FilterBottomSheet(
801840 columns: List<String>,
841+ logs: List<JsonObject> = emptyList(),
802842 onDismiss: () -> Unit,
803843 onApplyFilter: (column: String, operator: String, value: String) -> Unit,
804844) {
@@ -810,6 +850,28 @@ fun FilterBottomSheet(
810850
811851 val operators = listOf("=", "!=", "LIKE", "ILIKE", ">", "<", ">=", "<=", "IS NULL", "IS NOT NULL")
812852
853+ // Extract distinct values for the selected column from current logs
854+ val columnValues = remember(selectedColumn, logs) {
855+ if (selectedColumn.isBlank() || logs.isEmpty()) {
856+ emptyList()
857+ } else {
858+ logs.asSequence()
859+ .mapNotNull { entry ->
860+ try {
861+ entry[selectedColumn]?.let { element ->
862+ formatJsonValue(element).takeIf { it != "null" && it.isNotBlank() }
863+ }
864+ } catch (_: Exception) {
865+ null
866+ }
867+ }
868+ .distinct()
869+ .take(50)
870+ .sorted()
871+ .toList()
872+ }
873+ }
874+
813875 ModalBottomSheet(onDismissRequest = onDismiss) {
814876 Column(
815877 modifier = Modifier.padding(16.dp),
@@ -845,6 +907,7 @@ fun FilterBottomSheet(
845907 text = { Text(column) },
846908 onClick = {
847909 selectedColumn = column
910+ filterValue = ""
848911 columnDropdownExpanded = false
849912 },
850913 )
@@ -913,6 +976,44 @@ fun FilterBottomSheet(
913976 }
914977 }),
915978 )
979+
980+ // Show existing values as selectable suggestions
981+ if (columnValues.isNotEmpty() && selectedOperator in listOf("=", "!=")) {
982+ Text(
983+ text = "Existing values:",
984+ style = MaterialTheme.typography.labelMedium,
985+ color = MaterialTheme.colorScheme.onSurfaceVariant,
986+ )
987+ // Filter suggestions by typed text
988+ val filteredValues = remember(columnValues, filterValue) {
989+ if (filterValue.isBlank()) columnValues
990+ else columnValues.filter {
991+ it.contains(filterValue, ignoreCase = true)
992+ }
993+ }
994+ Row(
995+ modifier = Modifier
996+ .fillMaxWidth()
997+ .horizontalScroll(rememberScrollState()),
998+ horizontalArrangement = Arrangement.spacedBy(6.dp),
999+ ) {
1000+ filteredValues.forEach { value ->
1001+ SuggestionChip(
1002+ onClick = {
1003+ onApplyFilter(selectedColumn, selectedOperator, value)
1004+ },
1005+ label = {
1006+ Text(
1007+ text = if (value.length > 40) value.take(40) + "\u2026" else value,
1008+ maxLines = 1,
1009+ overflow = TextOverflow.Ellipsis,
1010+ fontSize = 12.sp,
1011+ )
1012+ },
1013+ )
1014+ }
1015+ }
1016+ }
9161017 }
9171018
9181019 val hasValueError = selectedOperator in listOf(">", "<", ">=", "<=") &&