git.iko.soy/parseable-android
on master help sign in
Add column value filtering with include/exclude from log entries (#25)
3b7ea33iko
app/src/main/java/com/parseable/android/ui/screens/logviewer/LogViewerScreen.kt+115−14
@@ -393,6 +393,12 @@ fun LogViewerScreen(
393 )
393 )
394 }
394 }
395 },
395 },
396+ onFilterInclude = { column, value ->
397+ viewModel.addFilter(column, "=", value)
398+ },
399+ onFilterExclude = { column, value ->
400+ viewModel.addFilter(column, "!=", value)
401+ },
396 )
402 )
397 }
403 }
398
404
@@ -445,6 +451,7 @@ fun LogViewerScreen(
445 if (showFilterSheet) {
451 if (showFilterSheet) {
446 FilterBottomSheet(
452 FilterBottomSheet(
447 columns = state.columns,
453 columns = state.columns,
454+ logs = state.logs,
448 onDismiss = { showFilterSheet = false },
455 onDismiss = { showFilterSheet = false },
449 onApplyFilter = { column, operator, value ->
456 onApplyFilter = { column, operator, value ->
450 viewModel.addFilter(column, operator, value)
457 viewModel.addFilter(column, operator, value)
@@ -681,6 +688,8 @@ fun LogEntryCard(
681 isExpanded: Boolean,
688 isExpanded: Boolean,
682 onClick: () -> Unit,
689 onClick: () -> Unit,
683 onCopied: () -> Unit = {},
690 onCopied: () -> Unit = {},
691+ onFilterInclude: (column: String, value: String) -> Unit = { _, _ -> },
692+ onFilterExclude: (column: String, value: String) -> Unit = { _, _ -> },
684 clipboardManager: android.content.ClipboardManager,
693 clipboardManager: android.content.ClipboardManager,
685) {
694) {
686 val severity = remember(logEntry) { detectSeverity(logEntry) }
695 val severity = remember(logEntry) { detectSeverity(logEntry) }
@@ -761,25 +770,55 @@ fun LogEntryCard(
761 }
770 }
762 HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
771 HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
763 logEntry.entries.forEach { (key, value) ->
772 logEntry.entries.forEach { (key, value) ->
773+ val displayValue = formatJsonValue(value)
764 Row(
774 Row(
765 modifier = Modifier
775 modifier = Modifier
766 .fillMaxWidth()
776 .fillMaxWidth()
767 .padding(vertical = 2.dp),
777 .padding(vertical = 2.dp),
778+ verticalAlignment = Alignment.CenterVertically,
768 ) {
779 ) {
780+ Column(modifier = Modifier.weight(1f)) {
781+ Row {
769− Text(
782+ Text(
770− text = "$key: ",
783+ text = "$key: ",
771− style = MaterialTheme.typography.bodySmall,
784+ style = MaterialTheme.typography.bodySmall,
772− fontWeight = FontWeight.Bold,
785+ fontWeight = FontWeight.Bold,
773− fontFamily = FontFamily.Monospace,
786+ fontFamily = FontFamily.Monospace,
774− color = MaterialTheme.colorScheme.primary,
787+ color = MaterialTheme.colorScheme.primary,
775− fontSize = 12.sp,
788+ fontSize = 12.sp,
776− )
789+ )
777− Text(
790+ Text(
778− text = formatJsonValue(value),
791+ text = displayValue,
779− style = MaterialTheme.typography.bodySmall,
792+ style = MaterialTheme.typography.bodySmall,
780− fontFamily = FontFamily.Monospace,
793+ fontFamily = FontFamily.Monospace,
781− fontSize = 12.sp,
794+ fontSize = 12.sp,
782− )
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+ }
783 }
822 }
784 }
823 }
785 }
824 }
@@ -799,6 +838,7 @@ private fun formatJsonValue(element: JsonElement): String {
799@Composable
838@Composable
800fun FilterBottomSheet(
839fun FilterBottomSheet(
801 columns: List<String>,
840 columns: List<String>,
841+ logs: List<JsonObject> = emptyList(),
802 onDismiss: () -> Unit,
842 onDismiss: () -> Unit,
803 onApplyFilter: (column: String, operator: String, value: String) -> Unit,
843 onApplyFilter: (column: String, operator: String, value: String) -> Unit,
804) {
844) {
@@ -810,6 +850,28 @@ fun FilterBottomSheet(
810
850
811 val operators = listOf("=", "!=", "LIKE", "ILIKE", ">", "<", ">=", "<=", "IS NULL", "IS NOT NULL")
851 val operators = listOf("=", "!=", "LIKE", "ILIKE", ">", "<", ">=", "<=", "IS NULL", "IS NOT NULL")
812
852
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+
813 ModalBottomSheet(onDismissRequest = onDismiss) {
875 ModalBottomSheet(onDismissRequest = onDismiss) {
814 Column(
876 Column(
815 modifier = Modifier.padding(16.dp),
877 modifier = Modifier.padding(16.dp),
@@ -845,6 +907,7 @@ fun FilterBottomSheet(
845 text = { Text(column) },
907 text = { Text(column) },
846 onClick = {
908 onClick = {
847 selectedColumn = column
909 selectedColumn = column
910+ filterValue = ""
848 columnDropdownExpanded = false
911 columnDropdownExpanded = false
849 },
912 },
850 )
913 )
@@ -913,6 +976,44 @@ fun FilterBottomSheet(
913 }
976 }
914 }),
977 }),
915 )
978 )
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+ }
916 }
1017 }
917
1018
918 val hasValueError = selectedOperator in listOf(">", "<", ">=", "<=") &&
1019 val hasValueError = selectedOperator in listOf(">", "<", ">=", "<=") &&