@@ -393,6 +393,12 @@ fun LogViewerScreen(
396+ onFilterInclude = { column, value ->
397+ viewModel.addFilter(column, "=", value)
399+ onFilterExclude = { column, value ->
400+ viewModel.addFilter(column, "!=", value)
@@ -445,6 +451,7 @@ fun LogViewerScreen(
445 if (showFilterSheet) {
451 if (showFilterSheet) {
446 FilterBottomSheet(
452 FilterBottomSheet(
447 columns = state.columns,
453 columns = state.columns,
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,
686 val severity = remember(logEntry) { detectSeverity(logEntry) }
695 val severity = remember(logEntry) { detectSeverity(logEntry) }
@@ -761,25 +770,55 @@ fun LogEntryCard(
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)
765 modifier = Modifier
775 modifier = Modifier
766 .fillMaxWidth()
776 .fillMaxWidth()
767 .padding(vertical = 2.dp),
777 .padding(vertical = 2.dp),
778+ verticalAlignment = Alignment.CenterVertically,
780+ Column(modifier = Modifier.weight(1f)) {
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,
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,
798+ // Filter include (=) button
800+ onClick = { onFilterInclude(key, displayValue) },
801+ modifier = Modifier.size(28.dp),
804+ Icons.Filled.AddCircle,
805+ contentDescription = "Filter for $key = $displayValue",
806+ modifier = Modifier.size(16.dp),
807+ tint = MaterialTheme.colorScheme.primary,
810+ // Filter exclude (!=) button
812+ onClick = { onFilterExclude(key, displayValue) },
813+ modifier = Modifier.size(28.dp),
816+ Icons.Filled.RemoveCircle,
817+ contentDescription = "Filter out $key = $displayValue",
818+ modifier = Modifier.size(16.dp),
819+ tint = MaterialTheme.colorScheme.error,
@@ -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,
@@ -810,6 +850,28 @@ fun FilterBottomSheet(
811 val operators = listOf("=", "!=", "LIKE", "ILIKE", ">", "<", ">=", "<=", "IS NULL", "IS NOT NULL")
851 val operators = listOf("=", "!=", "LIKE", "ILIKE", ">", "<", ">=", "<=", "IS NULL", "IS NOT NULL")
853+ // Extract distinct values for the selected column from current logs
854+ val columnValues = remember(selectedColumn, logs) {
855+ if (selectedColumn.isBlank() || logs.isEmpty()) {
859+ .mapNotNull { entry ->
861+ entry[selectedColumn]?.let { element ->
862+ formatJsonValue(element).takeIf { it != "null" && it.isNotBlank() }
864+ } catch (_: Exception) {
813 ModalBottomSheet(onDismissRequest = onDismiss) {
875 ModalBottomSheet(onDismissRequest = onDismiss) {
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
848 columnDropdownExpanded = false
911 columnDropdownExpanded = false
@@ -913,6 +976,44 @@ fun FilterBottomSheet(
980+ // Show existing values as selectable suggestions
981+ if (columnValues.isNotEmpty() && selectedOperator in listOf("=", "!=")) {
983+ text = "Existing values:",
984+ style = MaterialTheme.typography.labelMedium,
985+ color = MaterialTheme.colorScheme.onSurfaceVariant,
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)
997+ .horizontalScroll(rememberScrollState()),
998+ horizontalArrangement = Arrangement.spacedBy(6.dp),
1000+ filteredValues.forEach { value ->
1003+ onApplyFilter(selectedColumn, selectedOperator, value)
1007+ text = if (value.length > 40) value.take(40) + "\u2026" else value,
1009+ overflow = TextOverflow.Ellipsis,
918 val hasValueError = selectedOperator in listOf(">", "<", ">=", "<=") &&
1019 val hasValueError = selectedOperator in listOf(">", "<", ">=", "<=") &&